|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +source "$(dirname "$0")/device-machine-allocation.sh" |
| 4 | + |
| 5 | +# # ===== Global Variables ===== |
| 6 | +WORKSPACE_DIR="$HOME/.browserstack" |
| 7 | +PROJECT_FOLDER="NOW" |
| 8 | + |
| 9 | +# URL handling |
| 10 | +DEFAULT_TEST_URL="https://bstackdemo.com" |
| 11 | + |
| 12 | +# ===== Log files (per-run) ===== |
| 13 | +LOG_DIR="$WORKSPACE_DIR/$PROJECT_FOLDER/logs" |
| 14 | +NOW_RUN_LOG_FILE="" |
| 15 | + |
| 16 | +# ===== Global Variables ===== |
| 17 | +USERNAME="" |
| 18 | +ACCESS_KEY="" |
| 19 | +TEST_TYPE="" # Web / App / Both |
| 20 | +TECH_STACK="" # Java / Python / JS |
| 21 | +CX_TEST_URL="$DEFAULT_TEST_URL" |
| 22 | + |
| 23 | +WEB_PLAN_FETCHED=false |
| 24 | +MOBILE_PLAN_FETCHED=false |
| 25 | +TEAM_PARALLELS_MAX_ALLOWED_WEB=0 |
| 26 | +TEAM_PARALLELS_MAX_ALLOWED_MOBILE=0 |
| 27 | + |
| 28 | +# App specific globals |
| 29 | +APP_URL="" |
| 30 | +APP_PLATFORM="" # ios | android | all |
| 31 | + |
| 32 | + |
| 33 | +# ===== Logging Functions ===== |
| 34 | +log_msg_to() { |
| 35 | + local message="$1" |
| 36 | + local dest_file=$NOW_RUN_LOG_FILE |
| 37 | + local ts |
| 38 | + ts="$(date +"%Y-%m-%d %H:%M:%S")" |
| 39 | + local line="[$ts] $message" |
| 40 | + |
| 41 | + # print to console |
| 42 | + if [[ "$RUN_MODE" == *"--debug"* ]]; then |
| 43 | + echo "$line" |
| 44 | + fi |
| 45 | + |
| 46 | + # write to dest file if provided |
| 47 | + if [ -n "$dest_file" ]; then |
| 48 | + mkdir -p "$(dirname "$dest_file")" |
| 49 | + echo "$line" >> $NOW_RUN_LOG_FILE |
| 50 | + fi |
| 51 | +} |
| 52 | + |
| 53 | +# Spinner function for long-running processes |
| 54 | +show_spinner() { |
| 55 | + local pid=$1 |
| 56 | + local spin='|/-\' |
| 57 | + local i=0 |
| 58 | + local ts |
| 59 | + ts="$(date +"%Y-%m-%d %H:%M:%S")" |
| 60 | + while kill -0 "$pid" 2>/dev/null; do |
| 61 | + i=$(( (i+1) %4 )) |
| 62 | + printf "\r⏳ Processing... ${spin:$i:1}" |
| 63 | + sleep 0.1 |
| 64 | + done |
| 65 | + echo "" |
| 66 | + log_info "Run Test command completed." |
| 67 | + sleep 5 |
| 68 | + #log_msg_to "✅ Done!" |
| 69 | +} |
| 70 | + |
| 71 | +# ===== Workspace Management ===== |
| 72 | +setup_workspace() { |
| 73 | + log_section "⚙️ Environment & Credentials" |
| 74 | + local full_path="$WORKSPACE_DIR/$PROJECT_FOLDER" |
| 75 | + if [ ! -d "$full_path" ]; then |
| 76 | + mkdir -p "$full_path" |
| 77 | + log_info "Created onboarding workspace: $full_path" |
| 78 | + else |
| 79 | + log_success "Onboarding workspace found at: $full_path" |
| 80 | + fi |
| 81 | +} |
| 82 | + |
| 83 | + |
| 84 | +# ===== App Upload Management ===== |
| 85 | +handle_app_upload() { |
| 86 | + local app_platform="" |
| 87 | + if [[ "$RUN_MODE" == *"--silent"* || "$RUN_MODE" == *"--debug"* ]]; then |
| 88 | + upload_sample_app |
| 89 | + app_platform="android" |
| 90 | + export APP_PLATFORM="$app_platform" |
| 91 | + log_msg_to "Exported APP_PLATFORM=$APP_PLATFORM" |
| 92 | + else |
| 93 | + local choice |
| 94 | + choice=$(osascript -e ' |
| 95 | + display dialog "How would you like to select your app?" ¬ |
| 96 | + with title "BrowserStack App Upload" ¬ |
| 97 | + with icon note ¬ |
| 98 | + buttons {"Use Sample App", "Upload my App (.apk/.ipa)", "Cancel"} ¬ |
| 99 | + default button "Upload my App (.apk/.ipa)" |
| 100 | + ' 2>/dev/null) |
| 101 | + |
| 102 | + if [[ "$choice" == *"Use Sample App"* ]]; then |
| 103 | + upload_sample_app |
| 104 | + app_platform="android" |
| 105 | + export APP_PLATFORM="$app_platform" |
| 106 | + log_msg_to "Exported APP_PLATFORM=$APP_PLATFORM" |
| 107 | + elif [[ "$choice" == *"Upload my App"* ]]; then |
| 108 | + upload_custom_app |
| 109 | + else |
| 110 | + return 1 |
| 111 | + fi |
| 112 | + fi |
| 113 | +} |
| 114 | + |
| 115 | +upload_sample_app() { |
| 116 | + log_msg_to "⬆️ Uploading sample app to BrowserStack..." |
| 117 | + local upload_response |
| 118 | + upload_response=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ |
| 119 | + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ |
| 120 | + -F "url=https://www.browserstack.com/app-automate/sample-apps/android/WikipediaSample.apk") |
| 121 | + |
| 122 | + app_url=$(echo "$upload_response" | grep -o '"app_url":"[^"]*' | cut -d'"' -f4) |
| 123 | + export BROWSERSTACK_APP=$app_url |
| 124 | + log_msg_to "Exported BROWSERSTACK_APP=$BROWSERSTACK_APP" |
| 125 | + |
| 126 | + if [ -z "$app_url" ]; then |
| 127 | + log_msg_to "❌ Upload failed. Response: $UPLOAD_RESPONSE" |
| 128 | + return 1 |
| 129 | + fi |
| 130 | + |
| 131 | + log_msg_to "✅ App uploaded successfully: $app_url" |
| 132 | + return 0 |
| 133 | +} |
| 134 | + |
| 135 | +upload_custom_app() { |
| 136 | + local -n app_url=$1 |
| 137 | + local -n platform=$2 |
| 138 | + local app_platform="" |
| 139 | + local file_path |
| 140 | + file_path=$(osascript -e 'choose file with prompt "Select your .apk or .ipa file:" of type {"apk", "ipa"}' 2>/dev/null) |
| 141 | + |
| 142 | + if [ -z "$file_path" ]; then |
| 143 | + log_msg_to "❌ No file selected" |
| 144 | + return 1 |
| 145 | + fi |
| 146 | + |
| 147 | + # Determine platform from file extension |
| 148 | + if [[ "$file_path" == *.ipa ]]; then |
| 149 | + platform="ios" |
| 150 | + app_platform="ios" |
| 151 | + elif [[ "$file_path" == *.apk ]]; then |
| 152 | + platform="android" |
| 153 | + app_platform="android" |
| 154 | + else |
| 155 | + log_msg_to "❌ Invalid file type. Must be .apk or .ipa" |
| 156 | + return 1 |
| 157 | + fi |
| 158 | + |
| 159 | + log_msg_to "⬆️ Uploading app to BrowserStack..." |
| 160 | + local upload_response |
| 161 | + upload_response=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ |
| 162 | + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ |
| 163 | + -F "file=@$file_path") |
| 164 | + |
| 165 | + app_url=$(echo "$upload_response" | grep -o '"app_url":"[^"]*' | cut -d'"' -f4) |
| 166 | + if [ -z "$app_url" ]; then |
| 167 | + log_msg_to "❌ Failed to upload app" |
| 168 | + return 1 |
| 169 | + fi |
| 170 | + |
| 171 | + export BROWSERSTACK_APP=$app_url |
| 172 | + log_msg_to "✅ App uploaded successfully" |
| 173 | + log_msg_to "Exported BROWSERSTACK_APP=$BROWSERSTACK_APP" |
| 174 | + |
| 175 | + export APP_PLATFORM="$app_platform" |
| 176 | + log_msg_to "Exported APP_PLATFORM=$APP_PLATFORM" |
| 177 | + return 0 |
| 178 | +} |
| 179 | + |
| 180 | +# ===== Dynamic config generators ===== |
| 181 | +generate_web_platforms() { |
| 182 | + local max_total_parallels=$1 |
| 183 | + local platformsListContentFormat=$2 |
| 184 | + local platform="web" |
| 185 | + export NOW_PLATFORM="$platform" |
| 186 | + local platformsList=$(pick_terminal_devices "$NOW_PLATFORM" $max_total_parallels "$platformsListContentFormat") |
| 187 | + echo "$platformsList" |
| 188 | +} |
| 189 | + |
| 190 | +generate_mobile_platforms() { |
| 191 | + local max_total_parallels=$1 |
| 192 | + local platformsListContentFormat=$2 |
| 193 | + local app_platform="$APP_PLATFORM" |
| 194 | + local platformsList=$(pick_terminal_devices "$app_platform" $max_total_parallels, "$platformsListContentFormat") |
| 195 | + echo "$platformsList" |
| 196 | +} |
| 197 | + |
| 198 | + |
| 199 | +# ===== Fetch plan details (writes to GLOBAL) ===== |
| 200 | +fetch_plan_details() { |
| 201 | + local test_type=$1 |
| 202 | + |
| 203 | + log_section "☁️ Account & Plan Details" |
| 204 | + log_info "Fetching BrowserStack plan for $test_type" |
| 205 | + local web_unauthorized=false |
| 206 | + local mobile_unauthorized=false |
| 207 | + |
| 208 | + if [[ "$test_type" == "web" || "$test_type" == "both" ]]; then |
| 209 | + RESPONSE_WEB=$(curl -s -w "\n%{http_code}" -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" https://api.browserstack.com/automate/plan.json) |
| 210 | + HTTP_CODE_WEB=$(echo "$RESPONSE_WEB" | tail -n1) |
| 211 | + RESPONSE_WEB_BODY=$(echo "$RESPONSE_WEB" | sed '$d') |
| 212 | + if [ "$HTTP_CODE_WEB" == "200" ]; then |
| 213 | + WEB_PLAN_FETCHED=true |
| 214 | + TEAM_PARALLELS_MAX_ALLOWED_WEB=$(echo "$RESPONSE_WEB_BODY" | grep -o '"parallel_sessions_max_allowed":[0-9]*' | grep -o '[0-9]*') |
| 215 | + export TEAM_PARALLELS_MAX_ALLOWED_WEB="$TEAM_PARALLELS_MAX_ALLOWED_WEB" |
| 216 | + log_msg_to "✅ Web Testing Plan fetched: Team max parallel sessions = $TEAM_PARALLELS_MAX_ALLOWED_WEB" |
| 217 | + else |
| 218 | + log_msg_to "❌ Web Testing Plan fetch failed ($HTTP_CODE_WEB)" |
| 219 | + [ "$HTTP_CODE_WEB" == "401" ] && web_unauthorized=true |
| 220 | + fi |
| 221 | + fi |
| 222 | + |
| 223 | + if [[ "$test_type" == "app" || "$test_type" == "both" ]]; then |
| 224 | + RESPONSE_MOBILE=$(curl -s -w "\n%{http_code}" -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" https://api-cloud.browserstack.com/app-automate/plan.json) |
| 225 | + HTTP_CODE_MOBILE=$(echo "$RESPONSE_MOBILE" | tail -n1) |
| 226 | + RESPONSE_MOBILE_BODY=$(echo "$RESPONSE_MOBILE" | sed '$d') |
| 227 | + if [ "$HTTP_CODE_MOBILE" == "200" ]; then |
| 228 | + MOBILE_PLAN_FETCHED=true |
| 229 | + TEAM_PARALLELS_MAX_ALLOWED_MOBILE=$(echo "$RESPONSE_MOBILE_BODY" | grep -o '"parallel_sessions_max_allowed":[0-9]*' | grep -o '[0-9]*') |
| 230 | + export TEAM_PARALLELS_MAX_ALLOWED_MOBILE="$TEAM_PARALLELS_MAX_ALLOWED_MOBILE" |
| 231 | + log_msg_to "✅ Mobile App Testing Plan fetched: Team max parallel sessions = $TEAM_PARALLELS_MAX_ALLOWED_MOBILE" |
| 232 | + else |
| 233 | + log_msg_to "❌ Mobile App Testing Plan fetch failed ($HTTP_CODE_MOBILE)" |
| 234 | + [ "$HTTP_CODE_MOBILE" == "401" ] && mobile_unauthorized=true |
| 235 | + fi |
| 236 | + fi |
| 237 | + |
| 238 | + log_info "Plan summary: Web $WEB_PLAN_FETCHED ($TEAM_PARALLELS_MAX_ALLOWED_WEB max), Mobile $MOBILE_PLAN_FETCHED ($TEAM_PARALLELS_MAX_ALLOWED_MOBILE max)" |
| 239 | + |
| 240 | + if [[ "$test_type" == "web" && "$web_unauthorized" == true ]] || \ |
| 241 | + [[ "$test_type" == "app" && "$mobile_unauthorized" == true ]] || \ |
| 242 | + [[ "$test_type" == "both" && "$web_unauthorized" == true && "$mobile_unauthorized" == true ]]; then |
| 243 | + log_msg_to "❌ Unauthorized to fetch required plan(s). Exiting." |
| 244 | + exit 1 |
| 245 | + fi |
| 246 | +} |
| 247 | + |
| 248 | +# Function to check if IP is private |
| 249 | +is_private_ip() { |
| 250 | + case $1 in |
| 251 | + 10.* | 192.168.* | 172.16.* | 172.17.* | 172.18.* | 172.19.* | \ |
| 252 | + 172.20.* | 172.21.* | 172.22.* | 172.23.* | 172.24.* | 172.25.* | \ |
| 253 | + 172.26.* | 172.27.* | 172.28.* | 172.29.* | 172.30.* | 172.31.* | "") |
| 254 | + return 0 ;; # Private |
| 255 | + *) |
| 256 | + return 1 ;; # Public |
| 257 | + esac |
| 258 | +} |
| 259 | + |
| 260 | +is_domain_private() { |
| 261 | + domain=${CX_TEST_URL#*://} # remove protocol |
| 262 | + domain=${domain%%/*} # remove everything after first "/" |
| 263 | + log_msg_to "Website domain: $domain" |
| 264 | + export NOW_WEB_DOMAIN="$CX_TEST_URL" |
| 265 | + |
| 266 | + # Resolve domain using Cloudflare DNS |
| 267 | + IP_ADDRESS=$(dig +short "$domain" @1.1.1.1 | head -n1) |
| 268 | + |
| 269 | + # Determine if domain is private |
| 270 | + if is_private_ip "$IP_ADDRESS"; then |
| 271 | + is_cx_domain_private=0 |
| 272 | + else |
| 273 | + is_cx_domain_private=-1 |
| 274 | + fi |
| 275 | + |
| 276 | + log_msg_to "Resolved IPs: $IP_ADDRESS" |
| 277 | + |
| 278 | + return $is_cx_domain_private |
| 279 | +} |
| 280 | + |
| 281 | + |
| 282 | +identify_run_status_java() { |
| 283 | + local log_file=$1 |
| 284 | + log_section "✅ Results" |
| 285 | + |
| 286 | + # Extract the test summary line |
| 287 | + local line=$(grep -m 2 -E "[INFO|ERROR].*Tests run" < "$log_file") |
| 288 | + # If not found, fail |
| 289 | + if [[ -z "$line" ]]; then |
| 290 | + log_warn "❌ No test summary line found." |
| 291 | + return 1 |
| 292 | + fi |
| 293 | + |
| 294 | + # Extract numbers using regex |
| 295 | + tests_run=$(echo "$line" | grep -m 1 -oE "Tests run: [0-9]+" | awk '{print $3}') |
| 296 | + failures=$(echo "$line" | grep -m 1 -oE "Failures: [0-9]+" | awk '{print $2}') |
| 297 | + errors=$(echo "$line" | grep -m 1 -oE "Errors: [0-9]+" | awk '{print $2}') |
| 298 | + skipped=$(echo "$line" | grep -m 1 -oE "Skipped: [0-9]+" | awk '{print $2}') |
| 299 | + |
| 300 | + # Calculate passed tests |
| 301 | + passed=$(( $tests_run - ($failures + $errors + $skipped) )) |
| 302 | + |
| 303 | + # Check condition |
| 304 | + if (( passed > 0 )); then |
| 305 | + log_success "Success: $passed test(s) passed." |
| 306 | + return 0 |
| 307 | + else |
| 308 | + log_error "Error: No tests passed (Tests run: $tests_run, Failures: $failures, Errors: $errors, Skipped: $skipped)" |
| 309 | + return 1 |
| 310 | + fi |
| 311 | + |
| 312 | + return 1 |
| 313 | +} |
| 314 | + |
| 315 | + |
| 316 | +identify_run_status_nodejs() { |
| 317 | + |
| 318 | + local log_file=$1 |
| 319 | + log_info "Identifying run status" |
| 320 | + local line=$(grep -m 1 -E "Spec Files:.*passed.*total" < "$log_file") |
| 321 | + # If not found, fail |
| 322 | + if [[ -z "$line" ]]; then |
| 323 | + log_warn "❌ No test summary line found." |
| 324 | + return 1 |
| 325 | + fi |
| 326 | + |
| 327 | + # Extract numbers using regex |
| 328 | + passed=$(echo "$line" | grep -oE '[0-9]+ passed' | awk '{print $1}') |
| 329 | + # Check condition |
| 330 | + if (( passed > 0 )); then |
| 331 | + log_success "Success: $passed test(s) passed" |
| 332 | + return 0 |
| 333 | + else |
| 334 | + log_error "❌ Error: No tests passed" |
| 335 | + return 1 |
| 336 | + fi |
| 337 | + |
| 338 | + return 1 |
| 339 | +} |
| 340 | + |
| 341 | + |
| 342 | +identify_run_status_python() { |
| 343 | + |
| 344 | + local log_file=$1 |
| 345 | + log_info "Identifying run status" |
| 346 | + |
| 347 | + # Extract numbers and sum them |
| 348 | + passed_sum=$(grep -oE '[0-9]+ passed' "$log_file" | awk '{sum += $1} END {print sum+0}') |
| 349 | + |
| 350 | + echo "✅ Total Passed: $passed_sum" |
| 351 | + |
| 352 | + local completed_test_count=passed_sum+warning_sum |
| 353 | + |
| 354 | + # If not found, fail |
| 355 | + if [[ -z "$passed_sum" ]]; then |
| 356 | + log_warn "❌ No test summary line found." |
| 357 | + return 1 |
| 358 | + fi |
| 359 | + |
| 360 | + # Check condition |
| 361 | + if (( passed_sum > 0 )); then |
| 362 | + log_success "Success: $passed_sum test(s) completed" |
| 363 | + return 0 |
| 364 | + else |
| 365 | + log_error "❌ Error: No tests completed" |
| 366 | + return 1 |
| 367 | + fi |
| 368 | + |
| 369 | + return 1 |
| 370 | +} |
| 371 | + |
| 372 | +clear_old_logs() { |
| 373 | + mkdir -p "$LOG_DIR" |
| 374 | + : > "$NOW_RUN_LOG_FILE" |
| 375 | + |
| 376 | + log_success "Logs cleared and fresh run initiated." |
| 377 | +} |
0 commit comments