diff --git a/Sources/Testing/Parameterization/Test.Case.Generator.swift b/Sources/Testing/Parameterization/Test.Case.Generator.swift index c682be19a..477391ae4 100644 --- a/Sources/Testing/Parameterization/Test.Case.Generator.swift +++ b/Sources/Testing/Parameterization/Test.Case.Generator.swift @@ -57,7 +57,7 @@ extension Test.Case { /// - Parameters: /// - testFunction: The test function called by the generated test case. init( - testFunction: @escaping @Sendable () async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable () async throws -> Void ) where S == CollectionOfOne { // A beautiful hack to give us the right number of cases: iterate over a // collection containing a single Void value. @@ -85,7 +85,7 @@ extension Test.Case { init( arguments collection: S, parameters: [Test.Parameter], - testFunction: @escaping @Sendable (S.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (S.Element) async throws -> Void ) where S: Collection { if parameters.count > 1 { self.init(sequence: collection) { element in @@ -124,7 +124,7 @@ extension Test.Case { init( arguments collection1: C1, _ collection2: C2, parameters: [Test.Parameter], - testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C1.Element, C2.Element) async throws -> Void ) where S == CartesianProduct { self.init(sequence: cartesianProduct(collection1, collection2)) { element in Test.Case(values: [element.0, element.1], parameters: parameters) { @@ -154,7 +154,7 @@ extension Test.Case { private init( sequence: S, parameters: [Test.Parameter], - testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((E1, E2)) async throws -> Void ) where S.Element == (E1, E2), E1: Sendable, E2: Sendable { if parameters.count > 1 { self.init(sequence: sequence) { element in @@ -192,7 +192,7 @@ extension Test.Case { init( arguments collection: S, parameters: [Test.Parameter], - testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((E1, E2)) async throws -> Void ) where S: Collection, S.Element == (E1, E2) { self.init(sequence: collection, parameters: parameters, testFunction: testFunction) } @@ -210,7 +210,7 @@ extension Test.Case { init( arguments zippedCollections: Zip2Sequence, parameters: [Test.Parameter], - testFunction: @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void ) where S == Zip2Sequence, C1: Collection, C2: Collection { self.init(sequence: zippedCollections, parameters: parameters, testFunction: testFunction) } @@ -234,7 +234,7 @@ extension Test.Case { init( arguments dictionary: Dictionary, parameters: [Test.Parameter], - testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((Key, Value)) async throws -> Void ) where S == Dictionary { if parameters.count > 1 { self.init(sequence: dictionary) { element in diff --git a/Sources/Testing/Parameterization/Test.Case.swift b/Sources/Testing/Parameterization/Test.Case.swift index 18faaf4e2..f62ac8c1b 100644 --- a/Sources/Testing/Parameterization/Test.Case.swift +++ b/Sources/Testing/Parameterization/Test.Case.swift @@ -155,9 +155,9 @@ extension Test { } } - private init(kind: _Kind, body: @escaping @Sendable () async throws -> Void) { - self._kind = kind - self.body = body + private init(kind: _Kind, body: nonisolated(nonsending) @escaping @Sendable () async throws -> Void) { + _kind = kind + _body = body } /// Initialize a test case for a non-parameterized test function. @@ -166,7 +166,7 @@ extension Test { /// - body: The body closure of this test case. /// /// The resulting test case will have zero arguments. - init(body: @escaping @Sendable () async throws -> Void) { + init(body: nonisolated(nonsending) @escaping @Sendable () async throws -> Void) { self.init(kind: .nonParameterized, body: body) } @@ -180,7 +180,7 @@ extension Test { init( values: [any Sendable], parameters: [Parameter], - body: @escaping @Sendable () async throws -> Void + body: nonisolated(nonsending) @escaping @Sendable () async throws -> Void ) { var isStable = true @@ -229,10 +229,25 @@ extension Test { } /// The body closure of this test case. + private var _body: nonisolated(nonsending) @Sendable () async throws -> Void + + /// Invoke the body closure of this test case. + /// + /// - Parameters: + /// - configuration: The configuration to use for running. /// - /// Do not invoke this closure directly. Always use a ``Runner`` to invoke a + /// Do not call this function directly. Always use a ``Runner`` to invoke a /// test or test case. - var body: @Sendable () async throws -> Void + nonisolated(nonsending) func run(configuration: borrowing Configuration) async throws { + if let actor = configuration.defaultSynchronousIsolationContext { + func runIsolated(to actor: isolated some Actor) async throws { + try await _body() + } + try await runIsolated(to: actor) + } else { + try await _body() + } + } } /// A type representing a single parameter to a parameterized test function. diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index 849e06b26..31969b4f9 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -104,7 +104,7 @@ extension Runner { private static func _applyScopingTraits( for test: Test, testCase: Test.Case?, - _ body: @escaping @Sendable () async throws -> Void + _ body: nonisolated(nonsending) @escaping @Sendable () async throws -> Void ) async throws { // If the test does not have any traits, exit early to avoid unnecessary // heap allocations below. @@ -140,7 +140,7 @@ extension Runner { /// /// - Throws: Whatever is thrown by `body` or by any of the traits' provide /// scope function calls. - private static func _applyIssueHandlingTraits(for test: Test, _ body: @escaping @Sendable () async throws -> Void) async throws { + private static func _applyIssueHandlingTraits(for test: Test, _ body: nonisolated(nonsending) @escaping @Sendable () async throws -> Void) async throws { // If the test does not have any traits, exit early to avoid unnecessary // heap allocations below. if test.traits.isEmpty { @@ -420,7 +420,7 @@ extension Runner { try await withTimeLimit(for: step.test, configuration: configuration) { try await _applyScopingTraits(for: step.test, testCase: testCase) { - try await testCase.body() + try await testCase.run(configuration: configuration) } } timeoutHandler: { timeLimit in let issue = Issue( diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 97fe104de..1f235b63b 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -163,7 +163,7 @@ extension Test { traits: [any TestTrait], sourceBounds: __SourceBounds, parameters: [__Parameter] = [], - testFunction: @escaping @Sendable () async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable () async throws -> Void ) -> Self where S: ~Copyable & ~Escapable { // Don't use Optional.map here due to a miscompile/crash. Expand out to an // if expression instead. SEE: rdar://134280902 @@ -248,7 +248,7 @@ extension Test { arguments collection: @escaping @Sendable () async throws -> C, sourceBounds: __SourceBounds, parameters paramTuples: [__Parameter], - testFunction: @escaping @Sendable (C.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C.Element) async throws -> Void ) -> Self where S: ~Copyable & ~Escapable, C: Collection & Sendable, C.Element: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) @@ -395,7 +395,7 @@ extension Test { arguments collection1: @escaping @Sendable () async throws -> C1, _ collection2: @escaping @Sendable () async throws -> C2, sourceBounds: __SourceBounds, parameters paramTuples: [__Parameter], - testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C1.Element, C2.Element) async throws -> Void ) -> Self where S: ~Copyable & ~Escapable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) @@ -423,7 +423,7 @@ extension Test { arguments collection: @escaping @Sendable () async throws -> C, sourceBounds: __SourceBounds, parameters paramTuples: [__Parameter], - testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((E1, E2)) async throws -> Void ) -> Self where S: ~Copyable & ~Escapable, C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) @@ -454,7 +454,7 @@ extension Test { arguments dictionary: @escaping @Sendable () async throws -> Dictionary, sourceBounds: __SourceBounds, parameters paramTuples: [__Parameter], - testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((Key, Value)) async throws -> Void ) -> Self where S: ~Copyable & ~Escapable, Key: Sendable, Value: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) @@ -479,7 +479,7 @@ extension Test { arguments zippedCollections: @escaping @Sendable () async throws -> Zip2Sequence, sourceBounds: __SourceBounds, parameters paramTuples: [__Parameter], - testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C1.Element, C2.Element) async throws -> Void ) -> Self where S: ~Copyable & ~Escapable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) @@ -547,7 +547,7 @@ extension Test { /// - Warning: This function is used to implement the `@Test` macro. Do not use /// it directly. @_lifetime(copy value) -@inlinable public func __requiringAwait(_ value: consuming T, isolation: isolated (any Actor)? = #isolation) async -> T where T: ~Copyable & ~Escapable { +@inlinable public nonisolated(nonsending) func __requiringAwait(_ value: consuming T) async -> T where T: ~Copyable & ~Escapable { value } diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 85f5ed227..5f9c4d345 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -507,7 +507,7 @@ extension ExitTestConditionMacro { } decls.append( """ - @Sendable func \(bodyThunkName)(\(bodyThunkParameterList)) async throws { + @Sendable nonisolated(nonsending) func \(bodyThunkName)(\(bodyThunkParameterList)) async throws { _ = \(applyEffectfulKeywords([.try, .await, .unsafe], to: bodyArgumentExpr))() } """ diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index dcf0a76bd..42de5f27c 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -130,8 +130,8 @@ public struct SuiteDeclarationMacro: PeerMacro, Sendable { let generatorName = context.makeUniqueName("generator") result.append( """ - @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") - @Sendable private \(staticKeyword(for: containingType)) func \(generatorName)() async -> Testing.Test { + @available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.") + @Sendable private nonisolated(nonsending) \(staticKeyword(for: containingType)) func \(generatorName)() async -> Testing.Test { .__type( \(declaration.type.trimmed).self, \(raw: attributeInfo.functionArgumentList(in: context)) diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index 8065d299e..8ea344754 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -27,13 +27,6 @@ extension FunctionDeclSyntax { .contains(.keyword(.mutating)) } - /// Whether or not this function is a `nonisolated` function. - var isNonisolated: Bool { - modifiers.lazy - .map(\.name.tokenKind) - .contains(.keyword(.nonisolated)) - } - /// Whether or not this function declares an operator. var isOperator: Bool { switch name.tokenKind { diff --git a/Sources/TestingMacros/Support/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift index 3a139abf5..3802e052c 100644 --- a/Sources/TestingMacros/Support/TestContentGeneration.swift +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -56,7 +56,7 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? private nonisolated \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = ( \(kindExpr), \(kind.commentRepresentation) 0, - \(accessorExpr), + \(raw: accessorExpr), \(contextExpr), 0 ) diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index c3d9f64fd..39ae46535 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -298,46 +298,13 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { thunkBody = "_ = \(forwardCall("\(functionDecl.name.trimmed)\(forwardedParamsExpr)"))" } - // If this function is synchronous, is not explicitly nonisolated, and is - // not explicitly isolated to some actor, it should run in the configured - // default isolation context. If the suite type is an actor, this will cause - // a hop off the actor followed by an immediate hop back on, but otherwise - // should be harmless. Note that we do not support specifying an `isolated` - // parameter on a test function at this time. - // - // We use a second, inner thunk function here instead of just adding the - // isolation parameter to the "real" thunk because adding it there prevents - // correct tuple desugaring of the "real" arguments to the thunk. - if functionDecl.signature.effectSpecifiers?.asyncSpecifier == nil && !isMainActorIsolated && !functionDecl.isNonisolated { - // Get a unique name for this secondary thunk. We don't need it to be - // uniqued against functionDecl because it's interior to the "real" thunk, - // so its name can't conflict with any other names visible in this scope. - let isolationThunkName = context.makeUniqueName("") - - // Insert a (defaulted) isolated argument. If we emit a closure (or inner - // function) that captured the arguments to the "real" thunk, the compiler - // has trouble reasoning about the lifetime of arguments to that closure - // especially if those arguments are borrowed or consumed, which results - // in hard-to-avoid compile-time errors. Fortunately, forwarding the full - // argument list is straightforward. - let thunkParamsExprCopy = FunctionParameterClauseSyntax { - for thunkParam in thunkParamsExpr.parameters { - thunkParam - } - FunctionParameterSyntax( - firstName: .wildcardToken(), - type: "isolated (any _Concurrency.Actor)?" as TypeSyntax, - defaultValue: InitializerClauseSyntax(value: "Testing.__defaultSynchronousIsolationContext" as ExprSyntax) - ) - } - - thunkBody = """ - @Sendable func \(isolationThunkName)\(thunkParamsExprCopy) async throws { - \(thunkBody) - } - try await \(isolationThunkName)\(forwardedParamsExpr) - """ - } + // Forward the nonisolated keyword from the original function. If none is + // present, use `nonisolated(nonsending)` by default. + let existingNonisolatedKeyword = functionDecl.modifiers.first { $0.name.tokenKind == .keyword(.nonisolated) } + let nonisolatedKeyword = existingNonisolatedKeyword?.trimmed ?? DeclModifierSyntax( + name: .keyword(.nonisolated), + detail: DeclModifierDetailSyntax(detail: .keyword(.nonsending)) + ) // Add availability guards if needed. thunkBody = createSyntaxNode( @@ -349,7 +316,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { let thunkName = context.makeUniqueName(thunking: functionDecl) let thunkDecl: DeclSyntax = """ @available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.") - @Sendable private \(staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void { + @Sendable private \(nonisolatedKeyword) \(staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void { \(thunkBody) } """ @@ -440,7 +407,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { result.append( """ @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") - private \(staticKeyword(for: typeName)) nonisolated func \(unavailableTestName)() async -> Testing.Test { + private nonisolated(nonsending) \(staticKeyword(for: typeName)) func \(unavailableTestName)() async -> Testing.Test { .__function( named: \(literal: functionDecl.completeName.trimmedDescription), in: \(typeNameExpr), @@ -465,8 +432,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { let generatorName = context.makeUniqueName(thunking: functionDecl, withPrefix: "generator") result.append( """ - @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") - @Sendable private \(staticKeyword(for: typeName)) func \(generatorName)() async -> Testing.Test { + @available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.") + @Sendable private nonisolated(nonsending) \(staticKeyword(for: typeName)) func \(generatorName)() async -> Testing.Test { \(raw: testsBody) } """ diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 8758ac9c6..1ac4de803 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -175,7 +175,7 @@ extension Test { sourceLocation: SourceLocation = #_sourceLocation, sourceBounds: __SourceBounds? = nil, name: String = #function, - testFunction: @escaping @Sendable () async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable () async throws -> Void ) { let sourceBounds = sourceBounds ?? __SourceBounds(lowerBoundOnly: sourceLocation) let caseGenerator = Case.Generator(testFunction: testFunction) @@ -207,7 +207,7 @@ extension Test { sourceBounds: __SourceBounds? = nil, column: Int = #column, name: String = #function, - testFunction: @escaping @Sendable (C.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C.Element) async throws -> Void ) where C: Collection & Sendable, C.Element: Sendable { let sourceBounds = sourceBounds ?? __SourceBounds(lowerBoundOnly: sourceLocation) let caseGenerator = Case.Generator(arguments: collection, parameters: parameters, testFunction: testFunction) @@ -224,7 +224,7 @@ extension Test { sourceBounds: __SourceBounds? = nil, column: Int = #column, name: String = #function, - testFunction: @escaping @Sendable (C.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C.Element) async throws -> Void ) where C: Collection & Sendable, C.Element: Sendable { let sourceBounds = sourceBounds ?? __SourceBounds(lowerBoundOnly: sourceLocation) let caseGenerator = { @Sendable in @@ -259,7 +259,7 @@ extension Test { sourceLocation: SourceLocation = #_sourceLocation, sourceBounds: __SourceBounds? = nil, name: String = #function, - testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable (C1.Element, C2.Element) async throws -> Void ) where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { let sourceBounds = sourceBounds ?? __SourceBounds(lowerBoundOnly: sourceLocation) let caseGenerator = Case.Generator(arguments: collection1, collection2, parameters: parameters, testFunction: testFunction) @@ -287,7 +287,7 @@ extension Test { sourceLocation: SourceLocation = #_sourceLocation, sourceBounds: __SourceBounds? = nil, name: String = #function, - testFunction: @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void + testFunction: nonisolated(nonsending) @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void ) where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { let sourceBounds = sourceBounds ?? __SourceBounds(lowerBoundOnly: sourceLocation) let caseGenerator = Case.Generator(arguments: zippedCollections, parameters: parameters, testFunction: testFunction) diff --git a/Tests/TestingTests/ZipTests.swift b/Tests/TestingTests/ZipTests.swift index bd767b222..71fe97c5f 100644 --- a/Tests/TestingTests/ZipTests.swift +++ b/Tests/TestingTests/ZipTests.swift @@ -26,3 +26,5 @@ struct ZipTests { #expect(i == j) } } + +