Skip to content
Draft
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
103 changes: 103 additions & 0 deletions Sources/Testing/Plan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

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

@_spi(Experimental)
public struct Plan: Sendable {
/// Traits to apply to all tests when they are run.
public var traits: [any Trait] = []
}

// MARK: - Result building

@_spi(Experimental)
extension Plan {
@_documentation(visibility: private)
@resultBuilder
public struct Builder {
public static func buildPartialBlock(first: Global) -> Plan {
buildPartialBlock(accumulated: Plan(), next: first)
}

public static func buildPartialBlock(accumulated: Plan, next: Global) -> Plan {
var result = accumulated
switch next.kind {
case let .trait(trait):
result.traits.append(trait)
}
return result
}
}

public init(@Builder _ planBuilder: @escaping @Sendable () -> Self) {
self = planBuilder()
}
}

@_spi(Experimental)
public struct Global: Sendable {
fileprivate enum Kind: Sendable {
case trait(any SuiteTrait)
}

fileprivate var kind: Kind

public init(_ trait: some SuiteTrait) {
kind = .trait(trait)
}
}

// MARK: - Macro

@_spi(Experimental)
@freestanding(declaration, names: named(__testingPlan)) public macro Plan(
@Plan.Builder _ planBuilder: @escaping @Sendable () -> Plan
) = #externalMacro(module: "TestingMacros", type: "PlanMacro")

