Skip to content

Commit 4fbe8e8

Browse files
authored
Add descriptive error when attempting to list tests before building (#8046)
Typically a build is performed before a `swift test list` command, but by passing the `--skip-build` flag it is possible to get a cryptic, unhelpful error message if the test artifacts haven't already been built. Add a descriptive error for this case, only checking for test artifact existence in the failure case to avoid any performance impact on the happy path.
1 parent 7e1c6b9 commit 4fbe8e8

File tree

4 files changed

+87
-2
lines changed

4 files changed

+87
-2
lines changed

Sources/Basics/Errors.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ public struct InternalError: Error {
2626
}
2727
}
2828

29+
/// Wraps another error and provides additional context when printed.
30+
/// This is useful for user facing errors that need to provide a user friendly message
31+
/// explaning why an error might have occured, while still showing the detailed underlying error.
32+
public struct ErrorWithContext<E: Error>: Error {
33+
public let error: E
34+
public let context: String
35+
public init(_ error: E, _ context: String) {
36+
self.error = error
37+
self.context = context
38+
}
39+
}
40+
41+
extension ErrorWithContext: LocalizedError {
42+
public var errorDescription: String? {
43+
return (context.split(separator: "\n") + [error.interpolationDescription])
44+
.map { "\t\($0)" }
45+
.joined(separator: "\n")
46+
}
47+
}
48+
2949
extension Error {
3050
public var interpolationDescription: String {
3151
switch self {

Sources/Commands/SwiftTestCommand.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import SPMBuildCore
3232
import func TSCLibc.exit
3333
import Workspace
3434

35+
import struct TSCBasic.FileSystemError
3536
import struct TSCBasic.ByteString
3637
import enum TSCBasic.JSON
3738
import class Basics.AsyncProcess
@@ -725,6 +726,23 @@ extension SwiftTestCommand {
725726
var _deprecated_passthrough: Bool = false
726727

727728
func run(_ swiftCommandState: SwiftCommandState) async throws {
729+
do {
730+
try await self.runCommand(swiftCommandState)
731+
} catch let error as FileSystemError {
732+
if sharedOptions.shouldSkipBuilding {
733+
throw ErrorWithContext(error, """
734+
Test build artifacts were not found in the build folder.
735+
The `--skip-build` flag was provided; either build the tests first with \
736+
`swift build --build tests` or rerun the `swift test list` command without \
737+
`--skip-build`
738+
"""
739+
)
740+
}
741+
throw error
742+
}
743+
}
744+
745+
func runCommand(_ swiftCommandState: SwiftCommandState) async throws {
728746
let (productsBuildParameters, toolsBuildParameters) = try swiftCommandState.buildParametersForTest(
729747
enableCodeCoverage: false,
730748
shouldSkipBuilding: sharedOptions.shouldSkipBuilding
@@ -781,6 +799,13 @@ extension SwiftTestCommand {
781799
})
782800
if result == .failure {
783801
swiftCommandState.executionStatus = .failure
802+
// If the runner reports failure do a check to ensure
803+
// all the binaries are present on the file system.
804+
for path in testProducts.map(\.binaryPath) {
805+
if !swiftCommandState.fileSystem.exists(path) {
806+
throw FileSystemError(.noEntry, path)
807+
}
808+
}
784809
}
785810
} else if let testEntryPointPath {
786811
// Cannot run Swift Testing because an entry point file was used and the developer

Sources/Commands/Utilities/TestingSupport.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import PackageModel
1616
import SPMBuildCore
1717
import Workspace
1818

19+
import struct TSCBasic.FileSystemError
1920
import class Basics.AsyncProcess
2021
import var TSCBasic.stderrStream
2122
import var TSCBasic.stdoutStream
@@ -123,8 +124,13 @@ enum TestingSupport {
123124
sanitizers: sanitizers,
124125
library: .xctest
125126
)
127+
try Self.runProcessWithExistenceCheck(
128+
path: path,
129+
fileSystem: swiftCommandState.fileSystem,
130+
args: args,
131+
env: env
132+
)
126133

127-
try AsyncProcess.checkNonZeroExit(arguments: args, environment: env)
128134
// Read the temporary file's content.
129135
return try swiftCommandState.fileSystem.readFileContents(AbsolutePath(tempFile.path))
130136
}
@@ -139,12 +145,36 @@ enum TestingSupport {
139145
library: .xctest
140146
)
141147
args = [path.description, "--dump-tests-json"]
142-
let data = try AsyncProcess.checkNonZeroExit(arguments: args, environment: env)
148+
let data = try Self.runProcessWithExistenceCheck(
149+
path: path,
150+
fileSystem: swiftCommandState.fileSystem,
151+
args: args,
152+
env: env
153+
)
143154
#endif
144155
// Parse json and return TestSuites.
145156
return try TestSuite.parse(jsonString: data, context: args.joined(separator: " "))
146157
}
147158

159+
/// Run a process and throw a more specific error if the file doesn't exist.
160+
@discardableResult
161+
private static func runProcessWithExistenceCheck(
162+
path: AbsolutePath,
163+
fileSystem: FileSystem,
164+
args: [String],
165+
env: Environment
166+
) throws -> String {
167+
do {
168+
return try AsyncProcess.checkNonZeroExit(arguments: args, environment: env)
169+
} catch {
170+
// If the file doesn't exist, throw a more specific error.
171+
if !fileSystem.exists(path) {
172+
throw FileSystemError(.noEntry, path)
173+
}
174+
throw error
175+
}
176+
}
177+
148178
/// Creates the environment needed to test related tools.
149179
static func constructTestEnvironment(
150180
toolchain: UserToolchain,

Tests/CommandsTests/TestCommandTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,16 @@ final class TestCommandTests: CommandsTestCase {
284284
}
285285
}
286286

287+
func testListWithSkipBuildAndNoBuildArtifacts() async throws {
288+
try await fixture(name: "Miscellaneous/TestDiscovery/Simple") { fixturePath in
289+
await XCTAssertThrowsCommandExecutionError(
290+
try await SwiftPM.Test.execute(["list", "--skip-build"], packagePath: fixturePath)
291+
) { error in
292+
XCTAssertMatch(error.stderr, .contains("Test build artifacts were not found in the build folder"))
293+
}
294+
}
295+
}
296+
287297
func testBasicSwiftTestingIntegration() async throws {
288298
#if !canImport(TestingDisabled)
289299
try XCTSkipUnless(

0 commit comments

Comments
 (0)