Skip to content

Commit c447de0

Browse files
authored
Merge pull request #1010 from swiftlang/jgrynspan/test-content-container
Refine type-based test discovery mechanism to use test content records.
2 parents a5dfbc2 + f5690dc commit c447de0

14 files changed

+476
-141
lines changed

Sources/Testing/ExitTests/ExitTest.swift

+40-7
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,40 @@ extension ExitTest: DiscoverableAsTestContent {
244244
}
245245

246246
typealias TestContentAccessorHint = ID
247+
248+
/// Store the exit test into the given memory.
249+
///
250+
/// - Parameters:
251+
/// - id: The unique identifier of the exit test to store.
252+
/// - body: The body closure of the exit test to store.
253+
/// - outValue: The uninitialized memory to store the exit test into.
254+
/// - typeAddress: A pointer to the expected type of the exit test as passed
255+
/// to the test content record calling this function.
256+
/// - hintAddress: A pointer to an instance of ``ID`` to use as a hint.
257+
///
258+
/// - Returns: Whether or not an exit test was stored into `outValue`.
259+
///
260+
/// - Warning: This function is used to implement the `#expect(exitsWith:)`
261+
/// macro. Do not use it directly.
262+
public static func __store(
263+
_ id: (UInt64, UInt64),
264+
_ body: @escaping @Sendable () async throws -> Void,
265+
into outValue: UnsafeMutableRawPointer,
266+
asTypeAt typeAddress: UnsafeRawPointer,
267+
withHintAt hintAddress: UnsafeRawPointer? = nil
268+
) -> CBool {
269+
let callerExpectedType = TypeInfo(describing: typeAddress.load(as: Any.Type.self))
270+
let selfType = TypeInfo(describing: Self.self)
271+
guard callerExpectedType == selfType else {
272+
return false
273+
}
274+
let id = ID(id)
275+
if let hintedID = hintAddress?.load(as: ID.self), hintedID != id {
276+
return false
277+
}
278+
outValue.initializeMemory(as: Self.self, to: Self(id: id, body: body))
279+
return true
280+
}
247281
}
248282

249283
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
@@ -262,15 +296,14 @@ extension ExitTest {
262296
}
263297
}
264298

265-
#if !SWT_NO_LEGACY_TEST_DISCOVERY
266299
// Call the legacy lookup function that discovers tests embedded in types.
267-
return types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
268-
.compactMap { $0 as? any __ExitTestContainer.Type }
269-
.first { ID($0.__id) == id }
270-
.map { ExitTest(id: ID($0.__id), body: $0.__body) }
271-
#else
300+
for record in Self.allTypeMetadataBasedTestContentRecords() {
301+
if let exitTest = record.load(withHint: id) {
302+
return exitTest
303+
}
304+
}
305+
272306
return nil
273-
#endif
274307
}
275308
}
276309

Sources/Testing/Test+Discovery+Legacy.swift

+25-27
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,36 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
private import _TestingInternals
11+
@_spi(Experimental) @_spi(ForToolsIntegrationOnly) internal import _TestDiscovery
1212

13-
/// A protocol describing a type that contains tests.
13+
/// A shadow declaration of `_TestDiscovery.TestContentRecordContainer` that
14+
/// allows us to add public conformances to it without causing the
15+
/// `_TestDiscovery` module to appear in `Testing.private.swiftinterface`.
1416
///
15-
/// - Warning: This protocol is used to implement the `@Test` macro. Do not use
16-
/// it directly.
17+
/// This protocol is not part of the public interface of the testing library.
1718
@_alwaysEmitConformanceMetadata
18-
public protocol __TestContainer {
19-
/// The set of tests contained by this type.
20-
static var __tests: [Test] { get async }
21-
}
22-
23-
/// A string that appears within all auto-generated types conforming to the
24-
/// `__TestContainer` protocol.
25-
let testContainerTypeNameMagic = "__🟠$test_container__"
19+
protocol TestContentRecordContainer: _TestDiscovery.TestContentRecordContainer {}
2620

