Skip to content

Commit b9cfdcc

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

File tree

8 files changed

+86
-38
lines changed

8 files changed

+86
-38
lines changed

Documentation/ABI/TestContent.md

+49-23
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,39 @@ 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
33+
### Record headers
4034
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+
The size of `n_name` is dynamic and cannot be statically computed. The testing
49+
library always generates the name `"Swift Testing"` and specifies an `n_namesz`
50+
value of `20` (the string being null-padded to the correct length), but other
51+
content may be present in the same section whose header content differs. For
52+
more information about this structure such as its alignment requirements, see
53+
the documentation for the [ELF format](https://man7.org/linux/man-pages/man5/elf.5.html).
54+
55+
Each record's _kind_ (stored in the `n_type` field) determines how the record
56+
will be interpreted at runtime:
4657

4758
| Type Value | Interpretation |
4859
|-:|-|
@@ -54,18 +65,28 @@ interpreted at runtime:
5465
<!-- When adding cases to this enumeration, be sure to also update the
5566
corresponding enumeration in Discovery.h and TestContentGeneration.swift. -->
5667

57-
### Loading test content from a record
68+
### Record contents
5869

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

6276
```c
6377
struct SWTTestContent {
78+
SWTTestContentHeader header;
6479
bool (* accessor)(void *);
65-
uint64_t flags;
80+
uint32_t flags;
81+
uint32_t reserved;
6682
};
6783
```
6884

85+
This structure may grow in the future as needed. Check the `header.n_descsz`
86+
field to determine if there are additional fields present. Do not assume that
87+
the size of this structure will remain fixed over time or that all discovered
88+
test content records are the same size.
89+
6990
#### The accessor field
7091

7192
The function `accessor` is a C function whose signature in Swift can be restated
@@ -80,19 +101,20 @@ Swift type and returns `true`, or returns `false` if it could not generate the
80101
relevant content. On successful return, the caller is responsible for
81102
deinitializing the memory at `outValue` when done with it.
82103

83-
The concrete Swift type of `accessor`'s result depends on the type of record:
104+
The concrete Swift type of the value written to `outValue` depends on the type
105+
of record:
84106

85107
| Type Value | Return Type |
86108
|-:|-|
87109
| < `0` | Undefined (**do not use**) |
88-
| `0` ... `99` | `nil` |
110+
| `0` ... `99` | Reserved (**do not use**) |
89111
| `100` | `@Sendable () async -> Test`[^2] |
90-
| `101` | `ExitTest` (owned by caller) |
112+
| `101` | `ExitTest` (consumed by caller) |
91113

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

97119
#### The flags field
98120

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

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

130+
#### The reserved field
131+
132+
This field is reserved for future use. Always set it to `0`.
133+
108134
## Third-party test content
109135

110136
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/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ add_library(Testing
7979
Test.ID.swift
8080
Test.swift
8181
Test+Discovery.swift
82+
Test+Discovery+MachO.swift
8283
Test+Macro.swift
8384
Traits/Bug.swift
8485
Traits/Comment.swift

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/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/_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) {

Sources/_TestingInternals/include/Discovery.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ typedef struct SWTTestContentRecord {
5454
///
5555
/// The value of this property is dependent on the kind of test content this
5656
/// instance represents.
57-
uint64_t flags;
57+
uint32_t flags;
5858
} SWTTestContentRecord;
5959

6060
/// The type of callback called by `swt_enumerateTestContent()`.

0 commit comments

Comments
 (0)