Skip to content

Commit e3e5cfe

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 23af7e5 commit e3e5cfe

18 files changed

+425
-55
lines changed

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: 6 additions & 4 deletions
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

Lines changed: 34 additions & 0 deletions
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 test generator function 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)

Sources/Testing/Test+Discovery+Legacy.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
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
1617
/// it directly.
1718
@_alwaysEmitConformanceMetadata
18-
public protocol __TestContainer {
19+
public protocol __TestContainer: Sendable {
1920
/// The set of tests contained by this type.
2021
static var __tests: [Test] { get async }
2122
}
@@ -31,7 +32,7 @@ let testContainerTypeNameMagic = "__🟠$test_container__"
3132
/// macro. Do not use it directly.
3233
@_alwaysEmitConformanceMetadata
3334
@_spi(Experimental)
34-
public protocol __ExitTestContainer {
35+
public protocol __ExitTestContainer: Sendable {
3536
/// The unique identifier of the exit test.
3637
static var __id: (UInt64, UInt64) { get }
3738

@@ -43,3 +44,4 @@ public protocol __ExitTestContainer {
4344
/// `__ExitTestContainer` protocol.
4445
let exitTestContainerTypeNameMagic = "__🟠$exit_test_body__"
4546
#endif
47+
#endif

Sources/Testing/Test+Discovery.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ extension Test {
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.
@@ -41,6 +65,7 @@ extension Test {
4165
// the legacy and new mechanisms, but we can set an environment variable
4266
// to explicitly select one or the other. When we remove legacy support,
4367
// we can also remove this enumeration and environment variable check.
68+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
4469
let (useNewMode, useLegacyMode) = switch Environment.flag(named: "SWT_USE_LEGACY_TEST_DISCOVERY") {
4570
case .none:
4671
(true, true)
@@ -49,6 +74,9 @@ extension Test {
4974
case .some(false):
5075
(true, false)
5176
}
77+
#else
78+
let useNewMode = true
79+
#endif
5280

5381
// Walk all test content and gather generator functions, then call them in
5482
// a task group and collate their results.
@@ -62,6 +90,7 @@ extension Test {
6290
}
6391
}
6492

93+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
6594
// Perform legacy test discovery if needed.
6695
if useLegacyMode && result.isEmpty {
6796
let types = types(withNamesContaining: testContainerTypeNameMagic).lazy
@@ -75,6 +104,7 @@ extension Test {
75104
result = await taskGroup.reduce(into: result) { $0.formUnion($1) }
76105
}
77106
}
107+
#endif
78108

79109
return result
80110
}

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: 45 additions & 5 deletions
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
///
@@ -450,28 +454,64 @@ extension ExitTestConditionMacro {
450454
"""
451455
)
452456

457+
#if hasFeature(SymbolLinkageMarkers)
453458
// Create a local type that can be discovered at runtime and which contains
454459
// the exit test body.
455-
let enumName = context.makeUniqueName("__🟠$exit_test_body__")
460+
let enumName = context.makeUniqueName("")
461+
let testContentRecordDecl = makeTestContentRecordDecl(
462+
named: .identifier("testContentRecord"),
463+
in: TypeSyntax(IdentifierTypeSyntax(name: enumName)),
464+
ofKind: .exitTest,
465+
accessingWith: .identifier("accessor")
466+
)
467+
decls.append(
468+
"""
469+
#if hasFeature(SymbolLinkageMarkers)
470+
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
471+
enum \(enumName) {
472+
private static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
473+
Testing.ExitTest.__store(
474+
\(exitTestIDExpr),
475+
\(bodyThunkName),
476+
into: outValue,
477+
asTypeAt: type,
478+
withHintAt: hint
479+
)
480+
}
481+
482+
\(testContentRecordDecl)
483+
}
484+
#endif
485+
"""
486+
)
487+
#endif
488+
489+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
490+
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
491+
let legacyEnumName = context.makeUniqueName("__🟠$exit_test_body__")
456492
decls.append(
457493
"""
458494
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
459-
enum \(enumName): Testing.__ExitTestContainer, Sendable {
495+
enum \(legacyEnumName): Testing.__ExitTestContainer {
460496
static var __id: (Swift.UInt64, Swift.UInt64) {
461497
\(exitTestIDExpr)
462498
}
463-
static var __body: @Sendable () async throws -> Void {
499+
static var __body: @Sendable () async throws -> Swift.Void {
464500
\(bodyThunkName)
465501
}
466502
}
467503
"""
468504
)
505+
#endif
469506

470507
arguments[trailingClosureIndex].expression = ExprSyntax(
471508
ClosureExprSyntax {
472509
for decl in decls {
473-
CodeBlockItemSyntax(item: .decl(decl))
474-
.with(\.trailingTrivia, .newline)
510+
CodeBlockItemSyntax(
511+
leadingTrivia: .newline,
512+
item: .decl(decl),
513+
trailingTrivia: .newline
514+
)
475515
}
476516
}
477517
)

Sources/TestingMacros/SuiteDeclarationMacro.swift

Lines changed: 52 additions & 6 deletions
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
@@ -127,6 +131,50 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
127131
// Parse the @Suite attribute.
128132
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)
129133

134+
let generatorName = context.makeUniqueName("generator")
135+
result.append(
136+
"""
137+
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
138+
@Sendable private static func \(generatorName)() async -> Testing.Test {
139+
.__type(
140+
\(declaration.type.trimmed).self,
141+
\(raw: attributeInfo.functionArgumentList(in: context))
142+
)
143+
}
144+
"""
145+
)
146+
147+
#if hasFeature(SymbolLinkageMarkers)
148+
let accessorName = context.makeUniqueName("accessor")
149+
let accessorDecl: DeclSyntax = """
150+
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
151+
private static let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in
152+
Testing.Test.__store(\(generatorName), into: outValue, asTypeAt: type)
153+
}
154+
"""
155+
156+
let testContentRecordDecl = makeTestContentRecordDecl(
157+
named: context.makeUniqueName("testContentRecord"),
158+
in: declaration.type,
159+
ofKind: .testDeclaration,
160+
accessingWith: accessorName,
161+
context: attributeInfo.testContentRecordFlags
162+
)
163+
164+
result.append(
165+
"""
166+
#if hasFeature(SymbolLinkageMarkers)
167+
\(accessorDecl)
168+
169+
\(testContentRecordDecl)
170+
#endif
171+
"""
172+
)
173+
#endif
174+
175+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
176+
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
177+
//
130178
// The emitted type must be public or the compiler can optimize it away
131179
// (since it is not actually used anywhere that the compiler can see.)
132180
//
@@ -143,16 +191,14 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
143191
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
144192
enum \(enumName): Testing.__TestContainer {
145193
static var __tests: [Testing.Test] {
146-
get async {[
147-
.__type(
148-
\(declaration.type.trimmed).self,
149-
\(raw: attributeInfo.functionArgumentList(in: context))
150-
)
151-
]}
194+
get async {
195+
[await \(generatorName)()]
196+
}
152197
}
153198
}
154199
"""
155200
)
201+
#endif
156202

157203
return result
158204
}
Lines changed: 18 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)