@@ -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 {
0 commit comments