Split RunCommand.swift into Reports/ExecutionContext extensions #183
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Fastest feedback of all: static analysis needs no build. SwiftLint and | |
| # SwiftFormat run against the whole tree using the repo's own configs | |
| # (.swiftlint.yml / .swiftformat), which were tuned against this | |
| # codebase's actual style rather than stock defaults. | |
| # | |
| # `swiftlint lint` runs `--strict --baseline .swiftlint-baseline.json`: | |
| # 145 pre-existing violations by identity (rule + file + line), dominated | |
| # by `line_length` (82, mostly long test lines) with the complexity/length | |
| # remainder (function_body_length, type_body_length, cyclomatic_complexity, | |
| # file_length, large_tuple) concentrated in, but not limited to, a few | |
| # deliberately large orchestration files (see .swiftlint.yml's comments | |
| # for the full breakdown, including which CLI command handlers are | |
| # baselined too) — that aren't worth a risky structural refactor just to | |
| # silence a linter. The baseline freezes exactly those pre-existing | |
| # violations by identity (rule + file + line), so `--strict` still fails | |
| # the build on any *new* violation anywhere, including a new one in an | |
| # already-baselined file. Shrink the baseline over time by fixing an | |
| # entry and regenerating with | |
| # `swiftlint lint --write-baseline .swiftlint-baseline.json`; never grow | |
| # it to paper over new debt. | |
| # | |
| # SwiftLint is pinned to an exact version (matching the version the | |
| # baseline was generated with) rather than `brew install`'s floating | |
| # latest: baseline identity and violation detection are both | |
| # SwiftLint-version-sensitive, so an unpinned upgrade could silently | |
| # change what the baseline recognizes, in either direction. Bump | |
| # SWIFTLINT_VERSION and regenerate .swiftlint-baseline.json together, as | |
| # one deliberate change, not independently. | |
| lint: | |
| name: Lint & format check | |
| runs-on: macos-15 | |
| # Generous relative to observed reality (~20-25s on this exact job) — | |
| # bounds a hang (a stuck `brew install`/download, say) rather than | |
| # tuning for the common case. See the `unit` job's own comment for | |
| # why every job here now has an explicit ceiling. | |
| timeout-minutes: 10 | |
| env: | |
| SWIFTLINT_VERSION: "0.63.2" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Enforces the documented toolchain floor (README "Install", | |
| # docs/apple-support-matrix.md "Toolchain floor"): Xcode 16+ and | |
| # Swift 6.0+. The macos-15 runner image drifts over time, so printing | |
| # the versions (as every job's `Toolchain` step does) is not enough — | |
| # this fails the build if the floor regresses. Placed first so a | |
| # drift fails fast, before the installs below spend any time. | |
| - name: Toolchain floor (Xcode 16+, Swift 6+) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # `xcodebuild -version` piped directly into `head -n 1` crashes: | |
| # `head` closes the read end after its first line, xcodebuild's | |
| # own Foundation-based stdout write then hits a broken pipe, | |
| # which xcodebuild turns into an uncaught NSFileHandleOperationException | |
| # (SIGABRT, exit 134) instead of the plain SIGPIPE a non-Foundation | |
| # tool would just die from quietly — reproduced for real on this | |
| # exact runner image. Capturing full output to a variable first | |
| # (command substitution reads to EOF, no early pipe closure) sidesteps it. | |
| xcode_version_output="$(xcodebuild -version)" | |
| xcode_major="$(head -n 1 <<<"$xcode_version_output" | awk '{print $2}' | cut -d. -f1)" | |
| swift_major="$(swift --version 2>&1 | grep -oE 'Apple Swift version [0-9]+' | grep -oE '[0-9]+')" | |
| echo "Xcode major: $xcode_major, Swift major: $swift_major" | |
| if [ -z "$xcode_major" ] || [ "$xcode_major" -lt 16 ]; then | |
| echo "::error::Xcode 16+ required, found '${xcode_major:-unknown}'. See docs/apple-support-matrix.md." | |
| exit 1 | |
| fi | |
| if [ -z "$swift_major" ] || [ "$swift_major" -lt 6 ]; then | |
| echo "::error::Swift 6+ required, found '${swift_major:-unknown}'. See docs/apple-support-matrix.md." | |
| exit 1 | |
| fi | |
| - name: Install SwiftFormat | |
| run: brew install swiftformat | |
| - name: Install pinned SwiftLint | |
| run: | | |
| curl -fsSL -o portable_swiftlint.zip \ | |
| "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" | |
| unzip -o portable_swiftlint.zip swiftlint | |
| chmod +x swiftlint | |
| sudo mv swiftlint /usr/local/bin/swiftlint | |
| rm portable_swiftlint.zip | |
| installed_version="$(swiftlint version)" | |
| if [ "$installed_version" != "$SWIFTLINT_VERSION" ]; then | |
| echo "::error::Installed SwiftLint $installed_version does not match pinned SWIFTLINT_VERSION $SWIFTLINT_VERSION" | |
| exit 1 | |
| fi | |
| - name: SwiftLint | |
| run: swiftlint lint --strict --config .swiftlint.yml --baseline .swiftlint-baseline.json Sources Tests | |
| - name: SwiftFormat (check only) | |
| run: swiftformat --lint --config .swiftformat . | |
| # Fast feedback: everything that does not build another project. | |
| unit: | |
| name: Unit tests | |
| runs-on: macos-15 | |
| # A real, reproducible hang (this job's own `Test` step sitting for | |
| # 68+ minutes with no completion, versus ~15-20s for the identical | |
| # `swift test` locally) burned real Actions minutes on this account | |
| # before three compounding process-handling bugs were found and | |
| # fixed (Sources/BenchmarkRunner/ToolRunner.swift). This ceiling is | |
| # this project's own "nothing runs unbounded" principle applied to | |
| # its own CI: generous relative to known-good local runtime (cold | |
| # build + full unit suite) so it never fires on a real, if slow, run, | |
| # but bounded so a recurrence (this exact class of bug, or a new one) | |
| # fails fast and visibly instead of silently burning an hour+ again. | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Toolchain | |
| shell: bash | |
| run: | | |
| swift --version && xcodebuild -version | |
| echo "CPU count: $(sysctl -n hw.ncpu)" | |
| - name: Build | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| swift build --build-tests 2>&1 | tee build.log | |
| # Diagnostic instrumentation for the same real CI hang this job's | |
| # own `timeout-minutes` above exists for. `swift test` runs in the | |
| # background, writing straight to a file (never through a pipe | |
| # `tee`/GitHub's own log-streaming could stall on) — file-based | |
| # logging already ruled out a log-pipe-buffering artifact directly: | |
| # a prior run's own `test.log` genuinely froze (byte count static | |
| # for the full ~12 minutes before timeout), proving a real stall | |
| # inside the test process itself, not a reporting artifact. | |
| # | |
| # Detection: `test.log` not growing for 240s (sixteen 15s checks) is | |
| # treated as a stall — don't wait for this job's own 15-minute | |
| # `timeout-minutes` ceiling, which exists as an outer backstop only. | |
| # | |
| # 240s, not the original 75s: a real CI run on commit 8ed7d31 tripped | |
| # this detector, and both a stack sample and the raw heartbeat log | |
| # proved it was a false positive, not a real hang. The sample showed | |
| # the sole busy thread inside `ProcessSupervisor.wait()`'s own | |
| # ordinary poll loop (`usleep`/`nanosleep`), not stuck in any | |
| # Foundation-internal call; the heartbeat log showed `test.log` | |
| # frozen at a constant line count for exactly the-then-75s threshold | |
| # before the kill fired. Root cause: this same investigation's own | |
| # earlier widenings this session (`promptExitTimeoutSeconds` and | |
| # `eventuallyNoSurvivors`'s timeout, both raised 10-30s -> 60s, plus | |
| # `retryingKnownForkRaceWindow`'s up to 3 fresh attempts on the | |
| # documented fork-race miss in `ProcessSupervisorResidueTests.swift`) | |
| # legitimately raised the worst-case span a single test can run with | |
| # zero `test.log` output well past the old 75s threshold — a single | |
| # attempt alone can legitimately wait up to | |
| # `promptExitTimeoutSeconds` + `eventuallyNoSurvivors`'s ceiling | |
| # (60s + 60s = 120s) with no output at all before it either succeeds | |
| # or retries. 240s covers two such attempts back to back (a real, | |
| # if uncommon, case — not the fully compounded 3-attempt worst case, | |
| # which would need multiple independent rare fork-race misses to | |
| # land back to back and is treated as astronomically unlikely enough | |
| # not to design the stall threshold around) while staying well under | |
| # the 900s `timeout-minutes` outer backstop, so a genuine multi- | |
| # minute hang is still caught and sampled long before that backstop | |
| # would silently discard the evidence. | |
| # | |
| # On stall, this samples the *actual* stuck work, not just the | |
| # parent: `swift-test`'s own process typically shows only "waiting | |
| # on child" once sampled, since the real work — and therefore the | |
| # real stuck stack trace — runs in `swiftpm-testing-helper`. Both | |
| # are sampled, `swiftpm-testing-helper` first/foremost, plus | |
| # `lsof` on the helper to see directly whether it is holding an | |
| # unexpected pipe fd open (confirming or ruling out the | |
| # file-descriptor-inheritance hypothesis directly, rather than | |
| # inferring it). | |
| # | |
| # Then this step kills the stuck process tree itself and exits 124 | |
| # (the conventional "timed out" exit code) — deliberately *not* | |
| # left to this job's own `timeout-minutes`, which marks a job | |
| # `cancelled`, not `failed`; `if: failure()` (the existing | |
| # "Upload unit failure logs" step's own condition before this | |
| # comment was written) does not run on a `cancelled` job, so a | |
| # GitHub-initiated timeout would silently discard every sample | |
| # this step just captured. Exiting 124 from inside the step itself | |
| # makes this an ordinary step failure, which does trigger the | |
| # upload. | |
| # | |
| # SWT_EXPERIMENTAL_MAXIMUM_PARALLELIZATION_WIDTH: CI-only (never set | |
| # for local development), added after a real, repeated CI failure | |
| # pattern in ProcessSupervisorResidueTests and similar tests that | |
| # depend on the OS actually finishing real, timing-sensitive work | |
| # (reaping a killed descendant, a stall watchdog's own deadline) in | |
| # a bounded window. This runner has confirmed only 3 vCPUs (see the | |
| # "Toolchain" step above); Swift Testing schedules every test's own | |
| # concurrency onto the cooperative thread pool by default, sized to | |
| # the *full* core count with no user-facing throttle -- `swift | |
| # test`'s own `--num-workers`/`--parallel` flags were tried first and | |
| # confirmed, by direct local experiment, to have zero effect on | |
| # Swift Testing's scheduling (they only govern legacy XCTest-style | |
| # process-level parallelism). This environment variable is Swift | |
| # Testing's own internal mechanism instead -- found via `strings` on | |
| # the Testing framework binary (it is not documented public API, and | |
| # its own name says "EXPERIMENTAL"), then verified directly, locally, | |
| # to actually bound real concurrent test execution before trusting | |
| # it here. Set to 2, not 1: leaves the supervisory/monitoring | |
| # machinery real headroom rather than fully serializing the whole | |
| # suite, which this project's own standing policy is not to do | |
| # blindly for a speed-vs-reliability trade this narrow a problem | |
| # does not need. If a future toolchain silently drops support for | |
| # this variable, the worst case is reverting to today's already- | |
| # documented behavior, not a regression beyond it. | |
| - name: Test | |
| shell: bash | |
| env: | |
| SWT_EXPERIMENTAL_MAXIMUM_PARALLELIZATION_WIDTH: "2" | |
| run: | | |
| set -o pipefail | |
| mkdir -p samples | |
| swift test > test.log 2>&1 & | |
| test_pid=$! | |
| echo "swift test started, pid=$test_pid" | |
| last_lines=0 | |
| stall_seconds=0 | |
| while kill -0 "$test_pid" 2>/dev/null; do | |
| sleep 15 | |
| lines=$(wc -l < test.log | tr -d ' ') | |
| if [ "$lines" -eq "$last_lines" ]; then | |
| stall_seconds=$((stall_seconds + 15)) | |
| else | |
| stall_seconds=0 | |
| fi | |
| last_lines=$lines | |
| echo "--- heartbeat $(date -u +%H:%M:%S) --- test.log lines: $lines (no growth for ${stall_seconds}s) ---" | |
| # The real work happens in child processes (swiftpm-testing-helper, | |
| # swift-package) the parent `swift` pid's own %cpu never reflects — | |
| # snapshotting every swift-related process, not just $test_pid, | |
| # is what actually shows whether real work is still happening. | |
| ps -eo pid,ppid,%cpu,%mem,etime,comm | grep -iE "swift|xctest" | grep -v grep || true | |
| tail -n 5 test.log | |
| if [ "$stall_seconds" -ge 240 ]; then | |
| echo ">>> STALL DETECTED (test.log has not grown in ${stall_seconds}s) — capturing diagnostics <<<" | |
| ps -ef > samples/ps-tree.txt | |
| parent_pid=$(pgrep -f "swift-test" | head -1) | |
| if [ -n "$parent_pid" ]; then | |
| echo "sampling parent swift-test pid=$parent_pid (for completeness — the real work is in the helper below)" | |
| sample "$parent_pid" 5 -file "samples/sample-parent-${parent_pid}.txt" 2>&1 | tail -n 3 || true | |
| fi | |
| for helper_pid in $(pgrep -f "swiftpm-testing-helper"); do | |
| echo "sampling swiftpm-testing-helper pid=$helper_pid — the actual test-worker process" | |
| sample "$helper_pid" 5 -file "samples/sample-helper-${helper_pid}.txt" 2>&1 | tail -n 3 || true | |
| lsof -p "$helper_pid" > "samples/lsof-helper-${helper_pid}.txt" 2>&1 || true | |
| done | |
| echo ">>> killing the stuck process tree and failing this step (exit 124) so the job registers as an ordinary failure, not a GitHub-initiated cancellation <<<" | |
| kill -9 "$test_pid" 2>/dev/null | |
| pkill -9 -f swiftpm-testing-helper 2>/dev/null | |
| pkill -9 -f "swift-test" 2>/dev/null | |
| cat test.log | |
| exit 124 | |
| fi | |
| done | |
| wait "$test_pid" | |
| exit_code=$? | |
| echo "=== swift test exited with code $exit_code ===" | |
| cat test.log | |
| exit $exit_code | |
| # `always()`, not `failure()`: a job that hits its own | |
| # `timeout-minutes` is reported as *cancelled*, not failed, and | |
| # `if: failure()` does not run for a cancelled job's steps — the | |
| # exact scenario this whole diagnostic exists to capture evidence | |
| # from. Uploading unconditionally is the only way the stack | |
| # samples and logs from a timeout are ever actually retained. | |
| - name: Upload unit failure logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: unit-failure-logs | |
| if-no-files-found: ignore | |
| path: | | |
| build.log | |
| test.log | |
| samples/ | |
| - name: Publish unit failure excerpt | |
| if: failure() && github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- mutantkit-ci-unit-failure -->'; | |
| const readTail = (path) => { | |
| if (!fs.existsSync(path)) return ''; | |
| const lines = fs.readFileSync(path, 'utf8').split('\n'); | |
| return lines.slice(-180).join('\n'); | |
| }; | |
| const build = readTail('build.log'); | |
| const test = readTail('test.log'); | |
| const excerpt = (test || build || 'No captured build/test log was available.').slice(-30000); | |
| const body = `${marker}\n### Latest unit CI failure\n\n\`\`\`text\n${excerpt}\n\`\`\``; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }); | |
| const existing = comments.find(c => c.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| } | |
| # The suites that build and mutate real projects. Slow, and the only thing that | |
| # proves the tool works rather than merely compiles: every wiring bug this | |
| # project has had — a sandbox handed the wrong excludes, xcodebuild pointed at | |
| # unmutated sources, concurrent mutants fighting over one simulator — was | |
| # invisible to the unit tests and produced a confident, wrong score. | |
| acceptance: | |
| name: Acceptance (${{ matrix.fixture }}) | |
| runs-on: macos-15 | |
| needs: unit | |
| # Generous: real xcodebuild/simulator work, legitimately slower than | |
| # `unit`'s own plain unit suite — but still bounded, per the same | |
| # "nothing runs unbounded" reasoning as `unit`'s own ceiling above. | |
| timeout-minutes: 30 | |
| strategy: | |
| # Never cancel siblings: which fixtures fail together is a diagnosis. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - fixture: swift-package | |
| filter: SwiftPackageMacOSAcceptanceTests | |
| simulator: "0" | |
| - fixture: swift-package-coverage | |
| filter: SwiftPackageMacOSCoverageAcceptanceTests | |
| simulator: "0" | |
| - fixture: shard-merge | |
| filter: ShardMergeAcceptanceTests | |
| simulator: "0" | |
| - fixture: swift-package-ios | |
| filter: SwiftPackageIOSAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-project | |
| filter: XcodeProjectAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-workspace | |
| filter: XcodeWorkspaceAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-app-debug-dylib | |
| filter: XcodeAppDebugDylibAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-unlinked-source | |
| filter: XcodeUnlinkedSourceAcceptanceTests | |
| simulator: "1" | |
| # `simulator: "1"`, not "0": this suite is not purely a host-side | |
| # SwiftPM fixture — `CLICommandsAcceptanceTests | |
| # .initDetectsXcodeDestinationDespiteSchemeAmbiguity` runs a real | |
| # `mutantkit init` against `Fixtures/XcodeProject`, which calls | |
| # `XcodeConfigDetector.detectDestination()` -> a real | |
| # `xcrun simctl list devices available --json`. Classified "0" | |
| # originally, which skips the "Available simulators" preflight | |
| # step below (gated on `matrix.simulator == '1'`) — a real CI | |
| # failure traced this exactly: `Detected: ...`/`Multiple schemes | |
| # found (...)` printed correctly, but `Detected destination: ...` | |
| # never appeared, and the test itself reported "failed after | |
| # 66.165 seconds" — just past `SimulatorPool`'s 60s default | |
| # timeout on `simctl`, on a runner whose CoreSimulator subsystem | |
| # this job's own matrix classification told it not to expect to | |
| # need. A pre-existing test-topology misclassification P4's own | |
| # longer runtime happened to expose, not a P4 product regression | |
| # (confirmed: `InitCommand.swift`/`XcodeConfigDetector.swift`/ | |
| # `SimulatorPool.swift`/this test file are all untouched across | |
| # every P4 commit). | |
| - fixture: cli-commands | |
| filter: CLICommandsAcceptanceTests | |
| simulator: "1" | |
| # Real-machine XcodeConfigDetector coverage (a real `simctl` call | |
| # finding a real destination, including against a real | |
| # ambiguous-scheme project), relocated out of the always-run unit | |
| # suite: these are integration tests, not deterministic unit | |
| # tests, and CI must explicitly declare a simulator dependency for | |
| # them rather than run them unconditionally with no such | |
| # declaration at all. See `XcodeConfigDetectorAcceptanceTests.swift`'s | |
| # own header comment. | |
| - fixture: xcode-config-detector | |
| filter: XcodeConfigDetectorAcceptanceTests | |
| simulator: "1" | |
| - fixture: process-supervision | |
| filter: ProcessSupervisionAcceptanceTests | |
| simulator: "0" | |
| # The four below all build the same real .xcodeproj through the | |
| # batching/incremental/coverage-selection paths that a 100-mutant | |
| # benchmark against a real external project found broken while | |
| # every unit test (all fakes, no real xcodebuild invocation or | |
| # .xctestrun) stayed green. They are the whole reason this job | |
| # exists rather than trusting `unit` alone. | |
| - fixture: xcode-batch-testing | |
| filter: XcodeBatchTestingAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-batch-testing-ui-target | |
| filter: XcodeBatchTestingUITargetAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-coverage-selection | |
| filter: XcodeCoverageSelectionAcceptanceTests | |
| simulator: "1" | |
| - fixture: xcode-incremental-batch-testing | |
| filter: XcodeIncrementalBatchTestingAcceptanceTests | |
| simulator: "1" | |
| # Real `swiftc -typecheck` proof that RelationalOperatorReplacement- | |
| # SchemataLowerer's ternary lowering type-checks at every eligible | |
| # operand shape — the same discipline as the batch/incremental | |
| # fixtures above, applied to a lowerer still gated out of | |
| # SchemataLowererRegistry.builtIn. No schemata runtime binary | |
| # involved (see the ror-schemata-differential job below for that), | |
| # so it fits this matrix's plain build+test shape. | |
| - fixture: ror-schemata-compile | |
| filter: RORSchemataCompileViabilityAcceptanceTests | |
| simulator: "0" | |
| # P8 (CI Safe-Skip Policy): `Acceptance.waveEnabled` | |
| # (`Tests/MutantKitTests/Acceptance/AcceptanceWaveSupport.swift`) | |
| # gates `XcodeWaveEarlyKillAcceptanceTests` behind | |
| # `MUTANTKIT_WAVE_ACCEPTANCE`, which no workflow ever set — | |
| # confirmed by `git log` that wave-based early kill has been real, | |
| # shipped functionality on `main` for a while (this file's own doc | |
| # comment describing "wave execution does not exist yet" predates | |
| # that and was never updated), so this real, differential, Xcode | |
| # acceptance gate had never executed in any CI run. Reuses the same | |
| # `XcodeProject` fixture the `xcode-project` entry above already | |
| # builds — no new fixture, no new infra, just the missing opt-in. | |
| - fixture: xcode-wave-early-kill | |
| filter: XcodeWaveEarlyKillAcceptanceTests | |
| simulator: "1" | |
| wave: "1" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Toolchain | |
| run: swift --version && xcodebuild -version | |
| # The suites pick whichever iPhone this machine actually has rather than | |
| # pinning a model, so this is context for a failure, not a gate. | |
| - name: Available simulators | |
| if: matrix.simulator == '1' | |
| run: xcrun simctl list devices available | grep -i iphone || true | |
| - name: Build | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| swift build --build-tests 2>&1 | tee build.log | |
| - name: Acceptance | |
| shell: bash | |
| env: | |
| MUTANTKIT_ACCEPTANCE: "1" | |
| MUTANTKIT_ACCEPTANCE_SIMULATOR: ${{ matrix.simulator }} | |
| MUTANTKIT_WAVE_ACCEPTANCE: ${{ matrix.wave }} | |
| run: | | |
| set -o pipefail | |
| swift test --filter ${{ matrix.filter }} 2>&1 | tee acceptance.log | |
| test_exit=$? | |
| # P8: a filter matching zero tests must fail this job, not pass it | |
| # silently -- see scripts/assert-tests-ran.sh. A real test failure | |
| # above must still win even if the count check itself passes. | |
| scripts/assert-tests-ran.sh acceptance.log "fixture ${{ matrix.fixture }} (filter ${{ matrix.filter }})" || test_exit=1 | |
| exit $test_exit | |
| - name: Upload acceptance failure logs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: acceptance-${{ matrix.fixture }}-failure-logs | |
| if-no-files-found: ignore | |
| path: | | |
| build.log | |
| acceptance.log | |
| # Separate from the `acceptance` matrix above because this suite links and | |
| # runs the real schemata runtime binary (SchemataMutationRunner drives a | |
| # genuine `swift build`/`swift test` through it) rather than only | |
| # type-checking lowered source — it needs | |
| # MUTANTKIT_SCHEMATA_RUNTIME_LIB_OVERRIDE pointing at a real built | |
| # `libMutantKitSchemataRuntime.a`, which the matrix's plain build+test | |
| # steps never produce. Runs the exact MutationIDs discovered from one | |
| # fixture through both MutationRunner (isolated) and SchemataMutationRunner | |
| # (schemata, invoked directly — RelationalOperatorReplacementSchemataLowerer | |
| # is still not registered in SchemataLowererRegistry.builtIn) and asserts | |
| # zero disagreement — the real gate before that registration can happen. | |
| ror-schemata-differential: | |
| name: ROR schemata isolated-vs-schemata differential | |
| runs-on: macos-15 | |
| needs: unit | |
| # Same reasoning as `acceptance`'s own ceiling: real build/link/test | |
| # work, bounded rather than unbounded. | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Toolchain | |
| run: swift --version && xcodebuild -version | |
| - name: Build the schemata runtime static library | |
| run: swift build -c release --product MutantKitSchemataRuntime | |
| - name: Build tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| swift build --build-tests 2>&1 | tee build.log | |
| - name: Differential acceptance | |
| shell: bash | |
| env: | |
| MUTANTKIT_ACCEPTANCE: "1" | |
| MUTANTKIT_SCHEMATA_RUNTIME_LIB_OVERRIDE: ${{ github.workspace }}/.build/release | |
| run: | | |
| set -o pipefail | |
| swift test --filter RORSchemataIsolatedDifferentialAcceptanceTests 2>&1 | tee acceptance.log | |
| test_exit=$? | |
| # P8: see scripts/assert-tests-ran.sh -- zero matched tests must fail this job. | |
| scripts/assert-tests-ran.sh acceptance.log "RORSchemataIsolatedDifferentialAcceptanceTests" || test_exit=1 | |
| exit $test_exit | |
| - name: Upload failure logs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ror-schemata-differential-failure-logs | |
| if-no-files-found: ignore | |
| path: | | |
| build.log | |
| acceptance.log | |
| # Separate from both `acceptance` and `ror-schemata-differential` for the same | |
| # reason as the latter: it needs a real, non-default | |
| # MUTANTKIT_SCHEMATA_RUNTIME_LIB_OVERRIDE layout that the plain matrix build+test | |
| # steps never produce — here, BOTH the macOS archive (`swift build`'s normal | |
| # product, needed as this suite's negative control) AND the iOS-Simulator archive | |
| # (produced only by scripts/build-schemata-runtime.sh, which nothing else in CI | |
| # runs) living side by side under one override directory, exactly the shape | |
| # SchemataRuntimeLibraryLocator expects. An earlier draft of this feature had no | |
| # job that actually exercised this combination in CI at all — the new acceptance | |
| # suites existed but nothing wired MUTANTKIT_SCHEMATA_RUNTIME_LIB_OVERRIDE for | |
| # them, so they would have failed closed with `missingOverride` the first time | |
| # anyone ran them here, silently never proving anything past a developer's own | |
| # laptop. This job is that wiring. | |
| ios-simulator-schemata-runtime: | |
| name: iOS-Simulator schemata runtime (build + link viability) | |
| runs-on: macos-15 | |
| needs: unit | |
| # Same reasoning as `acceptance`'s own ceiling: real build/link/test | |
| # work, bounded rather than unbounded. | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Toolchain | |
| run: swift --version && xcodebuild -version | |
| - name: Build the macOS schemata runtime static library (this suite's negative control) | |
| run: swift build -c release --product MutantKitSchemataRuntime | |
| - name: Build the iOS-Simulator schemata runtime static library | |
| run: scripts/build-schemata-runtime.sh "$GITHUB_WORKSPACE/.build/release" | |
| - name: Build tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| swift build --build-tests 2>&1 | tee build.log | |
| - name: iOS-Simulator runtime acceptance | |
| shell: bash | |
| env: | |
| MUTANTKIT_ACCEPTANCE: "1" | |
| MUTANTKIT_SCHEMATA_RUNTIME_LIB_OVERRIDE: ${{ github.workspace }}/.build/release | |
| run: | | |
| set -o pipefail | |
| swift test --filter "SchemataIOSSimulatorRuntimeArtifactAcceptanceTests|SchemataIOSSimulatorRuntimeLinkAcceptanceTests" 2>&1 | tee acceptance.log | |
| test_exit=$? | |
| # P8: see scripts/assert-tests-ran.sh -- zero matched tests must fail this job. | |
| scripts/assert-tests-ran.sh acceptance.log "SchemataIOSSimulatorRuntime*AcceptanceTests" || test_exit=1 | |
| exit $test_exit | |
| - name: Upload failure logs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-simulator-schemata-runtime-failure-logs | |
| if-no-files-found: ignore | |
| path: | | |
| build.log | |
| acceptance.log |