Skip to content

Commit 2601a8c

Browse files
committed
Refine type-based test discovery mechanism to use test content records.
This PR changes how we discover tests in the type metadata section to more closely align with how the test content section works. This change will allow for a smoother transition to the test content section and our use of `@section` by using the same underlying structures (as much as is feasible.) Client code that uses the new `_TestDiscovery` module will need fewer changes to adapt.
1 parent 23af7e5 commit 2601a8c

14 files changed

+478
-145
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+
/// - outValue: The uninitialized memory to store the exit test into.
252+
/// - id: The unique identifier of the exit test to store.
253+
/// - body: The body closure of the exit test to store.
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-28
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,35 @@
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-
@_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__"
17+
/// This protocol is not part of the public interface of the testing library.
18+
protocol TestContentRecordContainer: _TestDiscovery.TestContentRecordContainer {}
2619

27-
#if !SWT_NO_EXIT_TESTS
28-
/// A protocol describing a type that contains an exit test.
20+
/// An abstract base class describing a type that contains tests.
2921
///
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 }
22+
/// - Warning: This class is used to implement the `@Test` macro. Do not use it
23+
/// directly.
24+
open class __TestContentRecordContainer: TestContentRecordContainer {
25+
/// The corresponding test content record.
26+
///
27+
/// - Warning: This property is used to implement the `@Test` macro. Do not
28+
/// use it directly.
29+
open nonisolated class var __testContentRecord: __TestContentRecord {
30+
(0, 0, nil, 0, 0)
31+
}
3732

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

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

Sources/Testing/Test+Discovery.swift

+30-9
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,38 @@ extension Test {
1919
/// indirect `async` accessor function rather than directly producing
2020
/// instances of ``Test``, but functions are non-nominal types and cannot
2121
/// directly conform to protocols.
22-
fileprivate struct Generator: DiscoverableAsTestContent, RawRepresentable {
22+
struct Generator: DiscoverableAsTestContent, RawRepresentable {
2323
static var testContentKind: UInt32 {
2424
0x74657374
2525
}
2626

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+
private 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)