Skip to content

Commit

Permalink
Update xUnit to display output on failures (XCTest only)
Browse files Browse the repository at this point in the history
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.

Update the XML output to dipslay the result output instead of static
"failure" message.
  • Loading branch information
bkhouri committed Dec 3, 2024
1 parent dca0cc2 commit b8da895
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "TestMultipleFailureSwiftTesting",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "TestMultipleFailureSwiftTesting",
targets: ["TestMultipleFailureSwiftTesting"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "TestMultipleFailureSwiftTesting"),
.testTarget(
name: "TestMultipleFailureSwiftTestingTests",
dependencies: ["TestMultipleFailureSwiftTesting"]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Testing
@testable import TestMultipleFailureSwiftTesting

@Test func testFailure1() async throws {
#expect(Bool(false), "ST Test failure 1")
}

@Test func testFailure2() async throws {
#expect(Bool(false), "ST Test failure 2")
}

@Test func testFailure3() async throws {
#expect(Bool(false), "ST Test failure 3")
}

@Test func testFailure4() async throws {
#expect(Bool(false), "ST Test failure 4")
}

@Test func testFailure5() async throws {
#expect(Bool(false), "ST Test failure 5")
}

@Test func testFailure6() async throws {
#expect(Bool(false), "ST Test failure 6")
}

@Test func testFailure7() async throws {
#expect(Bool(false), "ST Test failure 7")
}

@Test func testFailure8() async throws {
#expect(Bool(false), "ST Test failure 8")
}

@Test func testFailure9() async throws {
#expect(Bool(false), "ST Test failure 9")
}

@Test func testFailure10() async throws {
#expect(Bool(false), "ST Test failure 10")
}
8 changes: 8 additions & 0 deletions Fixtures/Miscellaneous/TestMultipleFailureXCTest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
24 changes: 24 additions & 0 deletions Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "TestMultipleFailureXCTest",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "TestMultipleFailureXCTest",
targets: ["TestMultipleFailureXCTest"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "TestMultipleFailureXCTest"),
.testTarget(
name: "TestMultipleFailureXCTestTests",
dependencies: ["TestMultipleFailureXCTest"]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import XCTest
@testable import TestMultipleFailureXCTest

final class TestMultipleFailureXCTestTests: XCTestCase {
func testFailure1() throws {
XCTAssertFalse(true, "Test failure 1")
}

func testFailure2() throws {
XCTAssertFalse(true, "Test failure 2")
}

func testFailure3() throws {
XCTAssertFalse(true, "Test failure 3")
}

func testFailure4() throws {
XCTAssertFalse(true, "Test failure 4")
}

func testFailure5() throws {
XCTAssertFalse(true, "Test failure 5")
}

func testFailure6() throws {
XCTAssertFalse(true, "Test failure 6")
}

func testFailure7() throws {
XCTAssertFalse(true, "Test failure 7")
}

func testFailure8() throws {
XCTAssertFalse(true, "Test failure 8")
}

func testFailure9() throws {
XCTAssertFalse(true, "Test failure 9")
}

func testFailure10() throws {
XCTAssertFalse(true, "Test failure 10")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/.index-build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
24 changes: 24 additions & 0 deletions Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "TestFailuresSwiftTesting",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "TestFailuresSwiftTesting",
targets: ["TestFailuresSwiftTesting"])
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "TestFailuresSwiftTesting"),
.testTarget(
name: "TestFailuresSwiftTestingTests",
dependencies: ["TestFailuresSwiftTesting"]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Testing
@testable import TestFailuresSwiftTesting

@Test func example() async throws {
#expect(Bool(false), "Purposely failing & validating XML espace \"'<>")
}
9 changes: 9 additions & 0 deletions Fixtures/Miscellaneous/TestSingleFailureXCTest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/.index-build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
24 changes: 24 additions & 0 deletions Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "TestFailures",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "TestFailures",
targets: ["TestFailures"])
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "TestFailures"),
.testTarget(
name: "TestFailuresTests",
dependencies: ["TestFailures"]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import XCTest
@testable import TestFailures

final class TestFailuresTests: XCTestCase {
func testExample() throws {
XCTAssertFalse(true, "Purposely failing & validating XML espace \"'<>")
}
}

29 changes: 24 additions & 5 deletions Sources/Commands/SwiftTestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ struct TestCommandOptions: ParsableArguments {
help: "Path where the xUnit xml file should be generated.")
var xUnitOutput: AbsolutePath?

@Flag(
name: .customLong("experimental-xunit-message-failure"),
help: "When Set, enabled an experimental message failure content (XCTest only)."
)
var shouldShowDetailedFailureMessage: Bool = false

/// Generate LinuxMain entries and exit.
@Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Enabled by default.")
var enableTestableImports: Bool = true
Expand Down Expand Up @@ -325,7 +331,11 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
result = runner.ranSuccessfully ? .success : .failure
}

try generateXUnitOutputIfRequested(for: testResults, swiftCommandState: swiftCommandState)
try generateXUnitOutputIfRequested(
for: testResults,
swiftCommandState: swiftCommandState,
detailedFailureMessage: self.options.shouldShowDetailedFailureMessage
)
results.append(result)
}
}
Expand Down Expand Up @@ -401,7 +411,8 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
/// Generate xUnit file if requested.
private func generateXUnitOutputIfRequested(
for testResults: [ParallelTestRunner.TestResult],
swiftCommandState: SwiftCommandState
swiftCommandState: SwiftCommandState,
detailedFailureMessage: Bool
) throws {
guard let xUnitOutput = options.xUnitOutput else {
return
Expand All @@ -411,7 +422,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
fileSystem: swiftCommandState.fileSystem,
results: testResults
)
try generator.generate(at: xUnitOutput)
try generator.generate(at: xUnitOutput, detailedFailureMessage: detailedFailureMessage)
}

