@@ -402,35 +402,87 @@ extension Runner {
402402 /// This function sets ``Test/Case/current``, then invokes the test case's
403403 /// body closure.
404404 private static func _runTestCase( _ testCase: Test . Case , within step: Plan . Step , context: _Context ) async {
405- let configuration = _configuration
405+ await _applyRepetitionPolicy ( test: step. test, testCase: testCase) {
406+ let configuration = _configuration
406407
407- Event . post ( . testCaseStarted, for: ( step. test, testCase) , configuration: configuration)
408- defer {
409- Event . post ( . testCaseEnded, for: ( step. test, testCase) , configuration: configuration)
410- }
408+ Event . post ( . testCaseStarted, for: ( step. test, testCase) , configuration: configuration)
409+ defer {
410+ Event . post ( . testCaseEnded, for: ( step. test, testCase) , configuration: configuration)
411+ }
411412
412- await Test . Case. withCurrent ( testCase) {
413- let sourceLocation = step. test. sourceLocation
414- await Issue . withErrorRecording ( at: sourceLocation, configuration: configuration) {
415- // Exit early if the task has already been cancelled.
416- try Task . checkCancellation ( )
413+ await Test . Case. withCurrent ( testCase) {
414+ let sourceLocation = step. test. sourceLocation
415+ await Issue . withErrorRecording ( at: sourceLocation, configuration: configuration) {
416+ // Exit early if the task has already been cancelled.
417+ try Task . checkCancellation ( )
417418
418- try await withTimeLimit ( for: step. test, configuration: configuration) {
419- try await _applyScopingTraits ( for: step. test, testCase: testCase) {
420- try await testCase. body ( )
419+ try await withTimeLimit ( for: step. test, configuration: configuration) {
420+ try await _applyScopingTraits ( for: step. test, testCase: testCase) {
421+ try await testCase. body ( )
422+ }
423+ } timeoutHandler: { timeLimit in
424+ let issue = Issue (
425+ kind: . timeLimitExceeded( timeLimitComponents: timeLimit) ,
426+ comments: [ ] ,
427+ sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation)
428+ )
429+ issue. record ( configuration: configuration)
421430 }
422- } timeoutHandler: { timeLimit in
423- let issue = Issue (
424- kind: . timeLimitExceeded( timeLimitComponents: timeLimit) ,
425- comments: [ ] ,
426- sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation)
427- )
428- issue. record ( configuration: configuration)
429431 }
430432 }
431433 }
432434 }
433435
436+ /// Applies the repetition policy specified in the current configuration by running the provided test case
437+ /// repeatedly until the continuation condition is satisfied.
438+ ///
439+ /// - Parameters:
440+ /// - test: The test being executed.
441+ /// - testCase: The test case being iterated.
442+ /// - body: The actual body of the function which must ultimately call into the test function.
443+ ///
444+ /// - Note: This function updates ``Configuration/current`` before invoking the test body.
445+ private static func _applyRepetitionPolicy(
446+ test: Test ,
447+ testCase: Test . Case ,
448+ perform body: ( ) async -> Void
449+ ) async {
450+ var config = _configuration
451+
452+ for i in 0 ..< config. repetitionPolicy. maximumIterationCount {
453+ let issueRecorded = Mutex ( false )
454+ config. eventHandler = { [ eventHandler = config. eventHandler] event, context in
455+ if case let . issueRecorded( issue) = event. kind, !issue. isKnown {
456+ issueRecorded. withLock { issueRecorded in
457+ issueRecorded = true
458+ }
459+ }
460+ eventHandler ( event, context)
461+ }
462+
463+ await Configuration . withCurrent ( config) {
464+ Event . post ( . iterationStarted( i) , for: ( test, testCase) )
465+ defer {
466+ Event . post ( . iterationEnded( i) , for: ( test, testCase) )
467+ }
468+ await body ( )
469+ }
470+
471+ // Determine if the test plan should iterate again.
472+ let shouldContinue = switch config. repetitionPolicy. continuationCondition {
473+ case nil :
474+ true
475+ case . untilIssueRecorded:
476+ !issueRecorded. rawValue
477+ case . whileIssueRecorded:
478+ issueRecorded. rawValue
479+ }
480+ guard shouldContinue else {
481+ break
482+ }
483+ }
484+ }
485+
434486 /// Run the tests in this runner's plan.
435487 public func run( ) async {
436488 await Self . _run ( self )
@@ -450,17 +502,6 @@ extension Runner {
450502 runner. configureAttachmentHandling ( )
451503#endif
452504
453- // Track whether or not any issues were recorded across the entire run.
454- let issueRecorded = Mutex ( false )
455- runner. configuration. eventHandler = { [ eventHandler = runner. configuration. eventHandler] event, context in
456- if case let . issueRecorded( issue) = event. kind, !issue. isKnown {
457- issueRecorded. withLock { issueRecorded in
458- issueRecorded = true
459- }
460- }
461- eventHandler ( event, context)
462- }
463-
464505 // Context to pass into the test run. We intentionally don't pass the Runner
465506 // itself (implicitly as `self` nor as an argument) because we don't want to
466507 // accidentally depend on e.g. the `configuration` property rather than the
@@ -490,43 +531,11 @@ extension Runner {
490531 Event . post ( . runEnded, for: ( nil , nil ) , configuration: runner. configuration)
491532 }
492533
493- let repetitionPolicy = runner. configuration. repetitionPolicy
494- let iterationCount = repetitionPolicy. maximumIterationCount
495- for iterationIndex in 0 ..< iterationCount {
496- Event . post ( . iterationStarted( iterationIndex) , for: ( nil , nil ) , configuration: runner. configuration)
497- defer {
498- Event . post ( . iterationEnded( iterationIndex) , for: ( nil , nil ) , configuration: runner. configuration)
499- }
500-
501- await withTaskGroup { [ runner] taskGroup in
502- var taskAction : String ?
503- if iterationCount > 1 {
504- taskAction = " running iteration # \( iterationIndex + 1 ) "
505- }
506- _ = taskGroup. addTaskUnlessCancelled ( name: decorateTaskName ( " test run " , withAction: taskAction) ) {
507- try ? await _runStep ( atRootOf: runner. plan. stepGraph, context: context)
508- }
509- await taskGroup. waitForAll ( )
510- }
511-
512- // Determine if the test plan should iterate again. (The iteration count
513- // is handled by the outer for-loop.)
514- let shouldContinue = switch repetitionPolicy. continuationCondition {
515- case nil :
516- true
517- case . untilIssueRecorded:
518- !issueRecorded. rawValue
519- case . whileIssueRecorded:
520- issueRecorded. rawValue
521- }
522- guard shouldContinue else {
523- break
524- }
525-
526- // Reset the run-wide "issue was recorded" flag for this iteration.
527- issueRecorded. withLock { issueRecorded in
528- issueRecorded = false
534+ await withTaskGroup { [ runner] taskGroup in
535+ _ = taskGroup. addTaskUnlessCancelled ( name: decorateTaskName ( " test run " , withAction: nil ) ) {
536+ try ? await _runStep ( atRootOf: runner. plan. stepGraph, context: context)
529537 }
538+ await taskGroup. waitForAll ( )
530539 }
531540 }
532541 }
0 commit comments