Skip to content

Commit a44121e

Browse files
committed
Store test content in a custom metadata section.
This PR uses the experimental symbol linkage margers feature in the Swift compiler to emit metadata about tests (and exit tests) into a dedicated section of the test executable being built. At runtime, we discover that section and read out the tests from it. This has several benefits over our current model, which involves walking Swift's type metadata table looking for types that conform to a protocol: 1. We don't need to define that protocol as public API in Swift Testing, 1. We don't need to emit type metadata (much larger than what we really need) for every test function, 1. We don't need to duplicate a large chunk of the Swift ABI sources in order to walk the type metadata table correctly, and 1. Almost all the new code is written in Swift, whereas the code it is intended to replace could not be fully represented in Swift and needed to be written in C++. The change also opens up the possibility of supporting generic types in the future because we can emit metadata without needing to emit a nested type (which is not always valid in a generic context.) That's a "future direction" and not covered by this PR specifically. I've defined a layout for entries in the new `swift5_tests` section that should be flexible enough for us in the short-to-medium term and which lets us define additional arbitrary test content record types. The layout of this section is covered in depth in the new [TestContent.md](Documentation/ABI/TestContent.md) article. This functionality is only available if a test target enables the experimental `"SymbolLinkageMarkers"` feature. We continue to emit protocol-conforming types for now—that code will be removed if and when the experimental feature is properly supported (modulo us adopting relevant changes to the feature's API.) #735 swiftlang/swift#76698 swiftlang/swift#78411
1 parent faaabba commit a44121e

17 files changed

+345
-45
lines changed

Documentation/Porting.md

+6
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,10 @@ to load that information:
145145
+ let resourceName: Str255 = switch kind {
146146
+ case .testContent:
147147
+ "__swift5_tests"
148+
+#if !SWT_NO_LEGACY_TEST_DISCOVERY
148149
+ case .typeMetadata:
149150
+ "__swift5_types"
151+
+#endif
150152
+ }
151153
+
152154
+ let oldRefNum = CurResFile()
@@ -219,15 +221,19 @@ diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals
219221
+#elif defined(macintosh)
220222
+extern "C" const char testContentSectionBegin __asm__("...");
221223
+extern "C" const char testContentSectionEnd __asm__("...");
224+
+#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
222225
+extern "C" const char typeMetadataSectionBegin __asm__("...");
223226
+extern "C" const char typeMetadataSectionEnd __asm__("...");
227+
+#endif
224228
#else
225229
#warning Platform-specific implementation missing: Runtime test discovery unavailable (static)
226230
static const char testContentSectionBegin = 0;
227231
static const char& testContentSectionEnd = testContentSectionBegin;
232+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
228233
static const char typeMetadataSectionBegin = 0;
229234
static const char& typeMetadataSectionEnd = testContentSectionBegin;
230235
#endif
236+
#endif
231237
```
232238

233239
These symbols must have unique addresses corresponding to the first byte of the

Package.swift

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ extension Array where Element == PackageDescription.SwiftSetting {
156156
.enableExperimentalFeature("AccessLevelOnImport"),
157157
.enableUpcomingFeature("InternalImportsByDefault"),
158158

159+
.enableExperimentalFeature("SymbolLinkageMarkers"),
160+
159161
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
160162

161163
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),

Sources/Testing/Discovery+Platform.swift

+19-6
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ struct SectionBounds: Sendable {
2424
/// The test content metadata section.
2525
case testContent
2626

27+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
2728
/// The type metadata section.
2829
case typeMetadata
30+
#endif
2931
}
3032

3133
/// All section bounds of the given kind found in the current process.
@@ -74,18 +76,23 @@ private let _startCollectingSectionBounds: Void = {
7476

7577
// If this image contains the Swift section(s) we need, acquire the lock and
7678
// store the section's bounds.
77-
func findSectionBounds(forSectionNamed segmentName: String, _ sectionName: String, ofKind kind: SectionBounds.Kind) {
79+
for kind in SectionBounds.Kind.allCases {
80+
let (segmentName, sectionName) = switch kind {
81+
case .testContent:
82+
("__DATA_CONST", "__swift5_tests")
83+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
84+
case .typeMetadata:
85+
("__TEXT", "__swift5_types")
86+
#endif
87+
}
7888
var size = CUnsignedLong(0)
7989
if let start = getsectiondata(mh, segmentName, sectionName, &size), size > 0 {
8090
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
81-
let sb = SectionBounds(imageAddress: mh, buffer: buffer)
8291
_sectionBounds.withLock { sectionBounds in
83-
sectionBounds[kind]!.append(sb)
92+
sectionBounds[kind]!.append(SectionBounds(imageAddress: mh, buffer: buffer))
8493
}
8594
}
8695
}
87-
findSectionBounds(forSectionNamed: "__DATA_CONST", "__swift5_tests", ofKind: .testContent)
88-
findSectionBounds(forSectionNamed: "__TEXT", "__swift5_types", ofKind: .typeMetadata)
8996
}
9097

9198
#if _runtime(_ObjC)
@@ -94,7 +101,7 @@ private let _startCollectingSectionBounds: Void = {
94101
}
95102
#else
96103
_dyld_register_func_for_add_image { mh, _ in
97-
addSectionBounds(from: mh)
104+
addSectionBounds(from: mh!)
98105
}
99106
#endif
100107
}()
@@ -144,8 +151,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
144151
let range = switch context.pointee.kind {
145152
case .testContent:
146153
sections.swift5_tests
154+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
147155
case .typeMetadata:
148156
sections.swift5_type_metadata
157+
#endif
149158
}
150159
let start = UnsafeRawPointer(bitPattern: range.start)
151160
let size = Int(clamping: range.length)
@@ -234,8 +243,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence<Section
234243
let sectionName = switch kind {
235244
case .testContent:
236245
".sw5test"
246+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
237247
case .typeMetadata:
238248
".sw5tymd"
249+
#endif
239250
}
240251
return HMODULE.all.lazy.compactMap { _findSection(named: sectionName, in: $0) }
241252
}
@@ -267,8 +278,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> CollectionOfOne<Secti
267278
let (sectionBegin, sectionEnd) = switch kind {
268279
case .testContent:
269280
SWTTestContentSectionBounds
281+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
270282
case .typeMetadata:
271283
SWTTypeMetadataSectionBounds
284+
#endif
272285
}
273286
let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin))
274287
let sb = SectionBounds(imageAddress: nil, buffer: buffer)

Sources/Testing/ExitTests/ExitTest.swift

+2
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,13 @@ extension ExitTest {
247247
}
248248
}
249249

250+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
250251
// Call the legacy lookup function that discovers tests embedded in types.
251252
return types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
252253
.compactMap { $0 as? any __ExitTestContainer.Type }
253254
.first { $0.__id == id }
254255
.map { ExitTest(__identifiedBy: $0.__id, body: $0.__body) }
256+
#endif
255257
}
256258
}
257259

Sources/Testing/Test+Discovery+Legacy.swift

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
private import _TestingInternals
1212

13+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
1314
/// A protocol describing a type that contains tests.
1415
///
1516
/// - Warning: This protocol is used to implement the `@Test` macro. Do not use
@@ -60,3 +61,4 @@ func types(withNamesContaining nameSubstring: String) -> some Sequence<Any.Type>
6061
.map { unsafeBitCast($0, to: Any.Type.self) }
6162
}
6263
}
64+
#endif

Sources/Testing/Test+Discovery.swift

+6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ extension Test {
4646
// the legacy and new mechanisms, but we can set an environment variable
4747
// to explicitly select one or the other. When we remove legacy support,
4848
// we can also remove this enumeration and environment variable check.
49+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
4950
let (useNewMode, useLegacyMode) = switch Environment.flag(named: "SWT_USE_LEGACY_TEST_DISCOVERY") {
5051
case .none:
5152
(true, true)
@@ -54,6 +55,9 @@ extension Test {
5455
case .some(false):
5556
(true, false)
5657
}
58+
#else
59+
let useNewMode = true
60+
#endif
5761

5862
// Walk all test content and gather generator functions, then call them in
5963
// a task group and collate their results.
@@ -72,6 +76,7 @@ extension Test {
7276
}
7377
}
7478

79+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
7580
// Perform legacy test discovery if needed.
7681
if useLegacyMode && result.isEmpty {
7782
let types = types(withNamesContaining: testContainerTypeNameMagic).lazy
@@ -85,6 +90,7 @@ extension Test {
8590
result = await taskGroup.reduce(into: result) { $0.formUnion($1) }
8691
}
8792
}
93+
#endif
8894

8995
return result
9096
}

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

+49-2
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,57 @@ extension ExitTestConditionMacro {
435435

436436
// Create a local type that can be discovered at runtime and which contains
437437
// the exit test body.
438-
let enumName = context.makeUniqueName("__🟠$exit_test_body__")
438+
let accessorName = context.makeUniqueName("")
439+
let outValueArgumentName = context.makeUniqueName("outValue")
440+
let hintArgumentName = context.makeUniqueName("hint")
441+
let idLocalName = context.makeUniqueName("id")
442+
decls.append(
443+
"""
444+
#if hasFeature(SymbolLinkageMarkers)
445+
func \(accessorName)(_ \(outValueArgumentName): UnsafeMutableRawPointer, _ \(hintArgumentName): UnsafeRawPointer?) -> Bool {
446+
let \(idLocalName) = \(exitTestIDExpr)
447+
if let \(hintArgumentName) = \(hintArgumentName)?.load(as: Testing.__ExitTest.ID.self),
448+
\(hintArgumentName) != \(idLocalName) {
449+
return false
450+
}
451+
\(outValueArgumentName).initializeMemory(
452+
as: Testing.__ExitTest.self,
453+
to: .init(
454+
__identifiedBy: \(idLocalName),
455+
body: \(bodyThunkName)
456+
)
457+
)
458+
return true
459+
}
460+
#endif
461+
"""
462+
)
463+
464+
let enumName = context.makeUniqueName("")
465+
let sectionContent = makeTestContentRecordDecl(
466+
named: .identifier("__sectionContent"),
467+
in: TypeSyntax(IdentifierTypeSyntax(name: enumName)),
468+
ofKind: .exitTest,
469+
accessingWith: accessorName
470+
)
471+
decls.append(
472+
"""
473+
#if hasFeature(SymbolLinkageMarkers)
474+
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
475+
enum \(enumName) {
476+
\(sectionContent)
477+
}
478+
#endif
479+
"""
480+
)
481+
482+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
483+
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
484+
let legacyEnumName = context.makeUniqueName("__🟠$exit_test_body__")
439485
decls.append(
440486
"""
441487
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
442-
enum \(enumName): Testing.__ExitTestContainer, Sendable {
488+
enum \(legacyEnumName): Testing.__ExitTestContainer, Sendable {
443489
static var __id: Testing.__ExitTest.ID {
444490
\(exitTestIDExpr)
445491
}
@@ -449,6 +495,7 @@ extension ExitTestConditionMacro {
449495
}
450496
"""
451497
)
498+
#endif
452499

453500
arguments[trailingClosureIndex].expression = ExprSyntax(
454501
ClosureExprSyntax {

Sources/TestingMacros/SuiteDeclarationMacro.swift

+43-3
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
127127
// Parse the @Suite attribute.
128128
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)
129129

130+
let accessorName = context.makeUniqueName("")
131+
result.append(
132+
"""
133+
#if hasFeature(SymbolLinkageMarkers)
134+
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
135+
private static let \(accessorName): @convention(c) (UnsafeMutableRawPointer, UnsafeRawPointer?) -> Bool = {
136+
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in
137+
.__type(
138+
\(declaration.type.trimmed).self,
139+
\(raw: attributeInfo.functionArgumentList(in: context))
140+
)
141+
}
142+
_ = $1 // Ignored.
143+
return true
144+
}
145+
#endif
146+
"""
147+
)
148+
149+
let sectionContentName = context.makeUniqueName("")
150+
result.append(
151+
makeTestContentRecordDecl(
152+
named: sectionContentName,
153+
in: declaration.type,
154+
ofKind: .testDeclaration,
155+
accessingWith: accessorName,
156+
context: 1 << 0 /* suite decl */
157+
)
158+
)
159+
160+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
161+
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
162+
//
130163
// The emitted type must be public or the compiler can optimize it away
131164
// (since it is not actually used anywhere that the compiler can see.)
132165
//
@@ -142,17 +175,24 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
142175
"""
143176
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
144177
enum \(enumName): Testing.__TestContainer {
145-
static var __tests: [Testing.Test] {
146-
get async {[
178+
private static var __test: Testing.Test {
179+
get async {
147180
.__type(
148181
\(declaration.type.trimmed).self,
149182
\(raw: attributeInfo.functionArgumentList(in: context))
150183
)
151-
]}
184+
}
185+
}
186+
187+
static var __tests: [Testing.Test] {
188+
get async {
189+
[await __test]
190+
}
152191
}
153192
}
154193
"""
155194
)
195+
#endif
156196

157197
return result
158198
}
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)