1- # Builds the app and runs the unit + UI test suite on every push / PR,
2- # then publishes a test report and code-coverage summary.
1+ # Builds the app once, then runs UNIT tests and UI tests as two separate steps.
2+ # UI tests run on every device in UI_SIMULATORS (one per line); any device not
3+ # installed on the runner is skipped with a warning. A combined report (MD / HTML /
4+ # best-effort PDF) plus the raw .xcresult bundles are attached as artifacts.
35#
46# Runs on a SELF-HOSTED runner (label: self-hosted) that already has Xcode,
57# xcbeautify, CocoaPods and the iOS simulators installed.
68#
7- # Knobs (env: block below):
8- # SIMULATOR - Simulator device name that exists on the runner.
9+ # Knobs - env: block below:
10+ # UNIT_SIMULATOR - one device to run the unit tests on
11+ # UI_SIMULATORS - newline-separated device list for the UI tests
912#
10- # Optional repository secrets (Settings > Secrets and variables > Actions):
11- # KARTAVIEW_ACCESS_TOKEN, JAWG_ACCESS_TOKEN
12- # Written to Secrets.xcconfig before the build *only if that file does not
13- # already exist on the runner*. If neither the file nor the secrets are
14- # present the build still succeeds (Config.xcconfig uses `#include?`), but
15- # map / Kartaview code gets empty tokens.
16- #
17- # Reports produced (uploaded as the `test-results` artifact + posted to the run summary):
18- # test-report.md / .html - always
19- # test-report.pdf - only if the runner has an HTML->PDF converter on PATH
20- # (Google Chrome, pandoc, wkhtmltopdf, or weasyprint)
21- # TestResults.xcresult, junit.xml, xcodebuild.log
13+ # Optional repo secrets (Settings > Secrets and variables > Actions):
14+ # KARTAVIEW_ACCESS_TOKEN, JAWG_ACCESS_TOKEN -> written to Secrets.xcconfig if it
15+ # is missing on the runner; the build tolerates their absence (#include?).
2216
2317name : iOS Tests
2418
@@ -37,15 +31,29 @@ permissions:
3731 pull-requests : write # ...and annotates the PR
3832
3933env :
40- SIMULATOR : " iPhone 16 Pro"
4134 WORKSPACE : GoInfoGame/GoInfoGame.xcworkspace
42- SCHEME : GoInfoGame # default test plan (GoInfoGame.xctestplan) = GoInfoGameTests + GoInfoGameUITests
35+ SCHEME : GoInfoGame
36+ UNIT_SIMULATOR : " iPhone 16 Pro"
37+ # One device name per line. Unknown names are skipped (warning), so this list
38+ # can be aspirational. Add every simulator you care about here.
39+ UI_SIMULATORS : |
40+ iPhone SE (3rd generation)
41+ iPhone 16
42+ iPhone 16 Plus
43+ iPhone 16 Pro
44+ iPhone 16 Pro Max
45+ iPad (10th generation)
46+ iPad mini (6th generation)
47+ iPad Air 11-inch (M2)
48+ iPad Air 13-inch (M2)
49+ iPad Pro 11-inch (M4)
50+ iPad Pro 13-inch (M4)
4351
4452jobs :
4553 test :
4654 name : Unit + UI tests
4755 runs-on : self-hosted
48- timeout-minutes : 75
56+ timeout-minutes : 150
4957
5058 steps :
5159 - name : Checkout (with submodules)
5866 set -euo pipefail
5967 xcodebuild -version
6068 pod --version
61- command -v xcbeautify >/dev/null || { echo "::error::xcbeautify not on PATH - install it on the runner (brew install xcbeautify)"; exit 1; }
69+ command -v xcbeautify >/dev/null || { echo "::error::xcbeautify not on PATH (brew install xcbeautify)"; exit 1; }
6270 xcbeautify --version
6371
6472 - name : Cache CocoaPods
@@ -83,12 +91,10 @@ jobs:
8391 set -euo pipefail
8492 dest="GoInfoGame/GoInfoGame/Resources/Secrets.xcconfig"
8593 if [ -f "$dest" ]; then
86- echo "$dest already present on runner - leaving it untouched."
94+ echo "$dest already present - leaving it untouched."
8795 elif [ -n "${KARTAVIEW_ACCESS_TOKEN:-}" ] || [ -n "${JAWG_ACCESS_TOKEN:-}" ]; then
88- {
89- echo "KARTAVIEW_ACCESS_TOKEN = ${KARTAVIEW_ACCESS_TOKEN:-}"
90- echo "JAWG_ACCESS_TOKEN = ${JAWG_ACCESS_TOKEN:-}"
91- } > "$dest"
96+ printf 'KARTAVIEW_ACCESS_TOKEN = %s\nJAWG_ACCESS_TOKEN = %s\n' \
97+ "${KARTAVIEW_ACCESS_TOKEN:-}" "${JAWG_ACCESS_TOKEN:-}" > "$dest"
9298 echo "Wrote $dest from repository secrets."
9399 else
94100 echo "No $dest and no token secrets - Config.xcconfig #include? tolerates this."
@@ -101,96 +107,151 @@ jobs:
101107 LC_ALL : en_US.UTF-8
102108 run : pod install
103109
104- - name : Pick & boot simulator
105- run : |
106- set -euo pipefail
107- xcrun simctl shutdown all || true
108- UDID=$(xcrun simctl list devices available | grep -m1 "$SIMULATOR (" | grep -oE '[0-9A-F-]{36}' || true)
109- if [ -z "$UDID" ]; then
110- echo "::error::Simulator '$SIMULATOR' not available on this runner. iPhones present:"
111- xcrun simctl list devices available | grep -i iphone || true
112- exit 1
113- fi
114- echo "SIM_UDID=$UDID" >> "$GITHUB_ENV"
115- xcrun simctl boot "$UDID" || true
116- xcrun simctl bootstatus "$UDID" -b
117-
118110 - name : Resolve Swift packages
119111 run : xcodebuild -resolvePackageDependencies -workspace "$WORKSPACE" -scheme "$SCHEME"
120112
121- - name : Run tests
113+ # -------- build once, then reuse for every test run --------
114+ - name : Build for testing
122115 run : |
123116 set -o pipefail
124- mkdir -p build/reports
125- NSUnbufferedIO=YES xcodebuild test \
126- -workspace "$WORKSPACE" \
127- -scheme "$SCHEME" \
128- -destination "platform=iOS Simulator,id=${SIM_UDID} " \
129- -resultBundlePath "build/TestResults.xcresult " \
117+ rm -rf build/reports build/results build/*.log
118+ mkdir -p build/reports build/results
119+ xcrun simctl shutdown all || true
120+ NSUnbufferedIO=YES xcodebuild build-for-testing \
121+ -workspace "$WORKSPACE" -scheme "$SCHEME " \
122+ -destination "generic/platform=iOS Simulator " \
130123 -enableCodeCoverage YES \
131- -retry-tests-on-failure \
132- -test-timeouts-enabled YES \
133124 ENABLE_USER_SCRIPT_SANDBOXING=NO \
134125 CODE_SIGNING_ALLOWED=NO \
135- | tee build/xcodebuild.log \
136- | xcbeautify --report junit --report-path build/reports
126+ | tee build/build.log \
127+ | xcbeautify
128+
129+ # -------- STEP 1: unit tests --------
130+ - name : Unit tests
131+ run : |
132+ set +e # keep the step alive so cleanup/report always run
133+ set -o pipefail
134+ udid=$(xcrun simctl list devices available | grep -F "$UNIT_SIMULATOR (" | grep -oE '[0-9A-F-]{36}' | head -1)
135+ if [ -z "$udid" ]; then
136+ echo "::error::unit simulator '$UNIT_SIMULATOR' not installed"
137+ xcrun simctl list devices available | grep -i iphone
138+ exit 1
139+ fi
140+ xcrun simctl boot "$udid"; xcrun simctl bootstatus "$udid" -b
141+ mkdir -p build/reports/unit
142+ NSUnbufferedIO=YES xcodebuild test-without-building \
143+ -workspace "$WORKSPACE" -scheme "$SCHEME" \
144+ -destination "platform=iOS Simulator,id=$udid" \
145+ -only-testing:GoInfoGameTests \
146+ -resultBundlePath build/results/unit.xcresult \
147+ -enableCodeCoverage YES -retry-tests-on-failure -test-timeouts-enabled YES \
148+ CODE_SIGNING_ALLOWED=NO \
149+ | tee build/unit.log \
150+ | xcbeautify --report junit --report-path build/reports/unit
151+ rc=${PIPESTATUS[0]}
152+ [ -f build/reports/unit/junit.xml ] && mv build/reports/unit/junit.xml build/reports/unit.xml
153+ rmdir build/reports/unit 2>/dev/null
154+ xcrun simctl shutdown "$udid"
155+ echo "unit tests exit code: $rc"
156+ exit "$rc"
157+
158+ # -------- STEP 2: UI tests, once per device in UI_SIMULATORS --------
159+ - name : UI tests (all devices)
160+ if : always() # still run UI tests even if a unit test failed
161+ run : |
162+ set +e # a failing device must NOT abort the loop
163+ set -o pipefail
164+ failed=""; ran=0
165+ while IFS= read -r device; do
166+ device="$(printf '%s' "$device" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
167+ [ -z "$device" ] && continue
168+ udid=$(xcrun simctl list devices available | grep -F "$device (" | grep -oE '[0-9A-F-]{36}' | head -1)
169+ if [ -z "$udid" ]; then
170+ echo "::warning::simulator '$device' not installed on this runner - skipping"
171+ continue
172+ fi
173+ slug=$(echo "$device" | tr -cs '[:alnum:]' '-' | sed 's/^-*//; s/-*$//')
174+ echo "::group::UI tests · $device ($udid)"
175+ xcrun simctl boot "$udid"; xcrun simctl bootstatus "$udid" -b
176+ mkdir -p "build/reports/ui-$slug"
177+ NSUnbufferedIO=YES xcodebuild test-without-building \
178+ -workspace "$WORKSPACE" -scheme "$SCHEME" \
179+ -destination "platform=iOS Simulator,id=$udid" \
180+ -only-testing:GoInfoGameUITests \
181+ -resultBundlePath "build/results/ui-$slug.xcresult" \
182+ -retry-tests-on-failure -test-timeouts-enabled YES \
183+ CODE_SIGNING_ALLOWED=NO \
184+ | tee "build/ui-$slug.log" \
185+ | xcbeautify --report junit --report-path "build/reports/ui-$slug"
186+ rc=${PIPESTATUS[0]}
187+ [ -f "build/reports/ui-$slug/junit.xml" ] && mv "build/reports/ui-$slug/junit.xml" "build/reports/ui-$slug.xml"
188+ rmdir "build/reports/ui-$slug" 2>/dev/null
189+ xcrun simctl shutdown "$udid"
190+ ran=$((ran + 1))
191+ [ "$rc" -ne 0 ] && failed="${failed}"$'\n'" - ${device} (exit $rc)"
192+ echo "::endgroup::"
193+ done <<< "$UI_SIMULATORS"
194+ echo "Ran UI tests on $ran device(s)."
195+ if [ "$ran" -eq 0 ]; then echo "::error::no UI simulators from the list are installed - nothing ran"; exit 1; fi
196+ if [ -n "$failed" ]; then printf '::error::UI tests failed on:%s\n' "$failed"; exit 1; fi
197+ echo "All UI device runs passed."
137198
138- - name : Publish test report (checks/PR annotations)
199+ # -------- reporting --------
200+ - name : Publish test report (checks / PR annotations)
139201 uses : mikepenz/action-junit-report@v5
140202 if : always()
141203 with :
142- report_paths : build/reports/junit .xml
204+ report_paths : build/reports/* .xml
143205 check_name : iOS test results
144206 include_passed : true
145207 require_tests : true
146208
147209 - name : Ensure a PDF engine (weasyprint)
148210 if : always()
149- continue-on-error : true # PDF is optional; MD/HTML are produced regardless
211+ continue-on-error : true
150212 run : command -v weasyprint >/dev/null || brew install --quiet weasyprint
151213
152- - name : Generate test report (MD / HTML / PDF)
214+ - name : Generate combined report (MD / HTML / PDF)
153215 if : always()
154216 continue-on-error : true
155217 env :
156218 GIT_COMMIT_MESSAGE : ${{ github.event.head_commit.message }}
157219 run : |
158220 python3 scripts/ci/test_report.py \
159- --junit build/reports/junit .xml \
160- --xcresult build/TestResults .xcresult \
221+ --junit build/reports/unit.xml build/reports/ui-* .xml \
222+ --xcresult build/results/unit .xcresult \
161223 --out-dir build/report
162224
163- - name : Zip the .xcresult bundle
225+ - name : Zip .xcresult bundles
164226 if : always()
165227 continue-on-error : true
166228 run : |
167- if [ -d build/TestResults.xcresult ]; then
168- ditto -c -k --sequesterRsrc --keepParent \
169- build/TestResults.xcresult build/TestResults.xcresult.zip
229+ if [ -d build/results ]; then
230+ ditto -c -k --sequesterRsrc --keepParent build/results build/results.zip
170231 fi
171232
172233 - name : Upload report + logs
173234 uses : actions/upload-artifact@v4
174235 if : always()
175- continue-on-error : true # a runner<->GitHub network blip here must not fail the job
236+ continue-on-error : true
176237 with :
177238 name : test-report
178239 path : |
179240 build/report/test-report.md
180241 build/report/test-report.html
181242 build/report/test-report.pdf
182- build/reports/junit .xml
183- build/xcodebuild .log
243+ build/reports/* .xml
244+ build/* .log
184245 retention-days : 14
185246 if-no-files-found : warn
186247
187- - name : Upload .xcresult bundle
248+ - name : Upload .xcresult bundles
188249 uses : actions/upload-artifact@v4
189250 if : always()
190- continue-on-error : true # large/optional - keep it from blocking the run
251+ continue-on-error : true
191252 with :
192- name : xcresult
193- path : build/TestResults.xcresult .zip
253+ name : xcresults
254+ path : build/results .zip
194255 retention-days : 7
195256 if-no-files-found : ignore
196257
0 commit comments