Skip to content

Commit 801d5ea

Browse files
authored
Remove SPIAwareTrait and adopt TestScoping in its place (#901)
This is a follow-on from #733 which introduced the Test Scoping Traits feature. As that proposal indicated, the `SPIAwareTrait` SPI protocol is no longer necessary, so this PR removes it and adopts `TestScoping` in its place. ### Motivation: Adopting Test Scoping Traits for the purpose which `SPIAwareTrait` previously served simplifies a lot of runner and planning logic. ### Modifications: - Delete `SPIAwareTrait` and related SPI. - Adopt `TestScoping` in `ParallelizationTrait` instead. - Changed `ParallelizationTrait` to _not_ always have the value `true` for `isRecursive`. Test Scoping Traits makes this no longer necessary: the invocation of children of any suites which have `.serialized` is scoped to have parallelization disabled. - Removed tracking of `isParallelizationEnabled` on the `Runner.Plan.Action.RunOptions` type. For now, I kept the `RunOptions` struct, since it could be useful in the future and it's SPI. - Removed obsolete logic in `Runner` for propagating ancestor steps, and refactored slightly to make many things `static`. This is to help ensure certain functions don't access the `self.configuration` property when instead they should be accessing `Configuration.current`. ### Result: No functional change, but implementation is simplified. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 4005676 commit 801d5ea

File tree

6 files changed

+57
-136
lines changed

6 files changed

+57
-136
lines changed

Sources/Testing/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ add_library(Testing
9696
Traits/ConditionTrait+Macro.swift
9797
Traits/HiddenTrait.swift
9898
Traits/ParallelizationTrait.swift
99-
Traits/SPIAwareTrait.swift
10099
Traits/Tags/Tag.Color.swift
101100
Traits/Tags/Tag.Color+Loading.swift
102101
Traits/Tags/Tag.List.swift

Sources/Testing/Running/Runner.Plan.swift

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ extension Runner {
2828
/// ## See Also
2929
///
3030
/// - ``ParallelizationTrait``
31-
public var isParallelizationEnabled: Bool
31+
@available(*, deprecated, message: "The 'isParallelizationEnabled' property is deprecated and no longer used. Its value is always false.")
32+
public var isParallelizationEnabled: Bool {
33+
get {
34+
false
35+
}
36+
set {}
37+
}
3238
}
3339

3440
/// The test should be run.
@@ -65,19 +71,6 @@ extension Runner {
6571
return true
6672
}
6773
}
68-
69-
/// Whether or not this action enables parallelization.
70-
///
71-
/// If this action is of case ``run(options:)``, the value of this
72-
/// property equals the value of its associated
73-
/// ``RunOptions/isParallelizationEnabled`` property. Otherwise, the value
74-
/// of this property is `nil`.
75-
var isParallelizationEnabled: Bool? {
76-
if case let .run(options) = self {
77-
return options.isParallelizationEnabled
78-
}
79-
return nil
80-
}
8174
}
8275

