@@ -16,17 +16,15 @@ protocol TestCancellable: Sendable {
1616 /// Cancel the current instance of this type.
1717 ///
1818 /// - 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.
2220 ///
2321 /// - Throws: An error indicating that the current instance of this type has
2422 /// been cancelled.
2523 ///
2624 /// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a
2725 /// different signature and accepts a source location rather than a source
2826 /// context value.
29- static func cancel( comments : [ Comment ] , sourceContext : @autoclosure ( ) -> SourceContext ) throws -> Never
27+ static func cancel( with skipInfo : SkipInfo ) throws -> Never
3028
3129 /// Make an instance of ``Event/Kind`` appropriate for an instance of this
3230 /// type.
@@ -47,8 +45,17 @@ private struct _TaskReference: Sendable {
4745 private nonisolated ( unsafe) var _unsafeCurrentTask = Locked < UnsafeCurrentTask ? > ( )
4846
4947 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) }
5259 }
5360
5461 /// Take this instance's reference to its associated task.
@@ -69,8 +76,14 @@ private struct _TaskReference: Sendable {
6976
7077/// A dictionary of tracked tasks, keyed by types that conform to
7178/// ``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 ?
7487
7588extension TestCancellable {
7689 /// Call a function while the ``unsafeCurrentTask`` property of this instance
@@ -95,10 +108,9 @@ extension TestCancellable {
95108 } onCancel: {
96109 // The current task was cancelled, so cancel the test case or test
97110 // 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)
102114 }
103115 }
104116 }
@@ -112,24 +124,21 @@ extension TestCancellable {
112124/// - cancellableValue: The test or test case to cancel, or `nil` if neither
113125/// is set and we need fallback handling.
114126/// - 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.
118128///
119129/// - 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 {
123131 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.)
126134 let task = _currentTaskReferences [ ObjectIdentifier ( T . self) ] ? . takeUnsafeCurrentTask ( )
127- task? . cancel ( )
128135
129136 // If we just cancelled the current test case's task, post a corresponding
130137 // 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+ }
133142 Event . post ( T . makeCancelledEventKind ( with: skipInfo) , for: testAndTestCase)
134143 }
135144 } else {
@@ -147,13 +156,18 @@ private func _cancel<T>(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes
147156 // This code is running in an exit test. We don't have a "current test" or
148157 // "current test case" in the child process, so we'll let the parent
149158 // process sort that out.
150- skipInfo. sourceContext = sourceContext ( )
151159 Event . post ( T . makeCancelledEventKind ( with: skipInfo) , for: ( nil , nil ) )
152160 } else {
153161 // Record an API misuse issue for trying to cancel the current test/case
154162 // 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+ )
157171 issue. record ( )
158172 }
159173 }
@@ -208,15 +222,13 @@ extension Test: TestCancellable {
208222 /// test alone, call ``Test/Case/cancel(_:sourceLocation:)`` instead.
209223 @_spi ( Experimental)
210224 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)
215227 }
216228
217- static func cancel( comments : [ Comment ] , sourceContext : @autoclosure ( ) -> SourceContext ) throws -> Never {
229+ static func cancel( with skipInfo : SkipInfo ) throws -> Never {
218230 let test = Test . current
219- try _cancel ( test, for: ( test, nil ) , comments : comments , sourceContext : sourceContext ( ) )
231+ try _cancel ( test, for: ( test, nil ) , skipInfo : skipInfo )
220232 }
221233
222234 static func makeCancelledEventKind( with skipInfo: SkipInfo ) -> Event . Kind {
@@ -271,23 +283,20 @@ extension Test.Case: TestCancellable {
271283 /// ``Test/cancel(_:sourceLocation:)`` instead.
272284 @_spi ( Experimental)
273285 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)
278288 }
279289
280- static func cancel( comments : [ Comment ] , sourceContext : @autoclosure ( ) -> SourceContext ) throws -> Never {
290+ static func cancel( with skipInfo : SkipInfo ) throws -> Never {
281291 let test = Test . current
282292 let testCase = Test . Case. current
283- let sourceContext = sourceContext ( ) // evaluated twice, avoid laziness
284293
285294 do {
286295 // 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 )
288297 } catch _ where test? . isParameterized == false {
289298 // 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 )
291300 }
292301 }
293302
0 commit comments