Skip to content

Commit 440eced

Browse files
committed
Revert "Extend test/task cancellation support to test case evaluation"
This reverts commit 844bccb.
1 parent 844bccb commit 440eced

File tree

5 files changed

+49
-117
lines changed

5 files changed

+49
-117
lines changed

Sources/Testing/Issues/Issue+Recording.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ extension Issue {
186186
// This error is thrown by expectation checking functions to indicate a
187187
// condition evaluated to `false`. Those functions record their own issue,
188188
// so we don't need to record another one redundantly.
189-
} catch let error where SkipInfo(error) != nil {
189+
} catch is SkipInfo,
190+
is CancellationError where Task.isCancelled {
190191
// This error represents control flow rather than an issue, so we suppress
191192
// it here.
192193
} catch {
@@ -231,7 +232,8 @@ extension Issue {
231232
// This error is thrown by expectation checking functions to indicate a
232233
// condition evaluated to `false`. Those functions record their own issue,
233234
// so we don't need to record another one redundantly.
234-
} catch let error where SkipInfo(error) != nil {
235+
} catch is SkipInfo,
236+
is CancellationError where Task.isCancelled {
235237
// This error represents control flow rather than an issue, so we suppress
236238
// it here.
237239
} catch {

Sources/Testing/Running/Runner.Plan.swift

Lines changed: 45 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -201,75 +201,47 @@ extension Runner.Plan {
201201
/// - Parameters:
202202
/// - test: The test whose action will be determined.
203203
///
204-
/// - Returns:The action to take for `test`.
205-
private static func _determineAction(for test: inout Test) async -> Action {
206-
let result: Action
207-
204+
/// - Returns: A tuple containing the action to take for `test` as well as any
205+
/// error that was thrown during trait evaluation. If more than one error
206+
/// was thrown, the first-caught error is returned.
207+
private static func _determineAction(for test: Test) async -> (Action, (any Error)?) {
208208
// We use a task group here with a single child task so that, if the trait
209209
// code calls Test.cancel() we don't end up cancelling the entire test run.
210210
// We could also model this as an unstructured task except that they aren't
211211
// available in the "task-to-thread" concurrency model.
212212
//
213213
// FIXME: Parallelize this work. Calling `prepare(...)` on all traits and
214214
// evaluating all test arguments should be safely parallelizable.
215-
(test, result) = await withTaskGroup(returning: (Test, Action).self) { [test] taskGroup in
215+
await withTaskGroup(returning: (Action, (any Error)?).self) { taskGroup in
216216
taskGroup.addTask {
217-
var test = test
218217
var action = _runAction
218+
var firstCaughtError: (any Error)?
219219

220220
await Test.withCurrent(test) {
221-
do {
222-
var firstCaughtError: (any Error)?
223-
224-
for trait in test.traits {
225-
do {
226-
try await trait.prepare(for: test)
227-
} catch {
228-
if let skipInfo = SkipInfo(error) {
229-
action = .skip(skipInfo)
230-
break
231-
} else {
232-
// Only preserve the first caught error
233-
firstCaughtError = firstCaughtError ?? error
234-
}
235-
}
236-
}
237-
238-
// If no trait specified that the test should be skipped, but one
239-
// did throw an error, then the action is to record an issue for
240-
// that error.
241-
if case .run = action, let error = firstCaughtError {
242-
action = .recordIssue(Issue(for: error))
243-
}
244-
}
245-
246-
// If the test is still planned to run (i.e. nothing thus far has
247-
// caused it to be skipped), evaluate its test cases now.
248-
//
249-
// The argument expressions of each test are captured in closures so
250-
// they can be evaluated lazily only once it is determined that the
251-
// test will run, to avoid unnecessary work. But now is the
252-
// appropriate time to evaluate them.
253-
if case .run = action {
221+
for trait in test.traits {
254222
do {
255-
try await test.evaluateTestCases()
223+
try await trait.prepare(for: test)
224+
} catch let error as SkipInfo {
225+
action = .skip(error)
226+
break
227+
} catch is CancellationError where Task.isCancelled {
228+
// Synthesize skip info for this cancellation error.
229+
let sourceContext = SourceContext(backtrace: .current(), sourceLocation: nil)
230+
let skipInfo = SkipInfo(comment: nil, sourceContext: sourceContext)
231+
action = .skip(skipInfo)
232+
break
256233
} catch {
257-
if let skipInfo = SkipInfo(error) {
258-
action = .skip(skipInfo)
259-
} else {
260-
action = .recordIssue(Issue(for: error))
261-
}
234+
// Only preserve the first caught error
235+
firstCaughtError = firstCaughtError ?? error
262236
}
263237
}
264238
}
265239

266-
return (test, action)
240+
return (action, firstCaughtError)
267241
}
268242

269243
return await taskGroup.first { _ in true }!
270244
}
271-
272-
return result
273245
}
274246

275247
/// Construct a graph of runner plan steps for the specified tests.
@@ -337,12 +309,36 @@ extension Runner.Plan {
337309
return nil
338310
}
339311

312+
var action = runAction
313+
var firstCaughtError: (any Error)?
314+
340315
// Walk all the traits and tell each to prepare to run the test.
341316
// If any throw a `SkipInfo` error at this stage, stop walking further.
342317
// But if any throw another kind of error, keep track of the first error
343318
// but continue walking, because if any subsequent traits throw a
344319
// `SkipInfo`, the error should not be recorded.
345-
var action = await _determineAction(for: &test)
320+
(action, firstCaughtError) = await _determineAction(for: test)
321+
322+
// If no trait specified that the test should be skipped, but one did
323+
// throw an error, then the action is to record an issue for that error.
324+
if case .run = action, let error = firstCaughtError {
325+
action = .recordIssue(Issue(for: error))
326+
}
327+
328+
// If the test is still planned to run (i.e. nothing thus far has caused
329+
// it to be skipped), evaluate its test cases now.
330+
//
331+
// The argument expressions of each test are captured in closures so they
332+
// can be evaluated lazily only once it is determined that the test will
333+
// run, to avoid unnecessary work. But now is the appropriate time to
334+
// evaluate them.
335+
if case .run = action {
336+
do {
337+
try await test.evaluateTestCases()
338+
} catch {
339+
action = .recordIssue(Issue(for: error))
340+
}
341+
}
346342

347343
// If the test is parameterized but has no cases, mark it as skipped.
348344
if case .run = action, let testCases = test.testCases, testCases.first(where: { _ in true }) == nil {

Sources/Testing/Running/SkipInfo.swift

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,6 @@ extension SkipInfo: Equatable, Hashable {}
5454

5555
extension SkipInfo: Codable {}
5656

57-
// MARK: -
58-
59-
extension SkipInfo {
60-
/// Initialize an instance of this type from an arbitrary error.
61-
///
62-
/// - Parameters:
63-
/// - error: The error to convert to an instance of this type.
64-
///
65-
/// If `error` does not represent a skip or cancellation event, this
66-
/// initializer returns `nil`.
67-
init?(_ error: any Error) {
68-
if let skipInfo = error as? Self {
69-
self = skipInfo
70-
} else if error is CancellationError, Task.isCancelled {
71-
// Synthesize skip info for this cancellation error.
72-
let backtrace = Backtrace(forFirstThrowOf: error) ?? .current()
73-
let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: nil)
74-
self.init(comment: nil, sourceContext: sourceContext)
75-
} else {
76-
return nil
77-
}
78-
}
79-
}
80-
8157
// MARK: - Deprecated
8258

8359
extension SkipInfo {

Tests/TestingTests/TestCancellationTests.swift

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,6 @@
132132
}
133133
}
134134

135-
@Test func `Cancelling a test while evaluating test cases skips the test`() async {
136-
await testCancellation(testSkipped: 1) { configuration in
137-
await Test(arguments: { try await cancelledTestCases(cancelsTask: false) }) { _ in
138-
Issue.record("Recorded an issue!")
139-
}.run(configuration: configuration)
140-
}
141-
}
142-
143-
@Test func `Cancelling the current task while evaluating test cases skips the test`() async {
144-
await testCancellation(testSkipped: 1) { configuration in
145-
await Test(arguments: { try await cancelledTestCases(cancelsTask: true) }) { _ in
146-
Issue.record("Recorded an issue!")
147-
}.run(configuration: configuration)
148-
}
149-
}
150-
151135
#if !SWT_NO_EXIT_TESTS
152136
@Test func `Cancelling the current test from within an exit test`() async {
153137
await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in
@@ -235,15 +219,6 @@ struct CancelledTrait: TestTrait {
235219
}
236220
}
237221

238-
func cancelledTestCases(cancelsTask: Bool) async throws -> EmptyCollection<Int> {
239-
if cancelsTask {
240-
withUnsafeCurrentTask { $0?.cancel() }
241-
try Task.checkCancellation()
242-
}
243-
try Test.cancel("Cancelled from trait")
244-
}
245-
246-
247222
#if !SWT_NO_SNAPSHOT_TYPES
248223
struct `Shows as skipped in Xcode 16` {
249224
@Test func `Cancelled test`() throws {

Tests/TestingTests/TestSupport/TestingAdditions.swift

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -199,23 +199,6 @@ extension Test {
199199
self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters)
200200
}
201201

202-
init<C>(
203-
_ traits: any TestTrait...,
204-
arguments collection: @escaping @Sendable () async throws -> C,
205-
parameters: [Parameter] = [
206-
Parameter(index: 0, firstName: "x", type: C.Element.self),
207-
],
208-
sourceLocation: SourceLocation = #_sourceLocation,
209-
column: Int = #column,
210-
name: String = #function,
211-
testFunction: @escaping @Sendable (C.Element) async throws -> Void
212-
) where C: Collection & Sendable, C.Element: Sendable {
213-
let caseGenerator = { @Sendable in
214-
Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction)
215-
}
216-
self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters)
217-
}
218-
219202
/// Initialize an instance of this type with a function or closure to call,
220203
/// parameterized over two collections of values.
221204
///

0 commit comments

Comments
 (0)