Skip to content

Commit a9f0a15

Browse files
committed
Sink iteration down to a per-test-case iteration
1 parent b9e6319 commit a9f0a15

11 files changed

Lines changed: 307 additions & 85 deletions

Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,18 @@ extension ABI {
117117
case .testStarted:
118118
kind = .testStarted
119119
case .testCaseStarted:
120+
// For non-parameterized tests, we elide `testCaseStarted` calls because it would be
121+
// redundant. However, for multiple iterations of a test case within a non-parameterized
122+
// function, we need to emit another `testStarted` event.
120123
if eventContext.test?.isParameterized == false {
121-
return nil
124+
if let iteration = eventContext.iteration, iteration > 1 {
125+
kind = .testStarted
126+
} else {
127+
return nil
128+
}
129+
} else {
130+
kind = .testCaseStarted
122131
}
123-
kind = .testCaseStarted
124132
case let .issueRecorded(recordedIssue):
125133
kind = .issueRecorded
126134
issue = EncodedIssue(encoding: recordedIssue, in: eventContext)
@@ -129,9 +137,14 @@ extension ABI {
129137
self.attachment = EncodedAttachment(encoding: attachment)
130138
case .testCaseEnded:
131139
if eventContext.test?.isParameterized == false {
132-
return nil
140+
if let iteration = eventContext.iteration, iteration > 1 {
141+
kind = .testEnded
142+
} else {
143+
return nil
144+
}
145+
} else {
146+
kind = .testCaseEnded
133147
}
134-
kind = .testCaseEnded
135148
case .testCaseCancelled:
136149
kind = .testCaseCancelled
137150
case .testEnded:
@@ -164,6 +177,7 @@ extension ABI {
164177
let .testCancelled(skipInfo):
165178
_comments = Array(skipInfo.comment).map(\.rawValue)
166179
_sourceLocation = skipInfo.sourceLocation.map { EncodedSourceLocation(encoding: $0) }
180+
_iteration = eventContext.iteration
167181
default:
168182
break
169183
}

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ public struct __CommandLineArguments_v0: Sendable {
333333
/// The value of the `--repeat-until` argument.
334334
public var repeatUntil: String?
335335

336+
/// Whether or not to use the per-test-case repetition mode.
337+
var usePerTestCaseRepetition: Bool = false
338+
336339
/// The value of the `--attachments-path` argument.
337340
public var attachmentsPath: String?
338341
}
@@ -537,6 +540,9 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
537540
if let repeatUntil = args.argumentValue(forLabel: "--repeat-until") {
538541
result.repeatUntil = repeatUntil
539542
}
543+
if args.contains("--experimental-per-test-case-repetition") {
544+
result.usePerTestCaseRepetition = true
545+
}
540546

541547
return result
542548
}
@@ -665,6 +671,9 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr
665671
}
666672
configuration.repetitionPolicy = repetitionPolicy
667673

674+
// Opt in to per-test-case repetition
675+
configuration.shouldUseLegacyPlanLevelRepetition = !args.usePerTestCaseRepetition
676+
668677
#if !SWT_NO_EXIT_TESTS
669678
// Enable exit test handling via __swiftPMEntryPoint().
670679
configuration.exitTestHandler = ExitTest.handlerForEntryPoint()

