From 385842e6ce868f2270a2c77dbdeddc13839feb31 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 14 Apr 2025 14:47:42 -0400 Subject: [PATCH 1/4] Relax the alignment requirement for `DiscoverableAsTestContent.Context`. This PR allows `DiscoverableAsTestContent.Context` to be less-aligned than `UInt` so long as its stride remains the same. It also removes the sneaky conformance of `ExitTest` to `DiscoverableAsTestContent`, opting instead to use an internal type. Since the conformance to `DiscoverableAsTestContent` and the implementation of `__store()` form a closed system (where all type information is controlled by Swift Testing at runtime), we can do this without breaking any ABI. I've updated ABI/TestContent.md to remove some of the relevant implementation details. --- Documentation/ABI/TestContent.md | 26 ++------ Sources/Testing/Discovery+Macro.swift | 9 --- Sources/Testing/ExitTests/ExitTest.swift | 66 +++++++++++++------ .../_TestDiscovery/TestContentRecord.swift | 2 +- Tests/TestingTests/DiscoveryTests.swift | 2 +- 5 files changed, 55 insertions(+), 50 deletions(-) diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md index be2493530..2d0b9a4b8 100644 --- a/Documentation/ABI/TestContent.md +++ b/Documentation/ABI/TestContent.md @@ -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:_:)) @@ -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 diff --git a/Sources/Testing/Discovery+Macro.swift b/Sources/Testing/Discovery+Macro.swift index 97b925e55..35b276efe 100644 --- a/Sources/Testing/Discovery+Macro.swift +++ b/Sources/Testing/Discovery+Macro.swift @@ -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 diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index baabb7eaa..9d90ed7ff 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -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``. /// @@ -275,12 +278,37 @@ 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 + + /// The ID of the represented exit test. + private var _id: ExitTest.ID - fileprivate typealias TestContentAccessorHint = ID + /// The body of the represented exit test. + private var _body: ExitTest.Body + + /// The set of values captured in the parent process before the exit test is + /// called. + fileprivate var capturedValues = [CapturedValue]() + + init(id: ExitTest.ID, body: @escaping ExitTest.Body) { + _id = id + _body = body + } + + /// Make the exit test represented by this instance. + /// + /// - Returns: A new exit test as represented by this instance. + func makeExitTest() -> ExitTest { + ExitTest(id: _id, body: _body) + } + } /// Store the exit test into the given memory. /// @@ -304,10 +332,8 @@ extension ExitTest: DiscoverableAsTestContent { withHintAt hintAddress: UnsafeRawPointer? = nil ) -> 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 { + // Check that the type matches. + guard typeAddress.load(as: Any.Type.self) == Record.self else { return false } #endif @@ -320,15 +346,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 } } @@ -343,16 +369,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 } } diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index d893664ee..9224fc2ea 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -52,7 +52,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable { /// ([swift-#79667](https://github.com/swiftlang/swift/issues/79667)) fileprivate static func validateMemoryLayout() { precondition(MemoryLayout.stride == MemoryLayout.stride, "'\(self).TestContentContext' aka '\(TestContentContext.self)' must have the same stride as 'UInt'.") - precondition(MemoryLayout.alignment == MemoryLayout.alignment, "'\(self).TestContentContext' aka '\(TestContentContext.self)' must have the same alignment as 'UInt'.") + precondition(MemoryLayout.alignment <= MemoryLayout.alignment, "'\(self).TestContentContext' aka '\(TestContentContext.self)' must have an alignment less than or equal to that of 'UInt'.") } } diff --git a/Tests/TestingTests/DiscoveryTests.swift b/Tests/TestingTests/DiscoveryTests.swift index 2b53cd467..8ec185813 100644 --- a/Tests/TestingTests/DiscoveryTests.swift +++ b/Tests/TestingTests/DiscoveryTests.swift @@ -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 From 452c00d6115d010c2775045feb6f2be67961d615 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 14 Apr 2025 18:06:41 -0400 Subject: [PATCH 2/4] Simplify a tad --- Sources/Testing/ExitTests/ExitTest.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 9d90ed7ff..a021e2b04 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -288,25 +288,20 @@ extension ExitTest { typealias TestContentAccessorHint = ID /// The ID of the represented exit test. - private var _id: ExitTest.ID + var id: ExitTest.ID /// The body of the represented exit test. - private var _body: ExitTest.Body + var body: ExitTest.Body /// The set of values captured in the parent process before the exit test is /// called. - fileprivate var capturedValues = [CapturedValue]() - - init(id: ExitTest.ID, body: @escaping ExitTest.Body) { - _id = id - _body = body - } + var capturedValues = [CapturedValue]() /// Make the exit test represented by this instance. /// /// - Returns: A new exit test as represented by this instance. func makeExitTest() -> ExitTest { - ExitTest(id: _id, body: _body) + ExitTest(id: id, body: body) } } From 9b936026254dbe95e2302cf223de382b58a81103 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 17 Apr 2025 10:57:18 -0500 Subject: [PATCH 3/4] Ensure capturedValues are preserved --- Sources/Testing/ExitTests/ExitTest.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index a021e2b04..88a29ff51 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -301,7 +301,9 @@ extension ExitTest { /// /// - Returns: A new exit test as represented by this instance. func makeExitTest() -> ExitTest { - ExitTest(id: id, body: body) + var exitTest = ExitTest(id: id, body: body) + exitTest.capturedValues = capturedValues + return exitTest } } From d87d193a11d655f86adb8633dc17d033f857ca6e Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 17 Apr 2025 11:02:23 -0500 Subject: [PATCH 4/4] Fix indentation --- Sources/Testing/ExitTests/ExitTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 88a29ff51..2ad905379 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -329,7 +329,7 @@ extension ExitTest { withHintAt hintAddress: UnsafeRawPointer? = nil ) -> CBool where repeat each T: Codable & Sendable { #if !hasFeature(Embedded) - // Check that the type matches. + // Check that the type matches. guard typeAddress.load(as: Any.Type.self) == Record.self else { return false }