Skip to content

Commit 2118d86

Browse files
committed
Add test, ensure suites start/end appropriately
1 parent 9f26837 commit 2118d86

File tree

3 files changed

+78
-54
lines changed

3 files changed

+78
-54
lines changed

Diff for: Sources/Testing/Running/Runner.Plan.swift

+32-48
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ extension Runner {
7878
/// This enumeration conforms to `CaseIterable`, so callers can iterate over
7979
/// all stages by looping over `Stage.allCases`. `Stage.allCases.first` and
8080
/// `Stage.allCases.last` are respectively the first and last stages to run.
81-
enum Stage: Sendable, CaseIterable {
81+
///
82+
/// The names of cases are meant to describe what happens during them (so as
83+
/// to aid debugging.) Most code that uses test run stages doesn't need to
84+
/// care about them in isolation, but rather looks at `Stage.allCases`,
85+
/// ranges of stages, etc.
86+
enum Stage: Sendable, Comparable, CaseIterable {
8287
/// Tests that might run in parallel (globally or locally) are being run.
8388
case parallelizationAllowed
8489

@@ -102,48 +107,8 @@ extension Runner {
102107
/// The action to perform with ``test``.
103108
public var action: Action
104109

105-
/// The stage at which this step should be performed.
106-
var stage: Stage = .default
107-
108-
/// Whether or not this step may perform work over multiple stages of a
109-
/// test run.
110-
var isMultistaged: Bool {
111-
test.isSuite
112-
}
113-
114-
/// Whether or not this step performs its first work in the given test
115-
/// run stage.
116-
///
117-
/// - Parameters:
118-
/// - stage: The stage of interest.
119-
///
120-
/// - Returns: Whether or not `stage` is the first stage in which this
121-
/// step performs some work.
122-
func starts(in stage: Stage) -> Bool {
123-
let firstStage = if isMultistaged {
124-
Stage.allCases.first
125-
} else {
126-
self.stage
127-
}
128-
return stage == firstStage
129-
}
130-
131-
/// Whether or not this step performs its final work in the given test
132-
/// run stage.
133-
///
134-
/// - Parameters:
135-
/// - stage: The stage of interest.
136-
///
137-
/// - Returns: Whether or not `stage` is the last stage in which this
138-
/// step performs some work.
139-
func ends(in stage: Stage) -> Bool {
140-
let lastStage = if isMultistaged {
141-
Stage.allCases.last
142-
} else {
143-
self.stage
144-
}
145-
return stage == lastStage
146-
}
110+
/// The stages at which this step operates.
111+
var stages: ClosedRange<Stage> = .default ... .default
147112
}
148113

149114
/// The graph of the steps in the runner plan.
@@ -254,6 +219,23 @@ extension Runner.Plan {
254219
synthesizeSuites(in: &graph, sourceLocation: &sourceLocation)
255220
}
256221

222+
/// Recursively widen the range of test run stages each (yet-to-be-created)
223+
/// step in the specified graph will operate in.
224+
///
225+
/// - Parameters:
226+
/// - graph: The graph in which test run stage ranges should be computed.
227+
private static func _recursivelyComputeStageRanges(in graph: inout Graph<String, ClosedRange<Stage>>) {
228+
var minStage = graph.value.lowerBound
229+
var maxStage = graph.value.upperBound
230+
for (key, var childGraph) in graph.children {
231+
_recursivelyComputeStageRanges(in: &childGraph)
232+
graph.children[key] = childGraph
233+
minStage = min(minStage, childGraph.value.lowerBound)
234+
maxStage = max(maxStage, childGraph.value.upperBound)
235+
}
236+
graph.value = minStage ... maxStage
237+
}
238+
257239
/// Construct a graph of runner plan steps for the specified tests.
258240
///
259241
/// - Parameters:
@@ -379,25 +361,27 @@ extension Runner.Plan {
379361
(action, recursivelyApply: action.isRecursive)
380362
}
381363

382-
// Figure out what stage each test should operate in.
383-
let stageGraph: Graph<String, Runner.Plan.Stage> = testGraph.mapValues { _, test in
384-
switch test?.isGloballySerialized {
364+
// Figure out what stages each step should operate in.
365+
var stageGraph: Graph<String, ClosedRange<Stage>> = testGraph.mapValues { _, test in
366+
let bound: Stage = switch test?.isGloballySerialized {
385367
case nil:
386368
.default
387369
case .some(false):
388370
.parallelizationAllowed
389371
case .some(true):
390372
.globallySerialized
391373
}
374+
return bound ... bound
392375
}
376+
_recursivelyComputeStageRanges(in: &stageGraph)
393377

394378
// Zip the tests, actions, and stages together and return them.
395379
return zip(zip(testGraph, actionGraph), stageGraph).mapValues { _, tuple in
396380
let test = tuple.0.0
397381
let action = tuple.0.1
398-
let stage = tuple.1
382+
let stages = tuple.1
399383
return test.map { test in
400-
Step(test: test, action: action, stage: stage)
384+
Step(test: test, action: action, stages: stages)
401385
}
402386
}
403387
}

Diff for: Sources/Testing/Running/Runner.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ extension Runner {
163163
let configuration = _configuration
164164

165165
// Determine what action to take for this step.
166-
if let step = stepGraph.value, step.starts(in: stage) {
166+
if let step = stepGraph.value, step.stages.lowerBound == stage {
167167
Event.post(.planStepStarted(step), for: (step.test, nil), configuration: configuration)
168168

169169
// Determine what kind of event to send for this step based on its action.
@@ -177,15 +177,15 @@ extension Runner {
177177
}
178178
}
179179
defer {
180-
if let step = stepGraph.value, step.ends(in: stage) {
180+
if let step = stepGraph.value, step.stages.upperBound == stage {
181181
if case .run = step.action {
182182
Event.post(.testEnded, for: (step.test, nil), configuration: configuration)
183183
}
184184
Event.post(.planStepEnded(step), for: (step.test, nil), configuration: configuration)
185185
}
186186
}
187187

188-
if let step = stepGraph.value, case .run = step.action, step.stage == stage {
188+
if let step = stepGraph.value, case .run = step.action, step.stages.lowerBound == stage {
189189
await Test.withCurrent(step.test) {
190190
_ = await Issue.withErrorRecording(at: step.test.sourceLocation, configuration: configuration) {
191191
try await _applyScopingTraits(for: step.test, testCase: nil) {
@@ -200,8 +200,8 @@ extension Runner {
200200
}
201201
}
202202
} else {
203-
// There is no test at this node in the graph, so just skip down to the
204-
// child nodes.
203+
// There is no test at this node in the graph, or the test at this node
204+
// runs in another stage, so just skip down to the child nodes.
205205
try await _runChildren(of: stepGraph, stage: stage)
206206
}
207207
}
@@ -232,7 +232,7 @@ extension Runner {
232232
/// - Throws: Whatever is thrown from the test body. Thrown errors are
233233
/// normally reported as test failures.
234234
private static func _runChildren(of stepGraph: Graph<String, Plan.Step?>, stage: Plan.Stage) async throws {
235-
let childGraphs = if _configuration.isParallelizationEnabled {
235+
let childGraphs = if stage != .globallySerialized && _configuration.isParallelizationEnabled {
236236
// Explicitly shuffle the steps to help detect accidental dependencies
237237
// between tests due to their ordering.
238238
Array(stepGraph.children)

Diff for: Tests/TestingTests/Traits/ParallelizationTraitTests.swift

+40
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
//
1010

1111
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
12+
#if canImport(XCTest)
13+
import XCTest
14+
#endif
1215

1316
@Suite("Parallelization Trait Tests", .tags(.traitRelated))
1417
struct ParallelizationTraitTests {
@@ -44,6 +47,43 @@ struct ParallelizationTraitTests {
4447
}
4548
}
4649

50+
#if canImport(XCTest)
51+
final class ParallelizationTraitXCTests: XCTestCase {
52+
// Implemented in XCTest so we can use enforceOrder (see #297)
53+
func testSerializedGlobally() async {
54+
var expectations = [XCTestExpectation]()
55+
56+
var sourceLocation = #_sourceLocation
57+
let parallelizedRanFirst = expectation(description: "Parallelized tests ran first")
58+
var tests: [Test] = [
59+
Test(sourceLocation: sourceLocation) {
60+
if #available(_clockAPI, *) {
61+
try await Test.Clock.sleep(for: .nanoseconds(50_000_000))
62+
}
63+
parallelizedRanFirst.fulfill()
64+
},
65+
]
66+
expectations.append(parallelizedRanFirst)
67+
68+
for i in 0 ..< 100 {
69+
sourceLocation.line += 1
70+
let serializedTestRan = expectation(description: "Globally serialized test #\(i) ran")
71+
tests.append(
72+
Test(.serialized(.globally), sourceLocation: sourceLocation) {
73+
serializedTestRan.fulfill()
74+
}
75+
)
76+
expectations.append(serializedTestRan)
77+
}
78+
let plan = await Runner.Plan(tests: tests, configuration: .init())
79+
let runner = Runner(plan: plan, configuration: .init())
80+
await runner.run()
81+
await fulfillment(of: expectations, enforceOrder: true)
82+
}
83+
84+
}
85+
#endif
86+
4787
// MARK: - Fixtures
4888

4989
@Suite(.hidden, .serialized)

0 commit comments

Comments
 (0)