Skip to content

Commit 9d8cdc0

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 10ab127 commit 9d8cdc0

20 files changed

+386
-61
lines changed

Documentation/ABI/TestContent.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,15 @@ Regardless of platform, all test content records created and discoverable by the
4141
testing library have the following layout:
4242

4343
```swift
44+
typealias Accessor = @convention(c) (
45+
_ outValue: UnsafeMutableRawPointer,
46+
_ hint: UnsafeRawPointer?
47+
) -> CBool
48+
4449
typealias TestContentRecord = (
4550
kind: UInt32,
4651
reserved1: UInt32,
47-
accessor: (@convention(c) (_ outValue: UnsafeMutableRawPointer, _ hint: UnsafeRawPointer?) -> CBool)?,
52+
accessor: Accessor?,
4853
context: UInt,
4954
reserved2: UInt
5055
)
@@ -54,25 +59,30 @@ This type has natural size, stride, and alignment. Its fields are native-endian.
5459
If needed, this type can be represented in C as a structure:
5560

5661
```c
62+
typedef bool (* SWTAccessor)(
63+
void *outValue,
64+
const void *_Null_unspecified hint
65+
);
66+
5767
struct SWTTestContentRecord {
5868
uint32_t kind;
5969
uint32_t reserved1;
60-
bool (* _Nullable accessor)(void *outValue, const void *_Null_unspecified hint);
70+
SWTAccessor _Nullable accessor;
6171
uintptr_t context;
6272
uintptr_t reserved2;
6373
};
6474
```
6575

66-
Do not use the `__TestContentRecord` typealias defined in the testing library.
67-
This type exists to support the testing library's macros and may change in the
68-
future (e.g. to accomodate a generic argument or to make use of one of the
69-
reserved fields.)
76+
Do not use the `__TestContentRecord` or `__TestContentRecordAccessor` type
77+
aliases defined in the testing library. These types exist to support the testing
78+
library's macros and may change in the future (e.g. to accomodate a generic
79+
argument or to make use of a reserved field.)
7080

71-
Instead, define your own copy of this type where needed—you can copy the
72-
definition above _verbatim_. If your test record type's `context` field (as
81+
Instead, define your own copy of these types where needed—you can copy the
82+
definitions above _verbatim_. If your test record type's `context` field (as
7383
described below) is a pointer type, make sure to change its type in your version
7484
of `TestContentRecord` accordingly so that, on systems with pointer
75-
authentication enabled, the pointer is correctly resigned at load time.
85+
authentication enabled, the pointer is correctly re-signed at load time.
7686

7787
### Record content
7888

Documentation/Porting.md

Lines changed: 6 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ struct SectionBounds: Sendable {
2727
/// The test content metadata section.
2828
case testContent
2929

30+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
3031
/// The type metadata section.
3132
case typeMetadata
33+
#endif
3234
}
3335

3436
/// All section bounds of the given kind found in the current process.
@@ -60,8 +62,10 @@ extension SectionBounds.Kind {
6062
switch self {
6163
case .testContent:
6264
("__DATA_CONST", "__swift5_tests")
65+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
6366
case .typeMetadata:
6467
("__TEXT", "__swift5_types")
68+
#endif
6569
}
6670
}
6771
}
@@ -101,9 +105,8 @@ private let _startCollectingSectionBounds: Void = {
101105
var size = CUnsignedLong(0)
102106
if let start = getsectiondata(mh, segmentName.utf8Start, sectionName.utf8Start, &size), size > 0 {
103107
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
104-
let sb = SectionBounds(imageAddress: mh, buffer: buffer)
105108
_sectionBounds.withLock { sectionBounds in
106-
sectionBounds[kind]!.append(sb)
109+
sectionBounds[kind]!.append(SectionBounds(imageAddress: mh, buffer: buffer))
107110
}
108111
}
109112
}
@@ -165,8 +168,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
165168
let range = switch context.pointee.kind {
166169
case .testContent:
167170
sections.swift5_tests
171+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
168172
case .typeMetadata:
169173
sections.swift5_type_metadata
174+
#endif
170175
}
171176
let start = UnsafeRawPointer(bitPattern: range.start)
172177
let size = Int(clamping: range.length)
@@ -255,8 +260,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence<Section
255260
let sectionName = switch kind {
256261
case .testContent:
257262
".sw5test"
263+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
258264
case .typeMetadata:
259265
".sw5tymd"
266+
#endif
260267
}
261268
return HMODULE.all.lazy.compactMap { _findSection(named: sectionName, in: $0) }
262269
}
@@ -288,8 +295,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> CollectionOfOne<Secti
288295
let (sectionBegin, sectionEnd) = switch kind {
289296
case .testContent:
290297
SWTTestContentSectionBounds
298+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
291299
case .typeMetadata:
292300
SWTTypeMetadataSectionBounds
301+
#endif
293302
}
294303
let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin))
295304
let sb = SectionBounds(imageAddress: nil, buffer: buffer)

Sources/Testing/Discovery.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@
1010

1111
private import _TestingInternals
1212

13+
/// The type of the accessor function used to access a test content record.
14+
///
15+
/// - Parameters:
16+
/// - outValue: A pointer to uninitialized memory large enough to contain the
17+
/// corresponding test content record's value.
18+
/// - hint: An optional pointer to a hint value.
19+
///
20+
/// - Returns: Whether or not `outValue` was initialized. The caller is
21+
/// responsible for deinitializing `outValue` if it was initialized.
22+
///
23+
/// - Warning: This type is used to implement the `@Test` macro. Do not use it
24+
/// directly.
25+
public typealias __TestContentRecordAccessor = @convention(c) (
26+
_ outValue: UnsafeMutableRawPointer,
27+
_ hint: UnsafeRawPointer?
28+
) -> CBool
29+
1330
/// The content of a test content record.
1431
///
1532
/// - Parameters:
@@ -24,7 +41,7 @@ private import _TestingInternals
2441
public typealias __TestContentRecord = (
2542
kind: UInt32,
2643
reserved1: UInt32,
27-
accessor: (@convention(c) (_ outValue: UnsafeMutableRawPointer, _ hint: UnsafeRawPointer?) -> CBool)?,
44+
accessor: __TestContentRecordAccessor?,
2845
context: UInt,
2946
reserved2: UInt
3047
)

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,15 @@ 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+
#else
257+
return nil
258+
#endif
255259
}
256260
}
257261

Sources/Testing/Test+Discovery+Legacy.swift

Lines changed: 2 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,41 @@ 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 enumName = context.makeUniqueName("")
439+
let testContentRecordDecl = makeTestContentRecordDecl(
440+
named: .identifier("testContentRecord"),
441+
in: TypeSyntax(IdentifierTypeSyntax(name: enumName)),
442+
ofKind: .exitTest,
443+
accessingWith: .identifier("accessor")
444+
)
445+
decls.append(
446+
"""
447+
#if hasFeature(SymbolLinkageMarkers)
448+
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
449+
enum \(enumName) {
450+
private static let accessor: Testing.__TestContentRecordAccessor = { outValue, hint in
451+
let id = \(exitTestIDExpr)
452+
if let hintedID = hint?.load(as: Testing.__ExitTest.ID.self), hintedID != id {
453+
return false
454+
}
455+
let outValue = outValue.assumingMemoryBound(to: Testing.__ExitTest.self)
456+
outValue.initialize(to: .init(__identifiedBy: id, body: \(bodyThunkName)))
457+
return true
458+
}
459+
460+
\(testContentRecordDecl)
461+
}
462+
#endif
463+
"""
464+
)
465+
466+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
467+
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
468+
let legacyEnumName = context.makeUniqueName("__🟠$exit_test_body__")
439469
decls.append(
440470
"""
441471
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
442-
enum \(enumName): Testing.__ExitTestContainer, Sendable {
472+
enum \(legacyEnumName): Testing.__ExitTestContainer, Sendable {
443473
static var __id: Testing.__ExitTest.ID {
444474
\(exitTestIDExpr)
445475
}
@@ -449,12 +479,16 @@ extension ExitTestConditionMacro {
449479
}
450480
"""
451481
)
482+
#endif
452483

453484
arguments[trailingClosureIndex].expression = ExprSyntax(
454485
ClosureExprSyntax {
455486
for decl in decls {
456-
CodeBlockItemSyntax(item: .decl(decl))
457-
.with(\.trailingTrivia, .newline)
487+
CodeBlockItemSyntax(
488+
leadingTrivia: .newline,
489+
item: .decl(decl),
490+
trailingTrivia: .newline
491+
)
458492
}
459493
}
460494
)

0 commit comments

Comments
 (0)