Skip to content

Commit 8836b38

Browse files
authored
Make all test content types directly conform to TestContent. (#920)
This PR eliminates the `TestContentAccessorResult` associated type from the (currently internal, potentially eventually API) `TestContent` protocol. This associated type needed to be `~Copyable` so `ExitTest` could be used with it, but that appears to pose some _problems_ for the compiler (rdar://143049814&143080508). Instead, we remove the associated type and just say "the test content record is the type that conforms to `TestContent`". `ExitTest` is happy with this, but `Test`'s produced type is a non-nominal function type, so we wrap that function in a small private type with identical layout and have that type conform. The ultimate purpose of this PR is to get us a bit closer to turning `TestContent` into a public or tools-SPI protocol that other components can use for test discovery. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent fb95c48 commit 8836b38

File tree

6 files changed

+60
-37
lines changed

6 files changed

+60
-37
lines changed

Documentation/ABI/TestContent.md

+14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ struct SWTTestContentRecord {
6363
};
6464
```
6565

66+
Do not use the `__TestContentRecord` typealias defined in the testing library.
67+
This type exists to support the testing library's macros and may change in the
68+
future (e.g. to accomodate a generic argument or to make use of one of the
69+
reserved fields.)
70+
71+
Instead, define your own copy of this type where needed—you can copy the
72+
definition above _verbatim_. If your test record type's `context` field (as
73+
described below) is a pointer type, make sure to change its type in your version
74+
of `TestContentRecord` accordingly so that, on systems with pointer
75+
authentication enabled, the pointer is correctly resigned at load time.
76+
6677
### Record content
6778

6879
#### The kind field
@@ -79,6 +90,9 @@ record's kind is a 32-bit unsigned value. The following kinds are defined:
7990
<!-- When adding cases to this enumeration, be sure to also update the
8091
corresponding enumeration in TestContentGeneration.swift. -->
8192

93+
If a test content record's `kind` field equals `0x00000000`, the values of all
94+
other fields in that record are undefined.
95+
8296
#### The accessor field
8397

8498
The function `accessor` is a C function. When called, it initializes the memory

Sources/Testing/Discovery+Platform.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private func _findSection(named sectionName: String, in hModule: HMODULE) -> Sec
230230
///
231231
/// - Returns: An array of structures describing the bounds of all known test
232232
/// content sections in the current process.
233-
private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
233+
private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence<SectionBounds> {
234234
let sectionName = switch kind {
235235
case .testContent:
236236
".sw5test"

Sources/Testing/Discovery.swift

+7-19
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,7 @@ protocol TestContent: ~Copyable {
4848
/// `ABI/TestContent.md` for a list of values and corresponding types.
4949
static var testContentKind: UInt32 { get }
5050

51-
/// The type of value returned by the test content accessor for this type.
52-
///
53-
/// This type may or may not equal `Self` depending on the type's compile-time
54-
/// and runtime requirements. If it does not equal `Self`, it should equal a
55-
/// type whose instances can be converted to instances of `Self` (e.g. by
56-
/// calling them if they are functions.)
57-
associatedtype TestContentAccessorResult: ~Copyable
58-
59-
/// A type of "hint" passed to ``discover(withHint:)`` to help the testing
51+
/// A type of "hint" passed to ``allTestContentRecords()`` to help the testing
6052
/// library find the correct result.
6153
///
6254
/// By default, this type equals `Never`, indicating that this type of test
@@ -75,7 +67,7 @@ protocol TestContent: ~Copyable {
7567
/// This type is not part of the public interface of the testing library. In the
7668
/// future, we could make it public if we want to support runtime discovery of
7769
/// test content by second- or third-party code.
78-
struct TestContentRecord<T>: Sendable where T: ~Copyable {
70+
struct TestContentRecord<T>: Sendable where T: TestContent & ~Copyable {
7971
/// The base address of the image containing this instance, if known.
8072
///
8173
/// On platforms such as WASI that statically link to the testing library, the
@@ -93,11 +85,7 @@ struct TestContentRecord<T>: Sendable where T: ~Copyable {
9385
self.imageAddress = imageAddress
9486
self._record = record
9587
}
96-
}
9788

98-
// This `T: TestContent` constraint is in an extension in order to work around a
99-
// compiler crash. SEE: rdar://143049814
100-
extension TestContentRecord where T: TestContent & ~Copyable {
10189
/// The context value for this test content record.
10290
var context: UInt {
10391
_record.context
@@ -109,18 +97,18 @@ extension TestContentRecord where T: TestContent & ~Copyable {
10997
/// - hint: An optional hint value. If not `nil`, this value is passed to
11098
/// the accessor function of the underlying test content record.
11199
///
112-
/// - Returns: An instance of the associated ``TestContentAccessorResult``
113-
/// type, or `nil` if the underlying test content record did not match
114-
/// `hint` or otherwise did not produce a value.
100+
/// - Returns: An instance of the test content type `T`, or `nil` if the
101+
/// underlying test content record did not match `hint` or otherwise did not
102+
/// produce a value.
115103
///
116104
/// If this function is called more than once on the same instance, a new
117105
/// value is created on each call.
118-
func load(withHint hint: T.TestContentAccessorHint? = nil) -> T.TestContentAccessorResult? {
106+
func load(withHint hint: T.TestContentAccessorHint? = nil) -> T? {
119107
guard let accessor = _record.accessor else {
120108
return nil
121109
}
122110

123-
return withUnsafeTemporaryAllocation(of: T.TestContentAccessorResult.self, capacity: 1) { buffer in
111+
return withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in
124112
let initialized = if let hint {
125113
withUnsafePointer(to: hint) { hint in
126114
accessor(buffer.baseAddress!, hint)

Sources/Testing/ExitTests/ExitTest.swift

-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ extension ExitTest: TestContent {
228228
0x65786974
229229
}
230230

231-
typealias TestContentAccessorResult = Self
232231
typealias TestContentAccessorHint = ID
233232
}
234233

Sources/Testing/Test+Discovery.swift

+26-6
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,27 @@
1010

1111
private import _TestingInternals
1212

13-
extension Test: TestContent {
14-
static var testContentKind: UInt32 {
15-
0x74657374
16-
}
13+
extension Test {
14+
/// A type that encapsulates test content records that produce instances of
15+
/// ``Test``.
16+
///
17+
/// This type is necessary because such test content records produce an
18+
/// indirect `async` accessor function rather than directly producing
19+
/// instances of ``Test``, but functions are non-nominal types and cannot
20+
/// directly conform to protocols.
21+
///
22+
/// - Note: This helper type must have the exact in-memory layout of the
23+
/// `async` accessor function. Do not add any additional cases or associated
24+
/// values. The layout of this type is [guaranteed](https://github.com/swiftlang/swift/blob/main/docs/ABI/TypeLayout.rst#fragile-enum-layout)
25+
/// by the Swift ABI.
26+
/* @frozen */ private enum _Record: TestContent {
27+
static var testContentKind: UInt32 {
28+
0x74657374
29+
}
1730

18-
typealias TestContentAccessorResult = @Sendable () async -> Self
31+
/// The actual (asynchronous) accessor function.
32+
case generator(@Sendable () async -> Test)
33+
}
1934

2035
/// All available ``Test`` instances in the process, according to the runtime.
2136
///
@@ -43,7 +58,12 @@ extension Test: TestContent {
4358
// Walk all test content and gather generator functions, then call them in
4459
// a task group and collate their results.
4560
if useNewMode {
46-
let generators = Self.allTestContentRecords().lazy.compactMap { $0.load() }
61+
let generators = _Record.allTestContentRecords().lazy.compactMap { record in
62+
if case let .generator(generator) = record.load() {
63+
return generator
64+
}
65+
return nil // currently unreachable, but not provably so
66+
}
4767
await withTaskGroup(of: Self.self) { taskGroup in
4868
for generator in generators {
4969
taskGroup.addTask(operation: generator)

Tests/TestingTests/MiscellaneousTests.swift

+12-10
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,8 @@ struct MiscellaneousTests {
583583
#if !SWT_NO_DYNAMIC_LINKING && hasFeature(SymbolLinkageMarkers)
584584
struct DiscoverableTestContent: TestContent {
585585
typealias TestContentAccessorHint = UInt32
586-
typealias TestContentAccessorResult = UInt32
586+
587+
var value: UInt32
587588

588589
static var testContentKind: UInt32 {
589590
record.kind
@@ -593,7 +594,7 @@ struct MiscellaneousTests {
593594
0x01020304
594595
}
595596

596-
static var expectedResult: TestContentAccessorResult {
597+
static var expectedValue: UInt32 {
597598
0xCAFEF00D
598599
}
599600

@@ -618,7 +619,7 @@ struct MiscellaneousTests {
618619
if let hint, hint.load(as: TestContentAccessorHint.self) != expectedHint {
619620
return false
620621
}
621-
_ = outValue.initializeMemory(as: TestContentAccessorResult.self, to: expectedResult)
622+
_ = outValue.initializeMemory(as: Self.self, to: .init(value: expectedValue))
622623
return true
623624
},
624625
UInt(truncatingIfNeeded: UInt64(0x0204060801030507)),
@@ -628,32 +629,33 @@ struct MiscellaneousTests {
628629

629630
@Test func testDiscovery() async {
630631
// Check the type of the test record sequence (it should be lazy.)
631-
let allRecords = DiscoverableTestContent.allTestContentRecords()
632+
let allRecordsSeq = DiscoverableTestContent.allTestContentRecords()
632633
#if SWT_FIXED_143080508
633-
#expect(allRecords is any LazySequenceProtocol)
634-
#expect(!(allRecords is [TestContentRecord<DiscoverableTestContent>]))
634+
#expect(allRecordsSeq is any LazySequenceProtocol)
635+
#expect(!(allRecordsSeq is [TestContentRecord<DiscoverableTestContent>]))
635636
#endif
636637

637638
// It should have exactly one matching record (because we only emitted one.)
638-
#expect(Array(allRecords).count == 1)
639+
let allRecords = Array(allRecordsSeq)
640+
#expect(allRecords.count == 1)
639641

640642
// Can find a single test record
641643
#expect(allRecords.contains { record in
642-
record.load() == DiscoverableTestContent.expectedResult
644+
record.load()?.value == DiscoverableTestContent.expectedValue
643645
&& record.context == DiscoverableTestContent.expectedContext
644646
})
645647

646648
// Can find a test record with matching hint
647649
#expect(allRecords.contains { record in
648650
let hint = DiscoverableTestContent.expectedHint
649-
return record.load(withHint: hint) == DiscoverableTestContent.expectedResult
651+
return record.load(withHint: hint)?.value == DiscoverableTestContent.expectedValue
650652
&& record.context == DiscoverableTestContent.expectedContext
651653
})
652654

653655
// Doesn't find a test record with a mismatched hint
654656
#expect(!allRecords.contains { record in
655657
let hint = ~DiscoverableTestContent.expectedHint
656-
return record.load(withHint: hint) == DiscoverableTestContent.expectedResult
658+
return record.load(withHint: hint)?.value == DiscoverableTestContent.expectedValue
657659
&& record.context == DiscoverableTestContent.expectedContext
658660
})
659661
}

0 commit comments

Comments
 (0)