@@ -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 ?
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
@@ -405,31 +402,79 @@ 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+ }
412+
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 ( )
414418
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 ( )
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)
430+ }
431+ }
432+ }
433+ }
434+ }
420435
421- try await withTimeLimit ( for: step. test, configuration: configuration) {
422- try await _applyScopingTraits ( for: step. test, testCase: testCase) {
423- try await testCase. body ( )
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 = 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
424458 }
425- } timeoutHandler: { timeLimit in
426- let issue = Issue (
427- kind: . timeLimitExceeded( timeLimitComponents: timeLimit) ,
428- comments: [ ] ,
429- sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation)
430- )
431- issue. record ( configuration: configuration)
432459 }
460+ eventHandler ( event, context)
461+ }
462+
463+ await Configuration . withCurrent ( config) {
464+ await body ( iteration)
465+ }
466+
467+ // Determine if the test plan should iterate again.
468+ let shouldContinue = switch config. repetitionPolicy. continuationCondition {
469+ case nil :
470+ true
471+ case . untilIssueRecorded:
472+ !issueRecorded. rawValue
473+ case . whileIssueRecorded:
474+ issueRecorded. rawValue
475+ }
476+ guard shouldContinue else {
477+ break
433478 }
434479 }
435480 }
@@ -453,23 +498,12 @@ extension Runner {
453498 runner. configureAttachmentHandling ( )
454499#endif
455500
456- // Track whether or not any issues were recorded across the entire run.
457- let issueRecorded = Mutex ( false )
458- runner. configuration. eventHandler = { [ eventHandler = runner. configuration. eventHandler] event, context in
459- if case let . issueRecorded( issue) = event. kind, !issue. isKnown {
460- issueRecorded. withLock { issueRecorded in
461- issueRecorded = true
462- }
463- }
464- eventHandler ( event, context)
465- }
466-
467501 // Context to pass into the test run. We intentionally don't pass the Runner
468502 // itself (implicitly as `self` nor as an argument) because we don't want to
469503 // accidentally depend on e.g. the `configuration` property rather than the
470504 // current configuration.
471505 let context : _Context = {
472- var context = _Context ( iteration : 0 )
506+ var context = _Context ( )
473507
474508 let maximumParallelizationWidth = runner. configuration. maximumParallelizationWidth
475509 if maximumParallelizationWidth > 1 && maximumParallelizationWidth < . max {
@@ -493,46 +527,11 @@ extension Runner {
493527 Event . post ( . runEnded, for: ( nil , nil ) , configuration: runner. configuration)
494528 }
495529
496- let repetitionPolicy = runner. configuration. repetitionPolicy
497- let iterationCount = repetitionPolicy. maximumIterationCount
498- for iterationIndex in 0 ..< iterationCount {
499- Event . post ( . iterationStarted( iterationIndex) , for: ( nil , nil ) , configuration: runner. configuration)
500- defer {
501- Event . post ( . iterationEnded( iterationIndex) , for: ( nil , nil ) , configuration: runner. configuration)
502- }
503-
504- await withTaskGroup { [ runner] taskGroup in
505- var taskAction : String ?
506- if iterationCount > 1 {
507- taskAction = " running iteration # \( iterationIndex + 1 ) "
508- }
509- _ = taskGroup. addTaskUnlessCancelled ( name: decorateTaskName ( " test run " , withAction: taskAction) ) {
510- var iterationContext = context
511- // `iteration` is one-indexed, so offset that here.
512- iterationContext. iteration = iterationIndex + 1
513- try ? await _runStep ( atRootOf: runner. plan. stepGraph, context: iterationContext)
514- }
515- await taskGroup. waitForAll ( )
516- }
517-
518- // Determine if the test plan should iterate again. (The iteration count
519- // is handled by the outer for-loop.)
520- let shouldContinue = switch repetitionPolicy. continuationCondition {
521- case nil :
522- true
523- case . untilIssueRecorded:
524- !issueRecorded. rawValue
525- case . whileIssueRecorded:
526- issueRecorded. rawValue
527- }
528- guard shouldContinue else {
529- break
530- }
531-
532- // Reset the run-wide "issue was recorded" flag for this iteration.
533- issueRecorded. withLock { issueRecorded in
534- issueRecorded = false
530+ await withTaskGroup { [ runner] taskGroup in
531+ _ = taskGroup. addTaskUnlessCancelled ( name: decorateTaskName ( " test run " , withAction: nil ) ) {
532+ try ? await _runStep ( atRootOf: runner. plan. stepGraph, context: context)
535533 }
534+ await taskGroup. waitForAll ( )
536535 }
537536 }
538537 }
0 commit comments