Skip to content

Commit 6eabf01

Browse files
committed
Return the thrown error from #expect(throws:) and #require(throws:).
This PR changes the signatures of the various `throws:` overloads of `#expect` and `#require` so that on success they return the error that was thrown rather than `Void`. This then allows more ergonomic inspection of the error's properties: ```swift let error = try #require(throws: MyError.self) { try f() } #expect(error.hasWidget) #expect(error.userName == "John Smith") ``` It is not possible to overload a macro or function solely by return type without the compiler reporting `Ambiguous use of 'f()'`, so we are not able to stage this change in using `@_spi(Experimental)` without breaking test code that already imports our SPI. This change is potentially source-breaking for tests that inadvertently forward the result of these macro invocations to an enclosing scope. For example, the compiler will start emitting a warning here: ```swift func bar(_ pfoo: UnsafePointer<Foo>) throws { ... } withUnsafePointer(to: foo) { pfoo in // ⚠️ Result of call to 'withUnsafePointer(to:_:)' is unused #expect(throws: BadFooError.self) { bar(pfoo) } } ``` This warning can be suppressed by assigning the result of `#expect` (or of `withUnsafePointer(to:_:)`) to `_`: ```swift func bar(_ pfoo: UnsafePointer<Foo>) throws { ... } withUnsafePointer(to: foo) { pfoo in _ = #expect(throws: BadFooError.self) { bar(pfoo) } } ``` Because `#expect` and `#require` are macros, they cannot be referenced by name like functions, so you cannot assign them to variables (and then run into trouble with the types of those variables.)
1 parent e6abba8 commit 6eabf01

File tree

9 files changed

+164
-48
lines changed

9 files changed

+164
-48
lines changed

Diff for: Sources/Testing/Expectations/Expectation+Macro.swift

