Skip to content

Commit 20c2b2d

Browse files
committed
Update xUnit to display output on failures
The generated xUnit XML file is not helpful when tests fail as it contains the message "failure", which is redundant with the test results. Update the XML output to dipslay the result output instead of static "failure" message.
1 parent dca0cc2 commit 20c2b2d

File tree

11 files changed

+168
-5
lines changed

11 files changed

+168
-5
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
/.build
3+
/.index-build
4+
/Packages
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/configuration/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "TestFailuresSwiftTesting",
12+
targets: ["TestFailuresSwiftTesting"])
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.target(
18+
name: "TestFailuresSwiftTesting"),
19+
.testTarget(
20+
name: "TestFailuresSwiftTestingTests",
21+
dependencies: ["TestFailuresSwiftTesting"]
22+
)
23+
]
24+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Testing
2+
@testable import TestFailuresSwiftTesting
3+
4+
@Test func example() async throws {
5+
#expect(Bool(false), "Purposely failing & validating XML espace \"'<>")
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
/.build
3+
/.index-build
4+
/Packages
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/configuration/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "TestFailures",
12+
targets: ["TestFailures"])
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.target(
18+
name: "TestFailures"),
19+
.testTarget(
20+
name: "TestFailuresTests",
21+
dependencies: ["TestFailures"]
22+
)
23+
]
24+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import XCTest
2+
@testable import TestFailures
3+
4+
final class TestFailuresTests: XCTestCase {
5+
func testExample() throws {
6+
XCTAssertFalse(true, "Purposely failing & validating XML espace \"'<>")
7+
}
8+
}
9+

Sources/Commands/SwiftTestCommand.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1399,7 +1399,12 @@ final class XUnitGenerator {
13991399
"""
14001400

14011401
if !result.success {
1402-
content += "<failure message=\"failed\"></failure>\n"
1402+
var failureMessage = result.output
1403+
failureMessage.replace("&", with: "&amp;")
1404+
failureMessage.replace("\"", with:"&quot;")
1405+
failureMessage.replace(">", with: "&gt;")
1406+
failureMessage.replace("<", with: "&lt;")
1407+
content += "<failure message=\"\(failureMessage)\"></failure>\n"
14031408
}
14041409

14051410
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),

Tests/CommandsTests/TestCommandTests.swift

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ import _InternalTestSupport
1717
import XCTest
1818

1919
final class TestCommandTests: CommandsTestCase {
20-
private func execute(_ args: [String], packagePath: AbsolutePath? = nil) async throws -> (stdout: String, stderr: String) {
21-
try await SwiftPM.Test.execute(args, packagePath: packagePath)
20+
private func execute(
21+
_ args: [String],
22+
packagePath: AbsolutePath? = nil,
23+
errorIfCommandUnsuccessful: Bool = true
24+
) async throws -> (stdout: String, stderr: String) {
25+
try await SwiftPM.Test.execute(args, packagePath: packagePath, errorIfCommandUnsuccessful: errorIfCommandUnsuccessful)
2226
}
2327

2428
func testUsage() async throws {
@@ -157,6 +161,71 @@ final class TestCommandTests: CommandsTestCase {
157161
}
158162
}
159163

164+
enum TestRunner {
165+
case XCTest
166+
case SwiftTesting
167+
168+
var fileSuffix: String {
169+
switch self {
170+
case .XCTest: return ""
171+
case .SwiftTesting: return "-swift-testing"
172+
}
173+
}
174+
}
175+
func _testSwiftTestXMLOutputFailureMessage(
176+
fixtureName: String,
177+
testRunner: TestRunner
178+
) async throws {
179+
try await fixture(name: fixtureName) { fixturePath in
180+
// GIVEN we have a Package with a failing \(testRunner) test cases
181+
let xUnitOutput = fixturePath.appending("result.xml")
182+
let xUnitUnderTest = fixturePath.appending("result\(testRunner.fileSuffix).xml")
183+
184+
// WHEN we execute swift-test in parallel while specifying xUnit generation
185+
_ = try await execute(
186+
[
187+
"--parallel",
188+
"--verbose",
189+
"--enable-swift-testing",
190+
"--enable-xctest",
191+
"--xunit-output",
192+
xUnitOutput.pathString
193+
],
194+
packagePath: fixturePath,
195+
errorIfCommandUnsuccessful: false
196+
)
197+
198+
// THEN we expect \(xUnitUnderTest) to exists
199+
XCTAssertFileExists(xUnitUnderTest)
200+
let contents: String = try localFileSystem.readFileContents(xUnitUnderTest)
201+
// AND that the xUnit file has the expected contents"
202+
XCTAssertMatch(
203+
contents,
204+
.contains("Purposely failing &amp; validating XML espace &quot;'&lt;&gt;")
205+
)
206+
}
207+
}
208+
209+
func testSwiftTestXMLOutputVerifyFailureMessageXCTest() async throws {
210+
try await self._testSwiftTestXMLOutputFailureMessage(
211+
fixtureName: "Miscellaneous/TestFailuresXCTest",
212+
testRunner: .XCTest
213+
)
214+
}
215+
216+
func testSwiftTestXMLOutputVerifyFailureMessageSwiftTesting() async throws {
217+
#if compiler(<6)
218+
_ = XCTSkip("SwifT Testing is not available by default in this Swift compiler version")
219+
220+
#else
221+
222+
try await self._testSwiftTestXMLOutputFailureMessage(
223+
fixtureName: "Miscellaneous/TestFailuresSwiftTesting",
224+
testRunner: .SwiftTesting
225+
)
226+
#endif
227+
}
228+
160229
func testSwiftTestFilter() async throws {
161230
try await fixture(name: "Miscellaneous/SkipTests") { fixturePath in
162231
let (stdout, _) = try await SwiftPM.Test.execute(["--filter", ".*1"], packagePath: fixturePath)

0 commit comments

Comments
 (0)