-
Notifications
You must be signed in to change notification settings - Fork 32
Description
Summary
xcresultparser omits session-level test failures from the generated JUnit XML.
When Swift Testing reports a failure attributed to no specific test — shown as
Test «unknown» in the xcodebuild log and as
"testName": "Issues recorded without an associated test or suite" in the xcresult —
xcresultparser produces a JUnit report with failures="0", even though the overall
test session failed.
This is a follow-up to #64 (merged 2024-02-24), which improved session-level failure
handling but did not cover the case where the failure has no associated test identifier.
Environment
- xcresultparser: 1.9.4 (latest, post-Replace XCResultKit in order to support Swift testing fully #64)
- Xcode: 26.1.1 (Swift Testing bundled)
- Platform: macOS 26.3 / arm64
Steps to Reproduce
A self-contained reproduction project is attached as SessionLevelFailureRepro.zip.
Unzip and run the provided script:
unzip SessionLevelFailureRepro.zip
cd SessionLevelFailureRepro
bash run-repro.sh
Trigger mechanism
Task.detached in Swift does not inherit task-local values, including
Test.current — the value Swift Testing uses to attribute issues to a specific test.
When Issue.record() is called from a detached task (or from any other context
where Test.current is nil, such as deinit or a callback running after the test
has completed), Swift Testing attributes the failure to Test «unknown».
This is the same pattern produced by The Composable Architecture's TestStore
when a test returns without calling await store.finish(): the TestStore's teardown
code detects in-flight effects and calls Issue.record() outside the test's task
context.
The reproduction uses the simplest possible trigger without a third-party dependency:
@Test func triggerSessionLevelFailure() async {
Task.detached {
Issue.record("Issue recorded in Task.detached — no Test.current")
}
await Task.yield()
await Task.yield()
}What you will see
xcodebuild / Swift Testing console output:
Test Suite 'All tests' passed at 2026-03-02 16:47:26.655.
Test run started.
Test triggerSessionLevelFailure() passed after 0.001 seconds.
✘ Test «unknown» recorded an issue at SessionLevelFailureTests.swift:11:25: Issue recorded
Issue recorded in Task.detached — no Test.current
Test passingTest() passed after 0.001 seconds.
Suite "Session-level failure demo" passed after 0.001 seconds.
✘ Test run with 2 tests in 1 suite failed after 0.001 seconds with 1 issue.
** TEST SUCCEEDED **
Note: xcodebuild exits 0 (** TEST SUCCEEDED **), but the Swift Testing runner
reports the session as failed. The failure is only detectable via xcresulttool.
xcresulttool summary (ground truth — session is Failed):
result : Failed
failedTests: 1
passedTests: 2
testFailures:
- Issues recorded without an associated test or suite
| Issue recorded: Issue recorded in Task.detached — no Test.current
xcresultparser JUnit output (failure is missing — failures="0"):
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="2" failures="0" errors="0" time="9.567">
<testsuite name="All tests" tests="0" failures="0" errors="0" time="0"/>
<testsuite name="SessionLevelFailureTests" tests="2" failures="0" errors="0" time="0">
<testcase name="triggerSessionLevelFailure()" time="0" classname="SessionLevelFailureTests"/>
<testcase name="passingTest()" time="0" classname="SessionLevelFailureTests"/>
</testsuite>
</testsuites>Root Cause
The xcresult stores session-level failures under a synthetic test node with
testIdentifierString = "Issues recorded without an associated test or suite".
xcresultparser's data model (XCTestFailure) and its JUnit provider
(XCResultToolJunitXMLDataProvider) match failures by test identifier, so any
failure that belongs to this synthetic node is silently dropped — it never appears
in the output XML.
Impact
CI pipelines exit with a non-zero code (the session really failed via the Swift
Testing runner), but the JUnit report used in PR summaries and test dashboards shows
failures="0". The failure is effectively invisible to code reviewers.
This pattern occurs with any library that checks invariants outside the test's
task-local context — most notably TCA's TestStore, but also any code that performs
deferred cleanup in deinit, callbacks, or detached tasks. It is likely to become
more common as Swift 6 encourages deferred, actor-isolated teardown patterns.
Expected Behavior
The session-level failure should appear in the JUnit XML, attributed to the test case
most closely associated with it via the file/line location recorded in the issue.
In this reproduction, the issue is recorded at SessionLevelFailureTests.swift:11,
which is inside triggerSessionLevelFailure(). The expected output would be:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="2" failures="1" errors="0" time="9.567">
<testsuite name="SessionLevelFailureTests" tests="2" failures="1" errors="0" time="0">
<testcase name="triggerSessionLevelFailure()" time="0" classname="SessionLevelFailureTests">
<failure message="Issue recorded">
Issue recorded in Task.detached — no Test.current
(SessionLevelFailureTests.swift:11)
</failure>
</testcase>
<testcase name="passingTest()" time="0" classname="SessionLevelFailureTests"/>
</testsuite>
</testsuites>