diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md index 9b981e5c0..89412b29d 100644 --- a/Documentation/ABI/TestContent.md +++ b/Documentation/ABI/TestContent.md @@ -143,26 +143,21 @@ filtering is performed.) 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: -- For test or suite declarations (kind `0x74657374`), the accessor produces an - asynchronous Swift function[^notAccessorSignature] that returns an instance of - `Testing.Test`: +- 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]. - ```swift - @Sendable () async -> Test - ``` - - [^notAccessorSignature]: This signature is not the signature of `accessor`, - but of the Swift function reference it writes to `outValue`. This level of - indirection is necessary because loading a test or suite declaration is an - asynchronous operation, but C functions cannot be `async`. + [^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`.) + structure describing the exit test (of type `Testing.ExitTest`.) - Test content records of this kind accept a `hint` of type `Testing.__ExitTest.ID`. + 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`.) diff --git a/Sources/Testing/Discovery.swift b/Sources/Testing/Discovery.swift index d80d33826..38f4dfa58 100644 --- a/Sources/Testing/Discovery.swift +++ b/Sources/Testing/Discovery.swift @@ -74,20 +74,6 @@ protocol TestContent: ~Copyable { /// By default, this type equals `Never`, indicating that this type of test /// content does not support hinting during discovery. associatedtype TestContentAccessorHint: Sendable = Never - - /// The type to pass (by address) as the accessor function's `type` argument. - /// - /// The default value of this property is `Self.self`. A conforming type can - /// override the default implementation to substitute another type (e.g. if - /// the conforming type is not public but records are created during macro - /// expansion and can only reference public types.) - static var testContentAccessorTypeArgument: any ~Copyable.Type { get } -} - -extension TestContent where Self: ~Copyable { - static var testContentAccessorTypeArgument: any ~Copyable.Type { - self - } } // MARK: - Individual test content records @@ -142,7 +128,7 @@ struct TestContentRecord: Sendable where T: TestContent & ~Copyable { return nil } - return withUnsafePointer(to: T.testContentAccessorTypeArgument) { type in + return withUnsafePointer(to: T.self) { type in withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in let initialized = if let hint { withUnsafePointer(to: hint) { hint in diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 7a3597bfa..75102abe2 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -24,52 +24,31 @@ private import _TestingInternals /// A type describing an exit test. /// -/// Instances of this type describe an exit test defined by the test author and -/// discovered or called at runtime. Tools that implement custom exit test -/// handling will encounter instances of this type in two contexts: -/// -/// - When the current configuration's exit test handler, set with -/// ``Configuration/exitTestHandler``, is called; and -/// - When, in a child process, they need to look up the exit test to call. -/// -/// If you are writing tests, you don't usually need to interact directly with -/// an instance of this type. To create an exit test, use the +/// Instances of this type describe exit tests you create using the /// ``expect(exitsWith:_:sourceLocation:performing:)`` or -/// ``require(exitsWith:_:sourceLocation:performing:)`` macro. -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) -#if SWT_NO_EXIT_TESTS -@available(*, unavailable, message: "Exit tests are not available on this platform.") -#endif -public typealias ExitTest = __ExitTest - -/// A type describing an exit test. -/// -/// - Warning: This type is used to implement the `#expect(exitsWith:)` macro. -/// Do not use it directly. Tools can use the SPI ``ExitTest`` typealias if -/// needed. +/// ``require(exitsWith:_:sourceLocation:performing:)`` macro. You don't usually +/// need to interact directly with an instance of this type. @_spi(Experimental) #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif -public struct __ExitTest: Sendable, ~Copyable { - /// A type whose instances uniquely identify instances of `__ExitTest`. +public struct ExitTest: Sendable, ~Copyable { + /// A type whose instances uniquely identify instances of ``ExitTest``. + @_spi(ForToolsIntegrationOnly) public struct ID: Sendable, Equatable, Codable { /// An underlying UUID (stored as two `UInt64` values to avoid relying on /// `UUID` from Foundation or any platform-specific interfaces.) private var _lo: UInt64 private var _hi: UInt64 - /// Initialize an instance of this type. - /// - /// - Warning: This member is used to implement the `#expect(exitsWith:)` - /// macro. Do not use it directly. - public init(__uuid uuid: (UInt64, UInt64)) { + init(_ uuid: (UInt64, UInt64)) { self._lo = uuid.0 self._hi = uuid.1 } } /// A value that uniquely identifies this instance. + @_spi(ForToolsIntegrationOnly) public var id: ID /// The body closure of the exit test. @@ -77,7 +56,7 @@ public struct __ExitTest: Sendable, ~Copyable { /// 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 () async throws -> Void + fileprivate var body: @Sendable () async throws -> Void = {} /// Storage for ``observedValues``. /// @@ -113,21 +92,52 @@ public struct __ExitTest: Sendable, ~Copyable { _observedValues = newValue } } +} + +#if !SWT_NO_EXIT_TESTS +// MARK: - Current + +@_spi(Experimental) +extension ExitTest { + /// A container type to hold the current exit test. + /// + /// This class is temporarily necessary until `ManagedBuffer` is updated to + /// support storing move-only values. For more information, see [SE-NNNN](https://github.com/swiftlang/swift-evolution/pull/2657). + private final class _CurrentContainer: Sendable { + /// The exit test represented by this container. + /// + /// The value of this property must be optional to avoid a copy when reading + /// the value in ``ExitTest/current``. + let exitTest: ExitTest? + + init(exitTest: borrowing ExitTest) { + self.exitTest = ExitTest(id: exitTest.id, body: exitTest.body, _observedValues: exitTest._observedValues) + } + } + + /// Storage for ``current``. + private static let _current = Locked<_CurrentContainer?>() - /// Initialize an exit test at runtime. + /// The exit test that is running in the current process, if any. /// - /// - Warning: This initializer is used to implement the `#expect(exitsWith:)` - /// macro. Do not use it directly. - public init( - __identifiedBy id: ID, - body: @escaping @Sendable () async throws -> Void = {} - ) { - self.id = id - self.body = body + /// If the current process was created to run an exit test, the value of this + /// property describes that exit test. If this process is the parent process + /// of an exit test, or if no exit test is currently running, the value of + /// this property is `nil`. + /// + /// The value of this property is constant across all tasks in the current + /// process. + public static var current: ExitTest? { + _read { + if let current = _current.rawValue { + yield current.exitTest + } else { + yield nil + } + } } } -#if !SWT_NO_EXIT_TESTS // MARK: - Invocation @_spi(Experimental) @_spi(ForToolsIntegrationOnly) @@ -180,8 +190,7 @@ extension ExitTest { /// This function invokes the closure originally passed to /// `#expect(exitsWith:)` _in the current process_. That closure is expected /// to terminate the process; if it does not, the testing library will - /// terminate the process in a way that causes the corresponding expectation - /// to fail. + /// terminate the process as if its `main()` function returned naturally. public consuming func callAsFunction() async -> Never { Self._disableCrashReporting() @@ -209,6 +218,11 @@ extension ExitTest { } #endif + // Set ExitTest.current before the test body runs. + Self._current.withLock { current in + current = _CurrentContainer(exitTest: self) + } + do { try await body() } catch { @@ -247,11 +261,15 @@ extension ExitTest { } } +#if !SWT_NO_LEGACY_TEST_DISCOVERY // Call the legacy lookup function that discovers tests embedded in types. return types(withNamesContaining: exitTestContainerTypeNameMagic).lazy .compactMap { $0 as? any __ExitTestContainer.Type } - .first { $0.__id == id } - .map { ExitTest(__identifiedBy: $0.__id, body: $0.__body) } + .first { ID($0.__id) == id } + .map { ExitTest(id: ID($0.__id), body: $0.__body) } +#else + return nil +#endif } } @@ -280,7 +298,7 @@ extension ExitTest { /// `await #expect(exitsWith:) { }` invocations regardless of calling /// convention. func callExitTest( - identifiedBy exitTestID: ExitTest.ID, + identifiedBy exitTestID: (UInt64, UInt64), exitsWith expectedExitCondition: ExitCondition, observing observedValues: [any PartialKeyPath & Sendable], expression: __Expression, @@ -295,7 +313,7 @@ func callExitTest( var result: ExitTestArtifacts do { - var exitTest = ExitTest(__identifiedBy: exitTestID) + var exitTest = ExitTest(id: ExitTest.ID(exitTestID)) exitTest.observedValues = observedValues result = try await configuration.exitTestHandler(exitTest) @@ -426,10 +444,10 @@ extension ExitTest { /// configurations is undefined. static func findInEnvironmentForEntryPoint() -> Self? { // Find the ID of the exit test to run, if any, in the environment block. - var id: __ExitTest.ID? + var id: ExitTest.ID? if var idString = Environment.variable(named: "SWT_EXPERIMENTAL_EXIT_TEST_ID") { id = try? idString.withUTF8 { idBuffer in - try JSON.decode(__ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer)) + try JSON.decode(ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer)) } } guard let id else { diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 267fddba4..550f6039c 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1147,7 +1147,7 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. @_spi(Experimental) public func __checkClosureCall( - identifiedBy exitTestID: __ExitTest.ID, + identifiedBy exitTestID: (UInt64, UInt64), exitsWith expectedExitCondition: ExitCondition, observing observedValues: [any PartialKeyPath & Sendable], performing body: @convention(thin) () -> Void, diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index 6ee080051..301b1e955 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -33,7 +33,7 @@ let testContainerTypeNameMagic = "__🟠$test_container__" @_spi(Experimental) public protocol __ExitTestContainer { /// The unique identifier of the exit test. - static var __id: __ExitTest.ID { get } + static var __id: (UInt64, UInt64) { get } /// The body function of the exit test. static var __body: @Sendable () async throws -> Void { get } diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index d515bc98f..d42f00be6 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -18,25 +18,12 @@ extension Test { /// 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 { + fileprivate struct Generator: TestContent, RawRepresentable { static var testContentKind: UInt32 { 0x74657374 } - static var testContentAccessorTypeArgument: any ~Copyable.Type { - Generator.self - } - - /// The type of the actual (asynchronous) generator function. - typealias Generator = @Sendable () async -> Test - - /// The actual (asynchronous) accessor function. - case generator(Generator) + var rawValue: @Sendable () async -> Test } /// All available ``Test`` instances in the process, according to the runtime. @@ -65,15 +52,10 @@ extension Test { // Walk all test content and gather generator functions, then call them in // a task group and collate their results. if useNewMode { - 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 - } + let generators = Generator.allTestContentRecords().lazy.compactMap { $0.load() } await withTaskGroup(of: Self.self) { taskGroup in for generator in generators { - taskGroup.addTask(operation: generator) + taskGroup.addTask { await generator.rawValue() } } result = await taskGroup.reduce(into: result) { $0.insert($1) } } diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 0b39d2f78..f687aa631 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -435,7 +435,7 @@ extension ExitTestConditionMacro { // TODO: use UUID() here if we can link to Foundation let exitTestID = (UInt64.random(in: 0 ... .max), UInt64.random(in: 0 ... .max)) - let exitTestIDExpr: ExprSyntax = "Testing.__ExitTest.ID(__uuid: (\(literal: exitTestID.0), \(literal: exitTestID.1)))" + let exitTestIDExpr: ExprSyntax = "(\(literal: exitTestID.0), \(literal: exitTestID.1))" var decls = [DeclSyntax]() @@ -444,7 +444,7 @@ extension ExitTestConditionMacro { let bodyThunkName = context.makeUniqueName("") decls.append( """ - @Sendable func \(bodyThunkName)() async throws -> Void { + @Sendable func \(bodyThunkName)() async throws -> Swift.Void { return try await Testing.__requiringTry(Testing.__requiringAwait(\(bodyArgumentExpr.trimmed)))() } """ @@ -457,7 +457,7 @@ extension ExitTestConditionMacro { """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__ExitTestContainer, Sendable { - static var __id: Testing.__ExitTest.ID { + static var __id: (Swift.UInt64, Swift.UInt64) { \(exitTestIDExpr) } static var __body: @Sendable () async throws -> Void { diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 0c5a9dcab..00d54f3f6 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -416,6 +416,14 @@ private import _TestingInternals fatalError() } } + + @Test("ExitTest.current property") + func currentProperty() async { + #expect((ExitTest.current == nil) as Bool) + await #expect(exitsWith: .success) { + #expect((ExitTest.current != nil) as Bool) + } + } } // MARK: - Fixtures