Skip to content
Open
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
14 changes: 7 additions & 7 deletions Sources/Testing/Parameterization/Test.Case.Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void> {
// A beautiful hack to give us the right number of cases: iterate over a
// collection containing a single Void value.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -124,7 +124,7 @@ extension Test.Case {
init<C1, C2>(
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<C1, C2> {
self.init(sequence: cartesianProduct(collection1, collection2)) { element in
Test.Case(values: [element.0, element.1], parameters: parameters) {
Expand Down Expand Up @@ -154,7 +154,7 @@ extension Test.Case {
private init<E1, E2>(
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
Expand Down Expand Up @@ -192,7 +192,7 @@ extension Test.Case {
init<E1, E2>(
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)
}
Expand All @@ -210,7 +210,7 @@ extension Test.Case {
init<C1, C2>(
arguments zippedCollections: Zip2Sequence<C1, 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 == Zip2Sequence<C1, C2>, C1: Collection, C2: Collection {
self.init(sequence: zippedCollections, parameters: parameters, testFunction: testFunction)
}
Expand All @@ -234,7 +234,7 @@ extension Test.Case {
init<Key, Value>(
arguments dictionary: Dictionary<Key, Value>,
parameters: [Test.Parameter],
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
testFunction: nonisolated(nonsending) @escaping @Sendable ((Key, Value)) async throws -> Void
) where S == Dictionary<Key, Value> {
if parameters.count > 1 {
self.init(sequence: dictionary) { element in
Expand Down
29 changes: 22 additions & 7 deletions Sources/Testing/Parameterization/Test.Case.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}

Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 7 additions & 7 deletions Sources/Testing/Test+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -454,7 +454,7 @@ extension Test {
arguments dictionary: @escaping @Sendable () async throws -> Dictionary<Key, Value>,
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)
Expand All @@ -479,7 +479,7 @@ extension Test {
arguments zippedCollections: @escaping @Sendable () async throws -> Zip2Sequence<C1, 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)
Expand Down Expand Up @@ -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<T>(_ value: consuming T, isolation: isolated (any Actor)? = #isolation) async -> T where T: ~Copyable & ~Escapable {
@inlinable public nonisolated(nonsending) func __requiringAwait<T>(_ value: consuming T) async -> T where T: ~Copyable & ~Escapable {
value
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))()
}
"""
Expand Down
4 changes: 2 additions & 2 deletions Sources/TestingMacros/SuiteDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
55 changes: 11 additions & 44 deletions Sources/TestingMacros/TestDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
}
"""
Expand Down Expand Up @@ -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),
Expand All @@ -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)
}
"""
Expand Down
Loading
Loading