Skip to content

Commit 1e55aa7

Browse files
authored
Store test content in a custom metadata section. (#880)
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++. This change will be necessary to support Embedded Swift because there is no type metadata section emitted for embedded targets. 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](https://github.com/swiftlang/swift-testing/blob/main/Documentation/ABI/TestContent.md) article. This functionality is only available if a test target enables the experimental `"SymbolLinkageMarkers"` feature and only if Swift Testing is used as a package (not as a toolchain component.) 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.) ## See Also #735 #764 swiftlang/swift#76698 swiftlang/swift#78411 ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 2acac32 commit 1e55aa7

14 files changed

+140
-44
lines changed

Documentation/Porting.md

+7-1
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,14 +221,18 @@ 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;
229-
static const char& typeMetadataSectionEnd = testContentSectionBegin;
234+
static const char& typeMetadataSectionEnd = typeMetadataSectionBegin;
235+
#endif
230236
#endif
231237
```
232238

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

+48-27
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
///
@@ -452,42 +456,59 @@ extension ExitTestConditionMacro {
452456

453457
// Create a local type that can be discovered at runtime and which contains
454458
// the 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-
463-
decls.append(
464-
"""
465-
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
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-
)
475-
}
476-
477-
\(testContentRecordDecl)
459+
let enumName = context.makeUniqueName("")
460+
do {
461+
// Create the test content record.
462+
let testContentRecordDecl = makeTestContentRecordDecl(
463+
named: .identifier("testContentRecord"),
464+
in: TypeSyntax(IdentifierTypeSyntax(name: enumName)),
465+
ofKind: .exitTest,
466+
accessingWith: .identifier("accessor")
467+
)
478468

469+
// Create another local type for legacy test discovery.
470+
var recordDecl: DeclSyntax?
471+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
472+
let className = context.makeUniqueName("__🟡$")
473+
recordDecl = """
474+
private final class \(className): Testing.__TestContentRecordContainer {
479475
override nonisolated class var __testContentRecord: Testing.__TestContentRecord {
480-
testContentRecord
476+
\(enumName).testContentRecord
481477
}
482478
}
483479
"""
484-
)
480+
#endif
481+
482+
decls.append(
483+
"""
484+
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
485+
enum \(enumName) {
486+
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
487+
Testing.ExitTest.__store(
488+
\(exitTestIDExpr),
489+
\(bodyThunkName),
490+
into: outValue,
491+
asTypeAt: type,
492+
withHintAt: hint
493+
)
494+
}
495+
496+
\(testContentRecordDecl)
497+
498+
\(recordDecl)
499+
}
500+
"""
501+
)
502+
}
485503

486504
arguments[trailingClosureIndex].expression = ExprSyntax(
487505
ClosureExprSyntax {
488506
for decl in decls {
489-
CodeBlockItemSyntax(item: .decl(decl))
490-
.with(\.trailingTrivia, .newline)
507+
CodeBlockItemSyntax(
508+
leadingTrivia: .newline,
509+
item: .decl(decl),
510+
trailingTrivia: .newline
511+
)
491512
}
492513
}
493514
)

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

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

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

0 commit comments

Comments
 (0)