Sources/Testing/Events/Event.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ public struct Event: Sendable {
238238
static func post(
239239
_ kind: Kind,
240240
for testAndTestCase: (Test?, Test.Case?) = currentTestAndTestCase(),
241-
iteration: Int? = nil,
242241
instant: Test.Clock.Instant = .now,
243242
configuration: Configuration? = nil
244243
) {
@@ -262,7 +261,12 @@ public struct Event: Sendable {
262261
}
263262
}
264263
let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant)
265-
let context = Event.Context(test: test, testCase: testCase, iteration: iteration, configuration: nil)
264+
let context = Event.Context(
265+
test: test,
266+
testCase: testCase,
267+
iteration: Test.currentIteration,
268+
configuration: nil
269+
)
266270
event._post(in: context, configuration: configuration)
267271
}
268272
}
@@ -327,8 +331,6 @@ extension Event {
327331
iteration: Int?,
328332
configuration: Configuration?
329333
) {
330-
// Ensure that if `iteration` is specified, the test is also specified.
331-
precondition(iteration == nil || (iteration != nil && test != nil))
332334
self.test = test
333335
self.testCase = testCase
334336
self.iteration = iteration

Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ extension Event.HumanReadableOutputRecorder {
365365
String(describing: TimeValue(rawValue: end.suspending.rawValue - start.suspending.rawValue))
366366
}
367367

368+
func testStartedMessage(for test: Test) -> String {
369+
"\(_capitalizedTitle(for: test)) \(testName) started"
370+
}
371+
368372
// Finally, produce any messages for the event.
369373
switch event.kind {
370374
case .testDiscovered:
@@ -425,7 +429,7 @@ extension Event.HumanReadableOutputRecorder {
425429
return [
426430
Message(
427431
symbol: .default,
428-
stringValue: "\(_capitalizedTitle(for: test)) \(testName) started."
432+
stringValue: "\(testStartedMessage(for: test))."
429433
)
430434
]
431435

@@ -565,15 +569,26 @@ extension Event.HumanReadableOutputRecorder {
565569
return result
566570

567571
case .testCaseStarted:
568-
guard let testCase, testCase.isParameterized, let arguments = testCase.arguments else {
572+
guard let test, let testCase else { break }
573+
let iteration = eventContext.iteration ?? 1
574+
575+
var message: String
576+
if testCase.isParameterized, let arguments = testCase.arguments {
577+
message = "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) started"
578+
} else if iteration > 1 {
579+
message = testStartedMessage(for: test)
580+
} else {
569581
break
570582
}
571583

584+
if iteration > 1 {
585+
message += " (repetition \(iteration))"
586+
}
587+
588+
message += "."
589+
572590
return [
573-
Message(
574-
symbol: .default,
575-
stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) started."
576-
)
591+
Message(symbol: .default, stringValue: message)
577592
]
578593

579594
case .testCaseEnded:

Sources/Testing/Running/Configuration.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ public struct Configuration: Sendable {
132132
}
133133
}
134134

135+
/// Whether to perform test repetition at the plan level or on a per-test-
136+
/// case basis.
137+
@_spi(Experimental)
138+
public var shouldUseLegacyPlanLevelRepetition: Bool = true
139+
135140
/// Whether or not, and how, to iterate the test plan repeatedly.
136141
///
137142
/// By default, the value of this property allows for a single iteration.

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 26 additions & 1 deletion
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, if any.
33+
var iteration: Int?
34+
3235
/// The runtime state related to the runner running on the current task,
3336
/// if any.
3437
@TaskLocal
@@ -198,7 +201,7 @@ extension Configuration {
198201
}
199202
}
200203

201-
// MARK: - Current test and test case
204+
// MARK: - Current test, test case, and iteration
202205

203206
extension Test {
204207
/// The test that is running on the current task, if any.
@@ -233,6 +236,28 @@ extension Test {
233236
try await test.withCancellationHandling(body)
234237
}
235238
}
239+
240+
/// The current iteration of the currently-running test case, if any.
241+
static var currentIteration: Int? {
242+
Runner.RuntimeState.current?.iteration
243+
}
244+
245+
/// Call a function while the value of ``Test/currentIteration`` is set.
246+
///
247+
/// - Parameters:
248+
/// - iteration: The new value to set for ``Test/currentIteration``.
249+
/// - body: A function to call.
250+
///
251+
/// - Returns: Whatever is returned by `body`.
252+
///
253+
/// - Throws: Whatever is thrown by `body`.
254+
static func withCurrentIteration<R>(_ iteration: Int?, perform body: () async throws -> R) async rethrows -> R {
255+
var runtimeState = Runner.RuntimeState.current ?? .init()
256+
runtimeState.iteration = iteration
257+
return try await Runner.RuntimeState.$current.withValue(runtimeState) {
258+
try await body()
259+
}
260+
}
236261
}
237262

238263
extension Test.Case {

0 commit comments

Comments
 (0)