Skip to content

Share one build and one simulator across fixture generation - #425

Merged
tylervick merged 3 commits into
mainfrom
tylervick/412-share-build-and-simulator
Aug 11, 2026
Merged

Share one build and one simulator across fixture generation#425
tylervick merged 3 commits into
mainfrom
tylervick/412-share-build-and-simulator

Conversation

@tylervick

@tylervick tylervick commented Aug 11, 2026

Copy link
Copy Markdown
Member

Toward #412. Draft — it carries a temporary measurement workflow that must be deleted before merge.

What #412 actually is

Per-invocation timing pulled from CI logs (prepareTestResults.sh runs under set -ex, so every xcodebuild test is timestamped):

TestResults SanityResults (1 test) RetryResults sum
main, pre-#423 359s 103s 187s 649s
branch #423 235s 84s 102s 421s
main, post-#423 436s 169s 163s 768s

Rows 2 and 3 are identical code, 1.8× apart — that is runner speed, and it is why #422 was closed and why #423's apparent speedup evaporated. What survives is the within-run ratio: (Sanity + Retry) / total is 45% / 44% / 43% across those three runs. Nearly half of fixture generation goes to two invocations that produce one test and one retry suite, almost all of it fixed per-invocation overhead.

The change

Boot the simulator once, build-for-testing once, then run each fixture with test-without-building against the shared products. Three invocations become one build + one boot + three cheap test runs.

Also in here:

  • Pin the destination by UDID rather than name+OS — unambiguous when several runtimes carry the same device name, and it guarantees the simulator booted by the script is the one xcodebuild uses.
  • Drop || true from the build. The tests are allowed to fail; a build failure is not. Under xcodebuild test || true a broken sample app surfaced later as a confusing mv error.

Verified

  • script exits 0; all three bundles produced (51MB / <1MB / 51MB)
  • swift test — 23 tests, 1 skipped, 0 failures against the newly generated fixtures
  • shellcheck clean
  • I also confirmed by experiment that test-without-building accepts -test-iterations / -retry-tests-on-failure, rather than trusting the help text

Local wall clock moved 50.4s → 48.0s, which is not evidence of anything — locally the simulator is already booted and builds are cached, so the overhead this targets barely exists.

How the claim gets tested

.github/workflows/ab-412.yml runs both generators on the same runner, back to back, counterbalanced (old, new, new, old), resetting DerivedData and shutting down simulators between variants. It also records how many .xcresult bundles each variant produced — a run that generated nothing is not a fast run.

If the A/B shows no improvement, this PR gets closed like #422 did. The measurement decides, not the reasoning.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Test result preparation now targets the selected simulator more reliably.
    • Multiple test suites reuse a single test build, reducing repeated build steps.
    • Build failures are reported immediately, while individual fixture failures continue to be captured for review.
    • Simulator selection now provides clearer device and operating system details.
  • Chores

    • Added generated build data to the project’s ignore rules.

