Skip to content

Allow throwing an error from an exit test's body. #614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 9 additions & 5 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -1103,15 +1103,15 @@ public func __checkClosureCall<R>(
@_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,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> {
await callExitTest(
exitsWith: expectedExitCondition,
performing: { await body() },
performing: { try await body() },
expression: expression,
comments: comments(),
isRequired: isRequired,
Expand Down
12 changes: 10 additions & 2 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,15 +362,23 @@ 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 {
\(arguments[expectedExitConditionIndex].expression.trimmed)
}
}
"""
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.
Expand Down
3 changes: 3 additions & 0 deletions Tests/TestingTests/ExitTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion Tests/TestingTests/Support/FileHandleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down