Skip to content

Commit 30d3c4c

Browse files
committed
Create an xunit results file even if no tests are run.
This PR ensures that when `--xunit-output` is passed to `swift test --parallel`, the output XML file is generated even if no tests are run. Failure modes outside of test failures (i.e. errors thrown during the build process or when trying to run the test process) will still prevent the generation of this file. Resolves #7065.
1 parent 8e318dc commit 30d3c4c

File tree

6 files changed

+73
-32
lines changed

6 files changed

+73
-32
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// swift-tools-version:4.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "EmptyTestsPkg",
6+
targets: [
7+
.target(
8+
name: "EmptyTestsPkg",
9+
dependencies: []),
10+
.testTarget(
11+
name: "EmptyTestsPkgTests",
12+
dependencies: ["EmptyTestsPkg"]),
13+
]
14+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
struct EmptyTests {
2+
3+
var text = "Hello, World!"
4+
var bool = false
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import XCTest
2+
@testable import EmptyTestsPkg

Sources/Commands/SwiftTestTool.swift

+32-32
Original file line numberDiff line numberDiff line change
@@ -237,32 +237,45 @@ public struct SwiftTestTool: SwiftCommand {
237237
let tests = try testSuites
238238
.filteredTests(specifier: options.testCaseSpecifier)
239239
.skippedTests(specifier: options.skippedTests(fileSystem: swiftTool.fileSystem))
240+
var testResults = [ParallelTestRunner.TestResult]()
240241

241242
// If there were no matches, emit a warning and exit.
242243
if tests.isEmpty {
243244
swiftTool.observabilityScope.emit(.noMatchingTests)
244-
return
245-
}
245+
} else {
246+
// Clean out the code coverage directory that may contain stale
247+
// profraw files from a previous run of the code coverage tool.
248+
if self.options.enableCodeCoverage {
249+
try swiftTool.fileSystem.removeFileTree(buildParameters.codeCovPath)
250+
}
246251

247-
// Clean out the code coverage directory that may contain stale
248-
// profraw files from a previous run of the code coverage tool.
249-
if self.options.enableCodeCoverage {
250-
try swiftTool.fileSystem.removeFileTree(buildParameters.codeCovPath)
251-
}
252+
// Run the tests using the parallel runner.
253+
let runner = ParallelTestRunner(
254+
bundlePaths: testProducts.map { $0.bundlePath },
255+
cancellator: swiftTool.cancellator,
256+
toolchain: toolchain,
257+
numJobs: options.numberOfWorkers ?? ProcessInfo.processInfo.activeProcessorCount,
258+
buildOptions: globalOptions.build,
259+
buildParameters: buildParameters,
260+
shouldOutputSuccess: swiftTool.logLevel <= .info,
261+
observabilityScope: swiftTool.observabilityScope
262+
)
252263

253-
// Run the tests using the parallel runner.
254-
let runner = ParallelTestRunner(
255-
bundlePaths: testProducts.map { $0.bundlePath },
256-
cancellator: swiftTool.cancellator,
257-
toolchain: toolchain,
258-
numJobs: options.numberOfWorkers ?? ProcessInfo.processInfo.activeProcessorCount,
259-
buildOptions: globalOptions.build,
260-
buildParameters: buildParameters,
261-
shouldOutputSuccess: swiftTool.logLevel <= .info,
262-
observabilityScope: swiftTool.observabilityScope
263-
)
264+
testResults = try runner.run(tests)
265+
266+
// process code Coverage if request
267+
if self.options.enableCodeCoverage, runner.ranSuccessfully {
268+
try processCodeCoverage(testProducts, swiftTool: swiftTool, library: .xctest)
269+
}
264270

265-
let testResults = try runner.run(tests)
271+
if !runner.ranSuccessfully {
272+
swiftTool.executionStatus = .failure
273+
}
274+
275+
if self.options.enableExperimentalTestOutput, !runner.ranSuccessfully {
276+
try Self.handleTestOutput(buildParameters: buildParameters, packagePath: testProducts[0].packagePath)
277+
}
278+
}
266279

267280
// Generate xUnit file if requested
268281
if let xUnitOutput = options.xUnitOutput {
@@ -272,19 +285,6 @@ public struct SwiftTestTool: SwiftCommand {
272285
)
273286
try generator.generate(at: xUnitOutput)
274287
}
275-
276-
// process code Coverage if request
277-
if self.options.enableCodeCoverage, runner.ranSuccessfully {
278-
try processCodeCoverage(testProducts, swiftTool: swiftTool, library: .xctest)
279-
}
280-
281-
if !runner.ranSuccessfully {
282-
swiftTool.executionStatus = .failure
283-
}
284-
285-
if self.options.enableExperimentalTestOutput, !runner.ranSuccessfully {
286-
try Self.handleTestOutput(buildParameters: buildParameters, packagePath: testProducts[0].packagePath)
287-
}
288288
}
289289
}
290290

Tests/CommandsTests/TestToolTests.swift

+16
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ final class TestToolTests: CommandsTestCase {
128128
}
129129
}
130130

131+
func testSwiftTestXMLOutputWhenEmpty() throws {
132+
try fixture(name: "Miscellaneous/EmptyTestsPkg") { fixturePath in
133+
let xUnitOutput = fixturePath.appending("result.xml")
134+
// Run tests in parallel with verbose output.
135+
let stdout = try SwiftPM.Test.execute(["--parallel", "--verbose", "--xunit-output", xUnitOutput.pathString], packagePath: fixturePath).stdout
136+
// in "swift test" test output goes to stdout
137+
XCTAssertNoMatch(stdout, .contains("passed"))
138+
XCTAssertNoMatch(stdout, .contains("failed"))
139+
140+
// Check the xUnit output.
141+
XCTAssertFileExists(xUnitOutput)
142+
let contents: String = try localFileSystem.readFileContents(xUnitOutput)
143+
XCTAssertMatch(contents, .contains("tests=\"0\" failures=\"0\""))
144+
}
145+
}
146+
131147
func testSwiftTestFilter() throws {
132148
try fixture(name: "Miscellaneous/SkipTests") { fixturePath in
133149
let (stdout, _) = try SwiftPM.Test.execute(["--filter", ".*1"], packagePath: fixturePath)

0 commit comments

Comments
 (0)