Skip to content

feat(cli): add --swift-test-xunit-paths and fill in declared files - #1181

Merged
trunk-io[bot] merged 3 commits into
dylan/swift-test-declaration-locationsfrom
dylan/swift-test-xunit-plumbing
Sep 10, 2026
Merged

feat(cli): add --swift-test-xunit-paths and fill in declared files#1181
trunk-io[bot] merged 3 commits into
dylan/swift-test-declaration-locationsfrom
dylan/swift-test-xunit-plumbing

Conversation

@dfrankland

@dfrankland dfrankland commented Aug 31, 2026

Copy link
Copy Markdown
Member

Stacked on #1180#1179#1178. Review those first; this is one commit on top.

What it does

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.

swift test --parallel --xunit-output out.xml
trunk upload --swift-test-xunit-paths out-swift-testing.xml,out.xml

A single swift test run writes two files — swift-testing to <name>-swift-testing.xml and 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-paths

Three reasons, and the third is why this shape matters:

  • Provenance. Declaring that a file came from swift test is what licenses parsing its classname as a Swift type path. JUnit5 emits the same Type.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.
  • Cost. scan_sources walks the whole tree before filtering by extension — max_files caps what is collected, not what is walked. A repository with no Swift should not pay a directory walk on every upload.
  • Precedence. An .xcresult and a swift test xunit 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 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. Two different dependencies, two different platform stories.

Notes

  • One index per run, not per file, so uploading both files from a run costs one checkout scan rather than two.
  • A test that already carries a file keeps it; this only fills gaps.
  • The resolved/unresolved split is logged and unresolved warns — which is only a meaningful signal because the input was declared rather than guessed.
  • Help text calls out that swift-testing writes to <name>-swift-testing.xml, not <name> — a real trap when writing CI config.

Testing

upload_bundle_using_swift_test_xunit is 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 an XCTestCase method. Mutation-checked: dropping the write makes it fail with helloworld() 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:

shared()   xcresult e40658ab-…   xunit 73301b89-…

The two take different branches in context/src/junit/parser.rs: an xcresult test case carries an id extra (v5 over org#repo#identifierURL) which is used verbatim, while a swift test xunit carries none, so identity falls to gen_info_id over (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: identifierURL leads 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 .xcresult today. 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:

  • file is a gen_info_id input, so enabling this flag on a repository that previously uploaded the same file via --junit-paths changes every id once — and a test whose declaration later moves changes id again. The xcresult scheme is immune to both, since its id ignores file.
  • name is an input too, which is why the_two_inputs_spell_an_xctest_method_differently pins testOldStyle against testOldStyle().

🤖 Generated with Claude Code

@dfrankland
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch 5 times, most recently from 8460380 to 6faa884 Compare August 31, 2026 20:56
@dfrankland
dfrankland marked this pull request as ready for review September 8, 2026 21:37
@trunk-io

trunk-io Bot commented Sep 9, 2026

Copy link
Copy Markdown

😎 Stack merged successfully - details.

@dfrankland
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch from 6faa884 to dfd8f07 Compare September 9, 2026 20:11
@dfrankland
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch 3 times, most recently from 44184c4 to bc1ff89 Compare September 9, 2026 21:46
@dfrankland
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch from 7392ca1 to 88823f5 Compare September 10, 2026 04:55
@dfrankland
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch from 88823f5 to 4d53401 Compare September 10, 2026 05:33
dfrankland and others added 2 commits September 10, 2026 06:12
`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
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch from 4d53401 to 63a5c3f Compare September 10, 2026 06:14
…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
dfrankland force-pushed the dylan/swift-test-xunit-plumbing branch from 63a5c3f to baffa6a Compare September 10, 2026 06:44
@trunk-io
trunk-io Bot merged commit 8396875 into main Sep 10, 2026
23 checks passed
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.

3 participants