Skip to content

Commit f780c96

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

File tree

11 files changed

+143
-94
lines changed

11 files changed

+143
-94
lines changed

Documentation/ABI/TestContent.md

+79-36
Original file line numberDiff line numberDiff line change
@@ -21,78 +21,117 @@ the testing library are stored in dedicated platform-specific sections:
2121

2222
| Platform | Binary Format | Section Name |
2323
|-|:-:|-|
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] |
24+
| macOS, iOS, watchOS, tvOS, visionOS | Mach-O | `__DATA_CONST,__swift5_tests` |
25+
| Linux, FreeBSD, Android | ELF | `PT_NOTE`[^1] |
3226
| WASI | Statically Linked | `swift5_tests` |
3327
| Windows | PE/COFF | `.sw5test` |
3428

3529
[^1]: On platforms that use the ELF binary format natively, test content records
3630
are stored in ELF program headers of type `PT_NOTE`. Take care not to
3731
remove these program headers (for example, by invoking [`strip(1)`](https://www.man7.org/linux/man-pages/man1/strip.1.html).)
3832

39-
### Determining the type of test content
40-
33+
### Record headers
34+
4135
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:
36+
testing library have the following structure:
37+
38+
```c
39+
struct SWTTestContentHeader {
40+
int32_t n_namesz;
41+
int32_t n_descsz;
42+
int32_t n_type;
43+
char n_name[n_namesz];
44+
// ...
45+
};
46+
```
47+
48+
This structure can be represented in Swift as a heterogenous tuple:
49+
50+
```swift
51+
typealias SWTTestContentHeader = (
52+
n_namesz: Int32,
53+
n_descsz: Int32,
54+
n_type: Int32,
55+
n_name: (CChar, CChar, /* ... */),
56+
// ...
57+
)
58+
```
59+
60+
The size of `n_name` is dynamic and cannot be statically computed. The testing
61+
library always generates the name `"Swift Testing"` and specifies an `n_namesz`
62+
value of `20` (the string being null-padded to the correct length), but other
63+
content may be present in the same section whose header size differs. For more
64+
information about this structure such as its alignment requirements, see the
65+
documentation for the [ELF format](https://man7.org/linux/man-pages/man5/elf.5.html).
66+
67+
Each record's _kind_ (stored in the `n_type` field) determines how the record
68+
will be interpreted at runtime:
4669

4770
| Type Value | Interpretation |
4871
|-:|-|
49-
| < `0` | Undefined (**do not use**) |
50-
| `0` ... `99` | Reserved |
72+
| `< 0` | Undefined (**do not use**) |
73+
| `0 ... 99` | Reserved |
5174
| `100` | Test or suite declaration |
5275
| `101` | Exit test |
5376

5477
<!-- When adding cases to this enumeration, be sure to also update the
5578
corresponding enumeration in Discovery.h and TestContentGeneration.swift. -->
5679

57-
### Loading test content from a record
80+
### Record contents
5881

59-
For all currently-defined record types, the header and name are followed by a
60-
structure of the following form:
82+
For all currently-defined record types, the header structure is immediately
83+
followed by the actual content of the record. A test content record currently
84+
contains an `accessor` function to load the corresponding Swift content and a
85+
`flags` field whose value depends on the type of record. The overall structure
86+
of a record therefore looks like:
6187

6288
```c
6389
struct SWTTestContent {
64-
bool (* accessor)(void *);
65-
uint64_t flags;
90+
SWTTestContentHeader header;
91+
bool (* accessor)(void *outValue);
92+
uint32_t flags;
93+
uint32_t reserved;
6694
};
6795
```
6896

69-
#### The accessor field
70-
71-
The function `accessor` is a C function whose signature in Swift can be restated
72-
as:
97+
Or, in Swift as a tuple:
7398

7499
```swift
75-
@convention(c) (_ outValue: UnsafeMutableRawPointer) -> Bool
100+
typealias SWTTestContent = (
101+
header: SWTTestContentHeader,
102+
accessor: @convention(c) (_ outValue: UnsafeMutableRawPointer) -> Bool,
103+
flags: UInt32,
104+
reserved: UInt32
105+
)
76106
```
77107

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.
108+
This structure may grow in the future as needed. Check the `header.n_descsz`
109+
field to determine if there are additional fields present. Do not assume that
110+
the size of this structure will remain fixed over time or that all discovered
111+
test content records are the same size.
82112

83-
The concrete Swift type of `accessor`'s result depends on the type of record:
113+
#### The accessor field
114+
115+
The function `accessor` is a C function. When called, it initializes the memory
116+
at its argument `outValue` to an instance of some Swift type and returns `true`,
117+
or returns `false` if it could not generate the relevant content. On successful
118+
return, the caller is responsible for deinitializing the memory at `outValue`
119+
when done with it.
120+
121+
The concrete Swift type of the value written to `outValue` depends on the type
122+
of record:
84123

85124
| Type Value | Return Type |
86125
|-:|-|
87126
| < `0` | Undefined (**do not use**) |
88-
| `0` ... `99` | `nil` |
127+
| `0` ... `99` | Reserved (**do not use**) |
89128
| `100` | `@Sendable () async -> Test`[^2] |
90-
| `101` | `ExitTest` (owned by caller) |
129+
| `101` | `ExitTest` (consumed by caller) |
91130

92131
[^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`.
132+
function reference it writes to `outValue`. This level of indirection is
133+
necessary because loading a test or suite declaration is an asynchronous
134+
operation, but C functions cannot be `async`.
96135

97136
#### The flags field
98137

@@ -105,6 +144,10 @@ For test or suite declarations (type `100`), the following flags are defined:
105144

106145
For exit test declarations (type `101`), no flags are currently defined.
107146

147+
#### The reserved field
148+
149+
This field is reserved for future use. Always set it to `0`.
150+
108151
## Third-party test content
109152

110153
TODO: elaborate how tools can reuse the same `n_name` and `n_type` fields to

Package.swift

+9
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ extension Array where Element == PackageDescription.CXXSetting {
164164
static var packageSettings: Self {
165165
var result = Self()
166166

167+
result += [
168+
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
169+
170+
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
171+
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .windows, .wasi])),
172+
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
173+
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),
174+
]
175+
167176
// Capture the testing library's version as a C++ string constant.
168177
if let git = Context.gitInformation {
169178
let testingLibraryVersion = if let tag = git.currentTag {

Sources/Testing/ExitTests/ExitTest.swift

+14-5
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,28 @@ private import _TestingInternals
1717

1818
/// A type describing an exit test.
1919
///
20-
/// Instances of this type describe an exit test defined by the test author and
21-
/// discovered or called at runtime.
22-
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
20+
/// An instance of this type describes an exit test defined in a test target and
21+
/// discovered or called at runtime. You do not create instances of this type.
22+
///
23+
/// You don't usually need to interact with an instance of this type. To create
24+
/// an exit test, use the ``expect(exitsWith:_:sourceLocation:performing:)``
25+
/// or ``require(exitsWith:_:sourceLocation:performing:)`` macro.
26+
@_spi(Experimental)
2327
public struct ExitTest: Sendable, ~Copyable {
24-
/// The expected exit condition of the exit test.
28+
/// This exit test's expected exit condition.
2529
public var expectedExitCondition: ExitCondition
2630

27-
/// The source location of the exit test.
31+
/// The source location of this exit test.
2832
///
2933
/// The source location is unique to each exit test and is consistent between
3034
/// processes, so it can be used to uniquely identify an exit test at runtime.
3135
public var sourceLocation: SourceLocation
3236

3337
/// The body closure of the exit test.
38+
///
39+
/// Do not invoke this closure directly. Instead, invoke ``callAsFunction()``
40+
/// to run the exit test. Running the exit test will always terminate the
41+
/// current process.
3442
fileprivate var body: @Sendable () async throws -> Void
3543

3644
/// Initialize an exit test at runtime.
@@ -97,6 +105,7 @@ public struct ExitTest: Sendable, ~Copyable {
97105
/// to terminate the process; if it does not, the testing library will
98106
/// terminate the process in a way that causes the corresponding expectation
99107
/// to fail.
108+
@_spi(ForToolsIntegrationOnly)
100109
public consuming func callAsFunction() async -> Never {
101110
Self._disableCrashReporting()
102111

Sources/Testing/Test+Discovery.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ extension Test {
4444
nonisolated(unsafe) let imageAddress = imageAddress
4545
generators.append { @Sendable in
4646
var result = await generator()
47+
#if !SWT_NO_DYNAMIC_LINKING
4748
result.imageAddress = imageAddress
49+
#endif
4850
return result
4951
}
5052
}
@@ -152,7 +154,7 @@ private func _enumerateTestContent(_ body: _TestContentEnumerator) {
152154
/// - stop: An `inout` boolean variable indicating whether type enumeration
153155
/// should stop after the function returns. Set `stop` to `true` to stop
154156
/// type enumeration.
155-
typealias TestContentEnumerator<T> = (_ imageAddress: UnsafeRawPointer?, _ content: borrowing T, _ flags: UInt64, _ stop: inout Bool) -> Void where T: ~Copyable
157+
typealias TestContentEnumerator<T> = (_ imageAddress: UnsafeRawPointer?, _ content: borrowing T, _ flags: UInt32, _ stop: inout Bool) -> Void where T: ~Copyable
156158

157159
/// Enumerate all test content known to Swift and found in the current process.
158160
///

Sources/Testing/Test.swift

+16-9
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,25 @@ public struct Test: Sendable {
5353
/// The source location of this test.
5454
public var sourceLocation: SourceLocation
5555

56+
#if !SWT_NO_DYNAMIC_LINKING
5657
/// The base address of the image containing this test, if available.
5758
///
58-
/// On platforms that do not support dynamic loading of images, the value of
59-
/// this property is `nil`. Otherwise, the value is platform-specific, but
60-
/// generally equal to the address of the first byte of the image mapped into
61-
/// memory.
59+
/// This property's value represents the image that contains this test and is
60+
/// equivalent to various platform-specific values:
6261
///
63-
/// On Apple platforms, this property's value is equivalent to a pointer to a
64-
/// `mach_header` value. On Windows, it is equivalent to an `HMODULE`. It is
65-
/// never equivalent to the pointer returned from a call to `dlopen()` (on
66-
/// platforms that have that function.)
67-
nonisolated(unsafe) var imageAddress: UnsafeRawPointer?
62+
/// | Platform | Equivalent To |
63+
/// |-|-|
64+
/// | macOS, iOS, tvOS, visionOS | `UnsafePointer<mach_header_64>` |
65+
/// | watchOS | `UnsafePointer<mach_header>` |
66+
/// | Linux, FreeBSD, Android (32-bit) | `UnsafePointer<Elf32_Ehdr>` |
67+
/// | Linux, FreeBSD, Android (64-bit) | `UnsafePointer<Elf64_Ehdr>` |
68+
/// | Windows | `HMODULE` |
69+
///
70+
/// The value of this property is distinct from the pointer returned by
71+
/// `dlopen()` (on platforms that have that function.)
72+
@_spi(ForToolsIntegrationOnly)
73+
public nonisolated(unsafe) var imageAddress: UnsafeRawPointer?
74+
#endif
6875

6976
/// Information about the type containing this test, if any.
7077
///

Sources/TestingMacros/SuiteDeclarationMacro.swift

+8-20
Original file line numberDiff line numberDiff line change
@@ -126,29 +126,17 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
126126
// Parse the @Suite attribute.
127127
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)
128128

129-
// We need an extra trampoline through a static property getter (rather than
130-
// just having this logic in the C thunk) because some versions of the Swift
131-
// compiler think that the presence of a `try` keyword in `testsBody` means
132-
// that it must be throwing (disregarding autoclosures.)
133129
let accessorName = context.makeUniqueName("")
134130
result.append(
135131
"""
136132
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
137-
@Sendable private static func \(accessorName)() async -> Testing.Test {
138-
.__type(
139-
\(declaration.type.trimmed).self,
140-
\(raw: attributeInfo.functionArgumentList(in: context))
141-
)
142-
}
143-
"""
144-
)
145-
146-
let cAccessorName = context.makeUniqueName("")
147-
result.append(
148-
"""
149-
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
150-
private static let \(cAccessorName): @convention(c) (UnsafeMutableRawPointer) -> Bool = {
151-
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self, to: \(accessorName))
133+
private static let \(accessorName): @convention(c) (UnsafeMutableRawPointer) -> Bool = {
134+
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in
135+
.__type(
136+
\(declaration.type.trimmed).self,
137+
\(raw: attributeInfo.functionArgumentList(in: context))
138+
)
139+
}
152140
return true
153141
}
154142
"""
@@ -160,7 +148,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
160148
named: sectionContentName,
161149
in: declaration.type,
162150
ofKind: .testDeclaration,
163-
accessingWith: cAccessorName,
151+
accessingWith: accessorName,
164152
flags: 1 << 0 /* suite decl */
165153
)
166154
)

Sources/TestingMacros/Support/TestContentGeneration.swift

+5-3
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,16 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax?
107107
type: Int32,
108108
name: \(elfNoteName.type),
109109
accessor: @convention(c) (UnsafeMutableRawPointer) -> Bool,
110-
flags: UInt64
110+
flags: UInt32,
111+
reserved: UInt32
111112
) = (
112113
Int32(MemoryLayout<\(elfNoteName.type)>.stride),
113-
Int32(MemoryLayout<UnsafeRawPointer>.stride + MemoryLayout<UInt64>.stride),
114+
Int32(MemoryLayout<UnsafeRawPointer>.stride + MemoryLayout<UInt32>.stride + MemoryLayout<UInt32>.stride),
114115
\(raw: kind.rawValue) as Int32,
115116
\(elfNoteName.expression) as \(elfNoteName.type),
116117
\(accessorName) as @convention(c) (UnsafeMutableRawPointer) -> Bool,
117-
\(raw: flags) as UInt64
118+
\(raw: flags) as UInt32,
119+
0 as UInt32
118120
)
119121
"""
120122
}

Sources/TestingMacros/TestDeclarationMacro.swift

+5-17
Original file line numberDiff line numberDiff line change
@@ -467,26 +467,14 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
467467
)
468468
}
469469

470-
// We need an extra trampoline through a static property getter (rather than
471-
// just having this logic in the C thunk) because some versions of the Swift
472-
// compiler think that the presence of a `try` keyword in `testsBody` means
473-
// that it must be throwing (disregarding autoclosures.)
474470
let accessorName = context.makeUniqueName(thunking: functionDecl)
475471
result.append(
476472
"""
477473
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
478-
@Sendable private \(staticKeyword(for: typeName)) func \(accessorName)() async -> Testing.Test {
479-
\(raw: testsBody)
480-
}
481-
"""
482-
)
483-
484-
let cAccessorName = context.makeUniqueName(thunking: functionDecl)
485-
result.append(
486-
"""
487-
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
488-
private \(staticKeyword(for: typeName)) let \(cAccessorName): @convention(c) (UnsafeMutableRawPointer) -> Bool = {
489-
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self, to: \(accessorName))
474+
private \(staticKeyword(for: typeName)) let \(accessorName): @convention(c) (UnsafeMutableRawPointer) -> Bool = {
475+
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in
476+
\(raw: testsBody)
477+
}
490478
return true
491479
}
492480
"""
@@ -503,7 +491,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
503491
named: sectionContentName,
504492
in: typeName,
505493
ofKind: .testDeclaration,
506-
accessingWith: cAccessorName,
494+
accessingWith: accessorName,
507495
flags: flags
508496
)
509497
)

Sources/_TestingInternals/Discovery.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,8 @@ void swt_enumerateTestContent(void *context, SWTTestContentEnumerator body) {
414414
// Extract the content of this record now that we know it's ours.
415415
struct Content {
416416
bool (* accessor)(void *outValue);
417-
uint64_t flags;
417+
uint32_t flags;
418+
uint32_t reserved;
418419
};
419420
auto content = reinterpret_cast<const Content *>(record.getDescription());
420421
if (!content) {

0 commit comments

Comments
 (0)