Skip to content

Commit ab60142

Browse files
authored
[Tests] Consolidate expectThrowsCommandExecutionError usage (#9125)
- Add throwing and non-throwing overloads for expectThrowsCommandExecutionError in SwiftTesting+Helpers - Remove duplicate implementation from APIDiffTests - Simplify call sites by using appropriate overload without unnecessary `try`s where appropriate
1 parent 9fc88d2 commit ab60142

File tree

2 files changed

+62
-55
lines changed

2 files changed

+62
-55
lines changed

Sources/_InternalTestSupport/SwiftTesting+Helpers.swift

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -111,38 +111,64 @@ public func expectDirectoryDoesNotExist(
111111
)
112112
}
113113

114+
/// Expects that the expression throws a CommandExecutionError and passes it to the provided throwing error handler.
115+
/// - Parameters:
116+
/// - expression: The expression expected to throw
117+
/// - message: Optional message for the expectation
118+
/// - sourceLocation: Source location for error reporting
119+
/// - errorHandler: A throwing closure that receives the CommandExecutionError
114120
public func expectThrowsCommandExecutionError<T>(
115121
_ expression: @autoclosure () async throws -> T,
116122
_ message: @autoclosure () -> Comment = "",
117123
sourceLocation: SourceLocation = #_sourceLocation,
118-
_ errorHandler: (_ error: CommandExecutionError) -> Void = { _ in }
119-
) async {
120-
await expectAsyncThrowsError(try await expression(), message(), sourceLocation: sourceLocation) { error in
121-
guard case SwiftPMError.executionFailure(let processError, let stdout, let stderr) = error,
122-
case AsyncProcessResult.Error.nonZeroExit(let processResult) = processError,
123-
processResult.exitStatus != .terminated(code: 0)
124-
else {
125-
Issue.record("Unexpected error type: \(error.interpolationDescription)", sourceLocation: sourceLocation)
126-
return
127-
}
128-
errorHandler(CommandExecutionError(result: processResult, stdout: stdout, stderr: stderr))
129-
}
124+
_ errorHandler: (_ error: CommandExecutionError) throws -> Void = { _ in }
125+
) async rethrows {
126+
_ = try await _expectThrowsCommandExecutionError(try await expression(), message(), sourceLocation, errorHandler)
130127
}
131128

