Skip to content

Relax the alignment requirement for DiscoverableAsTestContent.Context. #1076

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 4 commits into from
Apr 17, 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
26 changes: 7 additions & 19 deletions Documentation/ABI/TestContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,25 +149,12 @@ The fourth argument to this function, `reserved`, is reserved for future use.
Accessor functions should assume it is `0` and must not access it.

The concrete Swift type of the value written to `outValue`, the type pointed to
by `type`, and the value pointed to by `hint` depend on the kind of record:
by `type`, and the value pointed to by `hint` depend on the kind of record.

- For test or suite declarations (kind `0x74657374`), the accessor produces a
structure of type `Testing.Test.Generator` that the testing library can use
to generate the corresponding test[^notAccessorSignature].

[^notAccessorSignature]: This level of indirection is necessary because
loading a test or suite declaration is an asynchronous operation, but C
functions cannot be `async`.

Test content records of this kind do not specify a type for `hint`. Always
pass `nil`.

- For exit test declarations (kind `0x65786974`), the accessor produces a
structure describing the exit test (of type `Testing.ExitTest`.)

Test content records of this kind accept a `hint` of type `Testing.ExitTest.ID`.
They only produce a result if they represent an exit test declared with the
same ID (or if `hint` is `nil`.)
The record kinds defined by Swift Testing (kinds `0x74657374` and `0x65786974`)
make use of the `DiscoverableAsTestContent` protocol in the `_TestDiscovery`
module and do not publicly expose the types of their accessor functions'
arguments. Do not call the accessor functions for these records directly.