+38-15
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ public macro require<T>(
142142
/// issues should be attributed.
143143
/// - expression: The expression to be evaluated.
144144
///
145+
/// - Returns: If the expectation passes, the instance of `errorType` that was
146+
/// thrown by `expression`. If the expectation fails, the result is `nil`.
147+
///
145148
/// Use this overload of `#expect()` when the expression `expression` _should_
146149
/// throw an error of a given type:
147150
///
@@ -158,7 +161,7 @@ public macro require<T>(
158161
/// discarded.
159162
///
160163
/// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error),
161-
/// use ``expect(throws:_:sourceLocation:performing:)-1xr34`` instead.
164+
/// use ``expect(throws:_:sourceLocation:performing:)-7du1h`` instead.
162165
///
163166
/// ## Expressions that should never throw
164167
///
@@ -181,12 +184,13 @@ public macro require<T>(
181184
/// fail when an error is thrown by `expression`, rather than to explicitly
182185
/// check that an error is _not_ thrown by it, do not use this macro. Instead,
183186
/// simply call the code in question and allow it to throw an error naturally.
187+
@discardableResult
184188
@freestanding(expression) public macro expect<E, R>(
185189
throws errorType: E.Type,
186190
_ comment: @autoclosure () -> Comment? = nil,
187191
sourceLocation: SourceLocation = #_sourceLocation,
188192
performing expression: () async throws -> R
189-
) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error
193+
) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error
190194

191195
/// Check that an expression always throws an error of a given type, and throw
192196
/// an error if it does not.
@@ -200,6 +204,8 @@ public macro require<T>(
200204
/// issues should be attributed.
201205
/// - expression: The expression to be evaluated.
202206
///
207+
/// - Returns: The instance of `errorType` that was thrown by `expression`.
208+
///
203209
/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not
204210
/// throw a matching error. The error thrown by `expression` is not rethrown.
205211
///
@@ -219,16 +225,17 @@ public macro require<T>(
219225
/// is thrown. Any value returned by `expression` is discarded.
220226
///
221227
/// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error),
222-
/// use ``require(throws:_:sourceLocation:performing:)-7v83e`` instead.
228+
/// use ``require(throws:_:sourceLocation:performing:)-4djuw`` instead.
223229
///
224230
/// If `expression` should _never_ throw, simply invoke the code without using
225231
/// this macro. The test will then fail if an error is thrown.
232+
@discardableResult
226233
@freestanding(expression) public macro require<E, R>(
227234
throws errorType: E.Type,
228235
_ comment: @autoclosure () -> Comment? = nil,
229236
sourceLocation: SourceLocation = #_sourceLocation,
230237
performing expression: () async throws -> R
231-
) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error
238+
) -> E = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error
232239

233240
/// Check that an expression never throws an error, and throw an error if it
234241
/// does.
@@ -261,6 +268,10 @@ public macro require<R>(
261268
/// issues should be attributed.
262269
/// - expression: The expression to be evaluated.
263270
///
271+
/// - Returns: If the expectation passes, the instance of `E` that was thrown by
272+
/// `expression` and is equal to `error`. If the expectation fails, the result
273+
/// is `nil`.
274+
///
264275
/// Use this overload of `#expect()` when the expression `expression` _should_
265276
/// throw a specific error:
266277
///
@@ -276,13 +287,14 @@ public macro require<R>(
276287
/// in the current task. Any value returned by `expression` is discarded.
277288
///
278289
/// If the thrown error need only be an instance of a particular type, use
279-
/// ``expect(throws:_:sourceLocation:performing:)-79piu`` instead.
290+
/// ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead.
291+
@discardableResult
280292
@freestanding(expression) public macro expect<E, R>(
281293
throws error: E,
282294
_ comment: @autoclosure () -> Comment? = nil,
283295
sourceLocation: SourceLocation = #_sourceLocation,
284296
performing expression: () async throws -> R
285-
) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable
297+
) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable
286298

287299
/// Check that an expression always throws a specific error, and throw an error
288300
/// if it does not.
@@ -293,6 +305,9 @@ public macro require<R>(
293305
/// - sourceLocation: The source location to which recorded expectations and
294306
/// issues should be attributed.
295307
/// - expression: The expression to be evaluated.
308+
309+
/// - Returns: The instance of `E` that was thrown by `expression` and is equal
310+
/// to `error`.
296311
///
297312
/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not
298313
/// throw a matching error. The error thrown by `expression` is not rethrown.
@@ -313,13 +328,14 @@ public macro require<R>(
313328
/// Any value returned by `expression` is discarded.
314329
///
315330
/// If the thrown error need only be an instance of a particular type, use
316-
/// ``require(throws:_:sourceLocation:performing:)-76bjn`` instead.
331+
/// ``require(throws:_:sourceLocation:performing:)-7n34r`` instead.
332+
@discardableResult
317333
@freestanding(expression) public macro require<E, R>(
318334
throws error: E,
319335
_ comment: @autoclosure () -> Comment? = nil,
320336
sourceLocation: SourceLocation = #_sourceLocation,
321337
performing expression: () async throws -> R
322-
) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable
338+
) -> E = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable
323339

324340
// MARK: - Arbitrary error matching
325341

@@ -333,6 +349,9 @@ public macro require<R>(
333349
/// - errorMatcher: A closure to invoke when `expression` throws an error that
334350
/// indicates if it matched or not.
335351
///
352+
/// - Returns: If the expectation passes, the error that was thrown by
353+
/// `expression`. If the expectation fails, the result is `nil`.
354+
///
336355
/// Use this overload of `#expect()` when the expression `expression` _should_
337356
/// throw an error, but the logic to determine if the error matches is complex:
338357
///
@@ -353,15 +372,16 @@ public macro require<R>(
353372
/// discarded.
354373
///
355374
/// If the thrown error need only be an instance of a particular type, use
356-
/// ``expect(throws:_:sourceLocation:performing:)-79piu`` instead. If the thrown
375+
/// ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead. If the thrown
357376
/// error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error),
358-
/// use ``expect(throws:_:sourceLocation:performing:)-1xr34`` instead.
377+
/// use ``expect(throws:_:sourceLocation:performing:)-7du1h`` instead.
378+
@discardableResult
359379
@freestanding(expression) public macro expect<R>(
360380
_ comment: @autoclosure () -> Comment? = nil,
361381
sourceLocation: SourceLocation = #_sourceLocation,
362382
performing expression: () async throws -> R,
363383
throws errorMatcher: (any Error) async throws -> Bool
364-
) = #externalMacro(module: "TestingMacros", type: "ExpectMacro")
384+
) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro")
365385

366386
/// Check that an expression always throws an error matching some condition, and
367387
/// throw an error if it does not.
@@ -374,6 +394,8 @@ public macro require<R>(
374394
/// - errorMatcher: A closure to invoke when `expression` throws an error that
375395
/// indicates if it matched or not.
376396
///
397+
/// - Returns: The error that was thrown by `expression`.
398+
///
377399
/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not
378400
/// throw a matching error. The error thrown by `expression` is not rethrown.
379401
///
@@ -398,18 +420,19 @@ public macro require<R>(
398420
/// discarded.
399421
///
400422
/// If the thrown error need only be an instance of a particular type, use
401-
/// ``require(throws:_:sourceLocation:performing:)-76bjn`` instead. If the thrown error need
423+
/// ``require(throws:_:sourceLocation:performing:)-7n34r`` instead. If the thrown error need
402424
/// only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error),
403-
/// use ``require(throws:_:sourceLocation:performing:)-7v83e`` instead.
425+
/// use ``require(throws:_:sourceLocation:performing:)-4djuw`` instead.
404426
///
405427
/// If `expression` should _never_ throw, simply invoke the code without using
406428
/// this macro. The test will then fail if an error is thrown.
429+
@discardableResult
407430
@freestanding(expression) public macro require<R>(
408431
_ comment: @autoclosure () -> Comment? = nil,
409432
sourceLocation: SourceLocation = #_sourceLocation,
410433
performing expression: () async throws -> R,
411434
throws errorMatcher: (any Error) async throws -> Bool
412-
) = #externalMacro(module: "TestingMacros", type: "RequireMacro")
435+
) -> any Error = #externalMacro(module: "TestingMacros", type: "RequireMacro")
413436

414437
// MARK: - Exit tests
415438

@@ -425,7 +448,7 @@ public macro require<R>(
425448
/// issues should be attributed.
426449
/// - expression: The expression to be evaluated.
427450
///
428-
/// - Returns: If the exit test passed, an instance of ``ExitTestArtifacts``
451+
/// - Returns: If the exit test passes, an instance of ``ExitTestArtifacts``
429452
/// describing the state of the exit test when it exited. If the exit test
430453
/// fails, the result is `nil`.
431454
///

Diff for: Sources/Testing/Expectations/ExpectationChecking+Macro.swift

+30-18
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,14 @@ public func __checkCast<V, T>(
821821

822822
// MARK: - Matching errors by type
823823

824+
/// A placeholder type representing `Never` if a test attempts to instantiate it
825+
/// by calling `#expect(throws:)` and taking the result.
826+
///
827+
/// Errors of this type are never thrown; they act as placeholders in `Result`
828+
/// so that `#expect(throws: Never.self)` always produces `nil` (since `Never`
829+
/// cannot be instantiated.)
830+
private struct _CannotInstantiateNeverError: Error {}
831+
824832
/// Check that an expression always throws an error.
825833
///
826834
/// This overload is used for `#expect(throws:) { }` invocations that take error
@@ -835,7 +843,7 @@ public func __checkClosureCall<E>(
835843
comments: @autoclosure () -> [Comment],
836844
isRequired: Bool,
837845
sourceLocation: SourceLocation
838-
) -> Result<Void, any Error> where E: Error {
846+
) -> Result<E?, any Error> where E: Error {
839847
if errorType == Never.self {
840848
__checkClosureCall(
841849
throws: Never.self,
@@ -844,7 +852,7 @@ public func __checkClosureCall<E>(
844852
comments: comments(),
845853
isRequired: isRequired,
846854
sourceLocation: sourceLocation
847-
)
855+
).map { _ in nil }
848856
} else {
849857
__checkClosureCall(
850858
performing: body,
@@ -854,7 +862,7 @@ public func __checkClosureCall<E>(
854862
comments: comments(),
855863
isRequired: isRequired,
856864
sourceLocation: sourceLocation
857-
)
865+
).map { $0 as? E }
858866
}
859867
}
860868

@@ -873,7 +881,7 @@ public func __checkClosureCall<E>(
873881
isRequired: Bool,
874882
isolation: isolated (any Actor)? = #isolation,
875883
sourceLocation: SourceLocation
876-
) async -> Result<Void, any Error> where E: Error {
884+
) async -> Result<E?, any Error> where E: Error {
877885
if errorType == Never.self {
878886
await __checkClosureCall(
879887
throws: Never.self,
@@ -883,7 +891,7 @@ public func __checkClosureCall<E>(
883891
isRequired: isRequired,
884892
isolation: isolation,
885893
sourceLocation: sourceLocation
886-
)
894+
).map { _ in nil }
887895
} else {
888896
await __checkClosureCall(
889897
performing: body,
@@ -894,7 +902,7 @@ public func __checkClosureCall<E>(
894902
isRequired: isRequired,
895903
isolation: isolation,
896904
sourceLocation: sourceLocation
897-
)
905+
).map { $0 as? E }
898906
}
899907
}
900908

@@ -915,7 +923,7 @@ public func __checkClosureCall(
915923
comments: @autoclosure () -> [Comment],
916924
isRequired: Bool,
917925
sourceLocation: SourceLocation
918-
) -> Result<Void, any Error> {
926+
) -> Result<Never?, any Error> {
919927
var success = true
920928
var mismatchExplanationValue: String? = nil
921929
do {
@@ -932,7 +940,7 @@ public func __checkClosureCall(
932940
comments: comments(),
933941
isRequired: isRequired,
934942
sourceLocation: sourceLocation
935-
)
943+
).map { _ in nil }
936944
}
937945

938946
/// Check that an expression never throws an error.
@@ -952,7 +960,7 @@ public func __checkClosureCall(
952960
isRequired: Bool,
953961
isolation: isolated (any Actor)? = #isolation,
954962
sourceLocation: SourceLocation
955-
) async -> Result<Void, any Error> {
963+
) async -> Result<Never?, any Error> {
956964
var success = true
957965
var mismatchExplanationValue: String? = nil
958966
do {
@@ -969,7 +977,7 @@ public func __checkClosureCall(
969977
comments: comments(),
970978
isRequired: isRequired,
971979
sourceLocation: sourceLocation
972-
)
980+
).map { _ in nil }
973981
}
974982

975983
// MARK: - Matching instances of equatable errors
@@ -988,7 +996,7 @@ public func __checkClosureCall<E>(
988996
comments: @autoclosure () -> [Comment],
989997
isRequired: Bool,
990998
sourceLocation: SourceLocation
991-
) -> Result<Void, any Error> where E: Error & Equatable {
999+
) -> Result<E?, any Error> where E: Error & Equatable {
9921000
__checkClosureCall(
9931001
performing: body,
9941002
throws: { true == (($0 as? E) == error) },
@@ -997,7 +1005,7 @@ public func __checkClosureCall<E>(
9971005
comments: comments(),
9981006
isRequired: isRequired,
9991007
sourceLocation: sourceLocation
1000-
)
1008+
).map { $0 as? E }
10011009
}
10021010

10031011
/// Check that an expression always throws an error.
@@ -1015,7 +1023,7 @@ public func __checkClosureCall<E>(
10151023
isRequired: Bool,
10161024
isolation: isolated (any Actor)? = #isolation,
10171025
sourceLocation: SourceLocation
1018-
) async -> Result<Void, any Error> where E: Error & Equatable {
1026+
) async -> Result<E?, any Error> where E: Error & Equatable {
10191027
await __checkClosureCall(
10201028
performing: body,
10211029
throws: { true == (($0 as? E) == error) },
@@ -1025,7 +1033,7 @@ public func __checkClosureCall<E>(
10251033
isRequired: isRequired,
10261034
isolation: isolation,
10271035
sourceLocation: sourceLocation
1028-
)
1036+
).map { $0 as? E }
10291037
}
10301038

10311039
// MARK: - Arbitrary error matching
@@ -1044,10 +1052,11 @@ public func __checkClosureCall<R>(
10441052
comments: @autoclosure () -> [Comment],
10451053
isRequired: Bool,
10461054
sourceLocation: SourceLocation
1047-
) -> Result<Void, any Error> {
1055+
) -> Result<(any Error)?, any Error> {
10481056
var errorMatches = false
10491057
var mismatchExplanationValue: String? = nil
10501058
var expression = expression
1059+
var caughtError: (any Error)?
10511060
do {
10521061
let result = try body()
10531062

@@ -1057,6 +1066,7 @@ public func __checkClosureCall<R>(
10571066
}
10581067
mismatchExplanationValue = explanation
10591068
} catch {
1069+
caughtError = error
10601070
expression = expression.capturingRuntimeValues(error)
10611071
let secondError = Issue.withErrorRecording(at: sourceLocation) {
10621072
errorMatches = try errorMatcher(error)
@@ -1075,7 +1085,7 @@ public func __checkClosureCall<R>(
10751085
comments: comments(),
10761086
isRequired: isRequired,
10771087
sourceLocation: sourceLocation
1078-
)
1088+
).map { caughtError }
10791089
}
10801090

10811091
/// Check that an expression always throws an error.
@@ -1093,10 +1103,11 @@ public func __checkClosureCall<R>(
10931103
isRequired: Bool,
10941104
isolation: isolated (any Actor)? = #isolation,
10951105
sourceLocation: SourceLocation
1096-
) async -> Result<Void, any Error> {
1106+
) async -> Result<(any Error)?, any Error> {
10971107
var errorMatches = false
10981108
var mismatchExplanationValue: String? = nil
10991109
var expression = expression
1110+
var caughtError: (any Error)?
11001111
do {
11011112
let result = try await body()
11021113

@@ -1106,6 +1117,7 @@ public func __checkClosureCall<R>(
11061117
}
11071118
mismatchExplanationValue = explanation
11081119
} catch {
1120+
caughtError = error
11091121
expression = expression.capturingRuntimeValues(error)
11101122
let secondError = await Issue.withErrorRecording(at: sourceLocation) {
11111123
errorMatches = try await errorMatcher(error)
@@ -1124,7 +1136,7 @@ public func __checkClosureCall<R>(
11241136
comments: comments(),
11251137
isRequired: isRequired,
11261138
sourceLocation: sourceLocation
1127-
)
1139+
).map { caughtError }
11281140
}
11291141

11301142
// MARK: - Exit tests

0 commit comments

Comments
 (0)