-
Notifications
You must be signed in to change notification settings - Fork 0
121 lines (119 loc) · 5.79 KB
/
Copy pathintegration.yaml
File metadata and controls
121 lines (119 loc) · 5.79 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
name: Integration
on:
push:
branches: [master]
tags: ['v*']
workflow_dispatch:
jobs:
integration:
runs-on: macos-15
# 60 min funds ~1 cold hang cycle (900s bound + recycle) plus bounded retries per file — not the absolute 3-attempt x 3-file worst case (~90 min); deliberate truncation.
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
working-directory: example
- name: Boot iOS simulator
run: |
xcrun simctl boot 'iPhone 16' || true
xcrun simctl bootstatus 'iPhone 16'
# Run each integration file as its own `flutter test` invocation. The
# directory form (`flutter test integration_test/`) pipelines every file
# through a single simulator session; the per-app teardown then races the
# next launch and the run intermittently reports a spurious non-zero exit
# ("N-1 tests passed" + exit 1) or hangs. One invocation per file gives
# each test a clean app lifecycle.
#
# Even per-file, `flutter test` can fail in teardown with the test itself
# green: the device-kill shutdown hook deletes the `flutter_tools.XXX`
# temp tree, then host finalization throws `PathNotFoundException` deleting
# the now-gone `flutter_test_listener` child, which corrupts the reporter
# (surfaces as "No tests were found" / exit 79). The on-device result is
# always printed first — a passing test prints "✅ <name>", a failing one
# "❌". So a file passes when the device reported ✅ with no failure marker,
# regardless of the host exit code; retries cover genuinely transient
# build/boot flakes.
- name: Run integration tests
working-directory: example
run: |
set -o pipefail
# A wedged simulator hangs `flutter test` after "Xcode build done."
# (device handshake never completes); unbounded, one hang consumed
# the whole job timeout and the per-file retries below never ran.
# Bound every invocation and recycle the simulator on timeout so a
# retry starts from a fresh boot. 900s clears the slowest observed
# legit cold run (677s) with margin — 480 sat inside the cold-run
# range and killed passing attempts. 124/137 = gtimeout TERM/KILL,
# 142 = perl fallback.
TEST_BOUND=900
TIMEOUT_BIN="$(command -v gtimeout || command -v timeout || true)"
echo "per-invocation bound: ${TIMEOUT_BIN:-perl group supervisor}"
bounded() {
local secs="$1"; shift
if [ -n "$TIMEOUT_BIN" ]; then
"$TIMEOUT_BIN" --kill-after=30 "$secs" "$@"
else
# No coreutils on the runner image (observed: the plain "$@"
# fallback here silently ran unbounded and a wedge ate the whole
# job cap again). A bare alarm+exec bounds only one process; an
# inheritStdio descendant survives, holds the tee pipe, and
# stalls the pipeline to the job cap. Fork the command into its
# own process group and TERM-then-KILL the group on timeout
# (rc 142) so pipe holders and orphaned xcodebuild die too.
perl -e 'my $t = shift; my $pid = fork // die "fork: $!"; if (!$pid) { setpgrp(0, 0); exec @ARGV or die "exec failed: $!" } $SIG{ALRM} = sub { kill "TERM", -$pid; sleep 30; kill "KILL", -$pid; exit 142 }; alarm $t; waitpid $pid, 0; exit(($? & 127) ? 128 + ($? & 127) : $? >> 8);' "$secs" "$@"
fi
}
recycle_sim() {
xcrun simctl shutdown 'iPhone 16' || true
sleep 5
# -b boots if needed; bounded so a wedged reboot costs 120s, not
# the job cap (bare bootstatus blocks forever on a failed boot).
bounded 120 xcrun simctl bootstatus 'iPhone 16' -b \
|| echo "::warning::simulator failed to reboot after recycle"
}
run_one() {
local test="$1" out rc
out="$(mktemp)"
rc=0
bounded "$TEST_BOUND" flutter test "$test" -d 'iPhone 16' 2>&1 | tee "$out" || rc=$?
if [ "$rc" -eq 0 ]; then
return 0
fi
# Salvage before timeout classification: the on-device verdict is
# printed before host teardown, so ✅ with no failure marker is a
# pass even when rc is a timeout code — a pass-then-teardown-hang
# is a pass, not a hang.
if grep -q '✅' "$out" && ! grep -q '❌' "$out" \
&& ! grep -qiE 'some tests failed' "$out"; then
echo "::warning::$test passed on-device (rc=$rc); ignoring host-side failure"
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ] || [ "$rc" -eq 142 ]; then
recycle_sim # the killed tree may have wedged the simulator; next file starts fresh
fi
return 0
fi
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ] || [ "$rc" -eq 142 ]; then
echo "::warning::$test hung past ${TEST_BOUND}s (rc=$rc); recycling the simulator"
recycle_sim
return 1
fi
return 1
}
for test in integration_test/*_test.dart; do
echo "::group::$test"
for attempt in 1 2 3; do
if run_one "$test"; then
echo "::endgroup::"
break
elif [ "$attempt" -eq 3 ]; then
echo "::error::$test failed after 3 attempts"
echo "::endgroup::"
exit 1
else
echo "::warning::$test attempt $attempt failed; retrying"
fi
done
done