Skip to content
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
21 changes: 8 additions & 13 deletions Documentation/ABI/TestContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.)

Expand Down
16 changes: 1 addition & 15 deletions Sources/Testing/Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -142,7 +128,7 @@ struct TestContentRecord<T>: 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
Expand Down
114 changes: 66 additions & 48 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,60 +24,39 @@ 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.
///
/// 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``.
///
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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<ExitTestArtifacts> & Sendable],
expression: __Expression,
Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ public func __checkClosureCall<R>(
/// `#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<ExitTestArtifacts> & Sendable],
performing body: @convention(thin) () -> Void,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Testing/Test+Discovery+Legacy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
26 changes: 4 additions & 22 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) }
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()

Expand All @@ -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)))()
}
"""
Expand All @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions Tests/TestingTests/ExitTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down