Skip to content

Commit ddbc073

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 e76a44f commit ddbc073

21 files changed

+459
-169
lines changed

Documentation/ABI/TestContent.md

+11-16
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,16 @@ struct SWTTestContentRecord {
7575
};
7676
```
7777

78-
Do not use the `__TestContentRecord` or `__TestContentRecordAccessor` typealias
79-
defined in the testing library. These types exist to support the testing
78+
Do not use the `__TestContentRecord` or `__TestContentRecordAccessor` type
79+
aliases defined in the testing library. These types exist to support the testing
8080
library's macros and may change in the future (e.g. to accomodate a generic
8181
argument or to make use of a reserved field.)
8282

8383
Instead, define your own copy of these types where needed—you can copy the
8484
definitions above _verbatim_. If your test record type's `context` field (as
8585
described below) is a pointer type, make sure to change its type in your version
8686
of `TestContentRecord` accordingly so that, on systems with pointer
87-
authentication enabled, the pointer is correctly resigned at load time.
87+
authentication enabled, the pointer is correctly re-signed at load time.
8888

8989
### Record content
9090

@@ -143,26 +143,21 @@ filtering is performed.)
143143
The concrete Swift type of the value written to `outValue`, the type pointed to
144144
by `type`, and the value pointed to by `hint` depend on the kind of record:
145145

146-
- For test or suite declarations (kind `0x74657374`), the accessor produces an
147-
asynchronous Swift function[^notAccessorSignature] that returns an instance of
148-
`Testing.Test`:
146+
- For test or suite declarations (kind `0x74657374`), the accessor produces a
147+
structure of type `Testing.Test.Generator` that the testing library can use
148+
to generate the corresponding test[^notAccessorSignature].
149149

150-
```swift
151-
@Sendable () async -> Test
152-
```
153-
154-
[^notAccessorSignature]: This signature is not the signature of `accessor`,
155-
but of the Swift function reference it writes to `outValue`. This level of
156-
indirection is necessary because loading a test or suite declaration is an
157-
asynchronous operation, but C functions cannot be `async`.
150+
[^notAccessorSignature]: This level of indirection is necessary because
151+
loading a test or suite declaration is an asynchronous operation, but C
152+
functions cannot be `async`.
158153

159154
Test content records of this kind do not specify a type for `hint`. Always
160155
pass `nil`.
161156

162157
- For exit test declarations (kind `0x65786974`), the accessor produces a
163-
structure describing the exit test (of type `Testing.__ExitTest`.)
158+
structure describing the exit test (of type `Testing.ExitTest`.)
164159

165-
Test content records of this kind accept a `hint` of type `Testing.__ExitTest.ID`.
160+
Test content records of this kind accept a `hint` of type `Testing.ExitTest.ID`.
166161
They only produce a result if they represent an exit test declared with the
167162
same ID (or if `hint` is `nil`.)
168163

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
@@ -165,6 +165,8 @@ extension Array where Element == PackageDescription.SwiftSetting {
165165
.enableExperimentalFeature("AccessLevelOnImport"),
166166
.enableUpcomingFeature("InternalImportsByDefault"),
167167

168+
.enableExperimentalFeature("SymbolLinkageMarkers"),
169+
168170
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
169171

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

Sources/Testing/Discovery+Platform.swift

+9-2
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
}

Sources/Testing/Discovery.swift

+10-23
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,7 @@ protocol TestContent: ~Copyable {
7373
///
7474
/// By default, this type equals `Never`, indicating that this type of test
7575
/// content does not support hinting during discovery.
76-
associatedtype TestContentAccessorHint: Sendable = Never
77-
78-
/// The type to pass (by address) as the accessor function's `type` argument.
79-
///
80-
/// The default value of this property is `Self.self`. A conforming type can
81-
/// override the default implementation to substitute another type (e.g. if
82-
/// the conforming type is not public but records are created during macro
83-
/// expansion and can only reference public types.)
84-
static var testContentAccessorTypeArgument: any ~Copyable.Type { get }
85-
}
86-
87-
extension TestContent where Self: ~Copyable {
88-
static var testContentAccessorTypeArgument: any ~Copyable.Type {
89-
self
90-
}
76+
associatedtype TestContentAccessorHintArgument: Sendable = Never
9177
}
9278

9379
// MARK: - Individual test content records
@@ -113,16 +99,16 @@ struct TestContentRecord<T>: Sendable where T: TestContent & ~Copyable {
11399
nonisolated(unsafe) var imageAddress: UnsafeRawPointer?
114100

115101
/// The underlying test content record loaded from a metadata section.
116-
private var _record: __TestContentRecord
102+
private nonisolated(unsafe) var _record: UnsafePointer<__TestContentRecord>
117103

118-
fileprivate init(imageAddress: UnsafeRawPointer?, record: __TestContentRecord) {
104+
fileprivate init(imageAddress: UnsafeRawPointer?, record: UnsafePointer<__TestContentRecord>) {
119105
self.imageAddress = imageAddress
120106
self._record = record
121107
}
122108

123109
/// The context value for this test content record.
124110
var context: UInt {
125-
_record.context
111+
_record.pointee.context
126112
}
127113

128114
/// Load the value represented by this record.
@@ -137,12 +123,12 @@ struct TestContentRecord<T>: Sendable where T: TestContent & ~Copyable {
137123
///
138124
/// If this function is called more than once on the same instance, a new
139125
/// value is created on each call.
140-
func load(withHint hint: T.TestContentAccessorHint? = nil) -> T? {
141-
guard let accessor = _record.accessor else {
126+
func load(withHint hint: T.TestContentAccessorHintArgument? = nil) -> T? {
127+
guard let accessor = _record.pointee.accessor else {
142128
return nil
143129
}
144130

145-
return withUnsafePointer(to: T.testContentAccessorTypeArgument) { type in
131+
return withUnsafePointer(to: T.self) { type in
146132
withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in
147133
let initialized = if let hint {
148134
withUnsafePointer(to: hint) { hint in
@@ -175,8 +161,9 @@ extension TestContent where Self: ~Copyable {
175161
static func allTestContentRecords() -> AnySequence<TestContentRecord<Self>> {
176162
let result = SectionBounds.all(.testContent).lazy.flatMap { sb in
177163
sb.buffer.withMemoryRebound(to: __TestContentRecord.self) { records in
178-
records.lazy
179-
.filter { $0.kind == testContentKind }
164+
(0 ..< records.count).lazy
165+
.map { records.baseAddress! + $0 }
166+
.filter { $0.pointee.kind == testContentKind }
180167
.map { TestContentRecord<Self>(imageAddress: sb.imageAddress, record: $0) }
181168
}
182169
}

0 commit comments

Comments
 (0)