Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

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: 11 additions & 3 deletions Documentation/ABI/TestContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,17 @@ to by `hint` depend on the kind of record:
- For exit test declarations (kind `0x65786974`), the accessor produces a
structure describing the exit test (of type `__ExitTest`.)

Test content records of this kind accept a `hint` of type `SourceLocation`.
They only produce a result if they represent an exit test declared at the same
source location (or if the hint is `nil`.)
Test content records of this kind accept a `hint` of type `__ExitTest.ID`.
They only produce a result if they represent an exit test declared with the
same ID (or if `hint` is `nil`.)

> [!WARNING]
> Calling code should use [`withUnsafeTemporaryAllocation(of:capacity:_:)`](https://developer.apple.com/documentation/swift/withunsafetemporaryallocation(of:capacity:_:))
> and [`withUnsafePointer(to:_:)`](https://developer.apple.com/documentation/swift/withunsafepointer(to:_:)-35wrn),
> respectively, to ensure the pointers passed to `accessor` are large enough and
> are well-aligned. If they are not large enough to contain values of the
> appropriate types (per above), or if `hint` points to uninitialized or
> incorrectly-typed memory, the result is undefined.

#### The context field

Expand Down
83 changes: 40 additions & 43 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,25 @@ public typealias ExitTest = __ExitTest
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
public struct __ExitTest: Sendable, ~Copyable {
/// The expected exit condition of the exit test.
@_spi(ForToolsIntegrationOnly)
public var expectedExitCondition: ExitCondition
/// A type whose instances uniquely identify instances of `__ExitTest`.
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)) {
self._lo = uuid.0
self._hi = uuid.1
}
}

/// The source location of the exit test.
///
/// The source location is unique to each exit test and is consistent between
/// processes, so it can be used to uniquely identify an exit test at runtime.
@_spi(ForToolsIntegrationOnly)
public var sourceLocation: SourceLocation
/// A value that uniquely identifies this instance.
public var id: ID

/// The body closure of the exit test.
///
Expand Down Expand Up @@ -110,12 +119,10 @@ public struct __ExitTest: Sendable, ~Copyable {
/// - Warning: This initializer is used to implement the `#expect(exitsWith:)`
/// macro. Do not use it directly.
public init(
__expectedExitCondition expectedExitCondition: ExitCondition,
sourceLocation: SourceLocation,
__identifiedBy id: ID,
body: @escaping @Sendable () async throws -> Void = {}
) {
self.expectedExitCondition = expectedExitCondition
self.sourceLocation = sourceLocation
self.id = id
self.body = body
}
}
Expand Down Expand Up @@ -222,28 +229,24 @@ extension ExitTest: TestContent {
}

typealias TestContentAccessorResult = Self
typealias TestContentAccessorHint = SourceLocation
typealias TestContentAccessorHint = ID
}

