Skip to content

Commit 133d65e

Browse files
authored
Add skipped test tracking to JUnit output. (#549)
This PR adds skipped test reporting to our JUnit XML output. For example, given this test: ```swift @test(.disabled("Because I said so")) func f() {} ``` The XML output would be, approximately: ```xml <?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="TestResults" errors="0" tests="1" failures="0" skipped="1" time="12345.0"> <testcase classname="MyTests" name="f()" time="12344.0"> <skipped>Because I said so</skipped> </testcase> </testsuite> </testsuites> ``` See also swiftlang/swift-package-manager#7383 which asks for this for XCTest. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 2b1d626 commit 133d65e

File tree

1 file changed

+40
-9
lines changed

1 file changed

+40
-9
lines changed

Sources/Testing/Events/Recorder/Event.JUnitXMLRecorder.swift

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
extension Event {
1212
/// A type which handles ``Event`` instances and outputs representations of
1313
/// them as JUnit-compatible XML.
14+
///
15+
/// The maintainers of JUnit do not publish a formal XML schema. A _de facto_
16+
/// schema is described in the [JUnit repository](https://github.com/junit-team/junit5/blob/main/junit-platform-reporting/src/main/java/org/junit/platform/reporting/legacy/xml/XmlReportWriter.java).
1417
@_spi(ForToolsIntegrationOnly)
1518
public struct JUnitXMLRecorder: Sendable/*, ~Copyable*/ {
1619
/// The write function for this event recorder.
@@ -43,6 +46,9 @@ extension Event {
4346

4447
/// Any issues recorded for the test.
4548
var issues = [Issue]()
49+
50+
/// Information about the test if it was skipped.
51+
var skipInfo: SkipInfo?
4652
}
4753

4854
/// Data tracked on a per-test basis.
@@ -105,7 +111,12 @@ extension Event.JUnitXMLRecorder {
105111
context.testData[keyPath]?.endInstant = instant
106112
}
107113
return nil
108-
case .testSkipped where false == test?.isSuite:
114+
case let .testSkipped(skipInfo) where false == test?.isSuite:
115+
let id = test!.id
116+
let keyPath = id.keyPathRepresentation
117+
_context.withLock { context in
118+
context.testData[keyPath] = _Context.TestData(id: id, startInstant: instant, skipInfo: skipInfo)
119+
}
109120
return nil
110121
case let .issueRecorded(issue):
111122
if issue.isKnown {
@@ -124,10 +135,13 @@ extension Event.JUnitXMLRecorder {
124135
let issueCount = context.testData
125136
.compactMap(\.value?.issues.count)
126137
.reduce(into: 0, +=)
138+
let skipCount = context.testData
139+
.compactMap(\.value?.skipInfo)
140+
.count
127141
let durationNanoseconds = context.runStartInstant.map { $0.nanoseconds(until: instant) } ?? 0
128142
let durationSeconds = Double(durationNanoseconds) / 1_000_000_000
129143
return #"""
130-
<testsuite name="TestResults" errors="0" tests="\#(context.testCount)" failures="\#(issueCount)" time="\#(durationSeconds)">
144+
<testsuite name="TestResults" errors="0" tests="\#(context.testCount)" failures="\#(issueCount)" skipped="\#(skipCount)" time="\#(durationSeconds)">
131145
\#(Self._xml(for: context.testData))
132146
</testsuite>
133147
</testsuites>
@@ -158,13 +172,25 @@ extension Event.JUnitXMLRecorder {
158172
let name = id.nameComponents.last!
159173
let durationNanoseconds = testData.startInstant.nanoseconds(until: testData.endInstant ?? .now)
160174
let durationSeconds = Double(durationNanoseconds) / 1_000_000_000
161-
if testData.issues.isEmpty {
175+
176+
// Build out any child nodes contained within this <testcase> node.
177+
var minutiae = [String]()
178+
for issue in testData.issues.lazy.map(String.init(describingForTest:)) {
179+
minutiae.append(#" <failure message="\#(Self._escapeForXML(issue))" />"#)
180+
}
181+
if let skipInfo = testData.skipInfo {
182+
if let comment = skipInfo.comment.map(String.init(describingForTest:)) {
183+
minutiae.append(#" <skipped>\#(Self._escapeForXML(comment))</skipped>"#)
184+
} else {
185+
minutiae.append(#" <skipped />"#)
186+
}
187+
}
188+
189+
if minutiae.isEmpty {
162190
result.append(#" <testcase classname="\#(className)" name="\#(name)" time="\#(durationSeconds)" />"#)
163191
} else {
164192
result.append(#" <testcase classname="\#(className)" name="\#(name)" time="\#(durationSeconds)">"#)
165-
result += testData.issues.lazy
166-
.map(String.init(describing:))
167-
.map { #" <failure message="\#(Self._escapeForXML($0))" />"# }
193+
result += minutiae
168194
result.append(#" </testcase>"#)
169195
}
170196
} else {
@@ -183,14 +209,19 @@ extension Event.JUnitXMLRecorder {
183209
///
184210
/// - Returns: `character`, or a string containing its escaped form.
185211
private static func _escapeForXML(_ character: Character) -> String {
186-
if character == #"""# {
212+
switch character {
213+
case #"""#:
187214
"&quot;"
188-
} else if !character.isASCII {
215+
case "<":
216+
"&lt;"
217+
case ">":
218+
"&gt;"
219+
case _ where !character.isASCII:
189220
character.unicodeScalars.lazy
190221
.map(\.value)
191222
.map { "&#\($0);" }
192223
.joined()
193-
} else {
224+
default:
194225
String(character)
195226
}
196227
}

0 commit comments

Comments
 (0)