diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 8a6749a31..2f64aff3a 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -131,6 +131,74 @@ public macro require( // MARK: - Matching errors by type +#if compiler(>=6.2) +/// Check that an expression always throws an error of a given type. +/// +/// - Parameters: +/// - errorType: The type of error that is expected to be thrown. If +/// `expression` could throw _any_ error, or the specific type of thrown +/// error is unimportant, pass `(any Error).self`. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Returns: If the expectation passes, the instance of `errorType` that was +/// thrown by `expression`. If the expectation fails, the result is `nil`. +/// +/// Use this overload of `#expect()` when the expression `expression` _should_ +/// throw an error of a given type: +/// +/// ```swift +/// #expect(throws: EngineFailureError.self) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not an instance of `errorType`, an ``Issue`` is recorded for the test that +/// is running in the current task. Any value returned by `expression` is +/// discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``expect(throws:_:sourceLocation:performing:)-7du1h`` instead. +/// +/// ## Expressions that should never throw +/// +/// If the expression `expression` should _never_ throw any error, you can pass +/// [`Never.self`](https://developer.apple.com/documentation/swift/never): +/// +/// ```swift +/// #expect(throws: Never.self) { +/// FoodTruck.shared.engine.batteryLevel = 100 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` throws an error, an ``Issue`` is recorded for the test that +/// is running in the current task. Any value returned by `expression` is +/// discarded. +/// +/// Test functions can be annotated with `throws` and can throw errors which are +/// then recorded as issues when the test runs. If the intent is for a test to +/// fail when an error is thrown by `expression`, rather than to explicitly +/// check that an error is _not_ thrown by it, do not use this macro. Instead, +/// simply call the code in question and allow it to throw an error naturally. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro expect( + throws errorType: E.Type, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error +#endif + /// Check that an expression always throws an error of a given type. /// /// - Parameters: @@ -195,6 +263,58 @@ public macro require( performing expression: () async throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error +#if compiler(>=6.2) +/// Check that an expression always throws an error of a given type, and throw +/// an error if it does not. +/// +/// - Parameters: +/// - errorType: The type of error that is expected to be thrown. If +/// `expression` could throw _any_ error, or the specific type of thrown +/// error is unimportant, pass `(any Error).self`. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Returns: The instance of `errorType` that was thrown by `expression`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not +/// throw a matching error. The error thrown by `expression` is not rethrown. +/// +/// Use this overload of `#require()` when the expression `expression` _should_ +/// throw an error of a given type: +/// +/// ```swift +/// try #require(throws: EngineFailureError.self) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not an instance of `errorType`, an ``Issue`` is recorded for the test that +/// is running in the current task and an instance of ``ExpectationFailedError`` +/// is thrown. Any value returned by `expression` is discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``require(throws:_:sourceLocation:performing:)-4djuw`` instead. +/// +/// If `expression` should _never_ throw, simply invoke the code without using +/// this macro. The test will then fail if an error is thrown. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + throws errorType: E.Type, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E = #externalMacro(module: "TestingMacros", type: "RequireThrowsMacro") where E: Error +#endif + /// Check that an expression always throws an error of a given type, and throw /// an error if it does not. /// @@ -243,6 +363,28 @@ public macro require( performing expression: () async throws -> R ) -> E = #externalMacro(module: "TestingMacros", type: "RequireThrowsMacro") where E: Error +#if compiler(>=6.2) +/// Check that an expression never throws an error, and throw an error if it +/// does. +/// +/// - Parameters: +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` throws +/// any error. The error thrown by `expression` is not rethrown. +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + throws _: Never.Type, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) = #externalMacro(module: "TestingMacros", type: "RequireThrowsNeverMacro") +#endif + /// Check that an expression never throws an error, and throw an error if it /// does. /// @@ -265,6 +407,50 @@ public macro require( // MARK: - Matching instances of equatable errors +#if compiler(>=6.2) +/// Check that an expression always throws a specific error. +/// +/// - Parameters: +/// - error: The error that is expected to be thrown. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Returns: If the expectation passes, the instance of `E` that was thrown by +/// `expression` and is equal to `error`. If the expectation fails, the result +/// is `nil`. +/// +/// Use this overload of `#expect()` when the expression `expression` _should_ +/// throw a specific error: +/// +/// ```swift +/// #expect(throws: EngineFailureError.batteryDied) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not equal to `error`, an ``Issue`` is recorded for the test that is running +/// in the current task. Any value returned by `expression` is discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro expect( + throws error: E, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable +#endif + /// Check that an expression always throws a specific error. /// /// - Parameters: @@ -305,6 +491,54 @@ public macro require( performing expression: () async throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable +#if compiler(>=6.2) +/// Check that an expression always throws a specific error, and throw an error +/// if it does not. +/// +/// - Parameters: +/// - error: The error that is expected to be thrown. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. + +/// - Returns: The instance of `E` that was thrown by `expression` and is equal +/// to `error`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not +/// throw a matching error. The error thrown by `expression` is not rethrown. +/// +/// Use this overload of `#require()` when the expression `expression` _should_ +/// throw a specific error: +/// +/// ```swift +/// try #require(throws: EngineFailureError.batteryDied) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not equal to `error`, an ``Issue`` is recorded for the test that is running +/// in the current task and an instance of ``ExpectationFailedError`` is thrown. +/// Any value returned by `expression` is discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``require(throws:_:sourceLocation:performing:)-7n34r`` instead. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + throws error: E, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable +#endif + /// Check that an expression always throws a specific error, and throw an error /// if it does not. /// @@ -351,6 +585,72 @@ public macro require( // MARK: - Arbitrary error matching +#if compiler(>=6.2) +/// Check that an expression always throws an error matching some condition. +/// +/// - Parameters: +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// - errorMatcher: A closure to invoke when `expression` throws an error that +/// indicates if it matched or not. +/// +/// - Returns: If the expectation passes, the error that was thrown by +/// `expression`. If the expectation fails, the result is `nil`. +/// +/// Use this overload of `#expect()` when the expression `expression` _should_ +/// throw an error, but the logic to determine if the error matches is complex: +/// +/// ```swift +/// #expect { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } throws: { error in +/// return error == EngineFailureError.batteryDied +/// || error == EngineFailureError.stillCharging +/// } +/// ``` +/// +/// If `expression` does not throw an error, if it throws an error that is +/// not matched by `errorMatcher`, or if `errorMatcher` throws an error +/// (including the error passed to it), an ``Issue`` is recorded for the test +/// that is running in the current task. Any value returned by `expression` is +/// discarded. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead. If the thrown +/// error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``expect(throws:_:sourceLocation:performing:)-7du1h`` instead. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.0) +/// @Available(Xcode, introduced: 16.0) +/// } +/// +/// @DeprecationSummary { +/// Examine the result of ``expect(throws:_:sourceLocation:performing:)-7du1h`` +/// or ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead: +/// +/// ```swift +/// let error = #expect(throws: FoodTruckError.self) { +/// ... +/// } +/// #expect(error?.napkinCount == 0) +/// ``` +/// } +@available(swift, deprecated: 100000.0, message: "Examine the result of '#expect(throws:)' instead.") +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro expect( + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R, + throws errorMatcher: (any Error) throws -> Bool +) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") +#endif + /// Check that an expression always throws an error matching some condition. /// /// - Parameters: @@ -413,6 +713,79 @@ public macro require( throws errorMatcher: (any Error) async throws -> Bool ) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") +#if compiler(>=6.2) +/// Check that an expression always throws an error matching some condition, and +/// throw an error if it does not. +/// +/// - Parameters: +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// - errorMatcher: A closure to invoke when `expression` throws an error that +/// indicates if it matched or not. +/// +/// - Returns: The error that was thrown by `expression`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not +/// throw a matching error. The error thrown by `expression` is not rethrown. +/// +/// Use this overload of `#require()` when the expression `expression` _should_ +/// throw an error, but the logic to determine if the error matches is complex: +/// +/// ```swift +/// #expect { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } throws: { error in +/// return error == EngineFailureError.batteryDied +/// || error == EngineFailureError.stillCharging +/// } +/// ``` +/// +/// If `expression` does not throw an error, if it throws an error that is +/// not matched by `errorMatcher`, or if `errorMatcher` throws an error +/// (including the error passed to it), an ``Issue`` is recorded for the test +/// that is running in the current task and an instance of +/// ``ExpectationFailedError`` is thrown. Any value returned by `expression` is +/// discarded. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``require(throws:_:sourceLocation:performing:)-7n34r`` instead. If the thrown error need +/// only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``require(throws:_:sourceLocation:performing:)-4djuw`` instead. +/// +/// If `expression` should _never_ throw, simply invoke the code without using +/// this macro. The test will then fail if an error is thrown. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.0) +/// @Available(Xcode, introduced: 16.0) +/// } +/// +/// @DeprecationSummary { +/// Examine the result of ``expect(throws:_:sourceLocation:performing:)-7du1h`` +/// or ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead: +/// +/// ```swift +/// let error = try #require(throws: FoodTruckError.self) { +/// ... +/// } +/// #expect(error.napkinCount == 0) +/// ``` +/// } +@available(swift, deprecated: 100000.0, message: "Examine the result of '#require(throws:)' instead.") +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R, + throws errorMatcher: (any Error) throws -> Bool +) -> any Error = #externalMacro(module: "TestingMacros", type: "RequireMacro") +#endif + /// Check that an expression always throws an error matching some condition, and /// throw an error if it does not. ///