feat(cli): add --swift-test-xunit-paths and fill in declared files - #1181
Merged
trunk-io[bot] merged 3 commits intoSep 10, 2026
Merged
Conversation
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
5 times, most recently
from
August 31, 2026 20:56
8460380 to
6faa884
Compare
dfrankland
marked this pull request as ready for review
September 8, 2026 21:37
dfrankland
requested review from
TylerJang27,
acatxnamedvirtue and
max-trunk
September 8, 2026 21:38
TylerJang27
approved these changes
Sep 9, 2026
acatxnamedvirtue
approved these changes
Sep 9, 2026
|
😎 Stack merged successfully - details. |
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
from
September 9, 2026 20:11
6faa884 to
dfd8f07
Compare
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
3 times, most recently
from
September 9, 2026 21:46
44184c4 to
bc1ff89
Compare
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
from
September 10, 2026 04:55
7392ca1 to
88823f5
Compare
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
from
September 10, 2026 05:33
88823f5 to
4d53401
Compare
`swift test --xunit-output` writes no file path for any test, so a Swift test run outside Xcode has never been attributable to a file. This reads those files, resolves each test to where a language server says it is declared, and writes the `file` attribute back before the JUnit is bundled. It is a **path list rather than a boolean on `--junit-paths`**, for three reasons. Provenance: declaring that a file came from `swift test` is what licenses parsing its `classname` as a Swift type path, and JUnit5 emits the same `Type.method()` shape, so guessing would be unsound in a repository holding both. Cost: resolving walks the checkout, and a repository with no Swift at all should not pay that on every upload. Precedence: an `.xcresult` and a `swift test` xunit are different files, not two readings of one file, so with separate lists there is nothing to arbitrate — no mode, and no platform-conditional default. Taking a list rather than one path also matters because a single `swift test` run writes **two** of them: swift-testing to `<name>-swift-testing.xml` and XCTest to `<name>`, the latter only when `--parallel` is passed. A project using both frameworks uploads both, and one index serves them all, so that costs one checkout scan rather than one per file. Unlike `--xcresult-path`, this is not gated to macOS. The dependency is a language server rather than `xcresulttool`, and `sourcekit-lsp` ships with the Swift toolchain on Linux, so the flag is available wherever `swift test` is. The xcresult flags stay macOS-only because a bundle cannot be read without Xcode. A test that already carries a file keeps it. The resolved/unresolved split is logged, and unresolved warns — which is only a meaningful signal because the input was declared rather than guessed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`handle_swift_test_xunit` was inserted directly after the `#[cfg(target_os = "macos")]` belonging to `handle_xcresult`, so the new function took the gate and `handle_xcresult` lost it. That broke every non-macOS build five ways: the swift xunit handler went missing at its unconditional call site, `handle_xcresult` started compiling against `XCResult`/`XCResultOptions` that are gated out, and `write_all` lost the macOS-gated `Write` import. `--swift-test-xunit-paths` is not platform-gated and needs no Xcode, so the handler stays ungated and the gate goes back where it was. `Duration` moves under the gate instead -- it is only used by the macOS-only `XCResultOptions`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
from
September 10, 2026 06:14
4d53401 to
63a5c3f
Compare
…the file markdownlint MD046 defaults to `consistent`, and this was the file's only indented code block against three fenced ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dfrankland
force-pushed
the
dylan/swift-test-xunit-plumbing
branch
from
September 10, 2026 06:44
63a5c3f to
baffa6a
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1180 → #1179 → #1178. Review those first; this is one commit on top.
What it does
swift test --xunit-outputwrites no file path for any test, so a Swift test run outside Xcode has never been attributable to a file. This reads those files, resolves each test to where a language server says it is declared, and writes thefileattribute back before the JUnit is bundled.swift test --parallel --xunit-output out.xml trunk upload --swift-test-xunit-paths out-swift-testing.xml,out.xmlA single
swift testrun writes two files — swift-testing to<name>-swift-testing.xmland XCTest to<name>, the latter only under--parallel. A project using both frameworks uploads both, which is a second reason this takes a list.Why a path list, not a boolean on
--junit-pathsThree reasons, and the third is why this shape matters:
swift testis what licenses parsing itsclassnameas a Swift type path. JUnit5 emits the sameType.method()shape — a ClickHouse probe found the()-suffixed name population is dominated by JUnit5/Kotlin — so guessing would be unsound in any repository holding both. In a pure-Java repo the misparse is a harmless no-op (the scan only collects.swift/.m/.mm), but in a mixed monorepo it could attribute a Java test to a Swift file, and the suiteless fallback makes that worse, not better.scan_sourceswalks the whole tree before filtering by extension —max_filescaps what is collected, not what is walked. A repository with no Swift should not pay a directory walk on every upload..xcresultand aswift testxunit are different files, not two readings of one file. With separate lists there is nothing to arbitrate: no mode, no platform-conditional default, and a monorepo doing both passes both flags.Platform
Not gated to macOS, unlike
--xcresult-path. The dependency here is a language server rather thanxcresulttool, andsourcekit-lspships with the Swift toolchain on Linux — so the flag is available whereverswift testis. The xcresult flags stay macOS-only because a bundle cannot be read without Xcode. Two different dependencies, two different platform stories.Notes
<name>-swift-testing.xml, not<name>— a real trap when writing CI config.Testing
upload_bundle_using_swift_test_xunitis a full round trip: build a repo with Swift sources, upload both xunit files with no paths in them, extract the bundle, and assert each test in the bundled JUnit carries the file it is declared in — covering a top-level@Test func, a test in a@Suite, and anXCTestCasemethod. Mutation-checked: dropping the write makes it fail withhelloworld() got no file from its declaration.Full CLI suite (101) and xcresult suite (138) pass; clippy clean.
Test case ids differ between the two inputs, deliberately for now
The same test gets a different id depending on which format it arrived in — measured on one package captured both ways:
The two take different branches in
context/src/junit/parser.rs: an xcresult test case carries anidextra (v5 overorg#repo#identifierURL) which is used verbatim, while aswift testxunit carries none, so identity falls togen_info_idover(org, repo, file, classname, parent_name, name)— the scheme every other JUnit uploader already uses. A repository uploading both formats therefore sees every test twice.Not unified here because neither half is free:
identifierURLleads with the xcodebuild scheme name, absent from xunit output, so the xunit path cannot reproduce an xcresult id; and rederiving the xcresult id from normalised components would reset history for every repository uploading.xcresulttoday. Giving xunit a third bespoke scheme in the meantime would cost those users two resets instead of one. The plan is a single later migration moving both onto one format.Two consequences while that is outstanding, both documented in
CONTRIBUTING.md:fileis agen_info_idinput, so enabling this flag on a repository that previously uploaded the same file via--junit-pathschanges every id once — and a test whose declaration later moves changes id again. The xcresult scheme is immune to both, since its id ignoresfile.nameis an input too, which is whythe_two_inputs_spell_an_xctest_method_differentlypinstestOldStyleagainsttestOldStyle().🤖 Generated with Claude Code