@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
extension ExitTest {
/// Find the exit test function at the given source location.
///
/// - Parameters:
/// - sourceLocation: The source location of the exit test to find.
/// - id: The unique identifier of the exit test to find.
///
/// - Returns: The specified exit test function, or `nil` if no such exit test
/// could be found.
public static func find(at sourceLocation: SourceLocation) -> Self? {
public static func find(identifiedBy id: ExitTest.ID) -> Self? {
var result: Self?

enumerateTestContent(withHint: sourceLocation) { _, exitTest, _, stop in
if exitTest.sourceLocation == sourceLocation {
result = ExitTest(
__expectedExitCondition: exitTest.expectedExitCondition,
sourceLocation: exitTest.sourceLocation,
body: exitTest.body
)
enumerateTestContent(withHint: id) { _, exitTest, _, stop in
if exitTest.id == id {
result = ExitTest(__identifiedBy: id, body: exitTest.body)
stop = true
}
}
Expand All @@ -252,14 +255,8 @@ extension ExitTest {
// Call the legacy lookup function that discovers tests embedded in types.
result = types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
.compactMap { $0 as? any __ExitTestContainer.Type }
.first { $0.__sourceLocation == sourceLocation }
.map { type in
ExitTest(
__expectedExitCondition: type.__expectedExitCondition,
sourceLocation: type.__sourceLocation,
body: type.__body
)
}
.first { $0.__id == id }
.map { ExitTest(__identifiedBy: $0.__id, body: $0.__body) }
}

return result
Expand All @@ -272,6 +269,7 @@ extension ExitTest {
/// a given status.
///
/// - Parameters:
/// - exitTestID: The unique identifier of the exit test.
/// - expectedExitCondition: The expected exit condition.
/// - observedValues: An array of key paths representing results from within
/// the exit test that should be observed and returned by this macro. The
Expand All @@ -290,6 +288,7 @@ extension ExitTest {
/// `await #expect(exitsWith:) { }` invocations regardless of calling
/// convention.
func callExitTest(
identifiedBy exitTestID: ExitTest.ID,
exitsWith expectedExitCondition: ExitCondition,
observing observedValues: [any PartialKeyPath<ExitTestArtifacts> & Sendable],
expression: __Expression,
Expand All @@ -304,7 +303,7 @@ func callExitTest(

var result: ExitTestArtifacts
do {
var exitTest = ExitTest(__expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation)
var exitTest = ExitTest(__identifiedBy: exitTestID)
exitTest.observedValues = observedValues
result = try await configuration.exitTestHandler(exitTest)

Expand Down Expand Up @@ -424,23 +423,21 @@ extension ExitTest {
/// `__swiftPMEntryPoint()` function. The effect of using it under other
/// configurations is undefined.
static func findInEnvironmentForEntryPoint() -> Self? {
// Find the source location of the exit test to run, if any, in the
// environment block.
var sourceLocation: SourceLocation?
if var sourceLocationString = Environment.variable(named: "SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION") {
sourceLocation = try? sourceLocationString.withUTF8 { sourceLocationBuffer in
let sourceLocationBuffer = UnsafeRawBufferPointer(sourceLocationBuffer)
return try JSON.decode(SourceLocation.self, from: sourceLocationBuffer)
// Find the ID of the exit test to run, if any, in the environment block.
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))
}
}
guard let sourceLocation else {
guard let id else {
return nil
}

// If an exit test was found, inject back channel handling into its body.
// External tools authors should set up their own back channel mechanisms
// and ensure they're installed before calling ExitTest.callAsFunction().
guard var result = find(at: sourceLocation) else {
guard var result = find(identifiedBy: id) else {
return nil
}

Expand Down Expand Up @@ -560,8 +557,8 @@ extension ExitTest {

// Insert a specific variable that tells the child process which exit test
// to run.
try JSON.withEncoding(of: exitTest.sourceLocation) { json in
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION"] = String(decoding: json, as: UTF8.self)
try JSON.withEncoding(of: exitTest.id) { json in
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self)
}

typealias ResultUpdater = @Sendable (inout ExitTestArtifacts) -> Void
Expand Down
4 changes: 2 additions & 2 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ public macro require<R>(
observing observedValues: [any PartialKeyPath<ExitTestArtifacts> & Sendable] = [],
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: @convention(thin) () async throws -> Void
performing expression: @escaping @Sendable @convention(thin) () async throws -> Void
) -> ExitTestArtifacts? = #externalMacro(module: "TestingMacros", type: "ExitTestExpectMacro")

/// Check that an expression causes the process to terminate in a given fashion
Expand Down Expand Up @@ -658,5 +658,5 @@ public macro require<R>(
observing observedValues: [any PartialKeyPath<ExitTestArtifacts> & Sendable] = [],
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: @convention(thin) () async throws -> Void
performing expression: @escaping @Sendable @convention(thin) () async throws -> Void
) -> ExitTestArtifacts = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro")
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@ public func __checkClosureCall<R>(
/// `#require()` macros. Do not call it directly.
@_spi(Experimental)
public func __checkClosureCall(
identifiedBy exitTestID: __ExitTest.ID,
exitsWith expectedExitCondition: ExitCondition,
observing observedValues: [any PartialKeyPath<ExitTestArtifacts> & Sendable],
performing body: @convention(thin) () -> Void,
Expand All @@ -1157,6 +1158,7 @@ public func __checkClosureCall(
sourceLocation: SourceLocation
) async -> Result<ExitTestArtifacts?, any Error> {
await callExitTest(
identifiedBy: exitTestID,
exitsWith: expectedExitCondition,
observing: observedValues,
expression: expression,
Expand Down
7 changes: 2 additions & 5 deletions Sources/Testing/Test+Discovery+Legacy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,8 @@ let testContainerTypeNameMagic = "__🟠$test_container__"
@_alwaysEmitConformanceMetadata
@_spi(Experimental)
public protocol __ExitTestContainer {
/// The expected exit condition of the exit test.
static var __expectedExitCondition: ExitCondition { get }

/// The source location of the exit test.
static var __sourceLocation: SourceLocation { get }
/// The unique identifier of the exit test.
static var __id: __ExitTest.ID { get }

/// The body function of the exit test.
static var __body: @Sendable () async throws -> Void { get }
Expand Down
21 changes: 16 additions & 5 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ extension ExitTestConditionMacro {

let bodyArgumentExpr = arguments[trailingClosureIndex].expression

// 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)))"

var decls = [DeclSyntax]()

// Implement the body of the exit test outside the enum we're declaring so
Expand All @@ -436,15 +440,12 @@ 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 __sourceLocation: Testing.SourceLocation {
\(createSourceLocationExpr(of: macro, context: context))
static var __id: Testing.__ExitTest.ID {
\(exitTestIDExpr)
}
static var __body: @Sendable () async throws -> Void {
\(bodyThunkName)
}
static var __expectedExitCondition: Testing.ExitCondition {
\(arguments[expectedExitConditionIndex].expression.trimmed)
}
}
"""
)
Expand All @@ -458,6 +459,16 @@ extension ExitTestConditionMacro {
}
)

// Insert the exit test's ID as the first argument. Note that this will
// invalidate all indices into `arguments`!
arguments.insert(
Argument(
label: "identifiedBy",
expression: exitTestIDExpr
),
at: arguments.startIndex
)

// Replace the exit test body (as an argument to the macro) with a stub
// closure that hosts the type we created above.
var macro = macro
Expand Down
11 changes: 11 additions & 0 deletions Tests/TestingTests/ExitTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,17 @@ private import _TestingInternals
#expect(result.standardOutputContent.isEmpty)
#expect(result.standardErrorContent.contains("STANDARD ERROR".utf8.reversed()))
}

@Test("Arguments to the macro are not captured during expansion (do not need to be literals/const)")
func argumentsAreNotCapturedDuringMacroExpansion() async throws {
let unrelatedSourceLocation = #_sourceLocation
func nonConstExitCondition() async throws -> ExitCondition {
.failure
}
await #expect(exitsWith: try await nonConstExitCondition(), sourceLocation: unrelatedSourceLocation) {
fatalError()
}
}
}

// MARK: - Fixtures
Expand Down