Skip to content

fix(xcresult): attribute a failure to the test's own frame, not a dependency's - #1157

Closed
mmatheson wants to merge 1 commit into
mainfrom
claude/xcresult-attribute-failures-to-the-test-frame
Closed

fix(xcresult): attribute a failure to the test's own frame, not a dependency's#1157
mmatheson wants to merge 1 commit into
mainfrom
claude/xcresult-attribute-failures-to-the-test-frame

Conversation

@mmatheson

Copy link
Copy Markdown
Member

Why

The file we report for an XCTest / swift-testing case comes from the failure summary, and for a failure raised inside a helper that is the helper's file — a snapshot trait, a mocking framework, a page object, a launch helper. Those live under the package checkout, so we report e.g.:

/Users/anka/builds/zillow/ios/apps/ZillowMap/Tuist/.build/checkouts/ZUITesting/Sources/ZUITesting/PageObject.swift
/Users/anka/builds/zillow/ios/apps/ZillowMap/Tuist/.build/checkouts/swift-snapshot-testing/Sources/SnapshotTesting/SnapshotsTestTrait.swift

Codeowners are resolved from that path (context/src/junit/parser.rs), so the test then belongs to whoever owns the vendored directory. For ZillowGroup that is Tuist/ @reviewers/mobile-app-platform-ios, and it is a recurring support thread — 683 of their currently-active test cases sit on such a path, 940 rows owned by that team as a result.

The mechanism

From our own fixture's database.sqlite3 (SourceCodeFrames.orderInContainer, innermost first):

order symbolName filePath
3 recordIssue(_:fileID:filePath:line:column:) …/SourcePackages/checkouts/swift-snapshot-testing
4 assertSnapshot<A, B>(of:as:…) …/SourcePackages/checkouts/swift-snapshot-testing
5 SnapshotReproTests.failingSnapshot() …/Tests/SnapshotReproTests/SnapshotReproTests.swift
12 closure #1 in _SnapshotsTestTrait.provideScope(…) …/checkouts/swift-snapshot-testing

The test's own frame is in the middle — helpers it called below it, the framework that invoked it above — so find_file_in_source_code_context_call_stack's .last() selects a dependency by construction. It returns the right answer on this fixture only incidentally: those dependency locations point at the package directory, so the .swift/.m extension filter drops them. Zillow's dependency frames are real .swift files, so nothing stops them.

What changed

