Skip to content

Commit 037ebb1

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 037ebb1

21 files changed

+460
-113
lines changed

Documentation/ABI/TestContent.md

+3-3
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

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

+31-20
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@ public typealias __TestContentRecord = (
4949
reserved2: UInt
5050
)
5151

52+
/// Check if the type at the given address is equal to a given Swift type.
53+
///
54+
/// - Parameters:
55+
/// - typeAddress: A pointer to a Swift type, as in the `type` argument to a
56+
/// test content record accessor function.
57+
/// - type: The type expected to be at `typeAddress`.
58+
///
59+
/// - Returns: Whether or not the type at `typeAddress` equals `type`.
60+
///
61+
/// - Warning: This function is used to implement the `@Test`, `@Suite`, and
62+
/// `#expect(exitsWith:)` macros. Do not use it directly.
63+
public func __type(at typeAddress: UnsafeRawPointer, is type: (some ~Copyable).Type) -> Bool {
64+
// `typeAddress` may actually point to a move-only type, but attempting to
65+
// load it as such leads to a crash. SEE: rdar://134277439
66+
TypeInfo(describing: typeAddress.load(as: Any.Type.self)) == TypeInfo(describing: type)
67+
}
68+
5269
// MARK: -
5370

5471
/// A protocol describing a type that can be stored as test content at compile
@@ -73,21 +90,14 @@ protocol TestContent: ~Copyable {
7390
///
7491
/// By default, this type equals `Never`, indicating that this type of test
7592
/// content does not support hinting during discovery.
76-
associatedtype TestContentAccessorHint: Sendable = Never
93+
associatedtype TestContentAccessorHintArgument: Sendable = Never
7794

7895
/// The type to pass (by address) as the accessor function's `type` argument.
7996
///
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-
}
97+
/// By default, this type equals `Self`. A conforming type can substitute
98+
/// another type (e.g. if the conforming type is not public but records are
99+
/// created during macro expansion and can only reference public types.)
100+
associatedtype TestContentAccessorTypeArgument: ~Copyable = Self
91101
}
92102

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

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

