Skip to content

Commit 54a7db9

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 f5690dc commit 54a7db9

14 files changed

+118
-19
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

+6-4
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,7 @@ let package = Package(
8989
"_Testing_CoreGraphics",
9090
"_Testing_Foundation",
9191
],
92-
swiftSettings: .packageSettings + [
93-
// For testing test content section discovery only
94-
.enableExperimentalFeature("SymbolLinkageMarkers"),
95-
]
92+
swiftSettings: .packageSettings
9693
),
9794

9895
.macro(
@@ -205,6 +202,11 @@ extension Array where Element == PackageDescription.SwiftSetting {
205202
.enableExperimentalFeature("AccessLevelOnImport"),
206203
.enableUpcomingFeature("InternalImportsByDefault"),
207204

205+
// This setting is enabled in the package, but not in the toolchain build
206+
// (via CMake). Enabling it is dependent on acceptance of the @section
207+
// proposal via Swift Evolution.
208+
.enableExperimentalFeature("SymbolLinkageMarkers"),
209+
208210
// When building as a package, the macro plugin always builds as an
209211
// executable rather than a library.
210212
.define("SWT_NO_LIBRARY_MACRO_PLUGINS"),

Sources/Testing/ExitTests/ExitTest.swift

+2
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,14 @@ extension ExitTest {
296296
}
297297
}
298298

299+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
299300
// Call the legacy lookup function that discovers tests embedded in types.
300301
for record in Self.allTypeMetadataBasedTestContentRecords() {
301302
if let exitTest = record.load(withHint: id) {
302303
return exitTest
303304
}
304305
}
306+
#endif
305307

306308
return nil
307309
}

Sources/Testing/Test+Discovery+Legacy.swift

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
1112
@_spi(Experimental) @_spi(ForToolsIntegrationOnly) internal import _TestDiscovery
1213

1314
/// A shadow declaration of `_TestDiscovery.TestContentRecordContainer` that
@@ -41,3 +42,4 @@ open class __TestContentRecordContainer: TestContentRecordContainer {
4142

4243
@available(*, unavailable)
4344
extension __TestContentRecordContainer: Sendable {}
45+
#endif

Sources/Testing/Test+Discovery.swift

+6
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ extension Test {
6565
// the legacy and new mechanisms, but we can set an environment variable
6666
// to explicitly select one or the other. When we remove legacy support,
6767
// we can also remove this enumeration and environment variable check.
68+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
6869
let (useNewMode, useLegacyMode) = switch Environment.flag(named: "SWT_USE_LEGACY_TEST_DISCOVERY") {
6970
case .none:
7071
(true, true)
@@ -73,6 +74,9 @@ extension Test {
7374
case .some(false):
7475
(true, false)
7576
}
77+
#else
78+
let useNewMode = true
79+
#endif
7680

7781
// Walk all test content and gather generator functions, then call them in
7882
// a task group and collate their results.
@@ -86,6 +90,7 @@ extension Test {
8690
}
8791
}
8892

93+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
8994
// Perform legacy test discovery if needed.
9095
if useLegacyMode && result.isEmpty {
9196
let generators = Generator.allTypeMetadataBasedTestContentRecords().lazy.compactMap { $0.load() }
@@ -96,6 +101,7 @@ extension Test {
96101
result = await taskGroup.reduce(into: result) { $0.insert($1) }
97102
}
98103
}
104+
#endif
99105

100106
return result
101107
}

Sources/TestingMacros/ConditionMacro.swift

+29-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
public import SwiftSyntax
1212
public import SwiftSyntaxMacros
1313

14+
#if !hasFeature(SymbolLinkageMarkers) && SWT_NO_LEGACY_TEST_DISCOVERY
15+
#error("Platform-specific misconfiguration: either SymbolLinkageMarkers or legacy test discovery is required to expand #expect(exitsWith:)")
16+
#endif
17+
1418
/// A protocol containing the common implementation for the expansions of the
1519
/// `#expect()` and `#require()` macros.
1620
///
@@ -460,9 +464,9 @@ extension ExitTestConditionMacro {
460464
accessingWith: .identifier("accessor")
461465
)
462466

467+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
463468
decls.append(
464469
"""
465-
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
466470
final class \(className): Testing.__TestContentRecordContainer {
467471
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
468472
Testing.ExitTest.__store(
@@ -482,12 +486,34 @@ extension ExitTestConditionMacro {
482486
}
483487
"""
484488
)
489+
#else
490+
decls.append(
491+
"""
492+
final class \(className) {
493+
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
494+
Testing.ExitTest.__store(
495+
\(exitTestIDExpr),
496+
\(bodyThunkName),
497+
into: outValue,
498+
asTypeAt: type,
499+
withHintAt: hint
500+
)
501+
}
502+
503+
\(testContentRecordDecl)
504+
}
505+
"""
506+
)
507+
#endif
485508

486509
arguments[trailingClosureIndex].expression = ExprSyntax(
487510
ClosureExprSyntax {
488511
for decl in decls {
489-
CodeBlockItemSyntax(item: .decl(decl))
490-
.with(\.trailingTrivia, .newline)
512+
CodeBlockItemSyntax(
513+
leadingTrivia: .newline,
514+
item: .decl(decl),
515+
trailingTrivia: .newline
516+
)
491517
}
492518
}
493519
)

Sources/TestingMacros/SuiteDeclarationMacro.swift

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
public import SwiftSyntax
1212
public import SwiftSyntaxMacros
1313

14+
#if !hasFeature(SymbolLinkageMarkers) && SWT_NO_LEGACY_TEST_DISCOVERY
15+
#error("Platform-specific misconfiguration: either SymbolLinkageMarkers or legacy test discovery is required to expand @Suite")
16+
#endif
17+
1418
/// A type describing the expansion of the `@Suite` attribute macro.
1519
///
1620
/// This type is used to implement the `@Suite` attribute macro. Do not use it
@@ -160,6 +164,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
160164
)
161165
)
162166

