Skip to content

Commit 9e0b757

Browse files
authored
Allow non-escapable types as suites. (#1468)
This PR enables `@Suite` on non-escapable types and `@Test` on functions within them. We need to use the experimental `@_lifetime` attribute on some helper functions to ensure this works. Developers who want to write tests on `~Escapable` types will also need to enable the experimental "Lifetimes" feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent ed79161 commit 9e0b757

8 files changed

Lines changed: 33 additions & 56 deletions

File tree

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ extension Array where Element == PackageDescription.SwiftSetting {
384384
// proposal via Swift Evolution.
385385
.enableExperimentalFeature("SymbolLinkageMarkers"),
386386

387+
// Enabled to allow tests to be added to ~Escapable suites.
388+
.enableExperimentalFeature("Lifetimes"),
389+
387390
.enableUpcomingFeature("InferIsolatedConformances"),
388391

389392
// When building as a package, the macro plugin always builds as an

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public struct TypeInfo: Sendable {
1818
///
1919
/// - Parameters:
2020
/// - type: The concrete metatype.
21-
case type(_ type: any ~Copyable.Type)
21+
case type(_ type: any (~Copyable & ~Escapable).Type)
2222

2323
/// The type info represents a metatype, but a reference to that metatype is
2424
/// not available at runtime.
@@ -38,7 +38,7 @@ public struct TypeInfo: Sendable {
3838
///
3939
/// If this instance was created from a type name, or if it was previously
4040
/// encoded and decoded, the value of this property is `nil`.
41-
public var type: (any ~Copyable.Type)? {
41+
public var type: (any (~Copyable & ~Escapable).Type)? {
4242
if case let .type(type) = _kind {
4343
return type
4444
}
@@ -79,7 +79,7 @@ public struct TypeInfo: Sendable {
7979
///
8080
/// - Parameters:
8181
/// - type: The type which this instance should describe.
82-
init(describing type: (some ~Copyable).Type) {
82+
init<T>(describing type: T.Type) where T: ~Copyable & ~Escapable {
8383
_kind = .type(type)
8484
}
8585

@@ -101,7 +101,7 @@ public struct TypeInfo: Sendable {
101101
///
102102
/// - Parameters:
103103
/// - value: The value whose type this instance should describe.
104-
init<T>(describingTypeOf value: borrowing T) where T: ~Copyable {
104+
init<T>(describingTypeOf value: borrowing T) where T: ~Copyable & ~Escapable {
105105
self.init(describing: T.self)
106106
}
107107
}

Sources/Testing/Test+Macro.swift

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,8 @@ public typealias __XCTestCompatibleSelector = Never
5353
/// - Parameters:
5454
/// - traits: Zero or more traits to apply to this test suite.
5555
///
56-
/// A test suite is a type that contains one or more test functions. Any
57-
/// escapable type (that is, any type that is not marked `~Escapable`) may be a
58-
/// test suite.
56+
/// A test suite is a type that contains one or more test functions. Any type
57+
/// may be a test suite.
5958
///
6059
/// The use of the `@Suite` attribute is optional; types are recognized as test
6160
/// suites even if they do not have the `@Suite` attribute applied to them.
@@ -81,9 +80,8 @@ public macro Suite(
8180
/// from the associated type's name.
8281
/// - traits: Zero or more traits to apply to this test suite.
8382
///
84-
/// A test suite is a type that contains one or more test functions. Any
85-
/// escapable type (that is, any type that is not marked `~Escapable`) may be a
86-
/// test suite.
83+
/// A test suite is a type that contains one or more test functions. Any type
84+
/// may be a test suite.
8785
///
8886
/// The use of the `@Suite` attribute is optional; types are recognized as test
8987
/// suites even if they do not have the `@Suite` attribute applied to them.
@@ -105,12 +103,12 @@ extension Test {
105103
///
106104
/// - Warning: This function is used to implement the `@Suite` macro. Do not
107105
/// call it directly.
108-
public static func __type(
109-
_ containingType: any ~Copyable.Type,
106+
public static func __type<S>(
107+
_ containingType: S.Type,
110108
displayName: String? = nil,
111109
traits: [any SuiteTrait],
112110
sourceLocation: SourceLocation
113-
) -> Self {
111+
) -> Self where S: ~Copyable & ~Escapable {
114112
let containingTypeInfo = TypeInfo(describing: containingType)
115113
return Self(displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo)
116114
}
@@ -166,7 +164,7 @@ extension Test {
166164
sourceLocation: SourceLocation,
167165
parameters: [__Parameter] = [],
168166
testFunction: @escaping @Sendable () async throws -> Void
169-
) -> Self where S: ~Copyable {
167+
) -> Self where S: ~Copyable & ~Escapable {
170168
// Don't use Optional.map here due to a miscompile/crash. Expand out to an
171169
// if expression instead. SEE: rdar://134280902
172170
let containingTypeInfo: TypeInfo? = if let containingType {
@@ -251,7 +249,7 @@ extension Test {
251249
sourceLocation: SourceLocation,
252250
parameters paramTuples: [__Parameter],
253251
testFunction: @escaping @Sendable (C.Element) async throws -> Void
254-
) -> Self where S: ~Copyable, C: Collection & Sendable, C.Element: Sendable {
252+
) -> Self where S: ~Copyable & ~Escapable, C: Collection & Sendable, C.Element: Sendable {
255253
let containingTypeInfo: TypeInfo? = if let containingType {
256254
TypeInfo(describing: containingType)
257255
} else {
@@ -398,7 +396,7 @@ extension Test {
398396
sourceLocation: SourceLocation,
399397
parameters paramTuples: [__Parameter],
400398
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
401-
) -> Self where S: ~Copyable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
399+
) -> Self where S: ~Copyable & ~Escapable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
402400
let containingTypeInfo: TypeInfo? = if let containingType {
403401
TypeInfo(describing: containingType)
404402
} else {
@@ -426,7 +424,7 @@ extension Test {
426424
sourceLocation: SourceLocation,
427425
parameters paramTuples: [__Parameter],
428426
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
429-
) -> Self where S: ~Copyable, C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable {
427+
) -> Self where S: ~Copyable & ~Escapable, C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable {
430428
let containingTypeInfo: TypeInfo? = if let containingType {
431429
TypeInfo(describing: containingType)
432430
} else {
@@ -457,7 +455,7 @@ extension Test {
457455
sourceLocation: SourceLocation,
458456
parameters paramTuples: [__Parameter],
459457
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
460-
) -> Self where S: ~Copyable, Key: Sendable, Value: Sendable {
458+
) -> Self where S: ~Copyable & ~Escapable, Key: Sendable, Value: Sendable {
461459
let containingTypeInfo: TypeInfo? = if let containingType {
462460
TypeInfo(describing: containingType)
463461
} else {
@@ -482,7 +480,7 @@ extension Test {
482480
sourceLocation: SourceLocation,
483481
parameters paramTuples: [__Parameter],
484482
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
485-
) -> Self where S: ~Copyable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
483+
) -> Self where S: ~Copyable & ~Escapable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
486484
let containingTypeInfo: TypeInfo? = if let containingType {
487485
TypeInfo(describing: containingType)
488486
} else {
@@ -538,7 +536,8 @@ extension Test {
538536
///
539537
/// - Warning: This function is used to implement the `@Test` macro. Do not use
540538
/// it directly.
541-
@inlinable public func __requiringTry<T>(_ value: consuming T) throws -> T where T: ~Copyable {
539+
@_lifetime(copy value)
540+
@inlinable public func __requiringTry<T>(_ value: consuming T) throws -> T where T: ~Copyable & ~Escapable {
542541
value
543542
}
544543

@@ -547,7 +546,8 @@ extension Test {
547546
///
548547
/// - Warning: This function is used to implement the `@Test` macro. Do not use
549548
/// it directly.
550-
@inlinable public func __requiringAwait<T>(_ value: consuming T, isolation: isolated (any Actor)? = #isolation) async -> T where T: ~Copyable {
549+
@_lifetime(copy value)
550+
@inlinable public func __requiringAwait<T>(_ value: consuming T, isolation: isolated (any Actor)? = #isolation) async -> T where T: ~Copyable & ~Escapable {
551551
value
552552
}
553553

@@ -556,7 +556,8 @@ extension Test {
556556
///
557557
/// - Warning: This function is used to implement the `@Test` macro. Do not use
558558
/// it directly.
559-
@unsafe @inlinable public func __requiringUnsafe<T>(_ value: consuming T) -> T where T: ~Copyable {
559+
@_lifetime(copy value)
560+
@unsafe @inlinable public func __requiringUnsafe<T>(_ value: consuming T) -> T where T: ~Copyable & ~Escapable {
560561
value
561562
}
562563

@@ -579,7 +580,7 @@ public var __defaultSynchronousIsolationContext: (any Actor)? {
579580
_ selector: __XCTestCompatibleSelector?,
580581
onInstanceOf type: T.Type,
581582
sourceLocation: SourceLocation
582-
) async throws -> Bool where T: ~Copyable {
583+
) async throws -> Bool where T: ~Copyable & ~Escapable {
583584
false
584585
}
585586

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,19 +327,14 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
327327
/// generic.
328328
/// - attribute: The `@Test` or `@Suite` attribute.
329329
/// - decl: The declaration in question (contained in `node`.)
330-
/// - escapableNonConformance: The suppressed conformance to `Escapable` for
331-
/// `decl`, if present.
332330
///
333331
/// - Returns: A diagnostic message.
334-
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol, withSuppressedConformanceToEscapable escapableNonConformance: SuppressedTypeSyntax? = nil) -> Self {
332+
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol) -> Self {
335333
// Avoid using a syntax node from a lexical context (it won't have source
336334
// location information.)
337335
let syntax: Syntax = if let genericClause, attribute.root == genericClause.root {
338336
// Prefer the generic clause if available as the root cause.
339337
genericClause
340-
} else if let escapableNonConformance, attribute.root == escapableNonConformance.root {
341-
// Then the ~Escapable conformance if present.
342-
Syntax(escapableNonConformance)
343338
} else if attribute.root == node.root {
344339
// Next best choice is the unsupported containing node.
345340
Syntax(node)
@@ -373,9 +368,7 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
373368
message += " within \(_kindString(for: node, includeA: true))"
374369
}
375370
}
376-
if escapableNonConformance != nil {
377-
message += " because its conformance to 'Escapable' has been suppressed"
378-
} else if let decl = node.as(DeclSyntax.self), declarationInheritsFromXCTestClass(decl) == true {
371+
if let decl = node.as(DeclSyntax.self), declarationInheritsFromXCTestClass(decl) == true {
379372
message += " because it is a subclass of 'XCTest', 'XCTestCase', or 'XCTestSuite'"
380373
}
381374

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
134134
}
135135
}
136136

137-
// Disallow non-escapable types as suites. In order to support them, the
138-
// compiler team needs to finish implementing the lifetime dependency
139-
// feature so that `init()`, ``__requiringTry()`, and `__requiringAwait()`
140-
// can be correctly expressed.
141-
if let containingType = lexicalContext.first?.asProtocol((any DeclGroupSyntax).self),
142-
let inheritedTypes = containingType.inheritanceClause?.inheritedTypes {
143-
let escapableNonConformances = inheritedTypes
144-
.map(\.type)
145-
.compactMap { $0.as(SuppressedTypeSyntax.self) }
146-
.filter { $0.type.isNamed("Escapable", inModuleNamed: "Swift") }
147-
for escapableNonConformance in escapableNonConformances {
148-
diagnostics.append(.containingNodeUnsupported(containingType, whenUsing: testAttribute, on: function, withSuppressedConformanceToEscapable: escapableNonConformance))
149-
}
150-
}
151-
152137
return !diagnostics.lazy.map(\.severity).contains(.error)
153138
}
154139

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,6 @@ struct TestDeclarationMacroTests {
159159
"Attribute 'Test' cannot be applied to a function within a generic extension to type 'T!'",
160160
"extension T! { @Suite struct S {} }":
161161
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type 'T!'",
162-
"struct S: ~Escapable { @Test func f() {} }":
163-
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
164-
"struct S: ~Swift.Escapable { @Test func f() {} }":
165-
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
166-
"struct S: ~(Escapable) { @Test func f() {} }":
167-
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
168162
]
169163
)
170164
func apiMisuseErrors(input: String, expectedMessage: String) throws {

Tests/TestingTests/NonCopyableSuiteTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
@testable @_spi(ForToolsIntegrationOnly) import Testing
1212

13-
@Suite("Non-Copyable Tests")
14-
struct NonCopyableTests: ~Copyable {
13+
@Suite("Non-Copyable/Non-Escapable Tests")
14+
struct NonCopyableTests: ~Copyable & ~Escapable {
1515
@Test static func staticMe() {}
1616
@Test borrowing func borrowMe() {}
1717
@Test consuming func consumeMe() {}

cmake/modules/shared/CompilerSettings.cmake

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ add_compile_options(
1515
add_compile_options(
1616
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -require-explicit-sendable>")
1717
add_compile_options(
18-
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-experimental-feature -Xfrontend AccessLevelOnImport>")
18+
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-experimental-feature -Xfrontend AccessLevelOnImport>"
19+
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-experimental-feature -Xfrontend Lifetimes>")
1920
add_compile_options(
2021
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-upcoming-feature -Xfrontend ExistentialAny>"
2122
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-upcoming-feature -Xfrontend InternalImportsByDefault>"

0 commit comments

Comments
 (0)