tylervick and others added 2 commits August 10, 2026 19:44
prepareTestResults.sh ran three `xcodebuild test` invocations, each paying a
full build and its own simulator boot. Per-invocation timing extracted from CI
logs (#412) showed the two single-purpose invocations cost 43-45% of fixture
generation while producing one test and one retry suite between them -- a ratio
that held across three runs on runners differing 1.8x in speed.

Boot the simulator once, `build-for-testing` once, then run each fixture with
`test-without-building` against the shared products.

Also:
- Pin the destination by UDID instead of name+OS. Unambiguous when several
  runtimes offer the same device name, and it guarantees the simulator booted
  here is the one xcodebuild uses.
- Drop `|| true` from the build. Tests are allowed to fail; a build failure is
  not. Previously a broken sample app surfaced as a confusing `mv` error.

Verified locally: script exits 0, all three bundles produced, and
`swift test` passes 23 tests / 1 skipped / 0 failures against them.

Whether this is faster on CI is measured separately -- local timing is not
evidence for this repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cross-run wall clock cannot measure a change here: identical code has varied
1.8x between runners. This job runs both generators on the same runner, back to
back, counterbalanced (old, new, new, old), resetting DerivedData and shutting
down simulators between variants so neither benefits from the other's warmth.

It also records how many .xcresult bundles each variant produced -- a run that
generated nothing is not a fast run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af6f1489-d32b-49b7-9d33-a4b313650f56

📥 Commits

Reviewing files that changed from the base of the PR and between 70bb764 and 849c2c0.

📒 Files selected for processing (2)
  • .gitignore
  • prepareTestResults.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • .gitignore
  • prepareTestResults.sh

📝 Walkthrough

Walkthrough

The test preparation script targets a resolved simulator by UDID, builds test artifacts once into stable derived data, and reuses them for functional, sanity, and retry fixtures. The sample app’s derived-data directory is ignored.

Changes

Test preparation workflow

Layer / File(s) Summary
Simulator resolution and shared build
prepareTestResults.sh
Simulator selection retains complete device records, ranks numeric iPhone models, targets the selected UDID, boots the simulator, and performs one build-for-testing build.
Fixture execution and derived-data cleanup
prepareTestResults.sh, .gitignore
All fixtures use test-without-building with shared derived data. The sample app’s .derivedData directory is ignored.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant prepareTestResults.sh
  participant Simulator
  participant xcodebuild
  prepareTestResults.sh->>Simulator: Resolve and boot simulator by UDID
  prepareTestResults.sh->>xcodebuild: Build test artifacts once
  prepareTestResults.sh->>xcodebuild: Run functional fixture without building
  prepareTestResults.sh->>xcodebuild: Run sanity fixture without building
  prepareTestResults.sh->>xcodebuild: Run retry fixture without building
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: sharing one build and one simulator across fixture generation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tylervick/412-share-build-and-simulator

Comment @coderabbitai help to get the list of available commands.

It has served its purpose. Result, same runner, counterbalanced:

    run1  old  498s  3 bundles
    run2  new  308s  3 bundles
    run3  new  296s  3 bundles
    run4  old  434s  3 bundles

old mean 466s, new mean 302s -- a 35% reduction, with both new runs below
both old runs and no overlap between the groups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tylervick
tylervick marked this pull request as ready for review August 11, 2026 03:17
@tylervick
tylervick merged commit 822129f into main Aug 11, 2026
8 checks passed
@tylervick
tylervick deleted the tylervick/412-share-build-and-simulator branch August 11, 2026 03:31
tylervick added a commit that referenced this pull request Aug 11, 2026
Generating a report from TestResults.xcresult on main produced 16 .mp4
references, 0 .png/.jpeg, and 0 rendered `img class="screenshot"` elements.
The screenshot templates, the `screenshot` CSS class and the `-z` downsizing
branch were executed by no test at all -- that path could have been entirely
broken and `swift test` would still have been green. #357 (screenshots missing
from reports, open since June 2024) has been uninvestigable for the same
reason.

Adds FirstSuite.testAttachScreenshot, which attaches XCUIScreen.main.screenshot()
with .keepAlways. That yields a real public.png payload, which Attachment.swift
maps to isImage -> cssClass "screenshot". It deliberately does NOT launch the
app: XCUIScreen captures the simulator screen without it, and #423 removed the
launches from these suites as pure flake risk.

Restores the image assertion in CliTests, commented out when Xcode 15 began
attaching videos by default. Widened to include `img.screenshot-tail`, which
the original selector missed, and given failure messages that say what is
wrong.

Proved non-vacuous rather than assumed: removing the screenshot test and
regenerating makes it fail on BOTH the plain and the -z variants
("No image attachments rendered"). Restoring it returns the suite to green.

Also:
- CoreTests expected 16 rows; there are now 17 (14 XCTest + 3 Swift Testing).
  Its comment justified not asserting the pass/fail split on the grounds that
  the suites launch the app in setUp -- false since #423. Rewritten to say what
  is actually true.
- Excludes XCTestHTMLReportSampleApp/.derivedData from SwiftLint. #425 moved
  DerivedData inside the repo; SwiftLint does not read .gitignore and reported
  errors from Xcode-generated sources. CI never saw it because the lint job
  does not generate fixtures, so it only bit locally.

swift test: 23 tests, 1 skipped, 0 failures.

Closes #393.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
tylervick added a commit that referenced this pull request Aug 12, 2026
Fixture generation boots a simulator and runs the sample app's UI tests
on every CI run; even after #425 it dominates the test job's ~10-minute
wall time (#412). The bundles are deterministic for a given toolchain
and sample app (#423/#430), and every assertion in the suite compares
within one run, so identical bytes are safe to serve across runs.

Cache the three .xcresult bundles keyed on the exact Xcode build, the
newest installed iOS simulator runtime, prepareTestResults.sh itself,
and the sample-app sources. No restore-keys: an inexact match would
serve fixtures from a different toolchain, which the drift detector
(#392) and the legacy-vs-modern differential test cannot tolerate. A
verification step asserts all three bundles exist whether restored or
generated, so a cache hit that restores nothing fails instead of
passing as a suspiciously fast green run.

Local flow is untouched: prepareTestResults.sh is unchanged and swift
test still reads the same paths.

Fixes #412

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant