Skip to content
Open
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
61 changes: 46 additions & 15 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -406,29 +406,60 @@ extension Runner.Plan {
_recursivelyReduceTraits(in: &testGraph)
#endif

// For each test value, determine the appropriate action for it.
testGraph = await testGraph.mapValues { keyPath, test in
// Skip any nil test, which implies this node is just a placeholder and
// not actual test content.
guard var test else {
return nil
}

// Walk all the traits and tell each to prepare to run the test.
// If any throw a `SkipInfo` error at this stage, stop walking further.
// But if any throw another kind of error, keep track of the first error
// but continue walking, because if any subsequent traits throw a
// `SkipInfo`, the error should not be recorded.
// Resolve a single test's action by preparing its traits and evaluating its
// test cases. Factored into a closure because it runs concurrently below, so
// it must not touch the shared `actionGraph`.
let resolveAction: @Sendable (Test) async -> (test: Test, action: Action) = { test in
var test = test
var action = await _determineAction(for: &test)

// If the test is parameterized but has no cases, mark it as skipped.
if case .run = action, let testCases = test.testCases, testCases.first(where: { _ in true }) == nil {
action = .skip(SkipInfo(comment: "No test cases found.", sourceContext: .init(backtrace: nil, sourceLocation: test.sourceLocation)))
}

actionGraph.updateValue(action, at: keyPath)
return (test, action)
}

return test
// Gather the non-placeholder tests paired with their key paths.
var keyPathsAndTests = [(keyPath: [String], test: Test)]()
testGraph.forEach { keyPath, test in
if let test {
keyPathsAndTests.append((keyPath, test))
}
}

// Resolving each test's action is independent, so run it concurrently. This is
// planning work that runs once before any test, so it honors `--no-parallel`
// but is not throttled by `maximumParallelizationWidth` (which bounds how many
// test *cases* run at once). Actions are collected, then applied serially below.
var resolvedTests = [[String]: (test: Test, action: Action)](minimumCapacity: keyPathsAndTests.count)
if configuration.isParallelizationEnabled {
resolvedTests = await withTaskGroup(of: (keyPath: [String], resolved: (test: Test, action: Action)).self) { taskGroup in
for (keyPath, test) in keyPathsAndTests {
taskGroup.addTask {
(keyPath: keyPath, resolved: await resolveAction(test))
}
}
var resolvedTests = [[String]: (test: Test, action: Action)](minimumCapacity: keyPathsAndTests.count)
for await (keyPath, resolved) in taskGroup {
resolvedTests[keyPath] = resolved
}
return resolvedTests
}
} else {
// Parallelization is disabled (e.g. `--no-parallel`): resolve serially.
for (keyPath, test) in keyPathsAndTests {
resolvedTests[keyPath] = await resolveAction(test)
}
}

// Apply the resolved tests and their actions back into the graphs.
testGraph = testGraph.mapValues { keyPath, test in
test != nil ? (resolvedTests[keyPath]?.test ?? test) : nil
}
for (keyPath, resolved) in resolvedTests {
actionGraph.updateValue(resolved.action, at: keyPath)
}

// Now that we have allowed all the traits to update their corresponding
Expand Down
19 changes: 19 additions & 0 deletions Tests/TestingTests/TestCancellationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@
}
}

@Test func `Cancelling one test while evaluating traits does not affect sibling tests`() async {
// Plan construction now resolves each test's action concurrently, so a test
// cancelling itself in a trait's `prepare(for:)` must not leak cancellation to
// the others being resolved at the same time.
await testCancellation(testSkipped: 1, issueRecorded: 7) { configuration in
var configuration = configuration
configuration.isParallelizationEnabled = true
var tests = [Test(CancelledTrait(), name: "Canceller") {
Issue.record("Cancelled test's body should not run")
}]
for i in 0 ..< 7 {
tests.append(Test(name: "Sibling\(i)") {
Issue.record("Sibling \(i) ran")
})
}
await Runner(testing: tests, configuration: configuration).run()
}
}

@Test func `Cancelling the current task while evaluating traits skips the test`() async {
await testCancellation(testSkipped: 1) { configuration in
await Test(CancelledTrait(cancelsTask: true)) {
Expand Down
Loading