diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 798e95d0a..3b2e150f8 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -21,7 +21,7 @@ public struct ExitTest: Sendable { public var expectedExitCondition: ExitCondition /// The body closure of the exit test. - fileprivate var body: @Sendable () async -> Void + fileprivate var body: @Sendable () async throws -> Void /// The source location of the exit test. /// @@ -37,7 +37,11 @@ public struct ExitTest: Sendable { /// terminate the process in a way that causes the corresponding expectation /// to fail. public func callAsFunction() async -> Never { - await body() + do { + try await body() + } catch { + _errorInMain(error) + } // Run some glue code that terminates the process with an exit condition // that does not match the expected one. If the exit test's body doesn't @@ -63,7 +67,7 @@ public protocol __ExitTestContainer { static var __sourceLocation: SourceLocation { get } /// The body function of the exit test. - static var __body: @Sendable () async -> Void { get } + static var __body: @Sendable () async throws -> Void { get } } extension ExitTest { @@ -118,7 +122,7 @@ extension ExitTest { /// convention. func callExitTest( exitsWith expectedExitCondition: ExitCondition, - performing body: @escaping @Sendable () async -> Void, + performing body: @escaping @Sendable () async throws -> Void, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index dd4b8875d..c3cf787b0 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -440,7 +440,9 @@ public macro require( /// a clean environment for execution, it is not called within the context of /// the original test. If `expression` does not terminate the child process, the /// process is terminated automatically as if the main function of the child -/// process were allowed to return naturally. +/// process were allowed to return naturally. If an error is thrown from +/// `expression`, it is handed as if the error were thrown from `main()` and the +/// process is terminated. /// /// Once the child process terminates, the parent process resumes and compares /// its exit status against `exitCondition`. If they match, the exit test has @@ -488,8 +490,8 @@ public macro require( /// issues should be attributed. /// - expression: The expression to be evaluated. /// -/// - Throws: An instance of ``ExpectationFailedError`` if `condition` evaluates -/// to `false`. +/// - Throws: An instance of ``ExpectationFailedError`` if the exit condition of +/// the child process does not equal `expectedExitCondition`. /// /// Use this overload of `#require()` when an expression will cause the current /// process to terminate and the nature of that termination will determine if @@ -515,7 +517,9 @@ public macro require( /// a clean environment for execution, it is not called within the context of /// the original test. If `expression` does not terminate the child process, the /// process is terminated automatically as if the main function of the child -/// process were allowed to return naturally. +/// process were allowed to return naturally. If an error is thrown from +/// `expression`, it is handed as if the error were thrown from `main()` and the +/// process is terminated. /// /// Once the child process terminates, the parent process resumes and compares /// its exit status against `exitCondition`. If they match, the exit test has @@ -550,5 +554,5 @@ public macro require( exitsWith expectedExitCondition: ExitCondition, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, - performing expression: @convention(thin) () async -> Void + performing expression: @convention(thin) () async throws -> Void ) = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro") diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 2a3e137a3..edc6fac04 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1103,7 +1103,7 @@ public func __checkClosureCall( @_spi(Experimental) public func __checkClosureCall( exitsWith expectedExitCondition: ExitCondition, - performing body: @convention(thin) () async -> Void, + performing body: @convention(thin) () async throws -> Void, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -1111,7 +1111,7 @@ public func __checkClosureCall( ) async -> Result { await callExitTest( exitsWith: expectedExitCondition, - performing: { await body() }, + performing: { try await body() }, expression: expression, comments: comments(), isRequired: isRequired, diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 690489702..2ee461017 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -362,7 +362,7 @@ extension ExitTestConditionMacro { static var __sourceLocation: Testing.SourceLocation { \(createSourceLocationExpr(of: macro, context: context)) } - static var __body: @Sendable () async -> Void { + static var __body: @Sendable () async throws -> Void { \(bodyArgumentExpr.trimmed) } static var __expectedExitCondition: Testing.ExitCondition { @@ -370,7 +370,15 @@ extension ExitTestConditionMacro { } } """ - arguments[trailingClosureIndex].expression = "{ \(enumDecl) }" + + // Explicitly include a closure signature to work around a compiler bug + // type-checking thin throwing functions after macro expansion. + // SEE: rdar://133979438 + arguments[trailingClosureIndex].expression = """ + { () async throws in + \(enumDecl) + } + """ // Replace the exit test body (as an argument to the macro) with a stub // closure that hosts the type we created above. diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 55c22a9bb..a76776938 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -32,6 +32,9 @@ private import _TestingInternals await Task.yield() exit(123) } + await #expect(exitsWith: .failure) { + throw MyError() + } #if !os(Windows) await #expect(exitsWith: .signal(SIGKILL)) { _ = kill(getpid(), SIGKILL) diff --git a/Tests/TestingTests/Support/FileHandleTests.swift b/Tests/TestingTests/Support/FileHandleTests.swift index a8d6e8285..89a3d246f 100644 --- a/Tests/TestingTests/Support/FileHandleTests.swift +++ b/Tests/TestingTests/Support/FileHandleTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@testable import Testing +@testable @_spi(Experimental) import Testing private import _TestingInternals #if !SWT_NO_FILE_IO @@ -63,6 +63,16 @@ struct FileHandleTests { } } +#if !SWT_NO_EXIT_TESTS + @Test("Writing requires contiguous storage") + func writeIsContiguous() async { + await #expect(exitsWith: .failure) { + let fileHandle = try FileHandle.null(mode: "wb") + try fileHandle.write([1, 2, 3, 4, 5].lazy.filter { $0 == 1 }) + } + } +#endif + @Test("Can read from a file") func canRead() throws { let bytes: [UInt8] = (0 ..< 8192).map { _ in