Skip to content

Commit 4185c60

Browse files
authored
test: rerun only failed test on action (#6831)
1 parent 639ace2 commit 4185c60

File tree

3 files changed

+213
-92
lines changed

3 files changed

+213
-92
lines changed

.github/scripts/run-maestro.sh

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
PLATFORM="${1:-${PLATFORM:-android}}"
5+
SHARD="${2:-${SHARD:-default}}"
6+
FLOWS_DIR=".maestro/tests"
7+
MAIN_REPORT="maestro-report.xml"
8+
MAX_RERUN_ROUNDS="${MAX_RERUN_ROUNDS:-2}"
9+
RERUN_REPORT_PREFIX="maestro-rerun"
10+
export MAESTRO_DRIVER_STARTUP_TIMEOUT="${MAESTRO_DRIVER_STARTUP_TIMEOUT:-120000}"
11+
12+
if ! command -v maestro >/dev/null 2>&1; then
13+
echo "ERROR: maestro not found in PATH"
14+
exit 2
15+
fi
16+
17+
if [ "$PLATFORM" = "android" ]; then
18+
if ! command -v adb >/dev/null 2>&1; then
19+
echo "ERROR: adb not found"
20+
exit 2
21+
fi
22+
else
23+
if ! command -v xcrun >/dev/null 2>&1; then
24+
echo "ERROR: xcrun not found"
25+
exit 2
26+
fi
27+
fi
28+
29+
MAPFILE="$(mktemp)"
30+
trap 'rm -f "$MAPFILE"' EXIT
31+
32+
while IFS= read -r -d '' file; do
33+
if grep -qE "^[[:space:]]*-[[:space:]]*['\"]?test-${SHARD}['\"]?([[:space:]]*$|[[:space:]]*,|[[:space:]]*\\])" "$file"; then
34+
raw_name="$(grep -m1 -E '^[[:space:]]*name:' "$file" || true)"
35+
if [ -n "$raw_name" ]; then
36+
name_val="$(echo "$raw_name" | sed -E 's/^[[:space:]]*name:[[:space:]]*//; s/^["'\'']//; s/["'\'']$//; s/[[:space:]]*$//')"
37+
name_val="$(echo "$name_val" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
38+
if [ -n "$name_val" ]; then
39+
printf '%s\t%s\n' "$name_val" "$file" >> "$MAPFILE"
40+
fi
41+
fi
42+
fi
43+
done < <(find "$FLOWS_DIR" -type f \( -iname '*.yml' -o -iname '*.yaml' \) -print0)
44+
45+
if [ ! -s "$MAPFILE" ]; then
46+
echo "No flows for test-${SHARD}"
47+
exit 1
48+
fi
49+
50+
echo "Mapped flows for tag test-${SHARD}:"
51+
awk -F'\t' '{ printf " %s -> %s\n", $1, $2 }' "$MAPFILE"
52+
53+
FLOW_FILES=()
54+
declare -A SEEN
55+
while IFS=$'\t' read -r name path; do
56+
if [ -z "${SEEN[$path]:-}" ]; then
57+
FLOW_FILES+=("$path")
58+
SEEN[$path]=1
59+
fi
60+
done < "$MAPFILE"
61+
62+
echo "Main run will execute:"
63+
printf ' %s\n' "${FLOW_FILES[@]}"
64+
65+
if [ "$PLATFORM" = "android" ]; then
66+
adb shell settings put system show_touches 1 || true
67+
adb install -r "app-experimental-release.apk" || true
68+
adb shell monkey -p "chat.rocket.reactnative" -c android.intent.category.LAUNCHER 1 || true
69+
sleep 6
70+
adb shell am force-stop "chat.rocket.reactnative" || true
71+
72+
maestro test "${FLOW_FILES[@]}" \
73+
--exclude-tags=util \
74+
--include-tags="test-${SHARD}" \
75+
--format junit \
76+
--output "$MAIN_REPORT" || true
77+
78+
else
79+
maestro test "${FLOW_FILES[@]}" \
80+
--exclude-tags=util \
81+
--include-tags="test-${SHARD}" \
82+
--exclude-tags=android-only \
83+
--format junit \
84+
--output "$MAIN_REPORT" || true
85+
fi
86+
87+
if [ ! -f "$MAIN_REPORT" ]; then
88+
echo "Main report not found"
89+
exit 1
90+
fi
91+
92+
FAILED_NAMES="$(python3 - <<PY
93+
import sys,xml.etree.ElementTree as ET
94+
try:
95+
tree = ET.parse("$MAIN_REPORT")
96+
except:
97+
sys.exit(0)
98+
root = tree.getroot()
99+
failed=[]
100+
for tc in root.findall(".//testcase"):
101+
if tc.find("failure") is not None or tc.find("error") is not None:
102+
if tc.get("name"):
103+
failed.append(tc.get("name").strip())
104+
for n in sorted(set(failed)):
105+
print(n)
106+
PY
107+
)"
108+
109+
if [ -z "$FAILED_NAMES" ]; then
110+
echo "All tests passed."
111+
exit 0
112+
fi
113+
114+
IFS=$'\n' read -rd '' -a FAILED_ARRAY <<<"$FAILED_NAMES" || true
115+
116+
CANDIDATE_FILES=()
117+
SEEN2=""
118+
for NAME in "${FAILED_ARRAY[@]}"; do
119+
FILE="$(awk -F'\t' -v n="$NAME" '$1==n {print $2; exit}' "$MAPFILE" || true)"
120+
if [ -n "$FILE" ] && ! printf '%s\n' "$SEEN2" | grep -Fq "$FILE"; then
121+
CANDIDATE_FILES+=("$FILE")
122+
SEEN2="${SEEN2}"$'\n'"${FILE}"
123+
fi
124+
done
125+
126+
if [ ${#CANDIDATE_FILES[@]} -eq 0 ]; then
127+
echo "No flow files to retry"
128+
exit 1
129+
fi
130+
131+
CURRENT_FAILS=("${CANDIDATE_FILES[@]}")
132+
ROUND=1
133+
134+
while [ ${#CURRENT_FAILS[@]} -gt 0 ] && [ "$ROUND" -le "$MAX_RERUN_ROUNDS" ]; do
135+
echo "=== RERUN ROUND $ROUND (${#CURRENT_FAILS[@]} flows) ==="
136+
137+
RPT="${RERUN_REPORT_PREFIX}-round-${ROUND}.xml"
138+
139+
if [ "$PLATFORM" = "android" ]; then
140+
maestro test "${CURRENT_FAILS[@]}" \
141+
--exclude-tags=util \
142+
--include-tags="test-${SHARD}" \
143+
--format junit \
144+
--output "$RPT" || true
145+
else
146+
maestro test "${CURRENT_FAILS[@]}" \
147+
--exclude-tags=util \
148+
--include-tags="test-${SHARD}" \
149+
--exclude-tags=android-only \
150+
--format junit \
151+
--output "$RPT" || true
152+
fi
153+
154+
if [ ! -f "$RPT" ]; then
155+
echo "Rerun report missing"
156+
break
157+
fi
158+
159+
NEXT_FAILED="$(python3 - <<PY
160+
import sys,xml.etree.ElementTree as ET
161+
try:
162+
tree = ET.parse("$RPT")
163+
except:
164+
sys.exit(0)
165+
root = tree.getroot()
166+
failed=[]
167+
for tc in root.findall(".//testcase"):
168+
if tc.find("failure") is not None or tc.find("error") is not None:
169+
if tc.get("name"):
170+
failed.append(tc.get("name").strip())
171+
for n in sorted(set(failed)):
172+
print(n)
173+
PY
174+
)"
175+
176+
if [ -z "$NEXT_FAILED" ]; then
177+
echo "All retried flows passed in this round."
178+
exit 0
179+
fi
180+
181+
IFS=$'\n' read -rd '' -a NEXT_FAILED_ARRAY <<<"$NEXT_FAILED" || true
182+
183+
NEXT_FILES=()
184+
SEEN3=""
185+
for NAME in "${NEXT_FAILED_ARRAY[@]}"; do
186+
FILE="$(awk -F'\t' -v n="$NAME" '$1==n {print $2; exit}' "$MAPFILE" || true)"
187+
if [ -n "$FILE" ] && ! printf '%s\n' "$SEEN3" | grep -Fq "$FILE"; then
188+
NEXT_FILES+=("$FILE")
189+
SEEN3="${SEEN3}"$'\n'"${FILE}"
190+
fi
191+
done
192+
193+
CURRENT_FAILS=("${NEXT_FILES[@]}")
194+
ROUND=$((ROUND+1))
195+
done
196+
197+
echo "Retry strategy finished with remaining failures:"
198+
printf '%s\n' "${CURRENT_FAILS[@]}"
199+
exit 1

.github/workflows/maestro-android.yml

Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -43,63 +43,12 @@ jobs:
4343
sudo udevadm control --reload-rules
4444
sudo udevadm trigger --name-match=kvm
4545
46-
- name: Create Maestro script
47-
run: |
48-
cat << 'EOF' > run-maestro.sh
49-
#!/bin/bash
50-
SHARD=${{ inputs.shard }}
51-
echo "Running shard: $SHARD"
52-
53-
adb shell settings put system show_touches 1
54-
adb install app-experimental-release.apk
55-
adb shell monkey -p chat.rocket.reactnative -c android.intent.category.LAUNCHER 1
56-
sleep 10
57-
adb shell am force-stop chat.rocket.reactnative
58-
export MAESTRO_DRIVER_STARTUP_TIMEOUT=120000
59-
60-
MAX_RETRIES=3
61-
ATTEMPT=1
62-
FINAL_EXIT_CODE=1
63-
64-
while [ $ATTEMPT -le $MAX_RETRIES ]; do
65-
echo "Attempt $ATTEMPT of $MAX_RETRIES"
66-
67-
echo "Starting screen recording..."
68-
adb shell screenrecord /sdcard/test_run.mp4 > /dev/null 2>&1 &
69-
RECORD_PID=$!
70-
71-
maestro test .maestro --exclude-tags=util --include-tags=test-$SHARD --format junit --output maestro-report.xml
72-
TEST_EXIT_CODE=$?
73-
74-
echo "Stopping screen recording..."
75-
kill -INT $RECORD_PID || true
76-
sleep 2
77-
78-
echo "Pulling video from device..."
79-
adb pull /sdcard/test_run.mp4 test_run_${SHARD}_attempt_${ATTEMPT}.mp4 || true
80-
adb shell rm /sdcard/test_run.mp4 || true
81-
82-
if [ $TEST_EXIT_CODE -eq 0 ]; then
83-
echo "Maestro passed on attempt $ATTEMPT"
84-
FINAL_EXIT_CODE=0
85-
break
86-
else
87-
echo "Maestro failed on attempt $ATTEMPT"
88-
fi
89-
90-
ATTEMPT=$((ATTEMPT+1))
91-
done
92-
93-
exit $FINAL_EXIT_CODE
94-
EOF
95-
96-
chmod +x run-maestro.sh
97-
env:
98-
SHARD: ${{ inputs.shard }}
46+
- name: Make Maestro script executable
47+
run: chmod +x .github/scripts/run-maestro.sh
9948

10049
- name: Start Android Emulator and Run Maestro Tests
10150
uses: reactivecircus/android-emulator-runner@v2
102-
timeout-minutes: 60
51+
timeout-minutes: 120
10352
with:
10453
api-level: 34
10554
disk-size: 4096M
@@ -111,20 +60,12 @@ jobs:
11160
force-avd-creation: false
11261
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
11362
disable-animations: true
114-
script: ./run-maestro.sh
63+
script: ./.github/scripts/run-maestro.sh android ${{ inputs.shard }}
11564

116-
- name: Upload Test Report
117-
if: always()
118-
uses: actions/upload-artifact@v4
119-
with:
120-
name: Android Test Report - Shard ${{ inputs.shard }}
121-
path: maestro-report.xml
122-
retention-days: 7
123-
124-
- name: Upload Screen Recording
65+
- name: Android Maestro Logs
12566
if: always()
12667
uses: actions/upload-artifact@v4
12768
with:
128-
name: maestro-video-${{ inputs.shard }}
129-
path: test_run_${{ inputs.shard }}_attempt_*.mp4
69+
name: Android Maestro Logs - Shard ${{ inputs.shard }}
70+
path: ~/.maestro/tests/**/*.png
13071
retention-days: 7

.github/workflows/maestro-ios.yml

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,40 +84,21 @@ jobs:
8484
echo "UDID=$UDID"
8585
echo "UDID=$UDID" >> $GITHUB_ENV
8686
87+
- name: Make Maestro script executable
88+
run: chmod +x .github/scripts/run-maestro.sh
89+
8790
- name: Install App
8891
run: |
8992
xcrun simctl install $UDID "ios-simulator-app"
9093
9194
- name: Run Tests
92-
id: maestro-tests
93-
uses: nick-fields/retry@v3
94-
with:
95-
max_attempts: 3
96-
timeout_minutes: 60
97-
command: |
98-
SHARD=${{ inputs.shard }}
99-
echo "Running shard: $SHARD"
100-
export MAESTRO_DRIVER_STARTUP_TIMEOUT=120000
101-
102-
maestro test .maestro \
103-
--exclude-tags=util \
104-
--include-tags=test-$SHARD \
105-
--exclude-tags=android-only \
106-
--format junit \
107-
--output maestro-report.xml
108-
109-
- name: Maestro Test Report
110-
if: always()
111-
uses: actions/upload-artifact@v4
112-
with:
113-
name: Test Report - Shard ${{ inputs.shard }}
114-
path: maestro-report.xml
115-
retention-days: 28
95+
timeout-minutes: 120
96+
run: ./.github/scripts/run-maestro.sh ios ${{ inputs.shard }}
11697

11798
- name: iOS Maestro Logs
11899
if: always()
119100
uses: actions/upload-artifact@v4
120101
with:
121102
name: iOS Maestro Logs - Shard ${{ inputs.shard }}
122-
path: ~/.maestro/tests/
123-
retention-days: 28
103+
path: ~/.maestro/tests/**/*.png
104+
retention-days: 7

0 commit comments

Comments
 (0)