@@ -16,17 +16,15 @@ protocol TestCancellable: Sendable {
16
16
/// Cancel the current instance of this type.
17
17
///
18
18
/// - Parameters:
19
- /// - comments: Comments describing why you are cancelling the test/case.
20
- /// - sourceContext: The source context to which the testing library will
21
- /// attribute the cancellation.
19
+ /// - skipInfo: Information about the cancellation event.
22
20
///
23
21
/// - Throws: An error indicating that the current instance of this type has
24
22
/// been cancelled.
25
23
///
26
24
/// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a
27
25
/// different signature and accepts a source location rather than a source
28
26
/// context value.
29
- static func cancel( comments : [ Comment ] , sourceContext : @autoclosure ( ) -> SourceContext ) throws -> Never
27
+ static func cancel( with skipInfo : SkipInfo ) throws -> Never
30
28
31
29
/// Make an instance of ``Event/Kind`` appropriate for an instance of this
32
30
/// type.
@@ -47,8 +45,17 @@ private struct _TaskReference: Sendable {
47
45
private nonisolated ( unsafe) var _unsafeCurrentTask = Locked < UnsafeCurrentTask ? > ( )
48
46
49
47
init ( ) {
50
- let unsafeCurrentTask = withUnsafeCurrentTask { $0 }
51
- _unsafeCurrentTask = Locked ( rawValue: unsafeCurrentTask)
48
+ // WARNING! Normally, allowing an instance of `UnsafeCurrentTask` to escape
49
+ // its scope is dangerous because it could be used unsafely after the task
50
+ // ends. However, because we take care not to allow the task object to
51
+ // escape the task (by only storing it in a task-local value), we can ensure
52
+ // these unsafe scenarios won't occur.
53
+ //
54
+ // TODO: when our deployment targets allow, we should switch to calling the
55
+ // `async` overload of `withUnsafeCurrentTask()` from the body of
56
+ // `withCancellationHandling(_:)`. That will allow us to use the task object
57
+ // in a safely scoped fashion.
58
+ _unsafeCurrentTask = withUnsafeCurrentTask { Locked ( rawValue: $0) }
52
59
}
53
60
54
61
/// Take this instance's reference to its associated task.
@@ -69,8 +76,14 @@ private struct _TaskReference: Sendable {
69
76
70
77
/// A dictionary of tracked tasks, keyed by types that conform to
71
78
/// ``TestCancellable``.
72
- @TaskLocal
73
- private var _currentTaskReferences = [ ObjectIdentifier: _TaskReference] ( )
79
+ @TaskLocal private var _currentTaskReferences = [ ObjectIdentifier: _TaskReference] ( )
80
+
81
+ /// The instance of ``SkipInfo`` to propagate to children of the current task.
82
+ ///
83
+ /// We set this value while calling `UnsafeCurrentTask.cancel()` so that its
84
+ /// value is available in tracked child tasks when their cancellation handlers
85
+ /// are called (in ``TestCancellable/withCancellationHandling(_:)`` below).
86
+ @TaskLocal private var _currentSkipInfo : SkipInfo ?
74
87
75
88
extension TestCancellable {
76
89
/// Call a function while the ``unsafeCurrentTask`` property of this instance
@@ -95,10 +108,9 @@ extension TestCancellable {
95
108
} onCancel: {
96
109
// The current task was cancelled, so cancel the test case or test
97
110
// associated with it.
98
- _ = try ? Self . cancel (
99
- comments: [ ] ,
100
- sourceContext: SourceContext ( backtrace: . current( ) , sourceLocation: nil )
101
- )
111
+
112
+ let skipInfo = _currentSkipInfo ?? SkipInfo ( sourceContext: SourceContext ( backtrace: . current( ) , sourceLocation: nil ) )
113
+ _ = try ? Self . cancel ( with: skipInfo)
102
114
}
103
115
}
104
116
}
@@ -112,24 +124,21 @@ extension TestCancellable {
112
124
/// - cancellableValue: The test or test case to cancel, or `nil` if neither
113
125
/// is set and we need fallback handling.
114
126
/// - testAndTestCase: The test and test case to use when posting an event.
115
- /// - comments: Comments describing why you are cancelling the test/case.
116
- /// - sourceContext: The source context to which the testing library will
117
- /// attribute the cancellation.
127
+ /// - skipInfo: Information about the cancellation event.
118
128
///
119
129
/// - Throws: An instance of ``SkipInfo`` describing the cancellation.
120
- private func _cancel< T> ( _ cancellableValue: T ? , for testAndTestCase: ( Test ? , Test . Case ? ) , comments: [ Comment ] , sourceContext: @autoclosure ( ) -> SourceContext ) throws -> Never where T: TestCancellable {
121
- var skipInfo = SkipInfo ( comment: comments. first, sourceContext: . init( backtrace: nil , sourceLocation: nil ) )
122
-
130
+ private func _cancel< T> ( _ cancellableValue: T ? , for testAndTestCase: ( Test ? , Test . Case ? ) , skipInfo: SkipInfo ) throws -> Never where T: TestCancellable {
123
131
if cancellableValue != nil {
124
- // If the current test case is still running, cancel its task and clear its
125
- // task property (which signals that it has been cancelled.)
132
+ // If the current test case is still running, take its task property (which
133
+ // signals to subsequent callers that it has been cancelled.)
126
134
let task = _currentTaskReferences [ ObjectIdentifier ( T . self) ] ? . takeUnsafeCurrentTask ( )
127
- task? . cancel ( )
128
135
129
136
// If we just cancelled the current test case's task, post a corresponding
130
137
// event with the relevant skip info.
131
- if task != nil {
132
- skipInfo. sourceContext = sourceContext ( )
138
+ if let task {
139
+ $_currentSkipInfo. withValue ( skipInfo) {
140
+ task. cancel ( )
141
+ }
133
142
Event . post ( T . makeCancelledEventKind ( with: skipInfo) , for: testAndTestCase)
134
143
}
135
144
} else {
@@ -147,13 +156,18 @@ private func _cancel<T>(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes
147
156
// This code is running in an exit test. We don't have a "current test" or
148
157
// "current test case" in the child process, so we'll let the parent
149
158
// process sort that out.
150
- skipInfo. sourceContext = sourceContext ( )
151
159
Event . post ( T . makeCancelledEventKind ( with: skipInfo) , for: ( nil , nil ) )
152
160
} else {
153
161
// Record an API misuse issue for trying to cancel the current test/case
154
162
// outside of any useful context.
155
- let comments = [ " Attempted to cancel the current test or test case, but one is not associated with the current task. " ] + comments
156
- let issue = Issue ( kind: . apiMisused, comments: comments, sourceContext: sourceContext ( ) )
163
+ let issue = Issue (
164
+ kind: . apiMisused,
165
+ comments: [
166
+ " Attempted to cancel the current test or test case, but one is not associated with the current task. " ,
167
+ skipInfo. comment,
168
+ ] . compactMap ( \. self) ,
169
+ sourceContext: skipInfo. sourceContext
170
+ )
157
171
issue. record ( )
158
172
}
159
173
}
@@ -208,15 +222,13 @@ extension Test: TestCancellable {
208
222
/// test alone, call ``Test/Case/cancel(_:sourceLocation:)`` instead.
209
223
@_spi ( Experimental)
210
224
public static func cancel( _ comment: Comment ? = nil , sourceLocation: SourceLocation = #_sourceLocation) throws -> Never {
211
- try Self . cancel (
212
- comments: Array ( comment) ,
213
- sourceContext: SourceContext ( backtrace: . current( ) , sourceLocation: sourceLocation)
214
- )
225
+ let skipInfo = SkipInfo ( comment: comment, sourceContext: SourceContext ( backtrace: nil , sourceLocation: sourceLocation) )
226
+ try Self . cancel ( with: skipInfo)
215
227
}
216
228
217
- static func cancel( comments : [ Comment ] , sourceContext : @autoclosure ( ) -> SourceContext ) throws -> Never {
229
+ static func cancel( with skipInfo : SkipInfo ) throws -> Never {
218
230
let test = Test . current
219
- try _cancel ( test, for: ( test, nil ) , comments : comments , sourceContext : sourceContext ( ) )
231
+ try _cancel ( test, for: ( test, nil ) , skipInfo : skipInfo )
220
232
}
221
233
222
234
static func makeCancelledEventKind( with skipInfo: SkipInfo ) -> Event . Kind {
@@ -271,23 +283,20 @@ extension Test.Case: TestCancellable {
271
283
/// ``Test/cancel(_:sourceLocation:)`` instead.
272
284
@_spi ( Experimental)
273
285
public static func cancel( _ comment: Comment ? = nil , sourceLocation: SourceLocation = #_sourceLocation) throws -> Never {
274
- try Self . cancel (
275
- comments: Array ( comment) ,
276
- sourceContext: SourceContext ( backtrace: . current( ) , sourceLocation: sourceLocation)
277
- )
286
+ let skipInfo = SkipInfo ( comment: comment, sourceContext: SourceContext ( backtrace: nil , sourceLocation: sourceLocation) )
287
+ try Self . cancel ( with: skipInfo)
278
288
}
279
289
280
- static func cancel( comments : [ Comment ] , sourceContext : @autoclosure ( ) -> SourceContext ) throws -> Never {
290
+ static func cancel( with skipInfo : SkipInfo ) throws -> Never {
281
291
let test = Test . current
282
292
let testCase = Test . Case. current
283
- let sourceContext = sourceContext ( ) // evaluated twice, avoid laziness
284
293
285
294
do {
286
295
// Cancel the current test case (if it's nil, that's the API misuse path.)
287
- try _cancel ( testCase, for: ( test, testCase) , comments : comments , sourceContext : sourceContext )
296
+ try _cancel ( testCase, for: ( test, testCase) , skipInfo : skipInfo )
288
297
} catch _ where test? . isParameterized == false {
289
298
// The current test is not parameterized, so cancel the whole test too.
290
- try _cancel ( test, for: ( test, nil ) , comments : comments , sourceContext : sourceContext )
299
+ try _cancel ( test, for: ( test, nil ) , skipInfo : skipInfo )
291
300
}
292
301
}
293
302
0 commit comments