Skip to content

Commit 4f29d57

Browse files
committed
Update xUnit to display output on failures (XCTest only)
For XCTest, the generated xUnit XML file is not helpful when tests fail as it contains the message "failure", which is redundant with the test results. Add a hidden flag (--xunit-message-failure) that will update the XML output to dipslay the result output instead of static "failure" message.
1 parent 11ebe9e commit 4f29d57

File tree

11 files changed

+380
-9
lines changed

11 files changed

+380
-9
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "TestMultipleFailureSwiftTesting",
8+
targets: [
9+
.testTarget(
10+
name: "TestMultipleFailureSwiftTestingTests"
11+
)
12+
]
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Testing
2+
3+
@Test func testFailure1() async throws {
4+
#expect(Bool(false), "ST Test failure 1")
5+
}
6+
7+
@Test func testFailure2() async throws {
8+
#expect(Bool(false), "ST Test failure 2")
9+
}
10+
11+
@Test func testFailure3() async throws {
12+
#expect(Bool(false), "ST Test failure 3")
13+
}
14+
15+
@Test func testFailure4() async throws {
16+
#expect(Bool(false), "ST Test failure 4")
17+
}
18+
19+
@Test func testFailure5() async throws {
20+
#expect(Bool(false), "ST Test failure 5")
21+
}
22+
23+
@Test func testFailure6() async throws {
24+
#expect(Bool(false), "ST Test failure 6")
25+
}
26+
27+
@Test func testFailure7() async throws {
28+
#expect(Bool(false), "ST Test failure 7")
29+
}
30+
31+
@Test func testFailure8() async throws {
32+
#expect(Bool(false), "ST Test failure 8")
33+
}
34+
35+
@Test func testFailure9() async throws {
36+
#expect(Bool(false), "ST Test failure 9")
37+
}
38+
39+
@Test func testFailure10() async throws {
40+
#expect(Bool(false), "ST Test failure 10")
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "TestMultipleFailureXCTest",
8+
targets: [
9+
.testTarget(
10+
name: "TestMultipleFailureXCTestTests"
11+
)
12+
]
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import XCTest
2+
3+
final class TestMultipleFailureXCTestTests: XCTestCase {
4+
func testFailure1() throws {
5+
XCTAssertFalse(true, "Test failure 1")
6+
}
7+
8+
func testFailure2() throws {
9+
XCTAssertFalse(true, "Test failure 2")
10+
}
11+
12+
func testFailure3() throws {
13+
XCTAssertFalse(true, "Test failure 3")
14+
}
15+
16+
func testFailure4() throws {
17+
XCTAssertFalse(true, "Test failure 4")
18+
}
19+
20+
func testFailure5() throws {
21+
XCTAssertFalse(true, "Test failure 5")
22+
}
23+
24+
func testFailure6() throws {
25+
XCTAssertFalse(true, "Test failure 6")
26+
}
27+
28+
func testFailure7() throws {
29+
XCTAssertFalse(true, "Test failure 7")
30+
}
31+
32+
func testFailure8() throws {
33+
XCTAssertFalse(true, "Test failure 8")
34+
}
35+
36+
func testFailure9() throws {
37+
XCTAssertFalse(true, "Test failure 9")
38+
}
39+
40+
func testFailure10() throws {
41+
XCTAssertFalse(true, "Test failure 10")
42+
}
43+
44+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "TestFailuresSwiftTesting",
8+
targets: [
9+
.testTarget(
10+
name: "TestFailuresSwiftTestingTests"
11+
)
12+
]
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Testing
2+
3+
@Test func example() async throws {
4+
#expect(Bool(false), "Purposely failing & validating XML espace \"'<>")
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "TestFailures",
8+
targets: [
9+
.testTarget(
10+
name: "TestFailuresTests"
11+
)
12+
]
13+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import XCTest
2+
3+
final class TestFailuresTests: XCTestCase {
4+
func testExample() throws {
5+
XCTAssertFalse(true, "Purposely failing & validating XML espace \"'<>")
6+
}
7+
}
8+

Sources/Commands/SwiftTestCommand.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,15 @@ struct TestCommandOptions: ParsableArguments {
188188
help: "Path where the xUnit xml file should be generated.")
189189
var xUnitOutput: AbsolutePath?
190190

191+
@Flag(
192+
name: .customLong("experimental-xunit-message-failure"),
193+
help: ArgumentHelp(
194+
"When Set, enabled an experimental message failure content (XCTest only).",
195+
visibility: .hidden
196+
)
197+
)
198+
var shouldShowDetailedFailureMessage: Bool = false
199+
191200
/// Generate LinuxMain entries and exit.
192201
@Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Enabled by default.")
193202
var enableTestableImports: Bool = true
@@ -330,7 +339,11 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
330339
result = runner.ranSuccessfully ? .success : .failure
331340
}
332341

333-
try generateXUnitOutputIfRequested(for: testResults, swiftCommandState: swiftCommandState)
342+
try generateXUnitOutputIfRequested(
343+
for: testResults,
344+
swiftCommandState: swiftCommandState,
345+
detailedFailureMessage: self.options.shouldShowDetailedFailureMessage
346+
)
334347
results.append(result)
335348
}
336349
}
@@ -406,7 +419,8 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
406419
/// Generate xUnit file if requested.
407420
private func generateXUnitOutputIfRequested(
408421
for testResults: [ParallelTestRunner.TestResult],
409-
swiftCommandState: SwiftCommandState
422+
swiftCommandState: SwiftCommandState,
423+
detailedFailureMessage: Bool
410424
) throws {
411425
guard let xUnitOutput = options.xUnitOutput else {
412426
return
@@ -416,7 +430,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
416430
fileSystem: swiftCommandState.fileSystem,
417431
results: testResults
418432
)
419-
try generator.generate(at: xUnitOutput)
433+
try generator.generate(at: xUnitOutput, detailedFailureMessage: detailedFailureMessage)
420434
}
421435