1. Select the frame whose symbol names the test. Matched for Swift (Suite.testCase()), Objective-C (-[Suite testCase]), closures declared inside the test (closure #1 in …, still the test's file), and top-level swift-testing functions, which have no suite — a shape Zillow raised separately. That frame's file is a positive identification rather than a blocklist of everything else.

imageName looks like the natural discriminator and is not: SPM dependencies are statically linked into the test bundle, so every frame in the fixture — including the ones inside swift-snapshot-testing — reports imageName = SnapshotReproTests. Worth recording so nobody reaches for it later.

2. Reject vendored dependency paths from the remaining sources — the raised-from fileName, the source-code-context location, the last-frame fallback, and the document location used by the non-experimental path (otherwise the fix leaks straight through it). Segments: /.build/, /checkouts/, /DerivedData/, which covers both the Tuist layout and stock SPM under DerivedData.

When a test crashes or fails to launch it never reaches its own frame and every remaining source points into a dependency — we now report no file rather than one that would re-own the test. Consumers treat a missing file as "unchanged", not "cleared" (the ETL only takes non-empty values), so the test keeps the path and owners it last had.

Behaviour change worth a reviewer's judgement

This also changes attribution for a failure raised inside an in-repo helper: the test's own file now wins over the helper's. That is the intended reading of "the file of the test case", and it is what makes ownership follow the test rather than the utility it called — but it is broader than the dependency case in the title, so flagging it explicitly.

Tests

cargo test -p xcresult --lib — 24 pass, including new cases for: the test frame outranking a raised-from path; ObjC and closure symbols; a dependency fileName falling through to the location; dependency frames skipped in the last-frame fallback; a dependency-only summary (the crash/launch case) yielding None; a dependency document location rejected in the fallback map; and suite-less swift-testing functions.

cargo fmt clean; cargo clippy -p xcresult surfaces nothing new (the two warnings on this file, Iterator::last and an elidable lifetime, are pre-existing).

Not run: the #[cfg(target_os = "macos")] integration tests, which need xcresulttool — I only have Linux here. test_swift_snapshot_testing_trait_failure_uses_assertion_file should still pass unchanged (its expected file is the test file, which is exactly what the symbol-matched frame yields), but please confirm on a Mac before merge.

Also worth adding before this ships: a fixture built from one of the xcresults Zillow shared in #trunk-zillow — specifically a UI-test launch failure and a PageObject timeout, since those are the shapes I could only reconstruct synthetically here.

Related

trunk2 #5254 fixes the read side — the ClickHouse views that were showing a test's creation-time path forever and blanking collection-scoped codeowners on any upload that reported no file. This PR stops the bad path being written; that one stops a corrected path being ignored.

…endency's

The file we report for an XCTest/swift-testing case is whatever the failure
summary points at, and for a failure raised inside a helper that is the helper's
file: a snapshot trait, a mocking framework, a page object, a launch helper. The
path lands under the package checkout (`Tuist/.build/checkouts/...`,
`DerivedData/SourcePackages/checkouts/...`), and since codeowners are resolved
from that path the test is then owned by whoever owns the vendored directory.

Two changes:

- Select the call-stack frame whose symbol names the test itself, and take its
  file. Frames run innermost first, so the test's own frame sits in the middle
  of the stack — helpers it called below it, the framework that invoked it above
  — which is why taking the last frame lands on a dependency. Symbols are
  matched for Swift (`Suite.testCase()`), Objective-C (`-[Suite testCase]`),
  closures declared inside the test (`closure #1 in ...`), and top-level
  swift-testing functions, which have no suite.

  `imageName` looks like the natural discriminator here and is not: SPM
  dependencies are statically linked into the test bundle, so every frame in
  our own snapshot-testing fixture reports the test bundle's name.

- Reject vendored dependency paths from the remaining sources — the raised-from
  `fileName`, the source-code-context location, the last-frame fallback, and the
  document location used by the non-experimental path. When a test crashes or
  fails to launch it never reaches its own frame and every source points into a
  dependency; we now report no file at all rather than one that would re-own the
  test. Consumers already treat a missing file as "unchanged" rather than
  "cleared", so the test keeps the path and owners it last had.

Note this also changes attribution for a failure raised inside an in-repo
helper: the test's own file now wins over the helper's. That is the intended
reading of "the file of the test case".
@trunk-io

trunk-io Bot commented Aug 17, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@codecov-commenter

codecov-commenter commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.29%. Comparing base (64052e5) to head (80999b9).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1157      +/-   ##
==========================================
+ Coverage   82.97%   83.29%   +0.31%     
==========================================
  Files          71       71              
  Lines       16044    16117      +73     
==========================================
+ Hits        13312    13424     +112     
+ Misses       2732     2693      -39     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trunk-staging-io

trunk-staging-io Bot commented Aug 17, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

Failed Test Failure Summary Logs
pending_quarantine_test should be quarantined when run with variant A test marked as pending was expected to fail but unexpectedly passed. Logs ↗︎
variant_quarantine_test should be quarantined when run with variant A test expected the sum of 2 + 2 to be 5, but it was actually 4, indicating a failing assertion. Logs ↗︎

View Full Report ↗︎Docs

@trunk-io

trunk-io Bot commented Aug 17, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@dfrankland

Copy link
Copy Markdown
Member

Superseded by a three-PR stack that separates the refactor from the fix:

Same behaviour, same seven cases fail if the fix is reverted.

@dfrankland dfrankland closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants