File: XCTWaiter.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (484 lines) | stat: -rw-r--r-- 23,892 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//
//  XCTWaiter.swift
//
#if !DISABLE_XCTWAITER

#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
import CoreFoundation
#endif

/// Events are reported to the waiter's delegate via these methods. XCTestCase conforms to this
/// protocol and will automatically report timeouts and other unexpected events as test failures.
///
/// - Note: These methods are invoked on an arbitrary queue.
public protocol XCTWaiterDelegate: AnyObject {

    /// Invoked when not all waited on expectations are fulfilled during the timeout period. If the delegate
    /// is an XCTestCase instance, this will be reported as a test failure.
    ///
    /// - Parameter waiter: The waiter which timed out.
    /// - Parameter unfulfilledExpectations: The expectations which were unfulfilled when `waiter` timed out.
    func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation])

    /// Invoked when the wait specified that fulfillment order should be enforced and an expectation
    /// has been fulfilled in the wrong order. If the delegate is an XCTestCase instance, this will be reported
    /// as a test failure.
    ///
    /// - Parameter waiter: The waiter which had an ordering violation.
    /// - Parameter expectation: The expectation which was fulfilled instead of the required expectation.
    /// - Parameter requiredExpectation: The expectation which was fulfilled instead of the required expectation.
    func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation)

    /// Invoked when an expectation marked as inverted is fulfilled. If the delegate is an XCTestCase instance,
    /// this will be reported as a test failure.
    ///
    /// - Parameter waiter: The waiter which had an inverted expectation fulfilled.
    /// - Parameter expectation: The inverted expectation which was fulfilled.
    ///
    /// - SeeAlso: `XCTestExpectation.isInverted`
    func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation)

    /// Invoked when the waiter is interrupted prior to its expectations being fulfilled or timing out.
    /// This occurs when an "outer" waiter times out, resulting in any waiters nested inside it being
    /// interrupted to allow the call stack to quickly unwind.
    ///
    /// - Parameter waiter: The waiter which was interrupted.
    /// - Parameter outerWaiter: The "outer" waiter which interrupted `waiter`.
    func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter)

}

// All `XCTWaiterDelegate` methods are optional, so empty default implementations are provided
public extension XCTWaiterDelegate {
    func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) {}
    func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) {}
    func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) {}
    func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) {}
}

/// Manages waiting - pausing the current execution context - for an array of XCTestExpectations. Waiters
/// can be used with or without a delegate to respond to events such as completion, timeout, or invalid
/// expectation fulfillment. XCTestCase conforms to the delegate protocol and will automatically report
/// timeouts and other unexpected events as test failures.
///
/// Waiters can be used without a delegate or any association with a test case instance. This allows test
/// support libraries to provide convenience methods for waiting without having to pass test cases through
/// those APIs.
open class XCTWaiter {

    /// Values returned by a waiter when it completes, times out, or is interrupted due to another waiter
    /// higher in the call stack timing out.
    public enum Result: Int {
        case completed = 1
        case timedOut
        case incorrectOrder
        case invertedFulfillment
        case interrupted
    }

    private enum State: Equatable {
        case ready
        case waiting(state: Waiting)
        case finished(state: Finished)

        struct Waiting: Equatable {
            var enforceOrder: Bool
            var expectations: [XCTestExpectation]
            var fulfilledExpectations: [XCTestExpectation]
        }

        struct Finished: Equatable {
            let result: Result
            let fulfilledExpectations: [XCTestExpectation]
            let unfulfilledExpectations: [XCTestExpectation]
        }

        var allExpectations: [XCTestExpectation] {
            switch self {
            case .ready:
                return []
            case let .waiting(waitingState):
                return waitingState.expectations
            case let .finished(finishedState):
                return finishedState.fulfilledExpectations + finishedState.unfulfilledExpectations
            }
        }
    }

    internal static let subsystemQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter")

    private var state = State.ready
    internal var timeout: TimeInterval = 0
    internal var waitSourceLocation: SourceLocation?
    private weak var manager: WaiterManager<XCTWaiter>?
    private var runLoop: RunLoop?

    private weak var _delegate: XCTWaiterDelegate?
    private let delegateQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter.delegate")

    /// The waiter delegate will be called with various events described in the `XCTWaiterDelegate` protocol documentation.
    ///
    /// - SeeAlso: `XCTWaiterDelegate`
    open var delegate: XCTWaiterDelegate? {
        get {
            return XCTWaiter.subsystemQueue.sync { _delegate }
        }
        set {
            dispatchPrecondition(condition: .notOnQueue(XCTWaiter.subsystemQueue))
            XCTWaiter.subsystemQueue.async { self._delegate = newValue }
        }
    }

    /// Returns an array containing the expectations that were fulfilled, in that order, up until the waiter
    /// stopped waiting. Expectations fulfilled after the waiter stopped waiting will not be in the array.
    /// The array will be empty until the waiter has started waiting, even if expectations have already been
    /// fulfilled.
    open var fulfilledExpectations: [XCTestExpectation] {
        return XCTWaiter.subsystemQueue.sync {
            let fulfilledExpectations: [XCTestExpectation]

            switch state {
            case .ready:
                fulfilledExpectations = []
            case let .waiting(waitingState):
                fulfilledExpectations = waitingState.fulfilledExpectations
            case let .finished(finishedState):
                fulfilledExpectations = finishedState.fulfilledExpectations
            }

            // Sort by fulfillment token before returning, since it is the true fulfillment order.
            // The waiter being notified by the expectation isn't guaranteed to happen in the same order.
            return fulfilledExpectations.sorted { $0.queue_fulfillmentToken < $1.queue_fulfillmentToken }
        }
    }

    /// Initializes a waiter with an optional delegate.
    public init(delegate: XCTWaiterDelegate? = nil) {
        _delegate = delegate
    }

    /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they
    /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations.
    ///
    /// - Parameter expectations: The expectations to wait on.
    /// - Parameter timeout: The maximum total time duration to wait on all expectations.
    /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order
    ///   they are specified in the `expectations` Array. Default is false.
    /// - Parameter file: The file name to use in the error message if
    ///   expectations are not fulfilled before the given timeout. Default is the file
    ///   containing the call to this method. It is rare to provide this
    ///   parameter when calling this method.
    /// - Parameter line: The line number to use in the error message if the
    ///   expectations are not fulfilled before the given timeout. Default is the line
    ///   number of the call to this method in the calling file. It is rare to
    ///   provide this parameter when calling this method.
    ///
    /// - Note: Whereas Objective-C XCTest determines the file and line
    ///   number of the "wait" call using symbolication, this implementation
    ///   opts to take `file` and `line` as parameters instead. As a result,
    ///   the interface to these methods are not exactly identical between
    ///   these environments. To ensure compatibility of tests between
    ///   swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
    ///   explicit values for `file` and `line`.
    @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.")
    @discardableResult
    open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result {
        precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.")

        self.timeout = timeout
        waitSourceLocation = SourceLocation(file: file, line: line)
        let runLoop = RunLoop.current

        XCTWaiter.subsystemQueue.sync {
            precondition(state == .ready, "API violation - wait(...) has already been called on this waiter.")

            let previouslyWaitedOnExpectations = expectations.filter { $0.queue_hasBeenWaitedOn }
            let previouslyWaitedOnExpectationDescriptions = previouslyWaitedOnExpectations.map { $0.queue_expectationDescription }.joined(separator: "`, `")
            precondition(previouslyWaitedOnExpectations.isEmpty, "API violation - expectations can only be waited on once, `\(previouslyWaitedOnExpectationDescriptions)` have already been waited on.")

            let waitingState = State.Waiting(
                enforceOrder: enforceOrder,
                expectations: expectations,
                fulfilledExpectations: expectations.filter { $0.queue_isFulfilled }
            )
            queue_configureExpectations(expectations)
            state = .waiting(state: waitingState)
            self.runLoop = runLoop

            queue_validateExpectationFulfillment(dueToTimeout: false)
        }

        let manager = WaiterManager<XCTWaiter>.current
        manager.startManaging(self, timeout: timeout)
        self.manager = manager

        // Begin the core wait loop.
        let timeoutTimestamp = Date.timeIntervalSinceReferenceDate + timeout
        while !isFinished {
            let remaining = timeoutTimestamp - Date.timeIntervalSinceReferenceDate
            if remaining <= 0 {
                break
            }
            primitiveWait(using: runLoop, duration: remaining)
        }

        manager.stopManaging(self)
        self.manager = nil

        let result: Result = XCTWaiter.subsystemQueue.sync {
            queue_validateExpectationFulfillment(dueToTimeout: true)

            for expectation in expectations {
                expectation.cleanUp()
                expectation.queue_didFulfillHandler = nil
            }

            guard case let .finished(finishedState) = state else { fatalError("Unexpected state: \(state)") }
            return finishedState.result
        }

        delegateQueue.sync {
            // DO NOT REMOVE ME
            // This empty block, executed synchronously, ensures that inflight delegate callbacks from the
            // internal queue have been processed before wait returns.
        }

        return result
    }

    /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they
    /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations.
    ///
    /// - Parameter expectations: The expectations to wait on.
    /// - Parameter timeout: The maximum total time duration to wait on all expectations.
    /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order
    ///   they are specified in the `expectations` Array. Default is false.
    /// - Parameter file: The file name to use in the error message if
    ///   expectations are not fulfilled before the given timeout. Default is the file
    ///   containing the call to this method. It is rare to provide this
    ///   parameter when calling this method.
    /// - Parameter line: The line number to use in the error message if the
    ///   expectations are not fulfilled before the given timeout. Default is the line
    ///   number of the call to this method in the calling file. It is rare to
    ///   provide this parameter when calling this method.
    ///
    /// - Note: Whereas Objective-C XCTest determines the file and line
    ///   number of the "wait" call using symbolication, this implementation
    ///   opts to take `file` and `line` as parameters instead. As a result,
    ///   the interface to these methods are not exactly identical between
    ///   these environments. To ensure compatibility of tests between
    ///   swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
    ///   explicit values for `file` and `line`.
    @available(macOS 12.0, *)
    @discardableResult
    open func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result {
        return await withCheckedContinuation { continuation in
            // This function operates by blocking a background thread instead of one owned by libdispatch or by the
            // Swift runtime (as used by Swift concurrency.) To ensure we use a thread owned by neither subsystem, use
            // Foundation's Thread.detachNewThread(_:).
            Thread.detachNewThread { [self] in
                let result = wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
                continuation.resume(returning: result)
            }
        }
    }

    /// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they
    /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. The waiter
    /// is discarded when the wait completes.
    ///
    /// - Parameter expectations: The expectations to wait on.
    /// - Parameter timeout: The maximum total time duration to wait on all expectations.
    /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order
    ///   they are specified in the `expectations` Array. Default is false.
    /// - Parameter file: The file name to use in the error message if
    ///   expectations are not fulfilled before the given timeout. Default is the file
    ///   containing the call to this method. It is rare to provide this
    ///   parameter when calling this method.
    /// - Parameter line: The line number to use in the error message if the
    ///   expectations are not fulfilled before the given timeout. Default is the line
    ///   number of the call to this method in the calling file. It is rare to
    ///   provide this parameter when calling this method.
    @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.")
    open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result {
        return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
    }

    /// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they
    /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. The waiter
    /// is discarded when the wait completes.
    ///
    /// - Parameter expectations: The expectations to wait on.
    /// - Parameter timeout: The maximum total time duration to wait on all expectations.
    /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order
    ///   they are specified in the `expectations` Array. Default is false.
    /// - Parameter file: The file name to use in the error message if
    ///   expectations are not fulfilled before the given timeout. Default is the file
    ///   containing the call to this method. It is rare to provide this
    ///   parameter when calling this method.
    /// - Parameter line: The line number to use in the error message if the
    ///   expectations are not fulfilled before the given timeout. Default is the line
    ///   number of the call to this method in the calling file. It is rare to
    ///   provide this parameter when calling this method.
    @available(macOS 12.0, *)
    open class func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result {
        return await XCTWaiter().fulfillment(of: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
    }

    deinit {
        for expectation in state.allExpectations {
            expectation.cleanUp()
        }
    }

    private func queue_configureExpectations(_ expectations: [XCTestExpectation]) {
        dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))

        for expectation in expectations {
            expectation.queue_didFulfillHandler = { [weak self, unowned expectation] in
                self?.expectationWasFulfilled(expectation)
            }
            expectation.queue_hasBeenWaitedOn = true
        }
    }

    private func queue_validateExpectationFulfillment(dueToTimeout: Bool) {
        dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
        guard case let .waiting(waitingState) = state else { return }

        let validatableExpectations = waitingState.expectations.map { ValidatableXCTestExpectation(expectation: $0) }
        let validationResult = XCTWaiter.validateExpectations(validatableExpectations, dueToTimeout: dueToTimeout, enforceOrder: waitingState.enforceOrder)

        switch validationResult {
        case .complete:
            queue_finish(result: .completed, cancelPrimitiveWait: !dueToTimeout)

        case .fulfilledInvertedExpectation(let invertedValidationExpectation):
            queue_finish(result: .invertedFulfillment, cancelPrimitiveWait: true) { delegate in
                delegate.waiter(self, didFulfillInvertedExpectation: invertedValidationExpectation.expectation)
            }

        case .violatedOrderingConstraints(let validationExpectation, let requiredValidationExpectation):
            queue_finish(result: .incorrectOrder, cancelPrimitiveWait: true) { delegate in
                delegate.waiter(self, fulfillmentDidViolateOrderingConstraintsFor: validationExpectation.expectation, requiredExpectation: requiredValidationExpectation.expectation)
            }

        case .timedOut(let unfulfilledValidationExpectations):
            queue_finish(result: .timedOut, cancelPrimitiveWait: false) { delegate in
                delegate.waiter(self, didTimeoutWithUnfulfilledExpectations: unfulfilledValidationExpectations.map { $0.expectation })
            }

        case .incomplete:
            break

        }
    }

    private func queue_finish(result: Result, cancelPrimitiveWait: Bool, delegateBlock: ((XCTWaiterDelegate) -> Void)? = nil) {
        dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
        guard case let .waiting(waitingState) = state else { preconditionFailure("Unexpected state: \(state)") }

        let unfulfilledExpectations = waitingState.expectations.filter { !waitingState.fulfilledExpectations.contains($0) }

        state = .finished(state: State.Finished(
            result: result,
            fulfilledExpectations: waitingState.fulfilledExpectations,
            unfulfilledExpectations: unfulfilledExpectations
        ))

        if cancelPrimitiveWait {
            self.cancelPrimitiveWait()
        }

        if let delegateBlock = delegateBlock, let delegate = _delegate {
            delegateQueue.async {
                delegateBlock(delegate)
            }
        }
    }

    private func expectationWasFulfilled(_ expectation: XCTestExpectation) {
        XCTWaiter.subsystemQueue.sync {
            // If already finished, do nothing
            guard case var .waiting(waitingState) = state else { return }

            waitingState.fulfilledExpectations.append(expectation)
            queue_validateExpectationFulfillment(dueToTimeout: false)
        }
    }

}

private extension XCTWaiter {
    func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) {
        // The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested
        // by the `timeout` argument. Only run for a short time in case `cancelPrimitiveWait()` was called and
        // issued `CFRunLoopStop` just before we reach this point.
        let timeIntervalToRun = min(0.1, timeout)

        // RunLoop.run(mode:before:) should have @discardableResult <rdar://problem/45371901>
        _ = runLoop.run(mode: .default, before: Date(timeIntervalSinceNow: timeIntervalToRun))
    }

    func cancelPrimitiveWait() {
        guard let runLoop = runLoop else { return }
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
        CFRunLoopStop(runLoop.getCFRunLoop())
#else
        runLoop._stop()
#endif
    }
}

extension XCTWaiter: Equatable {
    public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool {
        return lhs === rhs
    }
}

extension XCTWaiter: CustomStringConvertible {
    public var description: String {
        return XCTWaiter.subsystemQueue.sync {
            let expectationsString = state.allExpectations.map { "'\($0.queue_expectationDescription)'" }.joined(separator: ", ")

            return "<XCTWaiter expectations: \(expectationsString)>"
        }
    }
}

extension XCTWaiter: ManageableWaiter {
    var isFinished: Bool {
        return XCTWaiter.subsystemQueue.sync {
            switch state {
            case .ready, .waiting: return false
            case .finished: return true
            }
        }
    }

    func queue_handleWatchdogTimeout() {
        dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))

        queue_validateExpectationFulfillment(dueToTimeout: true)
        manager!.queue_handleWatchdogTimeout(of: self)
        cancelPrimitiveWait()
    }

    func queue_interrupt(for interruptingWaiter: XCTWaiter) {
        dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))

        queue_finish(result: .interrupted, cancelPrimitiveWait: true) { delegate in
            delegate.nestedWaiter(self, wasInterruptedByTimedOutWaiter: interruptingWaiter)
        }
    }
}

#endif