> [!WARNING]
> Calling code should use [`withUnsafeTemporaryAllocation(of:capacity:_:)`](https://developer.apple.com/documentation/swift/withunsafetemporaryallocation(of:capacity:_:))
Expand Down Expand Up @@ -274,7 +261,8 @@ extension FoodTruckDiagnostic: DiscoverableAsTestContent {
```

If you customize `TestContentContext`, be aware that the type you specify must
have the same stride and alignment as `UInt`.
have the same stride as `UInt` and must have an alignment less than or equal to
that of `UInt`.

When you are done configuring your type's protocol conformance, you can then
enumerate all test content records matching it as instances of
Expand Down
9 changes: 0 additions & 9 deletions Sources/Testing/Discovery+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

@_spi(Experimental) @_spi(ForToolsIntegrationOnly) internal import _TestDiscovery

/// A shadow declaration of `_TestDiscovery.DiscoverableAsTestContent` that
/// allows us to add public conformances to it without causing the
/// `_TestDiscovery` module to appear in `Testing.private.swiftinterface`.
///
/// This protocol is not part of the public interface of the testing library.
protocol DiscoverableAsTestContent: _TestDiscovery.DiscoverableAsTestContent, ~Copyable {}

/// The type of the accessor function used to access a test content record.
///
/// The signature of this function type must match that of the corresponding
Expand Down
61 changes: 42 additions & 19 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,18 @@ public struct ExitTest: Sendable, ~Copyable {
@_spi(ForToolsIntegrationOnly)
public var id: ID

/// The body closure of the exit test.
/// An exit test body function.
///
/// - Parameters:
/// - exitTest: The exit test to which this body closure belongs.
fileprivate typealias Body = @Sendable (_ exitTest: inout Self) async throws -> Void

/// The body closure of the exit test.
///
/// Do not invoke this closure directly. Instead, invoke ``callAsFunction()``
/// to run the exit test. Running the exit test will always terminate the
/// current process.
fileprivate var body: @Sendable (_ exitTest: inout Self) async throws -> Void = { _ in }
fileprivate var body: Body = { _ in }

/// Storage for ``observedValues``.
///
Expand Down Expand Up @@ -275,12 +278,34 @@ extension ExitTest {

// MARK: - Discovery

extension ExitTest: DiscoverableAsTestContent {
fileprivate static var testContentKind: TestContentKind {
"exit"
}
extension ExitTest {
/// A type representing an exit test as a test content record.
fileprivate struct Record: Sendable, DiscoverableAsTestContent {
static var testContentKind: TestContentKind {
"exit"
}

typealias TestContentAccessorHint = ID

fileprivate typealias TestContentAccessorHint = ID
/// The ID of the represented exit test.
var id: ExitTest.ID

/// The body of the represented exit test.
var body: ExitTest.Body

/// The set of values captured in the parent process before the exit test is
/// called.
var capturedValues = [CapturedValue]()

/// Make the exit test represented by this instance.
///
/// - Returns: A new exit test as represented by this instance.
func makeExitTest() -> ExitTest {
var exitTest = ExitTest(id: id, body: body)
exitTest.capturedValues = capturedValues
return exitTest
}
}

/// Store the exit test into the given memory.
///
Expand All @@ -305,9 +330,7 @@ extension ExitTest: DiscoverableAsTestContent {
) -> CBool where repeat each T: Codable & Sendable {
#if !hasFeature(Embedded)
// Check that the type matches.
let callerExpectedType = TypeInfo(describing: typeAddress.load(as: Any.Type.self))
let selfType = TypeInfo(describing: Self.self)
guard callerExpectedType == selfType else {
guard typeAddress.load(as: Any.Type.self) == Record.self else {
return false
}
#endif
Expand All @@ -320,15 +343,15 @@ extension ExitTest: DiscoverableAsTestContent {

// Wrap the body function in a thunk that decodes any captured state and
// passes it along.
let body: @Sendable (inout Self) async throws -> Void = { exitTest in
let body: ExitTest.Body = { exitTest in
let values: (repeat each T) = try exitTest.capturedValues.takeCapturedValues()
try await body(repeat each values)
}

// Construct and return the instance.
var exitTest = Self(id: id, body: body)
exitTest.capturedValues = Array(repeat (each T).self)
outValue.initializeMemory(as: Self.self, to: exitTest)
// Construct and return the record.
var record = Record(id: id, body: body)
record.capturedValues = Array(repeat (each T).self)
outValue.initializeMemory(as: Record.self, to: record)
return true
}
}
Expand All @@ -343,16 +366,16 @@ extension ExitTest {
/// - Returns: The specified exit test function, or `nil` if no such exit test
/// could be found.
public static func find(identifiedBy id: ExitTest.ID) -> Self? {
for record in Self.allTestContentRecords() {
if let exitTest = record.load(withHint: id) {
for record in Record.allTestContentRecords() {
if let exitTest = record.load(withHint: id)?.makeExitTest() {
return exitTest
}
}

#if !SWT_NO_LEGACY_TEST_DISCOVERY
// Call the legacy lookup function that discovers tests embedded in types.
for record in Self.allTypeMetadataBasedTestContentRecords() {
if let exitTest = record.load(withHint: id) {
for record in Record.allTypeMetadataBasedTestContentRecords() {
if let exitTest = record.load(withHint: id)?.makeExitTest() {
return exitTest
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/_TestDiscovery/TestContentRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable {
/// ([swift-#79667](https://github.com/swiftlang/swift/issues/79667))
fileprivate static func validateMemoryLayout() {
precondition(MemoryLayout<TestContentContext>.stride == MemoryLayout<UInt>.stride, "'\(self).TestContentContext' aka '\(TestContentContext.self)' must have the same stride as 'UInt'.")
precondition(MemoryLayout<TestContentContext>.alignment == MemoryLayout<UInt>.alignment, "'\(self).TestContentContext' aka '\(TestContentContext.self)' must have the same alignment as 'UInt'.")
precondition(MemoryLayout<TestContentContext>.alignment <= MemoryLayout<UInt>.alignment, "'\(self).TestContentContext' aka '\(TestContentContext.self)' must have an alignment less than or equal to that of 'UInt'.")
}
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/TestingTests/DiscoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ struct DiscoveryTests {
#endif

#if !SWT_NO_DYNAMIC_LINKING && hasFeature(SymbolLinkageMarkers)
struct MyTestContent: Testing.DiscoverableAsTestContent {
struct MyTestContent: DiscoverableAsTestContent {
typealias TestContentAccessorHint = UInt32

var value: UInt32
Expand Down