167+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
163168
// Emit a type that contains a reference to the test content record.
164169
let className = context.makeUniqueName("__🟡$")
165170
result.append(
@@ -172,6 +177,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
172177
}
173178
"""
174179
)
180+
#endif
175181

176182
return result
177183
}

Sources/TestingMacros/Support/TestContentGeneration.swift

+10
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax?
6262
}
6363

6464
return """
65+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)
66+
@_section("__DATA_CONST,__swift5_tests")
67+
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI)
68+
@_section("swift5_tests")
69+
#elseif os(Windows)
70+
@_section(".sw5test$B")
71+
#else
72+
@__testing(warning: "Platform-specific implementation missing: test content section name unavailable")
73+
#endif
74+
@_used
6575
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
6676
private nonisolated \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = (
6777
\(kindExpr), \(kind.commentRepresentation)

Sources/TestingMacros/TestDeclarationMacro.swift

+7-12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
public import SwiftSyntax
1212
public import SwiftSyntaxMacros
1313

14+
#if !hasFeature(SymbolLinkageMarkers) && SWT_NO_LEGACY_TEST_DISCOVERY
15+
#error("Platform-specific misconfiguration: either SymbolLinkageMarkers or legacy test discovery is required to expand @Test")
16+
#endif
17+
1418
/// A type describing the expansion of the `@Test` attribute macro.
1519
///
1620
/// This type is used to implement the `@Test` attribute macro. Do not use it
@@ -188,17 +192,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
188192
return FunctionParameterClauseSyntax(parameters: parameterList)
189193
}
190194

191-
/// The `static` keyword, if `typeName` is not `nil`.
192-
///
193-
/// - Parameters:
194-
/// - typeName: The name of the type containing the macro being expanded.
195-
///
196-
/// - Returns: A token representing the `static` keyword, or one representing
197-
/// nothing if `typeName` is `nil`.
198-
private static func _staticKeyword(for typeName: TypeSyntax?) -> TokenSyntax {
199-
(typeName != nil) ? .keyword(.static) : .unknown("")
200-
}
201-
202195
/// Create a thunk function with a normalized signature that calls a
203196
/// developer-supplied test function.
204197
///
@@ -356,7 +349,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
356349
let thunkName = context.makeUniqueName(thunking: functionDecl)
357350
let thunkDecl: DeclSyntax = """
358351
@available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.")
359-
@Sendable private \(_staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void {
352+
@Sendable private \(staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void {
360353
\(thunkBody)
361354
}
362355
"""
@@ -496,6 +489,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
496489
)
497490
)
498491

