Skip to content

Inherit isolation in #expect(exitsWith:). #736

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

Merged
merged 1 commit into from
Sep 30, 2024
Merged
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
3 changes: 2 additions & 1 deletion Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,18 @@ extension ExitTest {
/// - isRequired: Whether or not the expectation is required. The value of
/// this argument does not affect whether or not an error is thrown on
/// failure.
/// - isolation: The actor to which the exit test is isolated, if any.
/// - sourceLocation: The source location of the expectation.
///
/// This function contains the common implementation for all
/// `await #expect(exitsWith:) { }` invocations regardless of calling
/// convention.
func callExitTest(
exitsWith expectedExitCondition: ExitCondition,
performing _: @escaping @Sendable () async throws -> Void,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> {
guard let configuration = Configuration.current ?? Configuration.all.first else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1142,15 +1142,15 @@ public func __checkClosureCall<R>(
@_spi(Experimental)
public func __checkClosureCall(
exitsWith expectedExitCondition: ExitCondition,
performing body: @convention(thin) () async throws -> Void,
performing body: @convention(thin) () -> Void,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> {
await callExitTest(
exitsWith: expectedExitCondition,
performing: { try await body() },
expression: expression,
comments: comments(),
isRequired: isRequired,
Expand Down
56 changes: 35 additions & 21 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,32 +376,46 @@ extension ExitTestConditionMacro {

let bodyArgumentExpr = arguments[trailingClosureIndex].expression

var decls = [DeclSyntax]()

// Implement the body of the exit test outside the enum we're declaring so
// that `Self` resolves to the type containing the exit test, not the enum.
let bodyThunkName = context.makeUniqueName("")
decls.append(
"""
@Sendable func \(bodyThunkName)() async throws -> Void {
return try await Testing.__requiringTry(Testing.__requiringAwait(\(bodyArgumentExpr.trimmed)))()
}
"""
)

// Create a local type that can be discovered at runtime and which contains
// the exit test body.
let enumName = context.makeUniqueName("__🟠$exit_test_body__")
let enumDecl: DeclSyntax = """
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName): Testing.__ExitTestContainer {
static var __sourceLocation: Testing.SourceLocation {
\(createSourceLocationExpr(of: macro, context: context))
}
static var __body: @Sendable () async throws -> Void {
\(bodyArgumentExpr.trimmed)
decls.append(
"""
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName): Testing.__ExitTestContainer, Sendable {
static var __sourceLocation: Testing.SourceLocation {
\(createSourceLocationExpr(of: macro, context: context))
}
static var __body: @Sendable () async throws -> Void {
\(bodyThunkName)
}
static var __expectedExitCondition: Testing.ExitCondition {
\(arguments[expectedExitConditionIndex].expression.trimmed)
}
}
static var __expectedExitCondition: Testing.ExitCondition {
\(arguments[expectedExitConditionIndex].expression.trimmed)
"""
)

arguments[trailingClosureIndex].expression = ExprSyntax(
ClosureExprSyntax {
for decl in decls {
CodeBlockItemSyntax(item: .decl(decl))
}
}
}
"""

// 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
14 changes: 14 additions & 0 deletions Tests/TestingTests/ExitTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,20 @@ private import _TestingInternals
#expect(ExitCondition.signal(SIGTERM) !== .signal(SIGINT))
#endif
}

@MainActor static func someMainActorFunction() {
MainActor.assertIsolated()
}

@Test("Exit test can be main-actor-isolated")
@MainActor
func mainActorIsolation() async {
await #expect(exitsWith: .success) {
await Self.someMainActorFunction()
_ = 0
exit(EXIT_SUCCESS)
}
}
}

// MARK: - Fixtures
Expand Down