-
Notifications
You must be signed in to change notification settings - Fork 398
Expand file tree
/
Copy pathintegration_test_runner.sh
More file actions
executable file
·465 lines (373 loc) · 14 KB
/
Copy pathintegration_test_runner.sh
File metadata and controls
executable file
·465 lines (373 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#!/bin/bash
set -euo pipefail
# Runner params/knobs, all overridable from the environment:
# SUITE_DIR directory to collect *_test.dart suites from, or a single suite file
# (default integration_test/suites)
# TEST_TIER tier0, tier1 or all, narrows SUITE_DIR to a tier subdirectory
# PLATFORM android, linux, macos or auto, picks the between-suite data reset
# ANDROID_APP_ID package cleared between suites on android (default read from android/app.properties)
# PREBUILT_APK prebuilt apk passed to flutter drive, its dart defines are already baked in
# EXTRA_DART_DEFINES semicolon separated KEY=VALUE pairs
# FLUTTER_DEVICE device id passed to flutter drive, needed when several devices are attached
# TEST_TIMEOUT per-attempt timeout in seconds (default 900)
# RETRY_COUNT retries per suite after a failure (default 1)
# MAX_VOID_ATTEMPTS extra attempts when the driver never attached (default 2)
# MAX_TOTAL_VOID_ATTEMPTS give up on the whole run after this many, the environment is broken (default 4)
# REMOVE_DATA_DIRECTORY set to N to keep app data between suites (default wipes it)
# REMOVE_DESKTOP_DATA set to Y to let a desktop run wipe the wallet directories below,
# they belong to the installed wallet so this is off by default
# SUMMARY_FILE path to write a key=value summary of the run to, for reporting
# VOID_GRACE seconds to let a driver that cannot attach recover before the
# attempt is ended early (default 60)
SUITE_DIR=${SUITE_DIR:-integration_test/suites}
TEST_TIER=${TEST_TIER:-all}
PLATFORM=${PLATFORM:-auto}
TEST_TIMEOUT=${TEST_TIMEOUT:-900}
RETRY_COUNT=${RETRY_COUNT:-1}
MAX_VOID_ATTEMPTS=${MAX_VOID_ATTEMPTS:-2}
MAX_TOTAL_VOID_ATTEMPTS=${MAX_TOTAL_VOID_ATTEMPTS:-4}
REMOVE_DATA_DIRECTORY=${REMOVE_DATA_DIRECTORY:-Y}
REMOVE_DESKTOP_DATA=${REMOVE_DESKTOP_DATA:-N}
EXTRA_DART_DEFINES=${EXTRA_DART_DEFINES:-}
SUMMARY_FILE=${SUMMARY_FILE:-}
VOID_GRACE=${VOID_GRACE:-60}
# Using this to capture as many of these annoying flutter drive errors as possible,
# so we can handle them, and the test doesn't just hang forever
VOID_MARKERS="unusually long time to connect to the VM|taking unusually long time to initialize|Service has disappeared|Flutter Driver extension is taking a long time to become available"
PREBUILT_APK=${PREBUILT_APK:-}
# Desktop data directories, shared with the installed wallet, see REMOVE_DESKTOP_DATA
DATA_DIRS=(
"$HOME/.local/share/com.example.cake_wallet"
"$HOME/Documents/cake_wallet"
"$HOME/.config/cake_wallet"
)
targets=()
passed_tests=()
failed_tests=()
funds_results=()
total_void_attempts=0
cleanup() {
echo "Received interrupt signal, cleaning up..."
exit 1
}
trap cleanup SIGINT SIGTERM
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2
}
format_duration() {
local seconds=$1
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
local secs=$((seconds % 60))
if (( hours > 0 )); then
echo "${hours}h ${minutes}m ${secs}s"
elif (( minutes > 0 )); then
echo "${minutes}m ${secs}s"
else
echo "${secs}s"
fi
}
resolve_platform() {
if [[ "$PLATFORM" != "auto" ]]; then
return
fi
if command -v adb > /dev/null 2>&1 && adb devices 2>/dev/null | grep -q "device$"; then
PLATFORM="android"
elif [[ "$(uname -s)" == "Darwin" ]]; then
PLATFORM="macos"
else
PLATFORM="linux"
fi
log "Resolved platform: $PLATFORM"
}
resolve_android_app_id() {
if [[ -n "${ANDROID_APP_ID:-}" ]]; then
return
fi
if [[ -f "android/app.properties" ]]; then
ANDROID_APP_ID=$(grep '^id=' android/app.properties | cut -d'=' -f2)
fi
ANDROID_APP_ID=${ANDROID_APP_ID:-com.cakewallet.cake_wallet}
}
restart_adb() {
if [[ "$PLATFORM" != "android" ]]; then
return
fi
bounded 30 adb kill-server
bounded 30 adb start-server
bounded 60 adb wait-for-device
if ! bounded 30 adb devices | grep -q "device$"; then
error "No device is attached after restarting adb"
return 1
fi
}
bounded() {
local limit="$1"
shift
if command -v timeout > /dev/null 2>&1; then
timeout "$limit" "$@" 2>/dev/null || return 1
elif command -v perl > /dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$limit" "$@" 2>/dev/null || return 1
else
"$@" 2>/dev/null || return 1
fi
}
capture_failure_logcat() {
local test_name="$1"
if [[ "$PLATFORM" != "android" ]]; then
return
fi
echo "===== logcat after failed attempt: $test_name ====="
bounded 60 adb logcat -d -t 400 || echo "logcat unavailable"
echo "===== end logcat ====="
}
clean_data_directories() {
if [[ "$REMOVE_DATA_DIRECTORY" != "Y" ]]; then
return
fi
if [[ "$PLATFORM" == "android" ]]; then
if ! command -v adb > /dev/null 2>&1; then
error "adb is not on PATH, every suite would run on whatever data is already installed"
exit 1
fi
if [[ "$ANDROID_APP_ID" != *".test_"* ]]; then
error "Refusing to clear $ANDROID_APP_ID, it is not a test build. Rename the app first: printf 'id=com.cakewallet.test_local\\nname=local\\n' > android/app.properties"
exit 1
fi
log "Cleaning app data..."
adb shell pm clear "$ANDROID_APP_ID" > /dev/null 2>&1 || log "pm clear skipped, $ANDROID_APP_ID not installed yet"
return
fi
if [[ "$REMOVE_DESKTOP_DATA" != "Y" ]]; then
log "Keeping the desktop wallet data, set REMOVE_DESKTOP_DATA=Y to wipe it between suites"
return
fi
log "Cleaning app data..."
for dir in "${DATA_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir" || error "Failed to remove $dir"
fi
done
}
build_drive_command() {
drive_command=(flutter drive --driver=test_driver/integration_test.dart --dart-define=CI_BUILD=true)
if [[ -n "${FLUTTER_DEVICE:-}" ]]; then
drive_command+=("-d" "$FLUTTER_DEVICE")
fi
if [[ -n "$PREBUILT_APK" ]]; then
drive_command+=("--use-application-binary=$PREBUILT_APK")
fi
if [[ -n "$EXTRA_DART_DEFINES" ]]; then
IFS=';' read -ra defines <<< "$EXTRA_DART_DEFINES"
for define in "${defines[@]}"; do
drive_command+=("--dart-define=$define")
done
fi
if command -v timeout > /dev/null 2>&1; then
drive_command=(timeout "$TEST_TIMEOUT" "${drive_command[@]}")
elif command -v perl > /dev/null 2>&1; then
drive_command=(perl -e 'alarm shift; exec @ARGV' "$TEST_TIMEOUT" "${drive_command[@]}")
fi
}
end_process_tree() {
local root="$1" signal="$2"
local child
for child in $(pgrep -P "$root" 2>/dev/null); do
end_process_tree "$child" "$signal"
done
kill "-$signal" "$root" 2>/dev/null || true
}
watch_for_dead_driver() {
local log_file="$1" drive_pid="$2"
local marker_seen=0
while kill -0 "$drive_pid" 2>/dev/null; do
if (( marker_seen == 0 )) && grep -qE "$VOID_MARKERS" "$log_file" 2>/dev/null; then
marker_seen=$(date +%s)
log "Driver is struggling to attach, giving it ${VOID_GRACE}s to recover"
fi
if (( marker_seen > 0 )) && (( $(date +%s) - marker_seen >= VOID_GRACE )); then
log "Driver never attached within ${VOID_GRACE}s, ending this attempt early"
end_process_tree "$drive_pid" TERM
sleep 5
end_process_tree "$drive_pid" KILL
return 0
fi
sleep 5
done
}
collect_funds_results() {
local log_file="$1"
local line
while IFS= read -r line; do
funds_results+=("$line")
done < <(sed -n 's/.*FUNDS_RESULT|//p' "$log_file" | sort -u)
}
run_test() {
local test_file="$1"
local test_name=$(basename "$test_file" .dart)
local retry_count=0
local void_attempts=0
while (( retry_count <= RETRY_COUNT )); do
log "Running test: $test_name (attempt $((retry_count + 1)))"
clean_data_directories
local start_time=$(date +%s)
local attempt_log
attempt_log=$(mktemp)
"${drive_command[@]}" --target="$test_file" > >(tee "$attempt_log") 2>&1 &
local drive_pid=$!
watch_for_dead_driver "$attempt_log" "$drive_pid" &
local watchdog_pid=$!
disown "$watchdog_pid" 2>/dev/null || true
local attempt_status=0
wait "$drive_pid" 2>/dev/null || attempt_status=$?
kill "$watchdog_pid" 2>/dev/null || true
if (( attempt_status == 0 )) && ! grep -q "All tests passed." "$attempt_log"; then
attempt_status=1
fi
if (( attempt_status == 0 )); then
local duration=$(( $(date +%s) - start_time ))
log "PASS: $test_name ($(format_duration $duration))"
passed_tests+=("$test_name|$duration")
collect_funds_results "$attempt_log"
rm -f "$attempt_log"
return 0
else
local duration=$(( $(date +%s) - start_time ))
local attempt_was_void=N
if grep -qE "$VOID_MARKERS" "$attempt_log"; then
attempt_was_void=Y
fi
collect_funds_results "$attempt_log"
rm -f "$attempt_log"
log "FAIL: $test_name ($(format_duration $duration))"
capture_failure_logcat "$test_name"
if [[ "$attempt_was_void" == "Y" ]]; then
total_void_attempts=$((total_void_attempts + 1))
if (( total_void_attempts > MAX_TOTAL_VOID_ATTEMPTS )); then
error "The flutter driver failed to attach $total_void_attempts times, the emulator or adb is broken"
failed_tests+=("$test_name|$duration|void")
return 1
fi
fi
if [[ "$attempt_was_void" == "Y" ]] && (( void_attempts < MAX_VOID_ATTEMPTS )); then
log "Driver never attached to the app, retrying without spending a retry"
void_attempts=$((void_attempts + 1))
if ! restart_adb; then
error "Could not bring adb back, aborting this suite"
failed_tests+=("$test_name|$duration|void")
return 1
fi
sleep 5
elif (( retry_count < RETRY_COUNT )); then
log "Retrying test: $test_name"
retry_count=$((retry_count + 1))
sleep 5
else
local fail_reason="test"
[[ "$attempt_was_void" == "Y" ]] && fail_reason="void"
failed_tests+=("$test_name|$duration|$fail_reason")
return 1
fi
fi
done
}
write_github_summary() {
if [[ -z "${GITHUB_STEP_SUMMARY:-}" ]]; then
return
fi
{
echo "### Integration tests ($SUITE_DIR, tier: $TEST_TIER)"
echo ""
echo "| Suite | Result | Duration |"
echo "|---|---|---|"
for entry in "${passed_tests[@]+"${passed_tests[@]}"}"; do
echo "| ${entry%%|*} | pass | $(format_duration "${entry##*|}") |"
done
for entry in "${failed_tests[@]+"${failed_tests[@]}"}"; do
local rest="${entry#*|}"
echo "| ${entry%%|*} | ${rest#*|} | $(format_duration "${rest%%|*}") |"
done
} >> "$GITHUB_STEP_SUMMARY"
}
write_summary_file() {
if [[ -z "$SUMMARY_FILE" ]]; then
return
fi
{
echo "tier=$TEST_TIER"
echo "total=${#targets[@]}"
echo "passed=${#passed_tests[@]}"
echo "failed=${#failed_tests[@]}"
echo "duration=$1"
for entry in "${passed_tests[@]+"${passed_tests[@]}"}"; do
echo "pass=$entry"
done
for entry in "${failed_tests[@]+"${failed_tests[@]}"}"; do
echo "fail=$entry"
done
for entry in "${funds_results[@]+"${funds_results[@]}"}"; do
echo "chain=$entry"
done
} > "$SUMMARY_FILE"
}
main() {
log "Starting integration test runner"
local search_dir="$SUITE_DIR"
if [[ "$TEST_TIER" != "all" ]]; then
search_dir="$SUITE_DIR/$TEST_TIER"
fi
if [[ -f "$SUITE_DIR" ]]; then
search_dir="$SUITE_DIR"
elif [[ ! -d "$search_dir" ]]; then
error "Suite directory not found: $search_dir"
exit 1
fi
resolve_platform
resolve_android_app_id
build_drive_command
log "Configuration: SUITE_DIR=$search_dir PLATFORM=$PLATFORM RETRY_COUNT=$RETRY_COUNT"
if [[ -f "$search_dir" ]]; then
targets+=("$search_dir")
else
while IFS= read -r -d $'\0' file; do
targets+=("$file")
done < <(find "$search_dir" -name "*_test.dart" -type f -print0 | sort -z)
fi
if (( ${#targets[@]} == 0 )); then
error "No test files found in $search_dir"
exit 1
fi
log "Found ${#targets[@]} test files"
local overall_start_time=$(date +%s)
for target in "${targets[@]}"; do
run_test "$target" || true
done
local total_duration=$(( $(date +%s) - overall_start_time ))
echo -e "\n===== Test Summary ====="
echo "Total tests: ${#targets[@]}"
echo "Passed: ${#passed_tests[@]}"
echo "Failed: ${#failed_tests[@]}"
echo "Total duration: $(format_duration $total_duration)"
if (( ${#passed_tests[@]} > 0 )); then
echo -e "\nPassed Tests:"
for entry in "${passed_tests[@]}"; do
echo " - ${entry%%|*} ($(format_duration "${entry##*|}"))"
done
fi
write_github_summary
write_summary_file "$total_duration"
if (( ${#failed_tests[@]} > 0 )); then
echo -e "\nFailed Tests:"
local rest
for entry in "${failed_tests[@]}"; do
rest="${entry#*|}"
echo " - ${entry%%|*} ($(format_duration "${rest%%|*}"), ${rest#*|})"
done
exit 1
fi
echo -e "\nAll tests passed successfully!"
}
main "$@"