422436
// MARK: - Common implementation
@@ -1371,7 +1385,7 @@ final class XUnitGenerator {
13711385
}
13721386

13731387
/// Generate the file at the given path.
1374-
func generate(at path: AbsolutePath) throws {
1388+
func generate(at path: AbsolutePath, detailedFailureMessage: Bool) throws {
13751389
var content =
13761390
"""
13771391
<?xml version="1.0" encoding="UTF-8"?>
@@ -1404,7 +1418,15 @@ final class XUnitGenerator {
14041418
"""
14051419

14061420
if !result.success {
1407-
content += "<failure message=\"failed\"></failure>\n"
1421+
var failureMessage: String = "failed"
1422+
if detailedFailureMessage {
1423+
failureMessage = result.output
1424+
failureMessage.replace("&", with: "&amp;")
1425+
failureMessage.replace("\"", with:"&quot;")
1426+
failureMessage.replace(">", with: "&gt;")
1427+
failureMessage.replace("<", with: "&lt;")
1428+
}
1429+
content += "<failure message=\"\(failureMessage)\"></failure>\n"
14081430
}
14091431

14101432
content += "</testcase>\n"

Sources/_InternalTestSupport/SwiftPMProduct.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ extension SwiftPM {
8282
public func execute(
8383
_ args: [String] = [],
8484
packagePath: AbsolutePath? = nil,
85-
env: Environment? = nil
85+
env: Environment? = nil,
86+
errorIfCommandUnsuccessful: Bool = true
8687
) async throws -> (stdout: String, stderr: String) {
8788
let result = try await executeProcess(
8889
args,
@@ -93,8 +94,11 @@ extension SwiftPM {
9394
let stdout = try result.utf8Output()
9495
let stderr = try result.utf8stderrOutput()
9596

97+
let returnValue = (stdout: stdout, stderr: stderr)
98+
if (!errorIfCommandUnsuccessful) { return returnValue }
99+
96100
if result.exitStatus == .terminated(code: 0) {
97-
return (stdout: stdout, stderr: stderr)
101+
return returnValue
98102
}
99103
throw SwiftPMError.executionFailure(
100104
underlying: AsyncProcessResult.Error.nonZeroExit(result),

0 commit comments

Comments
 (0)