From b8da89508805c47fa44067bc82e5743845ae0159 Mon Sep 17 00:00:00 2001
From: Bassam Khouri <bkhouri@apple.com>
Date: Wed, 27 Nov 2024 23:39:36 -0500
Subject: [PATCH] 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.

Update the XML output to dipslay the result output instead of static
"failure" message.
---
 .../.gitignore                                |   8 +
 .../Package.swift                             |  24 +++
 .../TestMultipleFailureSwiftTesting.swift     |   2 +
 ...TestMultipleFailureSwiftTestingTests.swift |  42 +++++
 .../TestMultipleFailureXCTest/.gitignore      |   8 +
 .../TestMultipleFailureXCTest/Package.swift   |  24 +++
 .../TestMultipleFailureXCTest.swift           |   2 +
 .../TestMultipleFailureXCTestTests.swift      |  45 +++++
 .../TestSingleFailureSwiftTesting/.gitignore  |   9 +
 .../Package.swift                             |  24 +++
 .../TestFailuresSwiftTesting.swift            |   2 +
 .../TestFailuresSwiftTestingTests.swift       |   6 +
 .../TestSingleFailureXCTest/.gitignore        |   9 +
 .../TestSingleFailureXCTest/Package.swift     |  24 +++
 .../Sources/TestFailures/TestFailures.swift   |   2 +
 .../TestFailuresTests/TestFailuresTests.swift |   9 +
 Sources/Commands/SwiftTestCommand.swift       |  29 ++-
 .../_InternalTestSupport/SwiftPMProduct.swift |   8 +-
 Tests/CommandsTests/TestCommandTests.swift    | 173 +++++++++++++++++-
 19 files changed, 441 insertions(+), 9 deletions(-)
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/.gitignore
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Sources/TestMultipleFailureSwiftTesting/TestMultipleFailureSwiftTesting.swift
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureXCTest/.gitignore
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureXCTest/Sources/TestMultipleFailureXCTest/TestMultipleFailureXCTest.swift
 create mode 100644 Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/.gitignore
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Sources/TestFailuresSwiftTesting/TestFailuresSwiftTesting.swift
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureXCTest/.gitignore
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureXCTest/Sources/TestFailures/TestFailures.swift
 create mode 100644 Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift

diff --git a/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/.gitignore b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/.gitignore
new file mode 100644
index 00000000000..0023a534063
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift
new file mode 100644
index 00000000000..37c94c43e26
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift
@@ -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"]
+        ),
+    ]
+)
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Sources/TestMultipleFailureSwiftTesting/TestMultipleFailureSwiftTesting.swift b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Sources/TestMultipleFailureSwiftTesting/TestMultipleFailureSwiftTesting.swift
new file mode 100644
index 00000000000..08b22b80fc0
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Sources/TestMultipleFailureSwiftTesting/TestMultipleFailureSwiftTesting.swift
@@ -0,0 +1,2 @@
+// The Swift Programming Language
+// https://docs.swift.org/swift-book
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift
new file mode 100644
index 00000000000..afcbc90fede
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift
@@ -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")
+}
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureXCTest/.gitignore b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/.gitignore
new file mode 100644
index 00000000000..0023a534063
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift
new file mode 100644
index 00000000000..83d7d49d933
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift
@@ -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"]
+        ),
+    ]
+)
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Sources/TestMultipleFailureXCTest/TestMultipleFailureXCTest.swift b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Sources/TestMultipleFailureXCTest/TestMultipleFailureXCTest.swift
new file mode 100644
index 00000000000..08b22b80fc0
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Sources/TestMultipleFailureXCTest/TestMultipleFailureXCTest.swift
@@ -0,0 +1,2 @@
+// The Swift Programming Language
+// https://docs.swift.org/swift-book
diff --git a/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift
new file mode 100644
index 00000000000..7ded16e5dde
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift
@@ -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")
+    }
+
+}
diff --git a/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/.gitignore b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/.gitignore
new file mode 100644
index 00000000000..10edc03d849
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/.gitignore
@@ -0,0 +1,9 @@
+.DS_Store
+/.build
+/.index-build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift
new file mode 100644
index 00000000000..ecd8c3a2c12
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift
@@ -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"]
+        )
+    ]
+)
diff --git a/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Sources/TestFailuresSwiftTesting/TestFailuresSwiftTesting.swift b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Sources/TestFailuresSwiftTesting/TestFailuresSwiftTesting.swift
new file mode 100644
index 00000000000..08b22b80fc0
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Sources/TestFailuresSwiftTesting/TestFailuresSwiftTesting.swift
@@ -0,0 +1,2 @@
+// The Swift Programming Language
+// https://docs.swift.org/swift-book
diff --git a/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift
new file mode 100644
index 00000000000..9c5c1a8b301
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift
@@ -0,0 +1,6 @@
+import Testing
+@testable import TestFailuresSwiftTesting
+
+@Test func example() async throws {
+    #expect(Bool(false), "Purposely failing & validating XML espace \"'<>")
+}
diff --git a/Fixtures/Miscellaneous/TestSingleFailureXCTest/.gitignore b/Fixtures/Miscellaneous/TestSingleFailureXCTest/.gitignore
new file mode 100644
index 00000000000..10edc03d849
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureXCTest/.gitignore
@@ -0,0 +1,9 @@
+.DS_Store
+/.build
+/.index-build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift
new file mode 100644
index 00000000000..33bc6ef13cd
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift
@@ -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"]
+        )
+    ]
+)
diff --git a/Fixtures/Miscellaneous/TestSingleFailureXCTest/Sources/TestFailures/TestFailures.swift b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Sources/TestFailures/TestFailures.swift
new file mode 100644
index 00000000000..08b22b80fc0
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Sources/TestFailures/TestFailures.swift
@@ -0,0 +1,2 @@
+// The Swift Programming Language
+// https://docs.swift.org/swift-book
diff --git a/Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift
new file mode 100644
index 00000000000..b17c7912535
--- /dev/null
+++ b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift
@@ -0,0 +1,9 @@
+import XCTest
+@testable import TestFailures
+
+final class TestFailuresTests: XCTestCase {
+    func testExample() throws {
+        XCTAssertFalse(true, "Purposely failing & validating XML espace \"'<>")
+    }
+}
+
diff --git a/Sources/Commands/SwiftTestCommand.swift b/Sources/Commands/SwiftTestCommand.swift
index 778e06ef785..9a437e7cf26 100644
--- a/Sources/Commands/SwiftTestCommand.swift
+++ b/Sources/Commands/SwiftTestCommand.swift
@@ -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
@@ -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)
             }
         }