8376
/// A type describing a step in a runner plan.
@@ -218,7 +211,7 @@ extension Runner.Plan {
218211
// Convert the list of test into a graph of steps. The actions for these
219212
// steps will all be .run() *unless* an error was thrown while examining
220213
// them, in which case it will be .recordIssue().
221-
let runAction = Action.run(options: .init(isParallelizationEnabled: configuration.isParallelizationEnabled))
214+
let runAction = Action.run(options: .init())
222215
var testGraph = Graph<String, Test?>()
223216
var actionGraph = Graph<String, Action>(value: runAction)
224217
for test in tests {
@@ -278,11 +271,7 @@ extension Runner.Plan {
278271
// `SkipInfo`, the error should not be recorded.
279272
for trait in test.traits {
280273
do {
281-
if let trait = trait as? any SPIAwareTrait {
282-
try await trait.prepare(for: test, action: &action)
283-
} else {
284-
try await trait.prepare(for: test)
285-
}
274+
try await trait.prepare(for: test)
286275
} catch let error as SkipInfo {
287276
action = .skip(error)
288277
break

Sources/Testing/Running/Runner.swift

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ public struct Runner: Sendable {
5656
// MARK: - Running tests
5757

5858
extension Runner {
59+
/// The current configuration _while_ running.
60+
///
61+
/// This should be used from the functions in this extension which access the
62+
/// current configuration. This is important since individual tests or suites
63+
/// may have traits which customize the execution scope of their children,
64+
/// including potentially modifying the current configuration.
65+
private static var _configuration: Configuration {
66+
.current ?? .init()
67+
}
68+
5969
/// Apply the custom scope for any test scope providers of the traits
6070
/// associated with a specified test by calling their
6171
/// ``TestScoping/provideScope(for:testCase:performing:)`` function.
@@ -70,7 +80,7 @@ extension Runner {
7080
///
7181
/// - Throws: Whatever is thrown by `body` or by any of the
7282
/// ``TestScoping/provideScope(for:testCase:performing:)`` function calls.
73-
private func _applyScopingTraits(
83+
private static func _applyScopingTraits(
7484
for test: Test,
7585
testCase: Test.Case?,
7686
_ body: @escaping @Sendable () async throws -> Void
@@ -103,21 +113,13 @@ extension Runner {
103113
///
104114
/// - Parameters:
105115
/// - sequence: The sequence to enumerate.
106-
/// - step: The plan step that controls parallelization. If `nil`, or if its
107-
/// ``Runner/Plan/Step/action`` property is not of case
108-
/// ``Runner/Plan/Action/run(options:)``, the
109-
/// ``Configuration/isParallelizationEnabled`` property of this runner's
110-
/// ``configuration`` property is used instead to determine if
111-
/// parallelization is enabled.
112116
/// - body: The function to invoke.
113117
///
114118
/// - Throws: Whatever is thrown by `body`.
115-
private func _forEach<E>(
119+
private static func _forEach<E>(
116120
in sequence: some Sequence<E>,
117-
for step: Plan.Step?,
118121
_ body: @Sendable @escaping (E) async throws -> Void
119122
) async throws where E: Sendable {
120-
let isParallelizationEnabled = step?.action.isParallelizationEnabled ?? configuration.isParallelizationEnabled
121123
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
122124
for element in sequence {
123125
// Each element gets its own subtask to run in.
@@ -126,7 +128,7 @@ extension Runner {
126128
}
127129

128130
// If not parallelizing, wait after each task.
129-
if !isParallelizationEnabled {
131+
if !_configuration.isParallelizationEnabled {
130132
try await taskGroup.waitForAll()
131133
}
132134
}
@@ -137,12 +139,6 @@ extension Runner {
137139
///
138140
/// - Parameters:
139141
/// - stepGraph: The subgraph whose root value, a step, is to be run.
140-
/// - depth: How deep into the step graph this call is. The first call has a
141-
/// depth of `0`.
142-
/// - lastAncestorStep: The last-known ancestral step, if any, of the step
143-
/// at the root of `stepGraph`. The options in this step (if its action is
144-
/// of case ``Runner/Plan/Action/run(options:)``) inform the execution of
145-
/// `stepGraph`.
146142
///
147143
/// - Throws: Whatever is thrown from the test body. Thrown errors are
148144
/// normally reported as test failures.
@@ -157,7 +153,7 @@ extension Runner {
157153
/// ## See Also
158154
///
159155
/// - ``Runner/run()``
160-
private func _runStep(atRootOf stepGraph: Graph<String, Plan.Step?>, depth: Int, lastAncestorStep: Plan.Step?) async throws {
156+
private static func _runStep(atRootOf stepGraph: Graph<String, Plan.Step?>) async throws {
161157
// Exit early if the task has already been cancelled.
162158
try Task.checkCancellation()
163159

@@ -166,6 +162,8 @@ extension Runner {
166162
// example, a skip event only sends `.testSkipped`.
167163
let shouldSendTestEnded: Bool
168164

165+
let configuration = _configuration
166+
169167
// Determine what action to take for this step.
170168
if let step = stepGraph.value {
171169
Event.post(.planStepStarted(step), for: (step.test, nil), configuration: configuration)
@@ -204,14 +202,14 @@ extension Runner {
204202
}
205203

206204
// Run the children of this test (i.e. the tests in this suite.)
207-
try await _runChildren(of: stepGraph, depth: depth, lastAncestorStep: lastAncestorStep)
205+
try await _runChildren(of: stepGraph)
208206
}
209207
}
210208
}
211209
} else {
212210
// There is no test at this node in the graph, so just skip down to the
213211
// child nodes.
214-
try await _runChildren(of: stepGraph, depth: depth, lastAncestorStep: lastAncestorStep)
212+
try await _runChildren(of: stepGraph)
215213
}
216214
}
217215

@@ -222,7 +220,7 @@ extension Runner {
222220
///
223221
/// - Returns: The source location of the root node of `stepGraph`, or of the
224222
/// first descendant node thereof (sorted by source location.)
225-
private func _sourceLocation(of stepGraph: Graph<String, Plan.Step?>) -> SourceLocation? {
223+
private static func _sourceLocation(of stepGraph: Graph<String, Plan.Step?>) -> SourceLocation? {
226224
if let result = stepGraph.value?.test.sourceLocation {
227225
return result
228226
}
@@ -234,26 +232,13 @@ extension Runner {
234232
/// Recursively run the tests that are children of a given plan step.
235233
///
236234
/// - Parameters:
237-
/// - stepGraph: The subgraph whose root value, a step, is to be run.
238-
/// - depth: How deep into the step graph this call is. The first call has a
239-
/// depth of `0`.
240-
/// - lastAncestorStep: The last-known ancestral step, if any, of the step
241-
/// at the root of `stepGraph`. The options in this step (if its action is
242-
/// of case ``Runner/Plan/Action/run(options:)``) inform the execution of
243-
/// `stepGraph`.
235+
/// - stepGraph: The subgraph whose root value, a step, will be used to
236+
/// find children to run.
244237
///
245238
/// - Throws: Whatever is thrown from the test body. Thrown errors are
246239
/// normally reported as test failures.
247-
private func _runChildren(of stepGraph: Graph<String, Plan.Step?>, depth: Int, lastAncestorStep: Plan.Step?) async throws {
248-
// Figure out the last-good step, either the one at the root of `stepGraph`
249-
// or, if it is nil, the one passed into this function. We need to track
250-
// this value in case we run into sparse sections of the graph so we don't
251-
// lose track of the recursive `isParallelizationEnabled` property in the
252-
// runnable steps' options.
253-
let stepOrAncestor = stepGraph.value ?? lastAncestorStep
254-
255-
let isParallelizationEnabled = stepOrAncestor?.action.isParallelizationEnabled ?? configuration.isParallelizationEnabled
256-
let childGraphs = if isParallelizationEnabled {
240+
private static func _runChildren(of stepGraph: Graph<String, Plan.Step?>) async throws {
241+
let childGraphs = if _configuration.isParallelizationEnabled {
257242
// Explicitly shuffle the steps to help detect accidental dependencies
258243
// between tests due to their ordering.
259244
Array(stepGraph.children)
@@ -282,8 +267,8 @@ extension Runner {
282267
}
283268

284269
// Run the child nodes.
285-
try await _forEach(in: childGraphs, for: stepOrAncestor) { _, childGraph in
286-
try await _runStep(atRootOf: childGraph, depth: depth + 1, lastAncestorStep: stepOrAncestor)
270+
try await _forEach(in: childGraphs) { _, childGraph in
271+
try await _runStep(atRootOf: childGraph)
287272
}
288273
}
289274

@@ -298,13 +283,14 @@ extension Runner {
298283
///
299284
/// If parallelization is supported and enabled, the generated test cases will
300285
/// be run in parallel using a task group.
301-
private func _runTestCases(_ testCases: some Sequence<Test.Case>, within step: Plan.Step) async throws {
286+
private static func _runTestCases(_ testCases: some Sequence<Test.Case>, within step: Plan.Step) async throws {
302287
// Apply the configuration's test case filter.
288+
let testCaseFilter = _configuration.testCaseFilter
303289
let testCases = testCases.lazy.filter { testCase in
304-
configuration.testCaseFilter(testCase, step.test)
290+
testCaseFilter(testCase, step.test)
305291
}
306292

307-
try await _forEach(in: testCases, for: step) { testCase in
293+
try await _forEach(in: testCases) { testCase in
308294
try await _runTestCase(testCase, within: step)
309295
}
310296
}
@@ -320,10 +306,12 @@ extension Runner {
320306
///
321307
/// This function sets ``Test/Case/current``, then invokes the test case's
322308
/// body closure.
323-
private func _runTestCase(_ testCase: Test.Case, within step: Plan.Step) async throws {
309+
private static func _runTestCase(_ testCase: Test.Case, within step: Plan.Step) async throws {
324310
// Exit early if the task has already been cancelled.
325311
try Task.checkCancellation()
326312

313+
let configuration = _configuration
314+
327315
Event.post(.testCaseStarted, for: (step.test, testCase), configuration: configuration)
328316
defer {
329317
Event.post(.testCaseEnded, for: (step.test, testCase), configuration: configuration)
@@ -357,9 +345,6 @@ extension Runner {
357345
///
358346
/// - Parameters:
359347
/// - runner: The runner to run.
360-
/// - configuration: The configuration to use for running. The value of this
361-
/// argument temporarily replaces the value of `runner`'s
362-
/// ``Runner/configuration`` property.
363348
///
364349
/// This function is `static` so that it cannot accidentally reference `self`
365350
/// or `self.configuration` when it should use a modified copy of either.
@@ -399,7 +384,7 @@ extension Runner {
399384

400385
await withTaskGroup(of: Void.self) { [runner] taskGroup in
401386
_ = taskGroup.addTaskUnlessCancelled {
402-
try? await runner._runStep(atRootOf: runner.plan.stepGraph, depth: 0, lastAncestorStep: nil)
387+
try? await _runStep(atRootOf: runner.plan.stepGraph)
403388
}
404389
await taskGroup.waitForAll()
405390
}

Sources/Testing/Traits/ParallelizationTrait.swift

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,32 @@
1111
/// A type that affects whether or not a test or suite is parallelized.
1212
///
1313
/// When added to a parameterized test function, this trait causes that test to
14-
/// run its cases serially instead of in parallel. When applied to a
15-
/// non-parameterized test function, this trait has no effect. When applied to a
16-
/// test suite, this trait causes that suite to run its contained test functions
17-
/// and sub-suites serially instead of in parallel.
14+
/// run its cases serially instead of in parallel. When added to a
15+
/// non-parameterized test function, this trait has no effect.
1816
///
19-
/// This trait is recursively applied: if it is applied to a suite, any
20-
/// parameterized tests or test suites contained in that suite are also
21-
/// serialized (as are any tests contained in those suites, and so on.)
17+
/// When added to a test suite, this trait causes that suite to run its
18+
/// contained test functions (including their cases, when parameterized) and
19+
/// sub-suites serially instead of in parallel. Any children of sub-suites are
20+
/// also run serially.
2221
///
2322
/// This trait does not affect the execution of a test relative to its peers or
2423
/// to unrelated tests. This trait has no effect if test parallelization is
2524
/// globally disabled (by, for example, passing `--no-parallel` to the
2625
/// `swift test` command.)
2726
///
2827
/// To add this trait to a test, use ``Trait/serialized``.
29-
public struct ParallelizationTrait: TestTrait, SuiteTrait {
30-
public var isRecursive: Bool {
31-
true
32-
}
33-
}
28+
public struct ParallelizationTrait: TestTrait, SuiteTrait {}
3429

35-
// MARK: - SPIAwareTrait
30+
// MARK: - TestScoping
3631

37-
@_spi(ForToolsIntegrationOnly)
38-
extension ParallelizationTrait: SPIAwareTrait {
39-
public func prepare(for test: Test, action: inout Runner.Plan.Action) async throws {
40-
if case var .run(options) = action {
41-
options.isParallelizationEnabled = false
42-
action = .run(options: options)
32+
extension ParallelizationTrait: TestScoping {
33+
public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws {
34+
guard var configuration = Configuration.current else {
35+
throw SystemError(description: "There is no current Configuration when attempting to provide scope for test '\(test.name)'. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
4336
}
37+
38+
configuration.isParallelizationEnabled = false
39+
try await Configuration.withCurrent(configuration, perform: function)
4440
}
4541
}
4642

Sources/Testing/Traits/SPIAwareTrait.swift

Lines changed: 0 additions & 38 deletions
This file was deleted.

Tests/TestingTests/Traits/ParallelizationTraitTests.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,6 @@
1212

1313
@Suite("Parallelization Trait Tests", .tags(.traitRelated))
1414
struct ParallelizationTraitTests {
15-
@Test(".serialized trait is recursively applied")
16-
func serializedTrait() async {
17-
var configuration = Configuration()
18-
configuration.isParallelizationEnabled = true
19-
let plan = await Runner.Plan(selecting: OuterSuite.self, configuration: configuration)
20-
for step in plan.steps {
21-
#expect(step.action.isParallelizationEnabled == false, "Step \(step) should have had parallelization disabled")
22-
}
23-
}
24-
2515
@Test(".serialized trait serializes parameterized test")
2616
func serializesParameterizedTestFunction() async {
2717
var configuration = Configuration()

0 commit comments

Comments
 (0)