// MARK: - Common implementation
Expand Down Expand Up @@ -1366,7 +1377,7 @@ final class XUnitGenerator {
}

/// Generate the file at the given path.
func generate(at path: AbsolutePath) throws {
func generate(at path: AbsolutePath, detailedFailureMessage: Bool) throws {
var content =
"""
<?xml version="1.0" encoding="UTF-8"?>
Expand Down Expand Up @@ -1399,7 +1410,15 @@ final class XUnitGenerator {
"""

if !result.success {
content += "<failure message=\"failed\"></failure>\n"
var failureMessage: String = "failed"
if detailedFailureMessage {
failureMessage = result.output
failureMessage.replace("&", with: "&amp;")
failureMessage.replace("\"", with:"&quot;")
failureMessage.replace(">", with: "&gt;")
failureMessage.replace("<", with: "&lt;")
}
content += "<failure message=\"\(failureMessage)\"></failure>\n"
}

content += "</testcase>\n"
Expand Down
8 changes: 6 additions & 2 deletions Sources/_InternalTestSupport/SwiftPMProduct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ extension SwiftPM {
public func execute(
_ args: [String] = [],
packagePath: AbsolutePath? = nil,
env: Environment? = nil
env: Environment? = nil,
errorIfCommandUnsuccessful: Bool = true
) async throws -> (stdout: String, stderr: String) {
let result = try await executeProcess(
args,
Expand All @@ -93,8 +94,11 @@ extension SwiftPM {
let stdout = try result.utf8Output()
let stderr = try result.utf8stderrOutput()

let returnValue = (stdout: stdout, stderr: stderr)
if (!errorIfCommandUnsuccessful) { return returnValue }

if result.exitStatus == .terminated(code: 0) {
return (stdout: stdout, stderr: stderr)
return returnValue
}
throw SwiftPMError.executionFailure(
underlying: AsyncProcessResult.Error.nonZeroExit(result),
Expand Down
Loading

0 comments on commit b8da895

Please sign in to comment.