Skip to content

Commit fda67e8

Browse files
committed
Change the semantics of Test.cancel().
This PR changes the semantics of `Test.cancel()` such that it only cancels the current test case if the current test is parameterized. This PR also removes `Test.Case.cancel()`, but we may opt to add `Test.Case.cancelAll()` or some such in the future to re-add an interface that cancels an entire parameterized test.
1 parent d8b63e6 commit fda67e8

File tree

4 files changed

+52
-133
lines changed

4 files changed

+52
-133
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ extension ExitTest {
762762
}
763763
configuration.eventHandler = { event, eventContext in
764764
switch event.kind {
765-
case .issueRecorded, .valueAttached, .testCancelled, .testCaseCancelled:
765+
case .issueRecorded, .valueAttached, .testCancelled:
766766
eventHandler(event, eventContext)
767767
default:
768768
// Don't forward other kinds of event.
@@ -1070,8 +1070,6 @@ extension ExitTest {
10701070
Attachment.record(attachment, sourceLocation: event._sourceLocation!)
10711071
} else if case .testCancelled = event.kind {
10721072
_ = try? Test.cancel(with: skipInfo)
1073-
} else if case .testCaseCancelled = event.kind {
1074-
_ = try? Test.Case.cancel(with: skipInfo)
10751073
}
10761074
}
10771075

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ extension Test {
206206
static func withCurrent<R>(_ test: Self, perform body: () async throws -> R) async rethrows -> R {
207207
var runtimeState = Runner.RuntimeState.current ?? .init()
208208
runtimeState.test = test
209+
runtimeState.testCase = nil
209210
return try await Runner.RuntimeState.$current.withValue(runtimeState) {
210211
try await test.withCancellationHandling(body)
211212
}

Sources/Testing/Test+Cancellation.swift

Lines changed: 49 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,6 @@
1313
/// This protocol is used to abstract away the common implementation of test and
1414
/// test case cancellation.
1515
protocol TestCancellable: Sendable {
16-
/// Cancel the current instance of this type.
17-
///
18-
/// - Parameters:
19-
/// - skipInfo: Information about the cancellation event.
20-
///
21-
/// - Throws: An error indicating that the current instance of this type has
22-
/// been cancelled.
23-
///
24-
/// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a
25-
/// different signature and accepts a source location rather than a source
26-
/// context value.
27-
static func cancel(with skipInfo: SkipInfo) throws -> Never
28-
2916
/// Make an instance of ``Event/Kind`` appropriate for an instance of this
3017
/// type.
3118
///
@@ -108,9 +95,8 @@ extension TestCancellable {
10895
} onCancel: {
10996
// The current task was cancelled, so cancel the test case or test
11097
// associated with it.
111-
11298
let skipInfo = _currentSkipInfo ?? SkipInfo(sourceContext: SourceContext(backtrace: .current(), sourceLocation: nil))
113-
_ = try? Self.cancel(with: skipInfo)
99+
_ = try? Test.cancel(with: skipInfo)
114100
}
115101
}
116102
}
@@ -125,9 +111,7 @@ extension TestCancellable {
125111
/// is set and we need fallback handling.
126112
/// - testAndTestCase: The test and test case to use when posting an event.
127113
/// - skipInfo: Information about the cancellation event.
128-
///
129-
/// - Throws: An instance of ``SkipInfo`` describing the cancellation.
130-
private func _cancel<T>(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) throws -> Never where T: TestCancellable {
114+
private func _cancel<T>(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) where T: TestCancellable {
131115
if cancellableValue != nil {
132116
// If the current test case is still running, take its task property (which
133117
// signals to subsequent callers that it has been cancelled.)
@@ -171,26 +155,25 @@ private func _cancel<T>(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes
171155
issue.record()
172156
}
173157
}
174-
175-
throw skipInfo
176158
}
177159

178160
// MARK: - Test cancellation
179161

180162
extension Test: TestCancellable {
181-
/// Cancel the current test.
163+
/// Cancel the current test or test case.
182164
///
183165
/// - Parameters:
184-
/// - comment: A comment describing why you are cancelling the test.
166+
/// - comment: A comment describing why you are cancelling the test or test
167+
/// case.
185168
/// - sourceLocation: The source location to which the testing library will
186169
/// attribute the cancellation.
187170
///
188-
/// - Throws: An error indicating that the current test case has been
171+
/// - Throws: An error indicating that the current test or test case has been
189172
/// cancelled.
190173
///
191-
/// The testing library runs each test in its own task. When you call this
192-
/// function, the testing library cancels the task associated with the current
193-
/// test:
174+
/// The testing library runs each test and each test case in its own task.
175+
/// When you call this function, the testing library cancels the task
176+
/// associated with the current test:
194177
///
195178
/// ```swift
196179
/// @Test func `Food truck is well-stocked`() throws {
@@ -201,11 +184,17 @@ extension Test: TestCancellable {
201184
/// }
202185
/// ```
203186
///
204-
/// If the current test is parameterized, all of its pending and running test
205-
/// cases are cancelled. If the current test is a suite, all of its pending
206-
/// and running tests are cancelled. If you have already cancelled the current
207-
/// test or if it has already finished running, this function throws an error
208-
/// but does not attempt to cancel the test a second time.
187+
/// If the current test is a parameterized test function, this function
188+
/// instead cancels the current test case. Other test cases in the test
189+
/// function are not affected.
190+
///
191+
/// If the current test is a suite, the testing library cancels all of its
192+
/// pending and running tests.
193+
///
194+
/// If you have already cancelled the current test or if it has already
195+
/// finished running, this function throws an error to indicate that the
196+
/// current test has been cancelled, but does not attempt to cancel the test a
197+
/// second time.
209198
///
210199
/// @Comment {
211200
/// TODO: Document the interaction between an exit test and test
@@ -217,89 +206,53 @@ extension Test: TestCancellable {
217206
/// - Important: If the current task is not associated with a test (for
218207
/// example, because it was created with [`Task.detached(name:priority:operation:)`](https://developer.apple.com/documentation/swift/task/detached(name:priority:operation:)-795w1))
219208
/// this function records an issue and cancels the current task.
220-
///
221-
/// To cancel the current test case but leave other test cases of the current
222-
/// test alone, call ``Test/Case/cancel(_:sourceLocation:)`` instead.
223209
@_spi(Experimental)
224210
public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never {
225211
let skipInfo = SkipInfo(comment: comment, sourceContext: SourceContext(backtrace: nil, sourceLocation: sourceLocation))
226212
try Self.cancel(with: skipInfo)
227213
}
228214

229-
static func cancel(with skipInfo: SkipInfo) throws -> Never {
230-
let test = Test.current
231-
try _cancel(test, for: (test, nil), skipInfo: skipInfo)
232-
}
233-
234-
static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind {
235-
.testCancelled(skipInfo)
236-
}
237-
}
238-
239-
// MARK: - Test case cancellation
240-
241-
extension Test.Case: TestCancellable {
242-
/// Cancel the current test case.
215+
/// Cancel the current test or test case.
243216
///
244217
/// - Parameters:
245-
/// - comment: A comment describing why you are cancelling the test case.
246-
/// - sourceLocation: The source location to which the testing library will
247-
/// attribute the cancellation.
218+
/// - skipInfo: Information about the cancellation event.
248219
///
249-
/// - Throws: An error indicating that the current test case has been
220+
/// - Throws: An error indicating that the current test or test case has been
250221
/// cancelled.
251222
///
252-
/// The testing library runs each test case of a test in its own task. When
253-
/// you call this function, the testing library cancels the task associated
254-
/// with the current test case:
255-
///
256-
/// ```swift
257-
/// @Test(arguments: [Food.burger, .fries, .iceCream])
258-
/// func `Food truck is well-stocked`(_ food: Food) throws {
259-
/// if food == .iceCream && Season.current == .winter {
260-
/// try Test.Case.cancel("It's too cold for ice cream.")
261-
/// }
262-
/// // ...
263-
/// }
264-
/// ```
265-
///
266-
/// If the current test is parameterized, the test's other test cases continue
267-
/// running. If the current test case has already been cancelled, this
268-
/// function throws an error but does not attempt to cancel the test case a
269-
/// second time.
270-
///
271-
/// @Comment {
272-
/// TODO: Document the interaction between an exit test and test
273-
/// cancellation. In particular, the error thrown by this function isn't
274-
/// thrown into the parent process and task cancellation doesn't propagate
275-
/// (because the exit test _de facto_ runs in a detached task.)
276-
/// }
277-
///
278-
/// - Important: If the current task is not associated with a test case (for
279-
/// example, because it was created with [`Task.detached(name:priority:operation:)`](https://developer.apple.com/documentation/swift/task/detached(name:priority:operation:)-795w1))
280-
/// this function records an issue and cancels the current task.
281-
///
282-
/// To cancel all test cases in the current test, call
283-
/// ``Test/cancel(_:sourceLocation:)`` instead.
284-
@_spi(Experimental)
285-
public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never {
286-
let skipInfo = SkipInfo(comment: comment, sourceContext: SourceContext(backtrace: nil, sourceLocation: sourceLocation))
287-
try Self.cancel(with: skipInfo)
288-
}
289-
223+
/// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a
224+
/// different signature and accepts a source location rather than an instance
225+
/// of ``SkipInfo``.
290226
static func cancel(with skipInfo: SkipInfo) throws -> Never {
291227
let test = Test.current
292228
let testCase = Test.Case.current
293229

294-
do {
295-
// Cancel the current test case (if it's nil, that's the API misuse path.)
296-
try _cancel(testCase, for: (test, testCase), skipInfo: skipInfo)
297-
} catch _ where test?.isParameterized == false {
298-
// The current test is not parameterized, so cancel the whole test too.
299-
try _cancel(test, for: (test, nil), skipInfo: skipInfo)
230+
if let testCase {
231+
// Cancel the current test case.
232+
_cancel(testCase, for: (test, testCase), skipInfo: skipInfo)
300233
}
234+
235+
if let test {
236+
if !test.isParameterized {
237+
// The current test is not parameterized, so cancel the whole test too.
238+
_cancel(test, for: (test, nil), skipInfo: skipInfo)
239+
}
240+
} else {
241+
// There is no current test (this is the API misuse path.)
242+
_cancel(test, for: (test, nil), skipInfo: skipInfo)
243+
}
244+
245+
throw skipInfo
301246
}
302247

248+
static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind {
249+
.testCancelled(skipInfo)
250+
}
251+
}
252+
253+
// MARK: - Test case cancellation
254+
255+
extension Test.Case: TestCancellable {
303256
static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind {
304257
.testCaseCancelled(skipInfo)
305258
}

Tests/TestingTests/TestCancellationTests.swift

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -60,32 +60,11 @@
6060
}
6161
}
6262

63-
@Test func `Cancelling a non-parameterized test via Test.Case.cancel()`() async {
64-
await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in
65-
await Test {
66-
try Test.Case.cancel("Cancelled test")
67-
}.run(configuration: configuration)
68-
}
69-
}
70-
7163
@Test func `Cancelling a test case in a parameterized test`() async {
7264
await testCancellation(testCaseCancelled: 5, issueRecorded: 5) { configuration in
7365
await Test(arguments: 0 ..< 10) { i in
7466
if (i % 2) == 0 {
75-
try Test.Case.cancel("\(i) is even!")
76-
}
77-
Issue.record("\(i) records an issue!")
78-
}.run(configuration: configuration)
79-
}
80-
}
81-
82-
@Test func `Cancelling an entire parameterized test`() async {
83-
await testCancellation(testCancelled: 1, testCaseCancelled: 10) { configuration in
84-
// .serialized to ensure that none of the cases complete before the first
85-
// one cancels the test.
86-
await Test(.serialized, arguments: 0 ..< 10) { i in
87-
if i == 0 {
88-
try Test.cancel("\(i) cancelled the test")
67+
try Test.cancel("\(i) is even!")
8968
}
9069
Issue.record("\(i) records an issue!")
9170
}.run(configuration: configuration)
@@ -183,18 +162,6 @@
183162
}
184163
}
185164

186-
@Test func `Cancelling the current test case from within an exit test`() async {
187-
await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in
188-
await Test {
189-
await #expect(processExitsWith: .success) {
190-
try Test.Case.cancel("Cancelled test")
191-
}
192-
#expect(Task.isCancelled)
193-
try Task.checkCancellation()
194-
}.run(configuration: configuration)
195-
}
196-
}
197-
198165
@Test func `Cancelling the current task in an exit test doesn't cancel the test`() async {
199166
await testCancellation(testCancelled: 0, testCaseCancelled: 0) { configuration in
200167
await Test {

0 commit comments

Comments
 (0)