@@ -82,9 +82,6 @@ extension Runner {
8282 private struct _Context : Sendable {
8383 /// A serializer used to reduce parallelism among test cases.
8484 var testCaseSerializer : Serializer < Void > ?
85-
86- /// Which iteration of the test plan is being executed.
87- var iteration : Int
8885 }
8986
9087 /// Apply the custom scope for any test scope providers of the traits
@@ -230,10 +227,10 @@ extension Runner {
230227 // Determine what kind of event to send for this step based on its action.
231228 switch step. action {
232229 case . run:
233- Event . post ( . testStarted, for: ( step. test, nil ) , iteration: context . iteration , configuration: configuration)
230+ Event . post ( . testStarted, for: ( step. test, nil ) , iteration: 1 , configuration: configuration)
234231 shouldSendTestEnded = true
235232 case let . skip( skipInfo) :
236- Event . post ( . testSkipped( skipInfo) , for: ( step. test, nil ) , iteration: context . iteration , configuration: configuration)
233+ Event . post ( . testSkipped( skipInfo) , for: ( step. test, nil ) , iteration: 1 , configuration: configuration)
237234 shouldSendTestEnded = false
238235 case let . recordIssue( issue) :
239236 // Scope posting the issue recorded event such that issue handling
@@ -257,7 +254,7 @@ extension Runner {
257254 defer {
258255 if let step = stepGraph. value {
259256 if shouldSendTestEnded {
260- Event . post ( . testEnded, for: ( step. test, nil ) , iteration: context . iteration , configuration: configuration)
257+ Event . post ( . testEnded, for: ( step. test, nil ) , iteration: 1 , configuration: configuration)
261258 }
262259 Event . post ( . planStepEnded( step) , for: ( step. test, nil ) , configuration: configuration)
263260 }
@@ -405,35 +402,81 @@ extension Runner {
405402 /// This function sets ``Test/Case/current``, then invokes the test case's
406403 /// body closure.
407404 private static func _runTestCase( _ testCase: Test . Case , within step: Plan . Step , context: _Context ) async {
408- let configuration = _configuration
405+ await _applyRepetitionPolicy ( test: step. test, testCase: testCase) { iteration in
406+ let configuration = _configuration
409407
410- Event . post ( . testCaseStarted, for: ( step. test, testCase) , iteration: context . iteration, configuration: configuration)
411- defer {
412- Event . post ( . testCaseEnded, for: ( step. test, testCase) , iteration: context . iteration, configuration: configuration)
413- }
408+ Event . post ( . testCaseStarted, for: ( step. test, testCase) , iteration: iteration, configuration: configuration)
409+ defer {
410+ Event . post ( . testCaseEnded, for: ( step. test, testCase) , iteration: iteration, configuration: configuration)
411+ }
414412
415- await Test . Case. withCurrent ( testCase) {
416- let sourceLocation = step. test. sourceLocation
417- await Issue . withErrorRecording ( at: sourceLocation, configuration: configuration) {
418- // Exit early if the task has already been cancelled.
419- 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 ( )
420418
421- try await withTimeLimit ( for: step. test, configuration: configuration) {
422- try await _applyScopingTraits ( for: step. test, testCase: testCase) {
423- 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( timeLimit: timeLimit) ,
426+ comments: [ ] ,
427+ sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation)
428+ )
429+ issue. record ( configuration: configuration)
424430 }
425- } timeoutHandler: { timeLimit in
426- let issue = Issue (
427- kind: . timeLimitExceeded( timeLimit: timeLimit) ,
428- comments: [ ] ,
429- sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation)
430- )
431- issue. record ( configuration: configuration)
432431 }
433432 }
434433 }
435434 }
436435
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: ( Int ) async -> Void
449+ ) async {
450+ var config = _configuration
451+
452+ for iteration in 1 ... config. repetitionPolicy. maximumIterationCount {
453+ let issueRecorded = Atomic ( false )
454+ config. eventHandler = { [ eventHandler = config. eventHandler] event, context in
455+ if case let . issueRecorded( issue) = event. kind, !issue. isKnown {
456+ issueRecorded. store ( true , ordering: . sequentiallyConsistent)
457+ }
458+ eventHandler ( event, context)
459+ }
460+
461+ await Configuration . withCurrent ( config) {
462+ await body ( iteration)
463+ }
464+
465+ // Determine if the test plan should iterate again.
466+ let shouldContinue = switch config. repetitionPolicy. continuationCondition {
467+ case nil :
468+ true
469+ case . untilIssueRecorded:
470+ !issueRecorded. load ( ordering: . sequentiallyConsistent)
471+ case . whileIssueRecorded:
472+ issueRecorded. load ( ordering: . sequentiallyConsistent)
473+ }
474+ guard shouldContinue else {
475+ break
476+ }
477+ }
478+ }
479+
437480 /// Run the tests in this runner's plan.
438481 public func run( ) async {
439482 await Self . _run ( self )
@@ -454,21 +497,12 @@ extension Runner {
454497#endif
455498 _ = Event . installFallbackEventHandler ( )
456499
457- // Track whether or not any issues were recorded across the entire run.
458- let issueRecorded = Atomic ( false )
459- runner. configuration. eventHandler = { [ eventHandler = runner. configuration. eventHandler] event, context in
460- if case let . issueRecorded( issue) = event. kind, !issue. isKnown {
461- issueRecorded. store ( true , ordering: . sequentiallyConsistent)
462- }
463- eventHandler ( event, context)
464- }
465-
466500 // Context to pass into the test run. We intentionally don't pass the Runner
467501 // itself (implicitly as `self` nor as an argument) because we don't want to
468502 // accidentally depend on e.g. the `configuration` property rather than the
469503 // current configuration.
470504 let context : _Context = {
471- var context = _Context ( iteration : 0 )
505+ var context = _Context ( )
472506
473507 let maximumParallelizationWidth = runner. configuration. maximumParallelizationWidth
474508 if maximumParallelizationWidth > 1 && maximumParallelizationWidth < . max {
@@ -492,44 +526,11 @@ extension Runner {
492526 Event . post ( . runEnded, for: ( nil , nil ) , configuration: runner. configuration)
493527 }
494528
495- let repetitionPolicy = runner. configuration. repetitionPolicy
496- let iterationCount = repetitionPolicy. maximumIterationCount
497- for iterationIndex in 0 ..< iterationCount {
498- Event . post ( . iterationStarted( iterationIndex) , for: ( nil , nil ) , configuration: runner. configuration)
499- defer {
500- Event . post ( . iterationEnded( iterationIndex) , for: ( nil , nil ) , configuration: runner. configuration)
529+ await withTaskGroup { [ runner] taskGroup in
530+ _ = taskGroup. addTaskUnlessCancelled ( name: decorateTaskName ( " test run " , withAction: nil ) ) {
531+ try ? await _runStep ( atRootOf: runner. plan. stepGraph, context: context)
501532 }
502-
503- await withTaskGroup { [ runner] taskGroup in
504- var taskAction : String ?
505- if iterationCount > 1 {
506- taskAction = " running iteration # \( iterationIndex + 1 ) "
507- }
508- _ = taskGroup. addTaskUnlessCancelled ( name: decorateTaskName ( " test run " , withAction: taskAction) ) {
509- var iterationContext = context
510- // `iteration` is one-indexed, so offset that here.
511- iterationContext. iteration = iterationIndex + 1
512- try ? await _runStep ( atRootOf: runner. plan. stepGraph, context: iterationContext)
513- }
514- await taskGroup. waitForAll ( )
515- }
516-
517- // Determine if the test plan should iterate again. (The iteration count
518- // is handled by the outer for-loop.)
519- let shouldContinue = switch repetitionPolicy. continuationCondition {
520- case nil :
521- true
522- case . untilIssueRecorded:
523- !issueRecorded. load ( ordering: . sequentiallyConsistent)
524- case . whileIssueRecorded:
525- issueRecorded. load ( ordering: . sequentiallyConsistent)
526- }
527- guard shouldContinue else {
528- break
529- }
530-
531- // Reset the run-wide "issue was recorded" flag for this iteration.
532- issueRecorded. store ( false , ordering: . sequentiallyConsistent)
533+ await taskGroup. waitForAll ( )
533534 }
534535 }
535536 }
0 commit comments