118-
fileprivate init(imageAddress: UnsafeRawPointer?, record: __TestContentRecord) {
128+
fileprivate init(imageAddress: UnsafeRawPointer?, record: UnsafePointer<__TestContentRecord>) {
119129
self.imageAddress = imageAddress
120130
self._record = record
121131
}
122132

123133
/// The context value for this test content record.
124134
var context: UInt {
125-
_record.context
135+
_record.pointee.context
126136
}
127137

128138
/// Load the value represented by this record.
@@ -137,12 +147,12 @@ struct TestContentRecord<T>: Sendable where T: TestContent & ~Copyable {
137147
///
138148
/// If this function is called more than once on the same instance, a new
139149
/// value is created on each call.
140-
func load(withHint hint: T.TestContentAccessorHint? = nil) -> T? {
141-
guard let accessor = _record.accessor else {
150+
func load(withHint hint: T.TestContentAccessorHintArgument? = nil) -> T? {
151+
guard let accessor = _record.pointee.accessor else {
142152
return nil
143153
}
144154

145-
return withUnsafePointer(to: T.testContentAccessorTypeArgument) { type in
155+
return withUnsafePointer(to: T.TestContentAccessorTypeArgument.self) { type in
146156
withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in
147157
let initialized = if let hint {
148158
withUnsafePointer(to: hint) { hint in
@@ -175,8 +185,9 @@ extension TestContent where Self: ~Copyable {
175185
static func allTestContentRecords() -> AnySequence<TestContentRecord<Self>> {
176186
let result = SectionBounds.all(.testContent).lazy.flatMap { sb in
177187
sb.buffer.withMemoryRebound(to: __TestContentRecord.self) { records in
178-
records.lazy
179-
.filter { $0.kind == testContentKind }
188+
(0 ..< records.count).lazy
189+
.map { records.baseAddress! + $0 }
190+
.filter { $0.pointee.kind == testContentKind }
180191
.map { TestContentRecord<Self>(imageAddress: sb.imageAddress, record: $0) }
181192
}
182193
}

Sources/Testing/ExitTests/ExitTest.swift

+45-23
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,7 @@ public struct __ExitTest: Sendable, ~Copyable {
5959
private var _lo: UInt64
6060
private var _hi: UInt64
6161

62-
/// Initialize an instance of this type.
63-
///
64-
/// - Warning: This member is used to implement the `#expect(exitsWith:)`
65-
/// macro. Do not use it directly.
66-
public init(__uuid uuid: (UInt64, UInt64)) {
62+
init(_ uuid: (UInt64, UInt64)) {
6763
self._lo = uuid.0
6864
self._hi = uuid.1
6965
}
@@ -77,7 +73,7 @@ public struct __ExitTest: Sendable, ~Copyable {
7773
/// Do not invoke this closure directly. Instead, invoke ``callAsFunction()``
7874
/// to run the exit test. Running the exit test will always terminate the
7975
/// current process.
80-
fileprivate var body: @Sendable () async throws -> Void
76+
fileprivate var body: @Sendable () async throws -> Void = {}
8177

8278
/// Storage for ``observedValues``.
8379
///
@@ -113,18 +109,6 @@ public struct __ExitTest: Sendable, ~Copyable {
113109
_observedValues = newValue
114110
}
115111
}
116-
117-
/// Initialize an exit test at runtime.
118-
///
119-
/// - Warning: This initializer is used to implement the `#expect(exitsWith:)`
120-
/// macro. Do not use it directly.
121-
public init(
122-
__identifiedBy id: ID,
123-
body: @escaping @Sendable () async throws -> Void = {}
124-
) {
125-
self.id = id
126-
self.body = body
127-
}
128112
}
129113

130114
#if !SWT_NO_EXIT_TESTS
@@ -228,7 +212,41 @@ extension ExitTest: TestContent {
228212
0x65786974
229213
}
230214

231-
typealias TestContentAccessorHint = ID
215+
typealias TestContentAccessorHintArgument = ID
216+
217+
/// Store the test generator function into the given memory.
218+
///
219+
/// - Parameters:
220+
/// - outValue: The uninitialized memory to store the exit test into.
221+
/// - id: The unique identifier of the exit test to store.
222+
/// - body: The body closure of the exit test to store.
223+
/// - typeAddress: A pointer to the expected type of the exit test as passed
224+
/// to the test content record calling this function.
225+
/// - hintAddress: A pointer to an instance of ``ID`` to use as a hint.
226+
///
227+
/// - Returns: Whether or not an exit test was stored into `outValue`.
228+
///
229+
/// - Warning: This function is used to implement the `#expect(exitsWith:)`
230+
/// macro. Do not use it directly.
231+
public static func __store(
232+
_ id: (UInt64, UInt64),
233+
_ body: @escaping @Sendable () async throws -> Void,
234+
into outValue: UnsafeMutableRawPointer,
235+
asTypeAt typeAddress: UnsafeRawPointer,
236+
withHintAt hintAddress: UnsafeRawPointer? = nil
237+
) -> CBool {
238+
let callerExpectedType = TypeInfo(describing: typeAddress.load(as: Any.Type.self))
239+
let selfType = TypeInfo(describing: Self.self)
240+
guard callerExpectedType == selfType else {
241+
return false
242+
}
243+
let id = ID(id)
244+
if let hintedID = hintAddress?.load(as: ID.self), hintedID != id {
245+
return false
246+
}
247+
outValue.initializeMemory(as: Self.self, to: Self(id: id, body: body))
248+
return true
249+
}
232250
}
233251

234252
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
@@ -247,11 +265,15 @@ extension ExitTest {
247265
}
248266
}
249267

268+
#if !SWT_NO_LEGACY_TEST_DISCOVERY
250269
// Call the legacy lookup function that discovers tests embedded in types.
251270
return types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
252271
.compactMap { $0 as? any __ExitTestContainer.Type }
253-
.first { $0.__id == id }
254-
.map { ExitTest(__identifiedBy: $0.__id, body: $0.__body) }
272+
.first { ID($0.__id) == id }
273+
.map { ExitTest(id: ID($0.__id), body: $0.__body) }
274+
#else
275+
return nil
276+
#endif
255277
}
256278
}
257279

@@ -280,7 +302,7 @@ extension ExitTest {
280302
/// `await #expect(exitsWith:) { }` invocations regardless of calling
281303
/// convention.
282304
func callExitTest(
283-
identifiedBy exitTestID: ExitTest.ID,
305+
identifiedBy exitTestID: (UInt64, UInt64),
284306
exitsWith expectedExitCondition: ExitCondition,
285307
observing observedValues: [any PartialKeyPath<ExitTestArtifacts> & Sendable],
286308
expression: __Expression,
@@ -295,7 +317,7 @@ func callExitTest(
295317

296318
var result: ExitTestArtifacts
297319
do {
298-
var exitTest = ExitTest(__identifiedBy: exitTestID)
320+
var exitTest = ExitTest(id: ExitTest.ID(exitTestID))
299321
exitTest.observedValues = observedValues
300322
result = try await configuration.exitTestHandler(exitTest)
301323

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1147,7 +1147,7 @@ public func __checkClosureCall<R>(
11471147
/// `#require()` macros. Do not call it directly.
11481148
@_spi(Experimental)
11491149
public func __checkClosureCall(
1150-
identifiedBy exitTestID: __ExitTest.ID,
1150+
identifiedBy exitTestID: (UInt64, UInt64),
11511151
exitsWith expectedExitCondition: ExitCondition,
11521152
observing observedValues: [any PartialKeyPath<ExitTestArtifacts> & Sendable],
11531153
performing body: @convention(thin) () -> Void,

Sources/Testing/Test+Discovery+Legacy.swift

+3-1
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
@@ -33,7 +34,7 @@ let testContainerTypeNameMagic = "__🟠$test_container__"
3334
@_spi(Experimental)
3435
public protocol __ExitTestContainer {
3536
/// The unique identifier of the exit test.
36-
static var __id: __ExitTest.ID { get }
37+
static var __id: (UInt64, UInt64) { get }
3738

3839
/// The body function of the exit test.
3940
static var __body: @Sendable () async throws -> Void { get }
@@ -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

0 commit comments

Comments
 (0)