| // 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 |
| // |
| |
| import CoreFoundation |
| |
| /// 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`. |
| @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 = CFAbsoluteTimeGetCurrent() + timeout |
| while !isFinished { |
| let remaining = timeoutTimestamp - CFAbsoluteTimeGetCurrent() |
| 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 |
| } |
| |
| /// 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. |
| 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) |
| } |
| |
| 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 } |
| CFRunLoopStop(runLoop.getCFRunLoop()) |
| } |
| } |
| |
| 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) |
| } |
| } |
| } |