Skip to content

Make all test content types directly conform to TestContent. #920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Documentation/ABI/TestContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ struct SWTTestContentRecord {
};
```

Do not use the `__TestContentRecord` typealias defined in the testing library.
This type exists to support the testing library's macros and may change in the
future (e.g. to accomodate a generic argument or to make use of one of the
reserved fields.)

Instead, define your own copy of this type where needed—you can copy the
definition above _verbatim_. If your test record type's `context` field (as
described below) is a pointer type, make sure to change its type in your version
of `TestContentRecord` accordingly so that, on systems with pointer
authentication enabled, the pointer is correctly resigned at load time.

### Record content

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

If a test content record's `kind` field equals `0x00000000`, the values of all
other fields in that record are undefined.

#### The accessor field

The function `accessor` is a C function. When called, it initializes the memory
Expand Down
2 changes: 1 addition & 1 deletion Sources/Testing/Discovery+Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private func _findSection(named sectionName: String, in hModule: HMODULE) -> Sec
///
/// - Returns: An array of structures describing the bounds of all known test
/// content sections in the current process.
private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence<SectionBounds> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix, this should be a lazy sequence rather than an array.

let sectionName = switch kind {
case .testContent:
".sw5test"
Expand Down
26 changes: 7 additions & 19 deletions Sources/Testing/Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,7 @@ protocol TestContent: ~Copyable {
/// `ABI/TestContent.md` for a list of values and corresponding types.
static var testContentKind: UInt32 { get }

/// The type of value returned by the test content accessor for this type.
///
/// This type may or may not equal `Self` depending on the type's compile-time
/// and runtime requirements. If it does not equal `Self`, it should equal a
/// type whose instances can be converted to instances of `Self` (e.g. by
/// calling them if they are functions.)
associatedtype TestContentAccessorResult: ~Copyable

/// A type of "hint" passed to ``discover(withHint:)`` to help the testing
/// A type of "hint" passed to ``allTestContentRecords()`` to help the testing
/// library find the correct result.
///
/// By default, this type equals `Never`, indicating that this type of test
Expand All @@ -75,7 +67,7 @@ protocol TestContent: ~Copyable {
/// This type is not part of the public interface of the testing library. In the
/// future, we could make it public if we want to support runtime discovery of
/// test content by second- or third-party code.
struct TestContentRecord<T>: Sendable where T: ~Copyable {
struct TestContentRecord<T>: Sendable where T: TestContent & ~Copyable {
/// The base address of the image containing this instance, if known.
///
/// On platforms such as WASI that statically link to the testing library, the
Expand All @@ -93,11 +85,7 @@ struct TestContentRecord<T>: Sendable where T: ~Copyable {
self.imageAddress = imageAddress
self._record = record
}
}

// This `T: TestContent` constraint is in an extension in order to work around a
// compiler crash. SEE: rdar://143049814
extension TestContentRecord where T: TestContent & ~Copyable {
/// The context value for this test content record.
var context: UInt {
_record.context
Expand All @@ -109,18 +97,18 @@ extension TestContentRecord where T: TestContent & ~Copyable {
/// - hint: An optional hint value. If not `nil`, this value is passed to
/// the accessor function of the underlying test content record.
///
/// - Returns: An instance of the associated ``TestContentAccessorResult``
/// type, or `nil` if the underlying test content record did not match
/// `hint` or otherwise did not produce a value.
/// - Returns: An instance of the test content type `T`, or `nil` if the
/// underlying test content record did not match `hint` or otherwise did not
/// produce a value.
///
/// If this function is called more than once on the same instance, a new
/// value is created on each call.
func load(withHint hint: T.TestContentAccessorHint? = nil) -> T.TestContentAccessorResult? {
func load(withHint hint: T.TestContentAccessorHint? = nil) -> T? {
guard let accessor = _record.accessor else {
return nil
}

return withUnsafeTemporaryAllocation(of: T.TestContentAccessorResult.self, capacity: 1) { buffer in
return withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in
let initialized = if let hint {
withUnsafePointer(to: hint) { hint in
accessor(buffer.baseAddress!, hint)
Expand Down
1 change: 0 additions & 1 deletion Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ extension ExitTest: TestContent {
0x65786974
}

typealias TestContentAccessorResult = Self
typealias TestContentAccessorHint = ID
}

Expand Down
32 changes: 26 additions & 6 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,27 @@

private import _TestingInternals

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

typealias TestContentAccessorResult = @Sendable () async -> Self
/// The actual (asynchronous) accessor function.
case generator(@Sendable () async -> Test)
}

/// All available ``Test`` instances in the process, according to the runtime.
///
Expand Down Expand Up @@ -43,7 +58,12 @@ extension Test: TestContent {
// Walk all test content and gather generator functions, then call them in
// a task group and collate their results.
if useNewMode {
let generators = Self.allTestContentRecords().lazy.compactMap { $0.load() }
let generators = _Record.allTestContentRecords().lazy.compactMap { record in
if case let .generator(generator) = record.load() {
return generator
}
return nil // currently unreachable, but not provably so
}
await withTaskGroup(of: Self.self) { taskGroup in
for generator in generators {
taskGroup.addTask(operation: generator)
Expand Down
22 changes: 12 additions & 10 deletions Tests/TestingTests/MiscellaneousTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,8 @@ struct MiscellaneousTests {
#if !SWT_NO_DYNAMIC_LINKING && hasFeature(SymbolLinkageMarkers)
struct DiscoverableTestContent: TestContent {
typealias TestContentAccessorHint = UInt32
typealias TestContentAccessorResult = UInt32

var value: UInt32

static var testContentKind: UInt32 {
record.kind
Expand All @@ -593,7 +594,7 @@ struct MiscellaneousTests {
0x01020304
}

static var expectedResult: TestContentAccessorResult {
static var expectedValue: UInt32 {
0xCAFEF00D
}

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

@Test func testDiscovery() async {
// Check the type of the test record sequence (it should be lazy.)
let allRecords = DiscoverableTestContent.allTestContentRecords()
let allRecordsSeq = DiscoverableTestContent.allTestContentRecords()
#if SWT_FIXED_143080508
#expect(allRecords is any LazySequenceProtocol)
#expect(!(allRecords is [TestContentRecord<DiscoverableTestContent>]))
#expect(allRecordsSeq is any LazySequenceProtocol)
#expect(!(allRecordsSeq is [TestContentRecord<DiscoverableTestContent>]))
#endif

// It should have exactly one matching record (because we only emitted one.)
#expect(Array(allRecords).count == 1)
let allRecords = Array(allRecordsSeq)
#expect(allRecords.count == 1)

// Can find a single test record
#expect(allRecords.contains { record in
record.load() == DiscoverableTestContent.expectedResult
record.load()?.value == DiscoverableTestContent.expectedValue
&& record.context == DiscoverableTestContent.expectedContext
})

// Can find a test record with matching hint
#expect(allRecords.contains { record in
let hint = DiscoverableTestContent.expectedHint
return record.load(withHint: hint) == DiscoverableTestContent.expectedResult
return record.load(withHint: hint)?.value == DiscoverableTestContent.expectedValue
&& record.context == DiscoverableTestContent.expectedContext
})

// Doesn't find a test record with a mismatched hint
#expect(!allRecords.contains { record in
let hint = ~DiscoverableTestContent.expectedHint
return record.load(withHint: hint) == DiscoverableTestContent.expectedResult
return record.load(withHint: hint)?.value == DiscoverableTestContent.expectedValue
&& record.context == DiscoverableTestContent.expectedContext
})
}
Expand Down