27-
#if !SWT_NO_EXIT_TESTS
28-
/// A protocol describing a type that contains an exit test.
21+
/// An abstract base class describing a type that contains tests.
2922
///
30-
/// - Warning: This protocol is used to implement the `#expect(exitsWith:)`
31-
/// macro. Do not use it directly.
32-
@_alwaysEmitConformanceMetadata
33-
@_spi(Experimental)
34-
public protocol __ExitTestContainer {
35-
/// The unique identifier of the exit test.
36-
static var __id: (UInt64, UInt64) { get }
23+
/// - Warning: This class is used to implement the `@Test` macro. Do not use it
24+
/// directly.
25+
open class __TestContentRecordContainer: TestContentRecordContainer {
26+
/// The corresponding test content record.
27+
///
28+
/// - Warning: This property is used to implement the `@Test` macro. Do not
29+
/// use it directly.
30+
open nonisolated class var __testContentRecord: __TestContentRecord {
31+
(0, 0, nil, 0, 0)
32+
}
3733

38-
/// The body function of the exit test.
39-
static var __body: @Sendable () async throws -> Void { get }
34+
static func storeTestContentRecord(to outTestContentRecord: UnsafeMutableRawPointer) -> Bool {
35+
outTestContentRecord.withMemoryRebound(to: __TestContentRecord.self, capacity: 1) { outTestContentRecord in
36+
outTestContentRecord.initialize(to: __testContentRecord)
37+
return true
38+
}
39+
}
4040
}
4141

42-
/// A string that appears within all auto-generated types conforming to the
43-
/// `__ExitTestContainer` protocol.
44-
let exitTestContainerTypeNameMagic = "__🟠$exit_test_body__"
45-
#endif
42+
@available(*, unavailable)
43+
extension __TestContentRecordContainer: Sendable {}

Sources/Testing/Test+Discovery.swift

+29-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ extension Test {
2727
var rawValue: @Sendable () async -> Test
2828
}
2929

30+
/// Store the test generator function into the given memory.
31+
///
32+
/// - Parameters:
33+
/// - generator: The generator function to store.
34+
/// - outValue: The uninitialized memory to store `generator` into.
35+
/// - typeAddress: A pointer to the expected type of `generator` as passed
36+
/// to the test content record calling this function.
37+
///
38+
/// - Returns: Whether or not `generator` was stored into `outValue`.
39+
///
40+
/// - Warning: This function is used to implement the `@Test` macro. Do not
41+
/// use it directly.
42+
public static func __store(
43+
_ generator: @escaping @Sendable () async -> Test,
44+
into outValue: UnsafeMutableRawPointer,
45+
asTypeAt typeAddress: UnsafeRawPointer
46+
) -> CBool {
47+
guard typeAddress.load(as: Any.Type.self) == Generator.self else {
48+
return false
49+
}
50+
outValue.initializeMemory(as: Generator.self, to: .init(rawValue: generator))
51+
return true
52+
}
53+
3054
/// All available ``Test`` instances in the process, according to the runtime.
3155
///
3256
/// The order of values in this sequence is unspecified.
@@ -64,15 +88,12 @@ extension Test {
6488

6589
// Perform legacy test discovery if needed.
6690
if useLegacyMode && result.isEmpty {
67-
let types = types(withNamesContaining: testContainerTypeNameMagic).lazy
68-
.compactMap { $0 as? any __TestContainer.Type }
69-
await withTaskGroup(of: [Self].self) { taskGroup in
70-
for type in types {
71-
taskGroup.addTask {
72-
await type.__tests
73-
}
91+
let generators = Generator.allTypeMetadataBasedTestContentRecords().lazy.compactMap { $0.load() }
92+
await withTaskGroup(of: Self.self) { taskGroup in
93+
for generator in generators {
94+
taskGroup.addTask { await generator.rawValue() }
7495
}
75-
result = await taskGroup.reduce(into: result) { $0.formUnion($1) }
96+
result = await taskGroup.reduce(into: result) { $0.insert($1) }
7697
}
7798
}
7899

Sources/TestingMacros/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ target_sources(TestingMacros PRIVATE
8787
Support/Additions/DeclGroupSyntaxAdditions.swift
8888
Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift
8989
Support/Additions/FunctionDeclSyntaxAdditions.swift
90+
Support/Additions/IntegerLiteralExprSyntaxAdditions.swift
9091
Support/Additions/MacroExpansionContextAdditions.swift
9192
Support/Additions/TokenSyntaxAdditions.swift
9293
Support/Additions/TriviaPieceAdditions.swift
@@ -103,6 +104,7 @@ target_sources(TestingMacros PRIVATE
103104
Support/DiagnosticMessage+Diagnosing.swift
104105
Support/SourceCodeCapturing.swift
105106
Support/SourceLocationGeneration.swift
107+
Support/TestContentGeneration.swift
106108
TagMacro.swift
107109
TestDeclarationMacro.swift
108110
TestingMacrosMain.swift)

Sources/TestingMacros/ConditionMacro.swift

+22-6
Original file line numberDiff line numberDiff line change
@@ -452,16 +452,32 @@ extension ExitTestConditionMacro {
452452

453453
// Create a local type that can be discovered at runtime and which contains
454454
// the exit test body.
455-
let enumName = context.makeUniqueName("__🟠$exit_test_body__")
455+
let className = context.makeUniqueName("__🟡$")
456+
let testContentRecordDecl = makeTestContentRecordDecl(
457+
named: .identifier("testContentRecord"),
458+
in: TypeSyntax(IdentifierTypeSyntax(name: className)),
459+
ofKind: .exitTest,
460+
accessingWith: .identifier("accessor")
461+
)
462+
456463
decls.append(
457464
"""
458465
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
459-
enum \(enumName): Testing.__ExitTestContainer, Sendable {
460-
static var __id: (Swift.UInt64, Swift.UInt64) {
461-
\(exitTestIDExpr)
466+
final class \(className): Testing.__TestContentRecordContainer {
467+
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
468+
Testing.ExitTest.__store(
469+
\(exitTestIDExpr),
470+
\(bodyThunkName),
471+
into: outValue,
472+
asTypeAt: type,
473+
withHintAt: hint
474+
)
462475
}
463-
static var __body: @Sendable () async throws -> Void {
464-
\(bodyThunkName)
476+
477+
\(testContentRecordDecl)
478+
479+
override nonisolated class var __testContentRecord: Testing.__TestContentRecord {
480+
testContentRecord
465481
}
466482
}
467483
"""

Sources/TestingMacros/SuiteDeclarationMacro.swift

+42-23
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
2525
guard _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) else {
2626
return []
2727
}
28-
return _createTestContainerDecls(for: declaration, suiteAttribute: node, in: context)
28+
return _createSuiteDecls(for: declaration, suiteAttribute: node, in: context)
2929
}
3030

3131
public static func expansion(
@@ -97,8 +97,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
9797
return !diagnostics.lazy.map(\.severity).contains(.error)
9898
}
9999

100-
/// Create a declaration for a type that conforms to the `__TestContainer`
101-
/// protocol and which contains the given suite type.
100+
/// Create the declarations necessary to discover a suite at runtime.
102101
///
103102
/// - Parameters:
104103
/// - declaration: The type declaration the result should encapsulate.
@@ -107,7 +106,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
107106
///
108107
/// - Returns: An array of declarations providing runtime information about
109108
/// the test suite type `declaration`.
110-
private static func _createTestContainerDecls(
109+
private static func _createSuiteDecls(
111110
for declaration: some DeclGroupSyntax,
112111
suiteAttribute: AttributeSyntax,
113112
in context: some MacroExpansionContext
@@ -127,28 +126,48 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
127126
// Parse the @Suite attribute.
128127
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)
129128

130-
// The emitted type must be public or the compiler can optimize it away
131-
// (since it is not actually used anywhere that the compiler can see.)
132-
//
133-
// The emitted type must be deprecated to avoid causing warnings in client
134-
// code since it references the suite metatype, which may be deprecated
135-
// to allow test functions to validate deprecated APIs. The emitted type is
136-
// also annotated unavailable, since it's meant only for use by the testing
137-
// library at runtime. The compiler does not allow combining 'unavailable'
138-
// and 'deprecated' into a single availability attribute: rdar://111329796
139-
let typeName = declaration.type.tokens(viewMode: .fixedUp).map(\.textWithoutBackticks).joined()
140-
let enumName = context.makeUniqueName("__🟠$test_container__suite__\(typeName)")
129+
let generatorName = context.makeUniqueName("generator")
130+
result.append(
131+
"""
132+
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
133+
@Sendable private static func \(generatorName)() async -> Testing.Test {
134+
.__type(
135+
\(declaration.type.trimmed).self,
136+
\(raw: attributeInfo.functionArgumentList(in: context))
137+
)
138+
}
139+
"""
140+
)
141+
142+
let accessorName = context.makeUniqueName("accessor")
143+
result.append(
144+
"""
145+
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
146+
private nonisolated static let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in
147+
Testing.Test.__store(\(generatorName), into: outValue, asTypeAt: type)
148+
}
149+
"""
150+
)
151+
152+
let testContentRecordName = context.makeUniqueName("testContentRecord")
153+
result.append(
154+
makeTestContentRecordDecl(
155+
named: testContentRecordName,
156+
in: declaration.type,
157+
ofKind: .testDeclaration,
158+
accessingWith: accessorName,
159+
context: attributeInfo.testContentRecordFlags
160+
)
161+
)
162+
163+
// Emit a type that contains a reference to the test content record.
164+
let className = context.makeUniqueName("__🟡$")
141165
result.append(
142166
"""
143167
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
144-
enum \(enumName): Testing.__TestContainer {
145-
static var __tests: [Testing.Test] {
146-
get async {[
147-
.__type(
148-
\(declaration.type.trimmed).self,
149-
\(raw: attributeInfo.functionArgumentList(in: context))
150-
)
151-
]}
168+
final class \(className): Testing.__TestContentRecordContainer {
169+
override nonisolated class var __testContentRecord: Testing.__TestContentRecord {
170+
\(testContentRecordName)
152171
}
153172
}
154173
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
import SwiftSyntax
12+
13+
extension IntegerLiteralExprSyntax {
14+
init(_ value: some BinaryInteger, radix: IntegerLiteralExprSyntax.Radix = .decimal) {
15+
let stringValue = "\(radix.literalPrefix)\(String(value, radix: radix.size))"
16+
self.init(literal: .integerLiteral(stringValue))
17+
}
18+
}

Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift

+11
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,14 @@ extension TokenSyntax {
4747
return nil
4848
}
4949
}
50+
51+
/// The `static` keyword, if `typeName` is not `nil`.
52+
///
53+
/// - Parameters:
54+
/// - typeName: The name of the type containing the macro being expanded.
55+
///
56+
/// - Returns: A token representing the `static` keyword, or one representing
57+
/// nothing if `typeName` is `nil`.
58+
func staticKeyword(for typeName: TypeSyntax?) -> TokenSyntax {
59+
(typeName != nil) ? .keyword(.static) : .unknown("")
60+
}

0 commit comments

Comments
 (0)