diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 9a61ff151..a92e648bf 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -83,15 +83,16 @@ /// /// 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. +/// is running in the current task. Any value returned by `expression` is +/// discarded. /// /// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), /// use ``expect(throws:_:performing:)-1s3lx`` instead. If `expression` should /// _never_ throw any error, use ``expect(throws:_:performing:)-jtjw`` instead. -@freestanding(expression) public macro expect( +@freestanding(expression) public macro expect( throws errorType: E.Type, _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error /// Check that an expression never throws an error. @@ -111,7 +112,8 @@ /// ``` /// /// If `expression` throws an error, an ``Issue`` is recorded for the test that -/// is running in the current task. +/// 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](doc:Issues) when the test runs. If the intent is @@ -124,10 +126,10 @@ /// ``expect(throws:_:performing:)-2j0od`` instead. If the thrown error need /// only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), /// use ``expect(throws:_:performing:)-1s3lx`` instead. -@freestanding(expression) public macro expect( +@freestanding(expression) public macro expect( throws _: Never.Type, _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") /// Check that an expression always throws an error of a given type, and throw @@ -156,17 +158,17 @@ /// 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. +/// is thrown. Any value returned by `expression` is discarded. /// /// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), /// use ``require(throws:_:performing:)-84jir`` instead. /// /// If `expression` should _never_ throw, simply invoke the code without using /// this macro. The test will then fail if an error is thrown. -@freestanding(expression) public macro require( +@freestanding(expression) public macro require( throws errorType: E.Type, _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error /// Check that an expression never throws an error, and throw an error if it @@ -179,10 +181,10 @@ /// - Throws: An instance of ``ExpectationFailedError`` if `expression` throws /// any error. The error thrown by `expression` is not rethrown. @available(*, deprecated, message: "try #require(throws: Never.self) is redundant. Invoke non-throwing test code directly instead.") -@freestanding(expression) public macro require( +@freestanding(expression) public macro require( throws _: Never.Type, _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") // MARK: - Matching instances of equatable errors @@ -206,15 +208,15 @@ /// /// 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. +/// 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:_:performing:)-2j0od`` instead. If `expression` should /// _never_ throw any error, use ``expect(throws:_:performing:)-jtjw`` instead. -@freestanding(expression) public macro expect( +@freestanding(expression) public macro expect( throws error: E, _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable /// Check that an expression always throws a specific error, and throw an error @@ -241,13 +243,14 @@ /// 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. /// /// If the thrown error need only be an instance of a particular type, use /// ``require(throws:_:performing:)-8762f`` instead. -@freestanding(expression) public macro require( +@freestanding(expression) public macro require( throws error: E, _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable // MARK: - Arbitrary error matching @@ -276,16 +279,17 @@ /// 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. +/// 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:_:performing:)-2j0od`` instead. If the thrown error need /// only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), /// use ``expect(throws:_:performing:)-1s3lx`` instead. If an error should /// _never_ be thrown, use ``expect(throws:_:performing:)-jtjw`` instead. -@freestanding(expression) public macro expect( +@freestanding(expression) public macro expect( _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void, + performing expression: () async throws -> R, throws errorMatcher: (any Error) async throws -> Bool ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") @@ -318,7 +322,8 @@ /// 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. +/// ``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:_:performing:)-8762f`` instead. If the thrown error need @@ -327,8 +332,8 @@ /// /// If `expression` should _never_ throw, simply invoke the code without using /// this macro. The test will then fail if an error is thrown. -@freestanding(expression) public macro require( +@freestanding(expression) public macro require( _ comment: @autoclosure () -> Comment? = nil, - performing expression: () async throws -> Void, + performing expression: () async throws -> R, throws errorMatcher: (any Error) async throws -> Bool ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index f35829c20..291ff3bbf 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -484,7 +484,7 @@ public func __checkCast( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws errorType: E.Type, - performing expression: () throws -> Void, + performing expression: () throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -521,7 +521,7 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws errorType: E.Type, - performing expression: () async throws -> Void, + performing expression: () async throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -561,7 +561,7 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws _: Never.Type, - performing expression: () throws -> Void, + performing expression: () throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -570,7 +570,7 @@ public func __checkClosureCall( var success = true var mismatchExplanationValue: String? = nil do { - try expression() + _ = try expression() } catch { success = false mismatchExplanationValue = "an error was thrown when none was expected: \(_description(of: error))" @@ -597,7 +597,7 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws _: Never.Type, - performing expression: () async throws -> Void, + performing expression: () async throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -606,7 +606,7 @@ public func __checkClosureCall( var success = true var mismatchExplanationValue: String? = nil do { - try await expression() + _ = try await expression() } catch { success = false mismatchExplanationValue = "an error was thrown when none was expected: \(_description(of: error))" @@ -633,7 +633,7 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws error: E, - performing expression: () throws -> Void, + performing expression: () throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -659,7 +659,7 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws error: E, - performing expression: () async throws -> Void, + performing expression: () async throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -684,8 +684,8 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( - performing expression: () throws -> Void, +public func __checkClosureCall( + performing expression: () throws -> R, throws errorMatcher: (any Error) throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, sourceCode: SourceCode, @@ -696,8 +696,13 @@ public func __checkClosureCall( var errorMatches = false var mismatchExplanationValue: String? = nil do { - try expression() - mismatchExplanationValue = "an error was expected but none was thrown" + let result = try expression() + + var explanation = "an error was expected but none was thrown" + if R.self != Void.self { + explanation += " and \"\(result)\" was returned" + } + mismatchExplanationValue = explanation } catch { do { errorMatches = try errorMatcher(error) @@ -726,8 +731,8 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( - performing expression: () async throws -> Void, +public func __checkClosureCall( + performing expression: () async throws -> R, throws errorMatcher: (any Error) async throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, sourceCode: SourceCode, @@ -738,8 +743,13 @@ public func __checkClosureCall( var errorMatches = false var mismatchExplanationValue: String? = nil do { - try await expression() - mismatchExplanationValue = "an error was expected but none was thrown" + let result = try await expression() + + var explanation = "an error was expected but none was thrown" + if R.self != Void.self { + explanation += " and \"\(result)\" was returned" + } + mismatchExplanationValue = explanation } catch { do { errorMatches = try await errorMatcher(error) diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index 7f18d4bd5..4cfe0d822 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -431,6 +431,10 @@ final class IssueTests: XCTestCase { #expect(throws: type) {} } genericExpectThrows(Never.self) + func nonVoidReturning() throws -> Int { throw MyError() } + #expect(throws: MyError.self) { + try nonVoidReturning() + } }.run(configuration: configuration) await fulfillment(of: [expectationFailed], timeout: 0.0) @@ -438,7 +442,7 @@ final class IssueTests: XCTestCase { func testErrorCheckingWithExpect_Mismatching() async throws { let expectationFailed = expectation(description: "Expectation failed") - expectationFailed.expectedFulfillmentCount = 10 + expectationFailed.expectedFulfillmentCount = 11 var configuration = Configuration() configuration.eventHandler = { event in @@ -493,6 +497,62 @@ final class IssueTests: XCTestCase { } } genericExpectThrows(Never.self) + func nonVoidReturning() throws -> Int { 0 } + #expect(throws: MyError.self) { + try nonVoidReturning() + } + }.run(configuration: configuration) + + await fulfillment(of: [expectationFailed], timeout: 0.0) + } + + func testErrorCheckingWithExpect_mismatchedErrorDescription() async throws { + let expectationFailed = expectation(description: "Expectation failed") + + var configuration = Configuration() + configuration.eventHandler = { event in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertEqual(expectation.mismatchedErrorDescription, "an error was expected but none was thrown") + expectationFailed.fulfill() + } + + await Test { + func voidReturning() throws {} + #expect(throws: MyError.self) { + try voidReturning() + } + }.run(configuration: configuration) + + await fulfillment(of: [expectationFailed], timeout: 0.0) + } + + func testErrorCheckingWithExpect_mismatchedErrorDescription_nonVoid() async throws { + let expectationFailed = expectation(description: "Expectation failed") + + var configuration = Configuration() + configuration.eventHandler = { event in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertEqual(expectation.mismatchedErrorDescription, "an error was expected but none was thrown and \"0\" was returned") + expectationFailed.fulfill() + } + + await Test { + func nonVoidReturning() throws -> Int { 0 } + #expect(throws: MyError.self) { + try nonVoidReturning() + } }.run(configuration: configuration) await fulfillment(of: [expectationFailed], timeout: 0.0) @@ -532,6 +592,10 @@ final class IssueTests: XCTestCase { await #expect(throws: type) { () async in } } await genericExpectThrows(Never.self) + func nonVoidReturning() async throws -> Int { throw MyError() } + await #expect(throws: MyError.self) { + try await nonVoidReturning() + } }.run(configuration: configuration) await fulfillment(of: [expectationFailed], timeout: 0.0) @@ -539,7 +603,7 @@ final class IssueTests: XCTestCase { func testErrorCheckingWithExpectAsync_Mismatching() async throws { let expectationFailed = expectation(description: "Expectation failed") - expectationFailed.expectedFulfillmentCount = 10 + expectationFailed.expectedFulfillmentCount = 11 var configuration = Configuration() configuration.eventHandler = { event in @@ -586,6 +650,62 @@ final class IssueTests: XCTestCase { } } await genericExpectThrows(Never.self) + func nonVoidReturning() async throws -> Int { 0 } + await #expect(throws: MyError.self) { + try await nonVoidReturning() + } + }.run(configuration: configuration) + + await fulfillment(of: [expectationFailed], timeout: 0.0) + } + + func testErrorCheckingWithExpectAsync_mismatchedErrorDescription() async throws { + let expectationFailed = expectation(description: "Expectation failed") + + var configuration = Configuration() + configuration.eventHandler = { event in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertEqual(expectation.mismatchedErrorDescription, "an error was expected but none was thrown") + expectationFailed.fulfill() + } + + await Test { + func voidReturning() async throws {} + await #expect(throws: MyError.self) { + try await voidReturning() + } + }.run(configuration: configuration) + + await fulfillment(of: [expectationFailed], timeout: 0.0) + } + + func testErrorCheckingWithExpectAsync_mismatchedErrorDescription_nonVoid() async throws { + let expectationFailed = expectation(description: "Expectation failed") + + var configuration = Configuration() + configuration.eventHandler = { event in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertEqual(expectation.mismatchedErrorDescription, "an error was expected but none was thrown and \"0\" was returned") + expectationFailed.fulfill() + } + + await Test { + func nonVoidReturning() async throws -> Int { 0 } + await #expect(throws: MyError.self) { + try await nonVoidReturning() + } }.run(configuration: configuration) await fulfillment(of: [expectationFailed], timeout: 0.0)