extension Plan {
fileprivate struct Generator: DiscoverableAsTestContent {
static var testContentKind: _TestDiscovery.TestContentKind {
"plan"
}

var buildPlan: @Sendable () async -> Plan
}

static var shared: Self {
get async {
var result: [Generator] = Generator.allTestContentRecords().lazy
.compactMap { $0.load() }

#if compiler(<6.3)
if result.isEmpty {
result = Generator.allTypeMetadataBasedTestContentRecords().lazy
.compactMap { $0.load() }
}
#endif

return await result.first?.buildPlan() ?? Plan()
}
}

@safe public static func __store(
_ plan: @escaping @Sendable () async -> Plan,
into outValue: UnsafeMutableRawPointer,
asTypeAt typeAddress: UnsafeRawPointer
) -> CBool {
#if !hasFeature(Embedded)
guard typeAddress.load(as: Any.Type.self) == Generator.self else {
return false
}
#endif
outValue.initializeMemory(as: Generator.self, to: .init(buildPlan: plan))
return true
}
}
13 changes: 8 additions & 5 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ extension Runner.Plan {
/// The traits in `testGraph.value?.traits` are added to each node in
/// `testGraph`, and then this function is called recursively on each child
/// node.
private static func _recursivelyApplyTraits(_ parentTraits: [any SuiteTrait] = [], to testGraph: inout Graph<String, Test?>) {
private static func _recursivelyApplyTraits(_ parentTraits: [any SuiteTrait], to testGraph: inout Graph<String, Test?>) {
let traits: [any SuiteTrait] = parentTraits + (testGraph.value?.traits ?? []).lazy
.compactMap { $0 as? any SuiteTrait }
.filter(\.isRecursive)
Expand Down Expand Up @@ -286,7 +286,7 @@ extension Runner.Plan {
/// - configuration: The configuration to use for planning.
///
/// - Returns: A graph of the steps corresponding to `tests`.
private static func _constructStepGraph(from tests: some Sequence<Test>, configuration: Configuration) async -> Graph<String, Step?> {
private static func _constructStepGraph(from tests: some Sequence<Test>, configuration: Configuration, globalTraits: [any Trait]) async -> Graph<String, Step?> {
// Ensure that we are capturing backtraces for errors before we start
// expecting to see them.
Backtrace.startCachingForThrownErrors()
Expand Down Expand Up @@ -334,7 +334,7 @@ extension Runner.Plan {
// correctly evaluate the filter. It's also more efficient, since it avoids
// needlessly applying non-filtering related traits to tests which might be
// filtered out.
_recursivelyApplyTraits(to: &testGraph)
_recursivelyApplyTraits([], to: &testGraph)

// For each test value, determine the appropriate action for it.
testGraph = await testGraph.mapValues { keyPath, test in
Expand Down Expand Up @@ -382,7 +382,7 @@ extension Runner.Plan {
///
/// This function produces a new runner plan for the provided tests.
public init(tests: some Sequence<Test>, configuration: Configuration) async {
let stepGraph = await Self._constructStepGraph(from: tests, configuration: configuration)
let stepGraph = await Self._constructStepGraph(from: tests, configuration: configuration, globalTraits: [])
self.init(stepGraph: stepGraph)
}

Expand All @@ -392,7 +392,10 @@ extension Runner.Plan {
/// - Parameters:
/// - configuration: The configuration to use for planning.
public init(configuration: Configuration) async {
await self.init(tests: Test.all, configuration: configuration)
let tests = await Test.all
let globalTraits = await Testing.Plan.shared.traits
let stepGraph = await Self._constructStepGraph(from: tests, configuration: configuration, globalTraits: globalTraits)
self.init(stepGraph: stepGraph)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/Testing/Traits/Trait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ public protocol SuiteTrait: Trait {
var isRecursive: Bool { get }
}

// MARK: -

extension Trait {
public func prepare(for test: Test) async throws {}

Expand Down
94 changes: 94 additions & 0 deletions Sources/TestingMacros/PlanMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

import SwiftDiagnostics
public import SwiftSyntax
import SwiftSyntaxBuilder
public import SwiftSyntaxMacros

struct PlanMacroError: Error, CustomStringConvertible {
var description: String
}

public struct PlanMacro: DeclarationMacro, Sendable {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard context.lexicalContext.isEmpty else {
throw PlanMacroError(description: "Must be at file root")
}

guard let closureExpr = node.trailingClosure?.trimmed else {
throw PlanMacroError(description: "Must have a result builder closure")
}

let filePath = context.location(of: node, at: .afterLeadingTrivia, filePathMode: .filePath)
.map(\.file)
.flatMap { $0.as(StringLiteralExprSyntax.self) }
.flatMap(\.representedLiteralValue)
if let filePath {
#if os(Windows)
let slashIndex = filePath.lastIndex(of: #"\"#)
#else
let slashIndex = filePath.lastIndex(of: "/")
#endif
if let slashIndex {
let fileName = filePath[slashIndex...].dropFirst()
guard fileName.lowercased() == "testplan.swift" else {
throw PlanMacroError(description: "Test plan must be declared in a file named 'TestPlan.swift', not '\(fileName)'")
}
}
}

var result = [DeclSyntax]()

result.append(
"""
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private let __testingPlan = Testing.Plan \(closureExpr)
"""
)

let testContentRecordName = context.makeUniqueName("")
result.append(
makeTestContentRecordDecl(
named: testContentRecordName,
in: nil,
ofKind: .testPlan,
accessingWith: """
{ outValue, type, _, _ in
Testing.Plan.__store({ __testingPlan }, into: outValue, asTypeAt: type)
}
""",
context: 0,
in: context
)
)

#if compiler(<6.3)
// Emit a type that contains a reference to the test content record.
let enumName = context.makeUniqueName("__🟡$")
result.append(
"""
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName): Testing.__TestContentRecordContainer {
nonisolated static var __testContentRecord: Testing.__TestContentRecord6_2 {
unsafe \(testContentRecordName)
}
}
"""
)
#endif

return result
}

}
3 changes: 3 additions & 0 deletions Sources/TestingMacros/Support/TestContentGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ enum TestContentKind: UInt32 {
/// An exit test.
case exitTest = 0x65786974

/// A global trait or set of traits.
case testPlan = 0x706C616E

/// This kind value as a comment (`/* 'abcd' */`) if it looks like it might be
/// a [FourCC](https://en.wikipedia.org/wiki/FourCC) value, or `nil` if not.
var commentRepresentation: Trivia {
Expand Down
1 change: 1 addition & 0 deletions Sources/TestingMacros/TestingMacrosMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct TestingMacrosMain: CompilerPlugin {
TagMacro.self,
SourceLocationMacro.self,
PragmaMacro.self,
PlanMacro.self,
]
}
}
Expand Down
1 change: 1 addition & 0 deletions Tests/TestingTests/IssueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,7 @@ final class IssueTests: XCTestCase {
XCTAssertFalse(issue.isKnown)
XCTAssertNotNil(event.testID)
XCTAssertEqual(event.testID, test.id)
dump(issue)
issueRecorded.fulfill()
}

Expand Down
16 changes: 16 additions & 0 deletions Tests/TestingTests/TestPlan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

@_spi(Experimental) import Testing

#Plan {
Global(.enabled(if: true))
Global(.serialized)
}
2 changes: 1 addition & 1 deletion Tests/TestingTests/ZipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

@testable import Testing
@testable @_spi(Experimental) import Testing

@Suite("zip Tests")
struct ZipTests {
Expand Down
Loading