Skip to content

Commit c44c50b

Browse files
committed
Move currentIteration to its own tasklocal
1 parent a006f5d commit c44c50b

6 files changed

Lines changed: 197 additions & 96 deletions

File tree

Sources/Testing/Events/Event.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,12 @@ public struct Event: Sendable {
261261
}
262262
}
263263
let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant)
264-
let context = Event.Context(test: test, testCase: testCase, iteration: testCase?.iteration, configuration: nil)
264+
let context = Event.Context(
265+
test: test,
266+
testCase: testCase,
267+
iteration: Test.currentIteration,
268+
configuration: nil
269+
)
265270
event._post(in: context, configuration: configuration)
266271
}
267272
}
@@ -326,8 +331,6 @@ extension Event {
326331
iteration: Int?,
327332
configuration: Configuration?
328333
) {
329-
// Ensure that if `iteration` is specified, the test is also specified.
330-
precondition(iteration == nil || (iteration != nil && test != nil))
331334
self.test = test
332335
self.testCase = testCase
333336
self.iteration = iteration

Sources/Testing/Parameterization/Test.Case.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,6 @@ extension Test {
228228
}
229229
}
230230

231-
/// The current iteration of this test case.
232-
public var iteration: Int?
233-
234231
/// The body closure of this test case.
235232
///
236233
/// Do not invoke this closure directly. Always use a ``Runner`` to invoke a

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ extension Runner {
2929
/// The test case that is running on the current task, if any.
3030
var testCase: Test.Case?
3131

32+
/// The current iteration of the test repetition policy.
33+
var iteration: Int?
34+
3235
/// The runtime state related to the runner running on the current task,
3336
/// if any.
3437
@TaskLocal
@@ -233,6 +236,27 @@ extension Test {
233236
try await test.withCancellationHandling(body)
234237
}
235238
}
239+
240+
static var currentIteration: Int? {
241+
Runner.RuntimeState.current?.iteration
242+
}
243+
244+
/// Call a function while the value of ``Test/currentIteration`` is set.
245+
///
246+
/// - Parameters:
247+
/// - iteration: The new value to set for ``Test/currentIteration``.
248+
/// - body: A function to call.
249+
///
250+
/// - Returns: Whatever is returned by `body`.
251+
///
252+
/// - Throws: Whatever is thrown by `body`.
253+
static func withCurrentIteration<R>(_ iteration: Int?, perform body: () async throws -> R) async rethrows -> R {
254+
var runtimeState = Runner.RuntimeState.current ?? .init()
255+
runtimeState.iteration = iteration
256+
return try await Runner.RuntimeState.$current.withValue(runtimeState) {
257+
try await body()
258+
}
259+
}
236260
}
237261

238262
extension Test.Case {

Sources/Testing/Running/Runner.swift

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
/// The current plan-level iteration (if using legacy plan-level iteration behavior).
87-
var planLevelIteration: Int?
8885
}
8986