@@ -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
@@ -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
@@ -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"?>
@@ -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"
diff --git a/Sources/_InternalTestSupport/SwiftPMProduct.swift b/Sources/_InternalTestSupport/SwiftPMProduct.swift
index b256097ae7b..8f4235a4025 100644
--- a/Sources/_InternalTestSupport/SwiftPMProduct.swift
+++ b/Sources/_InternalTestSupport/SwiftPMProduct.swift
@@ -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,
@@ -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),
diff --git a/Tests/CommandsTests/TestCommandTests.swift b/Tests/CommandsTests/TestCommandTests.swift
index 033e6e5bf1e..a378549dc37 100644
--- a/Tests/CommandsTests/TestCommandTests.swift
+++ b/Tests/CommandsTests/TestCommandTests.swift
@@ -14,11 +14,16 @@ import Basics
 import Commands
 import PackageModel
 import _InternalTestSupport
+import TSCTestSupport
 import XCTest
 
 final class TestCommandTests: CommandsTestCase {
-    private func execute(_ args: [String], packagePath: AbsolutePath? = nil) async throws -> (stdout: String, stderr: String) {
-        try await SwiftPM.Test.execute(args, packagePath: packagePath)
+    private func execute(
+        _ args: [String],
+        packagePath: AbsolutePath? = nil,
+        errorIfCommandUnsuccessful: Bool = true
+    ) async throws -> (stdout: String, stderr: String) {
+        try await SwiftPM.Test.execute(args, packagePath: packagePath, errorIfCommandUnsuccessful: errorIfCommandUnsuccessful)
     }
 
     func testUsage() async throws {
@@ -157,6 +162,170 @@ final class TestCommandTests: CommandsTestCase {
         }
     }
 
+    enum TestRunner {
+        case XCTest
+        case SwiftTesting
+
+        var fileSuffix: String {
+            switch self {
+                case .XCTest: return ""
+                case .SwiftTesting: return "-swift-testing"
+            }
+        }
+    }
+    func _testSwiftTestXMLOutputFailureMessage(
+        fixtureName: String,
+        testRunner: TestRunner,
+        enableExperimentalFlag: Bool,
+        matchesPattern: [StringPattern]
+    ) async throws {
+        try await fixture(name: fixtureName) { fixturePath in
+            // GIVEN we have a Package with a failing \(testRunner) test cases
+            let xUnitOutput = fixturePath.appending("result.xml")
+            let xUnitUnderTest = fixturePath.appending("result\(testRunner.fileSuffix).xml")
+
+            // WHEN we execute swift-test in parallel while specifying xUnit generation
+            let extraCommandArgs = enableExperimentalFlag ? ["--experimental-xunit-message-failure"]: [],
+            _ = try await execute(
+                [
+                    "--parallel",
+                    "--verbose",
+                    "--enable-swift-testing",
+                    "--enable-xctest",
+                    "--xunit-output",
+                    xUnitOutput.pathString
+                ] + extraCommandArgs,
+                packagePath: fixturePath,
+                errorIfCommandUnsuccessful: false
+            )
+
+            // THEN we expect \(xUnitUnderTest) to exists
+            XCTAssertFileExists(xUnitUnderTest)
+            let contents: String = try localFileSystem.readFileContents(xUnitUnderTest)
+            // AND that the xUnit file has the expected contents
+            for match in matchesPattern {
+                XCTAssertMatch(contents, match)
+            }
+        }
+    }
+
+    func testSwiftTestXMLOutputVerifySingleTestFailureMessageWithFlagEnabledXCTest() async throws {
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestSingleFailureXCTest",
+            testRunner: .XCTest,
+            enableExperimentalFlag: true,
+            matchesPattern: [.contains("Purposely failing &amp; validating XML espace &quot;'&lt;&gt;")]
+        )
+    }
+
+    func testSwiftTestXMLOutputVerifySingleTestFailureMessageWithFlagEnabledSwiftTesting() async throws {
+        #if compiler(<6)
+        _ = XCTSkip("SwifT Testing is not available by default in this Swift compiler version")
+        #else
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestSingleFailureSwiftTesting",
+            testRunner: .SwiftTesting,
+            enableExperimentalFlag: true,
+            matchesPattern: [.contains("Purposely failing &amp; validating XML espace &quot;'&lt;&gt;")]
+        )
+        #endif
+    }
+    func testSwiftTestXMLOutputVerifySingleTestFailureMessageWithFlagDisabledXCTest() async throws {
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestSingleFailureXCTest",
+            testRunner: .XCTest,
+            enableExperimentalFlag: false,
+            matchesPattern: [.contains("failure")]
+        )
+    }
+
+    func testSwiftTestXMLOutputVerifySingleTestFailureMessageWithFlagDisabledSwiftTesting() async throws {
+        #if compiler(<6)
+        _ = XCTSkip("SwifT Testing is not available by default in this Swift compiler version")
+        #else
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestSingleFailureSwiftTesting",
+            testRunner: .SwiftTesting,
+            enableExperimentalFlag: false,
+            matchesPattern: [.contains("failure")]
+        )
+        #endif
+    }
+
+    func testSwiftTestXMLOutputVerifyMultipleTestFailureMessageWithFlagEnabledXCTest() async throws {
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestMultipleFailureXCTest",
+            testRunner: .XCTest,
+            enableExperimentalFlag: true,
+            matchesPattern: [
+                .contains("Test failure 1"),
+                .contains("Test failure 2"),
+                .contains("Test failure 3"),
+                .contains("Test failure 4"),
+                .contains("Test failure 5"),
+                .contains("Test failure 6"),
+                .contains("Test failure 7"),
+                .contains("Test failure 8"),
+                .contains("Test failure 9"),
+                .contains("Test failure 10")
+            ]
+        )
+    }
+
+    func testSwiftTestXMLOutputVerifyMultipleTestFailureMessageWithFlagEnabledSwiftTesting() async throws {
+        #if compiler(<6)
+        _ = XCTSkip("SwifT Testing is not available by default in this Swift compiler version")
+        #else
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestMultipleFailureSwiftTesting",
+            testRunner: .SwiftTesting,
+            enableExperimentalFlag: true,
+            matchesPattern: [
+                .contains("Test failure 1"),
+                .contains("Test failure 2"),
+                .contains("Test failure 3"),
+                .contains("Test failure 4"),
+                .contains("Test failure 5"),
+                .contains("Test failure 6"),
+                .contains("Test failure 7"),
+                .contains("Test failure 8"),
+                .contains("Test failure 9"),
+                .contains("Test failure 10")
+            ]
+        )
+        #endif
+    }
+
+    func testSwiftTestXMLOutputVerifyMultipleTestFailureMessageWithFlagDisabledXCTest() async throws {
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestMultipleFailureXCTest",
+            testRunner: .XCTest,
+            enableExperimentalFlag: false,
+            matchesPattern: [
+                .contains("failure"),
+                .contains("failure"),
+                .contains("failure")
+            ]
+        )
+    }
+
+    func testSwiftTestXMLOutputVerifyMultipleTestFailureMessageWithFlagDisabledSwiftTesting() async throws {
+        #if compiler(<6)
+        _ = XCTSkip("SwifT Testing is not available by default in this Swift compiler version")
+        #else
+        try await self._testSwiftTestXMLOutputFailureMessage(
+            fixtureName: "Miscellaneous/TestMultipleFailureSwiftTesting",
+            testRunner: .SwiftTesting,
+            enableExperimentalFlag: false,
+            matchesPattern: [
+                .contains("failure"),
+                .contains("failure"),
+                .contains("failure")
+            ]
+        )
+        #endif
+    }
+
     func testSwiftTestFilter() async throws {
         try await fixture(name: "Miscellaneous/SkipTests") { fixturePath in
             let (stdout, _) = try await SwiftPM.Test.execute(["--filter", ".*1"], packagePath: fixturePath)