Skip to content

Commit f139694

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 e1388f3 commit f139694

File tree

10 files changed

+179
-50
lines changed

10 files changed

+179
-50
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func callExitTest(
204204
isRequired: Bool,
205205
isolation: isolated (any Actor)? = #isolation,
206206
sourceLocation: SourceLocation
207-
) async -> Result<ExitTestArtifacts, any Error> {
207+
) async -> Result<ExitTestArtifacts?, any Error> {
208208
guard let configuration = Configuration.current ?? Configuration.all.first else {
209209
preconditionFailure("A test must be running on the current task to use #expect(exitsWith:).")
210210
}

Sources/Testing/Expectations/Expectation+Macro.swift

Lines changed: 38 additions & 15 deletions
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

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

0 commit comments

Comments
 (0)