9087
/// Apply the custom scope for any test scope providers of the traits
@@ -408,21 +405,16 @@ extension Runner {
408405
within step: Plan.Step,
409406
context: _Context
410407
) async {
411-
if let iteration = context.planLevelIteration {
412-
var testCase = testCase
413-
testCase.iteration = iteration
408+
if _configuration.shouldUseLegacyPlanLevelRepetition {
414409
await _runSingleTestCaseIteration(testCase, within: step)
415410
} else {
416-
await _applyRepetitionPolicy { iteration in
417-
var testCase = testCase
418-
testCase.iteration = iteration
411+
await _applyRepetitionPolicy {
419412
await _runSingleTestCaseIteration(testCase, within: step)
420413
}
421414
}
422415
}
423416

424-
/// Run a single iteration of a test case. The test case's ``Test/Case/iteration`` must be set when
425-
/// this is called.
417+
/// Run a single iteration of a test case.
426418
///
427419
/// - Parameters:
428420
/// - testCase: The test case to run.
@@ -431,7 +423,6 @@ extension Runner {
431423
/// This function sets ``Test/Case/current``, then invokes the test case's
432424
/// body closure.
433425
private static func _runSingleTestCaseIteration(_ testCase: Test.Case, within step: Plan.Step) async {
434-
precondition(testCase.iteration != nil)
435426
let configuration = _configuration
436427

437428
Event.post(.testCaseStarted, for: (step.test, testCase), configuration: configuration)
@@ -471,21 +462,22 @@ extension Runner {
471462
///
472463
/// - Note: This function updates ``Configuration/current`` before invoking the test body.
473464
private static func _applyRepetitionPolicy(
474-
perform body: (Int) async -> Void
465+
perform body: () async -> Void
475466
) async {
476-
var config = _configuration
477-
478-
for iteration in 1...config.repetitionPolicy.maximumIterationCount {
467+
for iteration in 1..._configuration.repetitionPolicy.maximumIterationCount {
479468
let issueRecorded = Atomic(false)
469+
var config = _configuration
480470
config.eventHandler = { [eventHandler = config.eventHandler] event, context in
481471
if case let .issueRecorded(issue) = event.kind, !issue.isKnown {
482472
issueRecorded.store(true, ordering: .sequentiallyConsistent)
483473
}
484474
eventHandler(event, context)
485475
}
486476

487-
await Configuration.withCurrent(config) {
488-
await body(iteration)
477+
await Test.withCurrentIteration(iteration) {
478+
await Configuration.withCurrent(config) {
479+
await body()
480+
}
489481
}
490482

491483
// Determine if the test plan should iterate again.
@@ -553,9 +545,15 @@ extension Runner {
553545
}
554546

555547
if runner.configuration.shouldUseLegacyPlanLevelRepetition {
556-
await _applyRepetitionPolicy { [runner] iteration in
557-
var context = context
558-
context.planLevelIteration = iteration
548+
await _applyRepetitionPolicy { [runner] in
549+
let iteration = Test.currentIteration ?? 1
550+
551+
// Legacy clients expect these values to be zero-indexed.
552+
let iterationIndex = iteration - 1
553+
Event.post(.iterationStarted(iterationIndex), configuration: runner.configuration)
554+
defer {
555+
Event.post(.iterationEnded(iterationIndex), configuration: runner.configuration)
556+
}
559557
await runner.runAll(context: context)
560558
}
561559
} else {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
12+
13+
#if !SWT_TARGET_OS_APPLE && canImport(Synchronization)
14+
import Synchronization
15+
#endif
16+
17+
@Suite
18+
struct LegacyPlanIterationTests {
19+
@Test("One iteration (default behavior)")
20+
func oneIteration() async {
21+
await confirmation("N iterations started") { started in
22+
await confirmation("N iterations ended") { ended in
23+
var configuration = Configuration()
24+
configuration.eventHandler = { event, _ in
25+
if case .iterationStarted = event.kind {
26+
started()
27+
} else if case .iterationEnded = event.kind {
28+
ended()
29+
}
30+
}
31+
configuration.repetitionPolicy = .once
32+
33+
await Test {
34+
}.run(configuration: configuration)
35+
}
36+
}
37+
}
38+
39+
@Test("Unconditional iteration")
40+
func unconditionalIteration() async {
41+
let iterationCount = 10
42+
await confirmation("N iterations started", expectedCount: iterationCount) { started in
43+
await confirmation("N iterations ended", expectedCount: iterationCount) { ended in
44+
var configuration = Configuration()
45+
configuration.eventHandler = { event, _ in
46+
if case .iterationStarted = event.kind {
47+
started()
48+
} else if case .iterationEnded = event.kind {
49+
ended()
50+
}
51+
}
52+
configuration.repetitionPolicy = .repeating(maximumIterationCount: iterationCount)
53+
54+
await Test {
55+
if Bool.random() {
56+
#expect(Bool(false))
57+
}
58+
}.run(configuration: configuration)
59+
}
60+
}
61+
}
62+
63+
@Test("Iteration until issue recorded")
64+
func iterationUntilIssueRecorded() async {
65+
let iterationIndex = Atomic(0)
66+
let iterationCount = 10
67+
let iterationWithIssue = 5
68+
await confirmation("N iterations started", expectedCount: iterationWithIssue + 1) { started in
69+
await confirmation("N iterations ended", expectedCount: iterationWithIssue + 1) { ended in
70+
var configuration = Configuration()
71+
configuration.eventHandler = { event, _ in
72+
if case let .iterationStarted(index) = event.kind {
73+
iterationIndex.store(index, ordering: .sequentiallyConsistent)
74+
started()
75+
} else if case .iterationEnded = event.kind {
76+
ended()
77+
}
78+
}
79+
configuration.repetitionPolicy = .repeating(.untilIssueRecorded, maximumIterationCount: iterationCount)
80+
81+
await Test {
82+
let iterationIndex = iterationIndex.load(ordering: .sequentiallyConsistent)
83+
if iterationIndex == iterationWithIssue {
84+
#expect(Bool(false))
85+
}
86+
}.run(configuration: configuration)
87+
}
88+
}
89+
}
90+
91+
@Test("Iteration while issue recorded")
92+
func iterationWhileIssueRecorded() async {
93+
let iterationIndex = Atomic(0)
94+
let iterationCount = 10
95+
let iterationWithoutIssue = 5
96+
await confirmation("N iterations started", expectedCount: iterationWithoutIssue + 1) { started in
97+
await confirmation("N iterations ended", expectedCount: iterationWithoutIssue + 1) { ended in
98+
var configuration = Configuration()
99+
configuration.eventHandler = { event, _ in
100+
if case let .iterationStarted(index) = event.kind {
101+
iterationIndex.store(index, ordering: .sequentiallyConsistent)
102+
started()
103+
} else if case .iterationEnded = event.kind {
104+
ended()
105+
}
106+
}
107+
configuration.repetitionPolicy = .repeating(.whileIssueRecorded, maximumIterationCount: iterationCount)
108+
109+
await Test {
110+
let iterationIndex = iterationIndex.load(ordering: .sequentiallyConsistent)
111+
if iterationIndex < iterationWithoutIssue {
112+
#expect(Bool(false))
113+
}
114+
}.run(configuration: configuration)
115+
}
116+
}
117+
}
118+
119+
#if !SWT_NO_EXIT_TESTS
120+
@Test("Iteration count must be positive")
121+
func positiveIterationCount() async {
122+
await #expect(processExitsWith: .failure) {
123+
var configuration = Configuration()
124+
configuration.repetitionPolicy.maximumIterationCount = 0
125+
}
126+
await #expect(processExitsWith: .failure) {
127+
var configuration = Configuration()
128+
configuration.repetitionPolicy.maximumIterationCount = -1
129+
}
130+
}
131+
#endif
132+
}

0 commit comments

Comments
 (0)