132-
/// An `async`-friendly replacement for `XCTAssertThrowsError`.
133-
public func expectAsyncThrowsError<T>(
129+
/// Expects that the expression throws a CommandExecutionError and passes it to the provided non-throwing error handler.
130+
/// This version can be called without `try` when the error handler doesn't throw.
131+
/// - Parameters:
132+
/// - expression: The expression expected to throw
133+
/// - message: Optional message for the expectation
134+
/// - sourceLocation: Source location for error reporting
135+
/// - errorHandler: A non-throwing closure that receives the CommandExecutionError
136+
public func expectThrowsCommandExecutionError<T>(
134137
_ expression: @autoclosure () async throws -> T,
135-
_ message: @autoclosure () -> Comment? = nil,
138+
_ message: @autoclosure () -> Comment = "",
136139
sourceLocation: SourceLocation = #_sourceLocation,
137-
_ errorHandler: (_ error: any Error) -> Void = { _ in }
140+
_ errorHandler: (_ error: CommandExecutionError) -> Void
138141
) async {
139-
do {
140-
_ = try await expression()
141-
Issue.record(
142-
message() ?? "Expected an error, which did not occur.",
143-
sourceLocation: sourceLocation,
144-
)
145-
} catch {
142+
_ = try? await _expectThrowsCommandExecutionError(try await expression(), message(), sourceLocation) { error in
146143
errorHandler(error)
144+
return ()
145+
}
146+
}
147+
148+
private func _expectThrowsCommandExecutionError<R, T>(
149+
_ expressionClosure: @autoclosure () async throws -> T,
150+
_ message: @autoclosure () -> Comment,
151+
_ sourceLocation: SourceLocation,
152+
_ errorHandler: (_ error: CommandExecutionError) throws -> R
153+
) async rethrows -> R? {
154+
// Older toolchains don't have https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0006-return-errors-from-expect-throws.md
155+
// This can be removed once the CI smoke jobs build with 6.2.
156+
var err: SwiftPMError?
157+
await #expect(throws: SwiftPMError.self, message(), sourceLocation: sourceLocation) {
158+
do {
159+
let _ = try await expressionClosure()
160+
} catch {
161+
err = error as? SwiftPMError
162+
throw error
163+
}
164+
}
165+
166+
guard let error = err,
167+
case .executionFailure(let processError, let stdout, let stderr) = error,
168+
case AsyncProcessResult.Error.nonZeroExit(let processResult) = processError,
169+
processResult.exitStatus != .terminated(code: 0) else {
170+
Issue.record("Unexpected error type: \(err?.interpolationDescription ?? "<unknown>")", sourceLocation: sourceLocation)
171+
return Optional<R>.none
147172
}
173+
return try errorHandler(CommandExecutionError(result: processResult, stdout: stdout, stderr: stderr))
148174
}

Tests/CommandsTests/APIDiffTests.swift

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,6 @@ import _InternalTestSupport
2525
import Workspace
2626
import Testing
2727

28-
fileprivate func expectThrowsCommandExecutionError<T>(
29-
_ expression: @autoclosure () async throws -> T,
30-
sourceLocation: SourceLocation = #_sourceLocation,
31-
_ errorHandler: (_ error: CommandExecutionError) throws -> Void = { _ in }
32-
) async rethrows {
33-
let error = await #expect(throws: SwiftPMError.self, sourceLocation: sourceLocation) {
34-
try await expression()
35-
}
36-
37-
guard case .executionFailure(let processError, let stdout, let stderr) = error,
38-
case AsyncProcessResult.Error.nonZeroExit(let processResult) = processError,
39-
processResult.exitStatus != .terminated(code: 0) else {
40-
Issue.record("Unexpected error type: \(error?.interpolationDescription)", sourceLocation: sourceLocation)
41-
return
42-
}
43-
try errorHandler(CommandExecutionError(result: processResult, stdout: stdout, stderr: stderr))
44-
}
45-
46-
4728
extension Trait where Self == Testing.ConditionTrait {
4829
public static var requiresAPIDigester: Self {
4930
enabled("This test requires a toolchain with swift-api-digester") {
@@ -81,7 +62,7 @@ struct APIDiffTests {
8162
packageRoot.appending("Foo.swift"),
8263
string: "public let foo = 42"
8364
)
84-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
65+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
8566
#expect(!error.stdout.isEmpty)
8667
}
8768
}
@@ -96,7 +77,7 @@ struct APIDiffTests {
9677
packageRoot.appending("Foo.swift"),
9778
string: "public let foo = 42"
9879
)
99-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
80+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
10081
#expect(error.stdout.contains("1 breaking change detected in Foo"))
10182
#expect(error.stdout.contains("💔 API breakage: func foo() has been removed"))
10283
}
@@ -119,7 +100,7 @@ struct APIDiffTests {
119100
packageRoot.appending(components: "Sources", "Qux", "Qux.swift"),
120101
string: "public class Qux<T, U> { private let x = 1 }"
121102
)
122-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
103+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
123104
withKnownIssue {
124105
#expect(error.stdout.contains("2 breaking changes detected in Qux"))
125106
#expect(error.stdout.contains("💔 API breakage: class Qux has generic signature change from <T> to <T, U>"))
@@ -154,7 +135,7 @@ struct APIDiffTests {
154135
customAllowlistPath,
155136
string: "API breakage: class Qux has generic signature change from <T> to <T, U>\n"
156137
)
157-
try await expectThrowsCommandExecutionError(
138+
await expectThrowsCommandExecutionError(
158139
try await execute(["diagnose-api-breaking-changes", "1.2.3", "--breakage-allowlist-path", customAllowlistPath.pathString],
159140
packagePath: packageRoot, buildSystem: buildSystem)
160141
) { error in
@@ -277,7 +258,7 @@ struct APIDiffTests {
277258
}
278259

279260
// Test diagnostics
280-
try await expectThrowsCommandExecutionError(
261+
await expectThrowsCommandExecutionError(
281262
try await execute(["diagnose-api-breaking-changes", "1.2.3", "--targets", "NotATarget", "Exec", "--products", "NotAProduct", "Exec"],
282263
packagePath: packageRoot, buildSystem: buildSystem)
283264
) { error in
@@ -305,13 +286,13 @@ struct APIDiffTests {
305286
}
306287
"""
307288
)
308-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
289+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
309290
#expect(error.stdout.contains("1 breaking change detected in Bar"))
310291
#expect(error.stdout.contains("💔 API breakage: func bar() has return type change from Swift.Int to Swift.String"))
311292
}
312293

313294
// Report an error if we explicitly ask to diff a C-family target
314-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3", "--targets", "Foo"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
295+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3", "--targets", "Foo"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
315296
#expect(error.stderr.contains("error: 'Foo' is not a Swift language target"))
316297
}
317298
}
@@ -413,7 +394,7 @@ struct APIDiffTests {
413394
func testBadTreeish(buildSystem: BuildSystemProvider.Kind) async throws {
414395
try await fixture(name: "Miscellaneous/APIDiff/") { fixturePath in
415396
let packageRoot = fixturePath.appending("Foo")
416-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "7.8.9"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
397+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "7.8.9"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
417398
#expect(error.stderr.contains("error: Couldn’t get revision"))
418399
}
419400
}
@@ -433,7 +414,7 @@ struct APIDiffTests {
433414
)
434415
try repo.stage(file: "Foo.swift")
435416
try repo.commit(message: "Add foo")
436-
try await expectThrowsCommandExecutionError(
417+
await expectThrowsCommandExecutionError(
437418
try await execute(["diagnose-api-breaking-changes", "main", "--baseline-dir", baselineDir.pathString],
438419
packagePath: packageRoot,
439420
buildSystem: buildSystem)
@@ -473,7 +454,7 @@ struct APIDiffTests {
473454
let repo = GitRepository(path: packageRoot)
474455
let revision = try repo.resolveRevision(identifier: "1.2.3")
475456

476-
try await expectThrowsCommandExecutionError(
457+
await expectThrowsCommandExecutionError(
477458
try await execute(["diagnose-api-breaking-changes", "1.2.3", "--baseline-dir", baselineDir.pathString], packagePath: packageRoot, buildSystem: buildSystem)
478459
) { error in
479460
#expect(error.stdout.contains("1 breaking change detected in Foo"))
@@ -541,7 +522,7 @@ struct APIDiffTests {
541522
// Accomodate filesystems with low resolution timestamps
542523
try await Task.sleep(for: .seconds(1))
543524

544-
try await expectThrowsCommandExecutionError(
525+
await expectThrowsCommandExecutionError(
545526
try await execute(["diagnose-api-breaking-changes", "1.2.3",
546527
"--baseline-dir", baselineDir.pathString, "--regenerate-baseline"],
547528
packagePath: packageRoot,
@@ -556,7 +537,7 @@ struct APIDiffTests {
556537

557538
@Test(arguments: SupportedBuildSystemOnAllPlatforms)
558539
func testOldName(buildSystem: BuildSystemProvider.Kind) async throws {
559-
try await expectThrowsCommandExecutionError(try await execute(["experimental-api-diff", "1.2.3", "--regenerate-baseline"], packagePath: nil, buildSystem: buildSystem)) { error in
540+
await expectThrowsCommandExecutionError(try await execute(["experimental-api-diff", "1.2.3", "--regenerate-baseline"], packagePath: nil, buildSystem: buildSystem)) { error in
560541
#expect(error.stdout.contains("`swift package experimental-api-diff` has been renamed to `swift package diagnose-api-breaking-changes`"))
561542
}
562543
}
@@ -565,7 +546,7 @@ struct APIDiffTests {
565546
func testBrokenAPIDiff(buildSystem: BuildSystemProvider.Kind) async throws {
566547
try await fixture(name: "Miscellaneous/APIDiff/") { fixturePath in
567548
let packageRoot = fixturePath.appending("BrokenPkg")
568-
try await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
549+
await expectThrowsCommandExecutionError(try await execute(["diagnose-api-breaking-changes", "1.2.3"], packagePath: packageRoot, buildSystem: buildSystem)) { error in
569550
let expectedError: String
570551
if buildSystem == .swiftbuild {
571552
expectedError = "error: Build failed"

0 commit comments

Comments
 (0)