492+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
499493
// Emit a type that contains a reference to the test content record.
500494
let className = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟡$")
501495
result.append(
@@ -508,6 +502,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
508502
}
509503
"""
510504
)
505+
#endif
511506

512507
return result
513508
}

Sources/_TestDiscovery/SectionBounds.swift

+8
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
}
@@ -186,8 +190,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
186190
let range = switch context.pointee.kind {
187191
case .testContent:
188192
sections.swift5_tests
193+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
189194
case .typeMetadata:
190195
sections.swift5_type_metadata
196+
#endif
191197
}
192198
let start = UnsafeRawPointer(bitPattern: range.start)
193199
let size = Int(clamping: range.length)
@@ -276,8 +282,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence<Section
276282
let sectionName = switch kind {
277283
case .testContent:
278284
".sw5test"
285+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
279286
case .typeMetadata:
280287
".sw5tymd"
288+
#endif
281289
}
282290
return HMODULE.all.lazy.compactMap { _findSection(named: sectionName, in: $0) }
283291
}

Sources/_TestDiscovery/TestContentRecord.swift

+2
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable {
244244
}
245245
}
246246

247+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
247248
// MARK: - Legacy test content discovery
248249

249250
private import _TestingInternals
@@ -344,3 +345,4 @@ extension DiscoverableAsTestContent where Self: ~Copyable {
344345
return AnySequence(result)
345346
}
346347
}
348+
#endif

Sources/_TestingInternals/Discovery.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,44 @@
1111
#include "Discovery.h"
1212

1313
#include <algorithm>
14+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
1415
#include <cstdint>
1516
#include <cstring>
1617
#include <type_traits>
18+
#endif
1719

1820
#if defined(SWT_NO_DYNAMIC_LINKING)
1921
#pragma mark - Statically-linked section bounds
2022

2123
#if defined(__APPLE__)
2224
extern "C" const char testContentSectionBegin __asm("section$start$__DATA_CONST$__swift5_tests");
2325
extern "C" const char testContentSectionEnd __asm("section$end$__DATA_CONST$__swift5_tests");
26+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
2427
extern "C" const char typeMetadataSectionBegin __asm__("section$start$__TEXT$__swift5_types");
2528
extern "C" const char typeMetadataSectionEnd __asm__("section$end$__TEXT$__swift5_types");
29+
#endif
2630
#elif defined(__wasi__)
2731
extern "C" const char testContentSectionBegin __asm__("__start_swift5_tests");
2832
extern "C" const char testContentSectionEnd __asm__("__stop_swift5_tests");
33+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
2934
extern "C" const char typeMetadataSectionBegin __asm__("__start_swift5_type_metadata");
3035
extern "C" const char typeMetadataSectionEnd __asm__("__stop_swift5_type_metadata");
36+
#endif
3137
#else
3238
#warning Platform-specific implementation missing: Runtime test discovery unavailable (static)
3339
static const char testContentSectionBegin = 0;
3440
static const char& testContentSectionEnd = testContentSectionBegin;
41+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
3542
static const char typeMetadataSectionBegin = 0;
3643
static const char& typeMetadataSectionEnd = typeMetadataSectionBegin;
3744
#endif
45+
#endif
3846

3947
static constexpr const char *const staticallyLinkedSectionBounds[][2] = {
4048
{ &testContentSectionBegin, &testContentSectionEnd },
49+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
4150
{ &typeMetadataSectionBegin, &typeMetadataSectionEnd },
51+
#endif
4252
};
4353

4454
void swt_getStaticallyLinkedSectionBounds(size_t kind, const void **outSectionBegin, size_t *outByteCount) {
@@ -48,6 +58,7 @@ void swt_getStaticallyLinkedSectionBounds(size_t kind, const void **outSectionBe
4858
}
4959
#endif
5060

61+
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
5162
#pragma mark - Swift ABI
5263

5364
#if defined(__PTRAUTH_INTRINSICS__)
@@ -221,3 +232,4 @@ const void *swt_getTypeFromTypeMetadataRecord(const void *recordAddress, const c
221232

222233
return nullptr;
223234
}
235+
#endif

0 commit comments

Comments
 (0)