Skip to content

Commit b240f64

Browse files
committed
Store test content in a custom metadata section.
See also: swiftlang/swift#76698 Resolves #735.
1 parent 5763213 commit b240f64

20 files changed

+716
-442
lines changed

Documentation/ABI/TestContent.md

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Runtime-discoverable test content
2+
3+
<!--
4+
This source file is part of the Swift.org open source project
5+
6+
Copyright (c) 2024 Apple Inc. and the Swift project authors
7+
Licensed under Apache License v2.0 with Runtime Library Exception
8+
9+
See https://swift.org/LICENSE.txt for license information
10+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
11+
-->
12+
13+
This document describes the format and location of test content that the testing
14+
library emits at compile time and can discover at runtime.
15+
16+
## Basic format
17+
18+
Swift Testing uses the [ELF Note format](https://man7.org/linux/man-pages/man5/elf.5.html)
19+
to store individual records of test content. Records created and discoverable by
20+
the testing library are stored in dedicated platform-specific sections:
21+
22+
| Platform | Binary Format | Section Name |
23+
|-|:-:|-|
24+
| macOS | Mach-O | `__DATA_CONST,__swift5_tests` |
25+
| iOS | Mach-O | `__DATA_CONST,__swift5_tests` |
26+
| watchOS | Mach-O | `__DATA_CONST,__swift5_tests` |
27+
| tvOS | Mach-O | `__DATA_CONST,__swift5_tests` |
28+
| visionOS | Mach-O | `__DATA_CONST,__swift5_tests` |
29+
| Linux | ELF | `PT_NOTE`[^1] |
30+
| FreeBSD | ELF | `PT_NOTE`[^1] |
31+
| Android | ELF | `PT_NOTE`[^1] |
32+
| WASI | Statically Linked | `swift5_tests` |
33+
| Windows | PE/COFF | `.sw5test` |
34+
35+
[^1]: On platforms that use the ELF binary format natively, test content records
36+
are stored in ELF program headers of type `PT_NOTE`. Take care not to
37+
remove these program headers (for example, by invoking [`strip(1)`](https://www.man7.org/linux/man-pages/man1/strip.1.html).)
38+
39+
### Determining the type of test content
40+
41+
Regardless of platform, all test content records created and discoverable by the
42+
testing library start have the name `"Swift Testing"` stored in the implied
43+
`n_name` field of their underlying ELF Notes. Each record's _type_ (stored in
44+
the underlying ELF Note's `n_type` field) determines how the record will be
45+
interpreted at runtime:
46+
47+
| Type Value | Interpretation |
48+
|-:|-|
49+
| < `0` | Undefined (**do not use**) |
50+
| `0` ... `99` | Reserved |
51+
| `100` | Test or suite declaration |
52+
| `101` | Exit test |
53+
54+
<!-- When adding cases to this enumeration, be sure to also update the
55+
corresponding enumeration in Discovery.h and TestContentGeneration.swift. -->
56+
57+
### Loading test content from a record
58+
59+
For all currently-defined record types, the header and name are followed by a
60+
structure of the following form:
61+
62+
```c
63+
struct SWTTestContent {
64+
bool (* accessor)(void *);
65+
uint64_t flags;
66+
};
67+
```
68+
69+
#### The accessor field
70+
71+
The function `accessor` is a C function whose signature in Swift can be restated
72+
as:
73+
74+
```swift
75+
@convention(c) (_ outValue: UnsafeMutableRawPointer) -> Bool
76+
```
77+
78+
When called, it initializes the memory at `outValue` to an instance of some
79+
Swift type and returns `true`, or returns `false` if it could not generate the
80+
relevant content. On successful return, the caller is responsible for
81+
deinitializing the memory at `outValue` when done with it.
82+
83+
The concrete Swift type of `accessor`'s result depends on the type of record:
84+
85+
| Type Value | Return Type |
86+
|-:|-|
87+
| < `0` | Undefined (**do not use**) |
88+
| `0` ... `99` | `nil` |
89+
| `100` | `@Sendable () async -> Test`[^2] |
90+
| `101` | `ExitTest` (owned by caller) |
91+
92+
[^2]: This signature is not the signature of `accessor`, but of the Swift
93+
function reference it returns. This level of indirection is necessary
94+
because loading a test or suite declaration is an asynchronous operation,
95+
but C functions cannot be `async`.
96+
97+
#### The flags field
98+
99+
For test or suite declarations (type `100`), the following flags are defined:
100+
101+
| Bit | Description |
102+
|-:|-|
103+
| `1 << 0` | This record contains a suite declaration |
104+
| `1 << 1` | This record contains a parameterized test function declaration |
105+
106+
For exit test declarations (type `101`), no flags are currently defined.
107+
108+
## Third-party test content
109+
110+
TODO: elaborate how tools can reuse the same `n_name` and `n_type` fields to
111+
supplement Swift Testing's data, or use a different `n_name` field to store
112+
arbitrary other data in the test content section that Swift Testing will ignore.

Documentation/Porting.md

+17-11
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,14 @@ Once the header is included, we can call `GetDateTime()` from `Clock.swift`:
113113
## Runtime test discovery
114114

115115
When porting to a new platform, you may need to provide a new implementation for
116-
`enumerateTypeMetadataSections()` in `Discovery.cpp`. Test discovery is
117-
dependent on Swift metadata discovery which is an inherently platform-specific
118-
operation.
116+
`enumerateTestContentSections()` in `Discovery.cpp`. Test discovery is dependent
117+
on Swift metadata discovery which is an inherently platform-specific operation.
119118

120-
_Most_ platforms will be able to reuse the implementation used by Linux and
121-
Windows that calls an internal Swift runtime function to enumerate available
122-
metadata. If you are porting Swift Testing to Classic, this function won't be
119+
_Most_ platforms in use today use the ELF image format and will be able to reuse
120+
the implementation used by Linux. That implementation calls `dl_iterate_phdr()`
121+
in the GNU C Library to enumerate available metadata.
122+
123+
If you are porting Swift Testing to Classic, `dl_iterate_phdr()` won't be
123124
available, so you'll need to write a custom implementation instead. Assuming
124125
that the Swift compiler emits section information into the resource fork on
125126
Classic, you could use the [Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf)
@@ -132,16 +133,21 @@ to load that information:
132133
// ...
133134
+#elif defined(macintosh)
134135
+template <typename SectionEnumerator>
135-
+static void enumerateTypeMetadataSections(const SectionEnumerator& body) {
136+
+static void enumerateTestContentSections(const SectionEnumerator& body) {
136137
+ ResFileRefNum refNum;
137138
+ if (noErr == GetTopResourceFile(&refNum)) {
138139
+ ResFileRefNum oldRefNum = refNum;
139140
+ do {
140141
+ UseResFile(refNum);
141-
+ Handle handle = Get1NamedResource('swft', "\p__swift5_types");
142+
+ Handle handle = Get1NamedResource('swft', "\p__swift5_tests");
142143
+ if (handle && *handle) {
143-
+ size_t size = GetHandleSize(handle);
144-
+ body(*handle, size);
144+
+ auto imageAddress = reinterpret_cast<const void *>(static_cast<uintptr_t>(refNum));
145+
+ SWTSectionBounds sb = { imageAddress, *handle, GetHandleSize(handle) };
146+
+ bool stop = false;
147+
+ body(sb, &stop);
148+
+ if (stop) {
149+
+ break;
150+
+ }
145151
+ }
146152
+ } while (noErr == GetNextResourceFile(refNum, &refNum));
147153
+ UseResFile(oldRefNum);
@@ -150,7 +156,7 @@ to load that information:
150156
#else
151157
#warning Platform-specific implementation missing: Runtime test discovery unavailable
152158
template <typename SectionEnumerator>
153-
static void enumerateTypeMetadataSections(const SectionEnumerator& body) {}
159+
static void enumerateTestContentSections(const SectionEnumerator& body) {}
154160
#endif
155161
```
156162

Package.swift

+2
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ extension Array where Element == PackageDescription.SwiftSetting {
128128
.enableExperimentalFeature("AccessLevelOnImport"),
129129
.enableUpcomingFeature("InternalImportsByDefault"),
130130

131+
.enableExperimentalFeature("SymbolLinkageMarkers"),
132+
131133
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
132134

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

Sources/Testing/ExitTests/ExitTest.swift

+25-31
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,29 @@ public struct ExitTest: Sendable, ~Copyable {
2424
/// The expected exit condition of the exit test.
2525
public var expectedExitCondition: ExitCondition
2626

27-
/// The body closure of the exit test.
28-
fileprivate var body: @Sendable () async throws -> Void = {}
29-
3027
/// The source location of the exit test.
3128
///
3229
/// The source location is unique to each exit test and is consistent between
3330
/// processes, so it can be used to uniquely identify an exit test at runtime.
3431
public var sourceLocation: SourceLocation
3532

33+
/// The body closure of the exit test.
34+
fileprivate var body: @Sendable () async throws -> Void
35+
36+
/// Initialize an exit test at runtime.
37+
///
38+
/// - Warning: This initializer is used to implement the `#expect(exitsWith:)`
39+
/// macro. Do not use it directly.
40+
public init(
41+
__expectedExitCondition expectedExitCondition: ExitCondition,
42+
sourceLocation: SourceLocation,
43+
body: @escaping @Sendable () async throws -> Void = {}
44+
) {
45+
self.expectedExitCondition = expectedExitCondition
46+
self.sourceLocation = sourceLocation
47+
self.body = body
48+
}
49+
3650
/// Disable crash reporting, crash logging, or core dumps for the current
3751
/// process.
3852
private static func _disableCrashReporting() {
@@ -102,44 +116,24 @@ public struct ExitTest: Sendable, ~Copyable {
102116

103117
// MARK: - Discovery
104118

105-
/// A protocol describing a type that contains an exit test.
106-
///
107-
/// - Warning: This protocol is used to implement the `#expect(exitsWith:)`
108-
/// macro. Do not use it directly.
109-
@_alwaysEmitConformanceMetadata
110-
@_spi(Experimental)
111-
public protocol __ExitTestContainer {
112-
/// The expected exit condition of the exit test.
113-
static var __expectedExitCondition: ExitCondition { get }
114-
115-
/// The source location of the exit test.
116-
static var __sourceLocation: SourceLocation { get }
117-
118-
/// The body function of the exit test.
119-
static var __body: @Sendable () async throws -> Void { get }
120-
}
121-
122119
extension ExitTest {
123-
/// A string that appears within all auto-generated types conforming to the
124-
/// `__ExitTestContainer` protocol.
125-
private static let _exitTestContainerTypeNameMagic = "__🟠$exit_test_body__"
126-
127120
/// Find the exit test function at the given source location.
128121
///
129122
/// - Parameters:
130123
/// - sourceLocation: The source location of the exit test to find.
131124
///
132125
/// - Returns: The specified exit test function, or `nil` if no such exit test
133126
/// could be found.
127+
@_spi(ForToolsIntegrationOnly)
134128
public static func find(at sourceLocation: SourceLocation) -> Self? {
135129
var result: Self?
136130

137-
enumerateTypes(withNamesContaining: _exitTestContainerTypeNameMagic) { _, type, stop in
138-
if let type = type as? any __ExitTestContainer.Type, type.__sourceLocation == sourceLocation {
131+
enumerateTestContent(ofKind: .exitTest, as: ExitTest.self) { _, exitTest, _, stop in
132+
if exitTest.sourceLocation == sourceLocation {
139133
result = ExitTest(
140-
expectedExitCondition: type.__expectedExitCondition,
141-
body: type.__body,
142-
sourceLocation: type.__sourceLocation
134+
__expectedExitCondition: exitTest.expectedExitCondition,
135+
sourceLocation: exitTest.sourceLocation,
136+
body: exitTest.body
143137
)
144138
stop = true
145139
}
@@ -183,7 +177,7 @@ func callExitTest(
183177

184178
let actualExitCondition: ExitCondition
185179
do {
186-
let exitTest = ExitTest(expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation)
180+
let exitTest = ExitTest(__expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation)
187181
actualExitCondition = try await configuration.exitTestHandler(exitTest)
188182
} catch {
189183
// An error here would indicate a problem in the exit test handler such as a
@@ -295,7 +289,7 @@ extension ExitTest {
295289
// External tools authors should set up their own back channel mechanisms
296290
// and ensure they're installed before calling ExitTest.callAsFunction().
297291
guard var result = find(at: sourceLocation) else {
298-
return nil
292+
fatalError("Could not find an exit test that should have been located at \(sourceLocation).")
299293
}
300294

301295
// We can't say guard let here because it counts as a consume.

0 commit comments

Comments
 (0)