Skip to content

Commit

Permalink
Add test, ensure suites start/end appropriately
Browse files Browse the repository at this point in the history
  • Loading branch information
grynspan committed Jan 15, 2025
1 parent 9f26837 commit 2118d86
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 54 deletions.
80 changes: 32 additions & 48 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ extension Runner {
/// This enumeration conforms to `CaseIterable`, so callers can iterate over
/// all stages by looping over `Stage.allCases`. `Stage.allCases.first` and
/// `Stage.allCases.last` are respectively the first and last stages to run.
enum Stage: Sendable, CaseIterable {
///
/// The names of cases are meant to describe what happens during them (so as
/// to aid debugging.) Most code that uses test run stages doesn't need to
/// care about them in isolation, but rather looks at `Stage.allCases`,
/// ranges of stages, etc.
enum Stage: Sendable, Comparable, CaseIterable {
/// Tests that might run in parallel (globally or locally) are being run.
case parallelizationAllowed

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

/// The stage at which this step should be performed.
var stage: Stage = .default

/// Whether or not this step may perform work over multiple stages of a
/// test run.
var isMultistaged: Bool {
test.isSuite
}

/// Whether or not this step performs its first work in the given test
/// run stage.
///
/// - Parameters:
/// - stage: The stage of interest.
///
/// - Returns: Whether or not `stage` is the first stage in which this
/// step performs some work.
func starts(in stage: Stage) -> Bool {
let firstStage = if isMultistaged {
Stage.allCases.first
} else {
self.stage
}
return stage == firstStage
}

/// Whether or not this step performs its final work in the given test
/// run stage.
///
/// - Parameters:
/// - stage: The stage of interest.
///
/// - Returns: Whether or not `stage` is the last stage in which this
/// step performs some work.
func ends(in stage: Stage) -> Bool {
let lastStage = if isMultistaged {
Stage.allCases.last
} else {
self.stage
}
return stage == lastStage
}
/// The stages at which this step operates.
var stages: ClosedRange<Stage> = .default ... .default
}

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

/// Recursively widen the range of test run stages each (yet-to-be-created)
/// step in the specified graph will operate in.
///
/// - Parameters:
/// - graph: The graph in which test run stage ranges should be computed.
private static func _recursivelyComputeStageRanges(in graph: inout Graph<String, ClosedRange<Stage>>) {
var minStage = graph.value.lowerBound
var maxStage = graph.value.upperBound
for (key, var childGraph) in graph.children {
_recursivelyComputeStageRanges(in: &childGraph)
graph.children[key] = childGraph
minStage = min(minStage, childGraph.value.lowerBound)
maxStage = max(maxStage, childGraph.value.upperBound)
}
graph.value = minStage ... maxStage
}

/// Construct a graph of runner plan steps for the specified tests.
///
/// - Parameters:
Expand Down Expand Up @@ -379,25 +361,27 @@ extension Runner.Plan {
(action, recursivelyApply: action.isRecursive)
}

// Figure out what stage each test should operate in.
let stageGraph: Graph<String, Runner.Plan.Stage> = testGraph.mapValues { _, test in
switch test?.isGloballySerialized {
// Figure out what stages each step should operate in.
var stageGraph: Graph<String, ClosedRange<Stage>> = testGraph.mapValues { _, test in
let bound: Stage = switch test?.isGloballySerialized {
case nil:
.default
case .some(false):
.parallelizationAllowed
case .some(true):
.globallySerialized
}
return bound ... bound
}
_recursivelyComputeStageRanges(in: &stageGraph)

// Zip the tests, actions, and stages together and return them.
return zip(zip(testGraph, actionGraph), stageGraph).mapValues { _, tuple in
let test = tuple.0.0
let action = tuple.0.1
let stage = tuple.1
let stages = tuple.1
return test.map { test in
Step(test: test, action: action, stage: stage)
Step(test: test, action: action, stages: stages)
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ extension Runner {
let configuration = _configuration

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

// Determine what kind of event to send for this step based on its action.
Expand All @@ -177,15 +177,15 @@ extension Runner {
}
}
defer {
if let step = stepGraph.value, step.ends(in: stage) {
if let step = stepGraph.value, step.stages.upperBound == stage {
if case .run = step.action {
Event.post(.testEnded, for: (step.test, nil), configuration: configuration)
}
Event.post(.planStepEnded(step), for: (step.test, nil), configuration: configuration)
}
}

if let step = stepGraph.value, case .run = step.action, step.stage == stage {
if let step = stepGraph.value, case .run = step.action, step.stages.lowerBound == stage {
await Test.withCurrent(step.test) {
_ = await Issue.withErrorRecording(at: step.test.sourceLocation, configuration: configuration) {
try await _applyScopingTraits(for: step.test, testCase: nil) {
Expand All @@ -200,8 +200,8 @@ extension Runner {
}
}
} else {
// There is no test at this node in the graph, so just skip down to the
// child nodes.
// There is no test at this node in the graph, or the test at this node
// runs in another stage, so just skip down to the child nodes.
try await _runChildren(of: stepGraph, stage: stage)
}
}
Expand Down Expand Up @@ -232,7 +232,7 @@ extension Runner {
/// - Throws: Whatever is thrown from the test body. Thrown errors are
/// normally reported as test failures.
private static func _runChildren(of stepGraph: Graph<String, Plan.Step?>, stage: Plan.Stage) async throws {
let childGraphs = if _configuration.isParallelizationEnabled {
let childGraphs = if stage != .globallySerialized && _configuration.isParallelizationEnabled {
// Explicitly shuffle the steps to help detect accidental dependencies
// between tests due to their ordering.
Array(stepGraph.children)
Expand Down
40 changes: 40 additions & 0 deletions Tests/TestingTests/Traits/ParallelizationTraitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
//

@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
#if canImport(XCTest)
import XCTest
#endif

@Suite("Parallelization Trait Tests", .tags(.traitRelated))
struct ParallelizationTraitTests {
Expand Down Expand Up @@ -44,6 +47,43 @@ struct ParallelizationTraitTests {
}
}

#if canImport(XCTest)
final class ParallelizationTraitXCTests: XCTestCase {
// Implemented in XCTest so we can use enforceOrder (see #297)
func testSerializedGlobally() async {
var expectations = [XCTestExpectation]()

var sourceLocation = #_sourceLocation
let parallelizedRanFirst = expectation(description: "Parallelized tests ran first")
var tests: [Test] = [
Test(sourceLocation: sourceLocation) {
if #available(_clockAPI, *) {
try await Test.Clock.sleep(for: .nanoseconds(50_000_000))
}
parallelizedRanFirst.fulfill()
},
]
expectations.append(parallelizedRanFirst)

for i in 0 ..< 100 {
sourceLocation.line += 1
let serializedTestRan = expectation(description: "Globally serialized test #\(i) ran")
tests.append(
Test(.serialized(.globally), sourceLocation: sourceLocation) {
serializedTestRan.fulfill()
}
)
expectations.append(serializedTestRan)
}
let plan = await Runner.Plan(tests: tests, configuration: .init())
let runner = Runner(plan: plan, configuration: .init())
await runner.run()
await fulfillment(of: expectations, enforceOrder: true)
}

}
#endif

// MARK: - Fixtures

@Suite(.hidden, .serialized)
Expand Down

0 comments on commit 2118d86

Please sign in to comment.