Repository navigation
338 lines (308 loc) · 14.1 KB
/
Copy pathunit_tests.yml
File metadata and controls
338 lines (308 loc) · 14.1 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
name: zzz [DO NOT RUN] Automated unit tests
on:
pull_request:
branches:
- dev
types: [opened, synchronize]
paths-ignore:
- '**.md'
- '**/README'
- '**.yml'
- '**.txt'
push:
branches:
- dev
paths-ignore:
- '**.md'
- '**/README'
- '**.yml'
- '**.txt'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
algorithm-package:
name: Algorithm Package (swift test)
runs-on: macos-26
if: github.repository_owner == 'nightscout'
steps:
- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Restore cache
id: spm-cache-restore
uses: actions/cache/restore@v5
with:
path: AlgorithmPackage/.build
key: ${{ runner.os }}-spm-algorithm-${{ hashFiles('AlgorithmPackage/Package.swift', 'AlgorithmPackage/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-algorithm-
- name: Show Swift version
run: swift --version
- name: Run algorithm tests
run: time swift test --package-path AlgorithmPackage
- name: Save cache
# PRs read the dev cache but never write it — avoids churning the
# 10 GB per-repo cache limit. dev pushes refresh the shared baseline.
if: always() && github.event_name == 'push' && steps.spm-cache-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: AlgorithmPackage/.build
key: ${{ runner.os }}-spm-algorithm-${{ hashFiles('AlgorithmPackage/Package.swift', 'AlgorithmPackage/Package.resolved') }}
- name: Annotate algorithm package results
if: always()
run: |
if [ "${{ job.status }}" = "success" ]; then
echo "::notice title=Algorithm Package::✅ swift test passed"
echo "✅ Algorithm package builds standalone on macOS and all tests passed" >> $GITHUB_STEP_SUMMARY
else
echo "::error title=Algorithm Package::❌ swift test failed"
{
echo "## ❌ Algorithm package (\`swift test\`) failed"
echo ""
echo "\`AlgorithmPackage/Package.swift\` enumerates its sources by hand, so it"
echo "drifts when a listed file gains a dependency on a file that is not listed."
echo ""
echo "If the error is \`cannot find 'X' in scope\`, add the file declaring \`X\`"
echo "to \`algorithmModels\` or \`algorithmHelpers\` in that manifest."
} >> $GITHUB_STEP_SUMMARY
fi
shell: bash
test:
name: Run Unit Tests
runs-on: macos-26
if: github.repository_owner == 'nightscout'
env:
DESTINATION: "platform=iOS Simulator,name=iPhone 17,OS=26.2"
# Default DerivedData location — deliberately OUTSIDE the repo working
# tree. The Swiftformat build phase formats "${SRCROOT}", so anything
# placed inside the checkout (as an earlier revision did with
# -clonedSourcePackagesDirPath/-derivedDataPath) gets fed to SwiftFormat
# and it crawls the whole dependency tree. Keeping build state out of
# SRCROOT restores the original behaviour. The path is stable across runs
# (derived from the workspace path + name) so it caches cleanly.
DERIVED_DATA: "~/Library/Developer/Xcode/DerivedData"
steps:
- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 1
submodules: recursive
# Fingerprint dependency state (independent of app source) for the cache
# key, so the restore hits on nearly every PR — app code churns, pins
# rarely do.
- name: Capture submodule state for cache key
run: git submodule status --recursive | awk '{print $1, $2}' > .submodule-status
# DerivedData holds build products, the SwiftPM checkouts, AND the Xcode
# 26 compilation cache (CompilationCache.noindex at its root). The first
# restore-key matches DerivedData last built with the SAME dependency
# versions, so compiled deps come back warm and only changed app files
# recompile. Saved per-SHA so the newest is always fresh.
- name: Restore DerivedData
id: dd-restore
uses: actions/cache/restore@v5
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-dd-${{ hashFiles('**/Package.resolved', '.submodule-status') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-dd-${{ hashFiles('**/Package.resolved', '.submodule-status') }}-
${{ runner.os }}-dd-
# SwiftFormat is built from source by the Xcode build phase via
# `swift run --package-path BuildTools`. Cache its build dir so that
# ~1 min recompile only happens when BuildTools' manifest changes.
- name: Cache BuildTools (SwiftFormat)
uses: actions/cache@v5
with:
path: BuildTools/.build
key: ${{ runner.os }}-buildtools-${{ hashFiles('BuildTools/Package.swift', 'BuildTools/Package.resolved') }}
restore-keys: |
${{ runner.os }}-buildtools-
- name: List available simulators
run: xcrun simctl list devices available
# Explicit resolve so the build can run with automatic resolution off.
# No-op when DerivedData restored warm; pinned versions only, no network
# churn. Checkouts land in the default location inside DerivedData
# (out of SRCROOT), not in the working tree.
- name: Resolve Swift packages
run: |
set -o pipefail
_start=$(date +%s)
xcodebuild -resolvePackageDependencies \
-workspace Trio.xcworkspace \
-scheme "Trio Tests" \
-onlyUsePackageVersionsFromResolvedFile || _rc=$?
echo "Resolve packages|$(( $(date +%s) - _start ))" >> "$GITHUB_WORKSPACE/.timings"
exit ${_rc:-0}
- name: Build for testing
run: |
set -o pipefail
_start=$(date +%s)
xcodebuild build-for-testing \
-workspace Trio.xcworkspace \
-scheme "Trio Tests" \
-destination "$DESTINATION" \
-onlyUsePackageVersionsFromResolvedFile \
-disableAutomaticPackageResolution \
-skipPackagePluginValidation \
-skipMacroValidation \
-quiet \
COMPILATION_CACHE_ENABLE_CACHING=YES \
COMPILER_INDEX_STORE_ENABLE=NO \
CODE_SIGN_IDENTITY="-" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="" \
PROVISIONING_PROFILE_SPECIFIER="" \
| tee xcodebuild-build.log || _rc=$?
echo "Build for testing|$(( $(date +%s) - _start ))" >> "$GITHUB_WORKSPACE/.timings"
exit ${_rc:-0}
# Ad-hoc simulator signing (CODE_SIGN_IDENTITY="-"): applies the app's
# entitlements — notably the App Group — WITHOUT any Apple Developer
# credentials, so the test host can resolve its shared container at
# launch. Manual style + empty team/profile keeps xcodebuild from
# reaching for a real certificate. To isolate the Xcode 26 compilation
# cache if a build ever misbehaves, drop COMPILATION_CACHE_ENABLE_CACHING=YES.
# Surface the failing lines directly in the job log on any build failure,
# so exit-65 diagnosis doesn't require digging. Covers compiler errors,
# link errors, AND run-script/build-phase failures.
- name: Surface build errors
if: always()
run: |
if [ -f xcodebuild-build.log ]; then
echo "::group::Build errors (tail)"
grep -nE "error:|Undefined symbol|PhaseScriptExecution failed|The following build commands failed|BUILD FAILED|Trace/BPT trap" xcodebuild-build.log | tail -n 80 || echo "no matching error lines"
echo "::endgroup::"
else
echo "No xcodebuild-build.log produced."
fi
- name: Upload build log
if: failure()
uses: actions/upload-artifact@v4
with:
name: xcodebuild-build-log
path: xcodebuild-build.log
if-no-files-found: ignore
- name: Check for uncommitted changes
run: |
CHANGES=$(git status --porcelain -- . \
':(exclude).timings' \
':(exclude).submodule-status' \
':(exclude)xcodebuild-build.log' \
':(exclude)xcodebuild.log')
if [ -n "$CHANGES" ]; then
echo "Uncommitted changes detected:"
echo "$CHANGES"
echo "$CHANGES" | while read -r line; do
FILE=$(echo $line | cut -c4-)
echo "::warning file=$FILE::Uncommitted change detected"
done
exit 0
else
echo "No uncommitted changes detected."
fi
shell: bash
# Keep the cache small so it doesn't thrash the 10 GB per-repo limit.
# Do NOT touch CompilationCache.noindex — that's the cross-commit cache.
- name: Trim DerivedData before caching
if: always()
run: |
DD="$HOME/Library/Developer/Xcode/DerivedData"
find "$DD" -type d -name 'Logs' -prune -exec rm -rf {} + 2>/dev/null || true
find "$DD" -name '*.xcactivitylog' -delete 2>/dev/null || true
find "$DD" -type d -name 'Index.noindex' -prune -exec rm -rf {} + 2>/dev/null || true
- name: Save DerivedData
# dev pushes only: refreshes the shared 5.4 GB baseline (and lets the
# compilation cache accumulate). PRs restore it warm but never re-upload
# it, so a PR run pays no save cost and PRs don't evict each other.
if: always() && github.event_name == 'push' && steps.dd-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-dd-${{ hashFiles('**/Package.resolved', '.submodule-status') }}-${{ github.sha }}
- name: Run tests
id: run_tests
run: |
set -o pipefail
_start=$(date +%s)
xcodebuild test-without-building \
-workspace Trio.xcworkspace \
-scheme "Trio Tests" \
-destination "$DESTINATION" \
-parallel-testing-enabled NO \
2>&1 | tee xcodebuild.log || _rc=$?
echo "Run tests|$(( $(date +%s) - _start ))" >> "$GITHUB_WORKSPACE/.timings"
exit ${_rc:-0}
# Report the ACTUAL xcodebuild test outcome, not a grep guess. A crash or
# a test-host launch failure exits 65 without a "Failing tests:" section,
# so the old heuristic reported a false pass. On failure, surface the
# named failures AND crash/launch/entitlement indicators.
- name: Annotate test results
if: always()
run: |
case "${{ steps.run_tests.outcome }}" in
success)
echo "::notice title=Unit Tests Passed::✅ All tests passed"
echo "✅ All tests passed" >> $GITHUB_STEP_SUMMARY
;;
skipped)
echo "::warning title=Unit Tests Skipped::Test phase did not run (build failed earlier)"
echo "⚠️ Tests did not run — the build failed before this step." >> $GITHUB_STEP_SUMMARY
;;
*)
echo "::error title=Unit Tests Failed::xcodebuild test phase failed (exit 65)"
{
echo "## ❌ Test phase failed"
echo ""
echo "**Named test failures (if any):**"
echo '```'
grep -A 20 "Failing tests:" xcodebuild.log 2>/dev/null \
| grep -E '^[[:space:]]+[A-Za-z0-9]+\..+\(\)' | sed 's/^/ - /' \
|| echo "(none listed — likely a crash or launch failure, see indicators below)"
echo '```'
echo ""
echo "**Crash / launch / entitlement indicators:**"
echo '```'
grep -nE "Testing failed:|\*\* TEST.*FAILED|Fatal error|[Cc]rash|Restarting after unexpected exit|could not be launched|Application Group|containerURL|entitlement|Sandbox" xcodebuild.log 2>/dev/null | tail -n 40 \
|| echo "(no indicators matched — download the xcodebuild-test-log artifact)"
echo '```'
} >> $GITHUB_STEP_SUMMARY
;;
esac
- name: Upload test log
if: failure()
uses: actions/upload-artifact@v4
with:
name: xcodebuild-test-log
path: xcodebuild.log
if-no-files-found: ignore
# Per-phase wall-clock + cache signals, written to the run SUMMARY panel
# (not the step log). On a failed build only the phases that ran appear.
- name: Benchmark summary
if: always()
run: |
DD="$HOME/Library/Developer/Xcode/DerivedData"
{
echo "## ⏱ CI phase timings"
echo ""
echo "| Phase | Duration |"
echo "| --- | --- |"
if [ -f "$GITHUB_WORKSPACE/.timings" ]; then
while IFS='|' read -r name secs; do
printf '| %s | %dm %02ds |\n' "$name" $((secs/60)) $((secs%60))
done < "$GITHUB_WORKSPACE/.timings"
else
echo "| (no timings captured) | — |"
fi
echo ""
echo "### Cache signals"
echo ""
echo "- DerivedData exact cache hit: \`${{ steps.dd-restore.outputs.cache-hit }}\` (false is expected — warm-start comes from restore-keys)"
dd_size=$(du -sh "$DD" 2>/dev/null | cut -f1 || echo "n/a")
cc_size=$(du -sh "$DD/CompilationCache.noindex" 2>/dev/null | cut -f1 || echo "absent")
echo "- DerivedData size (post-trim): \`${dd_size}\`"
echo "- CompilationCache.noindex size: \`${cc_size}\` (grows across runs as the Xcode 26 cache warms up)"
} >> "$GITHUB_STEP_SUMMARY"