From f7d6a6cfe9e4797a8176b39a6667c0167cf63727 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Sun, 24 Sep 2023 14:01:10 -0500 Subject: [PATCH 1/6] Accept non-Void types in trailing closure of #expect(throws:) --- .../Expectations/Expectation+Macro.swift | 32 +++++------ .../ExpectationChecking+Macro.swift | 54 +++++++++++-------- Tests/TestingTests/IssueTests.swift | 10 +++- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 9a61ff151..523bb80ee 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -88,10 +88,10 @@ /// 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 -> T ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error /// Check that an expression never throws an error. @@ -124,10 +124,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 -> T ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") /// Check that an expression always throws an error of a given type, and throw @@ -163,10 +163,10 @@ /// /// 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 -> T ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error /// Check that an expression never throws an error, and throw an error if it @@ -179,10 +179,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 -> T ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") // MARK: - Matching instances of equatable errors @@ -211,10 +211,10 @@ /// 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 -> T ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable /// Check that an expression always throws a specific error, and throw an error @@ -244,10 +244,10 @@ /// /// 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 -> T ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable // MARK: - Arbitrary error matching @@ -283,9 +283,9 @@ /// 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 -> T, throws errorMatcher: (any Error) async throws -> Bool ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") @@ -327,8 +327,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 -> T, 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..d510258ac 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -482,9 +482,9 @@ public func __checkCast( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () throws -> Void, + performing expression: () throws -> T, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -519,9 +519,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () async throws -> Void, + performing expression: () async throws -> T, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -559,9 +559,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () throws -> Void, + performing expression: () throws -> T, 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))" @@ -595,9 +595,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () async throws -> Void, + performing expression: () async throws -> T, 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))" @@ -631,9 +631,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () throws -> Void, + performing expression: () throws -> T, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -657,9 +657,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () async throws -> Void, + performing expression: () async throws -> T, 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 -> T, 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 type(of: result) != 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 -> T, 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 type(of: result) != 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..d65cfd72e 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 zero() throws -> Int { throw MyError() } + #expect(throws: MyError.self) { + try zero() + } }.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,10 @@ final class IssueTests: XCTestCase { } } genericExpectThrows(Never.self) + func zero() throws -> Int { 0 } + #expect(throws: MyError.self) { + try zero() + } }.run(configuration: configuration) await fulfillment(of: [expectationFailed], timeout: 0.0) From 8cdd8019c9bd386c7b6ccb9d4ebef0c1b68a50c3 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 25 Sep 2023 08:46:57 -0500 Subject: [PATCH 2/6] Use Any instead of requiring new generic type parameters --- .../Expectations/Expectation+Macro.swift | 32 +++++++++---------- .../ExpectationChecking+Macro.swift | 32 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 523bb80ee..e68fa692a 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -88,10 +88,10 @@ /// 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 -> T + performing expression: () async throws -> Any ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error /// Check that an expression never throws an error. @@ -124,10 +124,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 -> T + performing expression: () async throws -> Any ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") /// Check that an expression always throws an error of a given type, and throw @@ -163,10 +163,10 @@ /// /// 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 -> T + performing expression: () async throws -> Any ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error /// Check that an expression never throws an error, and throw an error if it @@ -179,10 +179,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 -> T + performing expression: () async throws -> Any ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") // MARK: - Matching instances of equatable errors @@ -211,10 +211,10 @@ /// 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 -> T + performing expression: () async throws -> Any ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable /// Check that an expression always throws a specific error, and throw an error @@ -244,10 +244,10 @@ /// /// 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 -> T + performing expression: () async throws -> Any ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable // MARK: - Arbitrary error matching @@ -283,9 +283,9 @@ /// 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 -> T, + performing expression: () async throws -> Any, throws errorMatcher: (any Error) async throws -> Bool ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") @@ -327,8 +327,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 -> T, + performing expression: () async throws -> Any, 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 d510258ac..4e5077846 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -482,9 +482,9 @@ public func __checkCast( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () throws -> T, + performing expression: () throws -> Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -519,9 +519,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () async throws -> T, + performing expression: () async throws -> Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -559,9 +559,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () throws -> T, + performing expression: () throws -> Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -595,9 +595,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () async throws -> T, + performing expression: () async throws -> Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -631,9 +631,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () throws -> T, + performing expression: () throws -> Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -657,9 +657,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () async throws -> T, + performing expression: () async throws -> 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 -> T, +public func __checkClosureCall( + performing expression: () throws -> Any, throws errorMatcher: (any Error) throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, sourceCode: SourceCode, @@ -731,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 -> T, +public func __checkClosureCall( + performing expression: () async throws -> Any, throws errorMatcher: (any Error) async throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, sourceCode: SourceCode, From a1b18440e5baad968a5e5b3de79863169eeb9259 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 25 Sep 2023 09:04:48 -0500 Subject: [PATCH 3/6] Validate mismatchedErrorDescription property in tests --- Tests/TestingTests/IssueTests.swift | 122 ++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index d65cfd72e..4cfe0d822 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -431,9 +431,9 @@ final class IssueTests: XCTestCase { #expect(throws: type) {} } genericExpectThrows(Never.self) - func zero() throws -> Int { throw MyError() } + func nonVoidReturning() throws -> Int { throw MyError() } #expect(throws: MyError.self) { - try zero() + try nonVoidReturning() } }.run(configuration: configuration) @@ -497,9 +497,61 @@ final class IssueTests: XCTestCase { } } genericExpectThrows(Never.self) - func zero() throws -> Int { 0 } + func nonVoidReturning() throws -> Int { 0 } #expect(throws: MyError.self) { - try zero() + 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) @@ -540,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) @@ -547,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 @@ -594,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) From c7f5596d751f234b45d2a2fbc1507fb543fe5608 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 25 Sep 2023 09:50:17 -0500 Subject: [PATCH 4/6] Swich back to generic type param --- .../Expectations/Expectation+Macro.swift | 32 ++++++++--------- .../ExpectationChecking+Macro.swift | 36 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index e68fa692a..f0a29c591 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -88,10 +88,10 @@ /// 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 -> Any + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error /// Check that an expression never throws an error. @@ -124,10 +124,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 -> Any + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") /// Check that an expression always throws an error of a given type, and throw @@ -163,10 +163,10 @@ /// /// 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 -> Any + 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 +179,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 -> Any + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") // MARK: - Matching instances of equatable errors @@ -211,10 +211,10 @@ /// 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 -> Any + 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 @@ -244,10 +244,10 @@ /// /// 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 -> Any + performing expression: () async throws -> R ) = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable // MARK: - Arbitrary error matching @@ -283,9 +283,9 @@ /// 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 -> Any, + performing expression: () async throws -> R, throws errorMatcher: (any Error) async throws -> Bool ) = #externalMacro(module: "TestingMacros", type: "ExpectMacro") @@ -327,8 +327,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 -> Any, + 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 4e5077846..ddd4fec81 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -482,9 +482,9 @@ public func __checkCast( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () throws -> Any, + performing expression: () throws -> R, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -519,9 +519,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () async throws -> Any, + performing expression: () async throws -> R, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -559,9 +559,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () throws -> Any, + performing expression: () throws -> R, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -595,9 +595,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () async throws -> Any, + performing expression: () async throws -> R, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -631,9 +631,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () throws -> Any, + performing expression: () throws -> R, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -657,9 +657,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () async throws -> Any, + performing expression: () async throws -> R, 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 -> Any, +public func __checkClosureCall( + performing expression: () throws -> R, throws errorMatcher: (any Error) throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, sourceCode: SourceCode, @@ -699,7 +699,7 @@ public func __checkClosureCall( let result = try expression() var explanation = "an error was expected but none was thrown" - if type(of: result) != Void.self { + if R.self != Void.self { explanation += " and \"\(result)\" was returned" } mismatchExplanationValue = explanation @@ -731,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 -> Any, +public func __checkClosureCall( + performing expression: () async throws -> R, throws errorMatcher: (any Error) async throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, sourceCode: SourceCode, @@ -746,7 +746,7 @@ public func __checkClosureCall( let result = try await expression() var explanation = "an error was expected but none was thrown" - if type(of: result) != Void.self { + if R.self != Void.self { explanation += " and \"\(result)\" was returned" } mismatchExplanationValue = explanation From 0d6ff88633f0c49ac273f1e322776fa1f1689150 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 25 Sep 2023 10:00:35 -0500 Subject: [PATCH 5/6] Explain new behavior in documentation --- .../Expectations/Expectation+Macro.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index f0a29c591..a92e648bf 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -83,7 +83,8 @@ /// /// 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 @@ -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 @@ -156,7 +158,7 @@ /// 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. @@ -206,7 +208,7 @@ /// /// 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 @@ -241,6 +243,7 @@ /// 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. @@ -276,7 +279,8 @@ /// 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 @@ -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 From f126664595f26731e7943fe0a1a44b2983700fda Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 25 Sep 2023 10:47:42 -0500 Subject: [PATCH 6/6] Switch to `some Any` in eligible places --- .../ExpectationChecking+Macro.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index ddd4fec81..291ff3bbf 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -482,9 +482,9 @@ public func __checkCast( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () throws -> R, + performing expression: () throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -519,9 +519,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws errorType: E.Type, - performing expression: () async throws -> R, + performing expression: () async throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -559,9 +559,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () throws -> R, + performing expression: () throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -595,9 +595,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws _: Never.Type, - performing expression: () async throws -> R, + performing expression: () async throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -631,9 +631,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () throws -> R, + performing expression: () throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -657,9 +657,9 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public func __checkClosureCall( throws error: E, - performing expression: () async throws -> R, + performing expression: () async throws -> some Any, sourceCode: SourceCode, comments: @autoclosure () -> [Comment], isRequired: Bool,