blob: 0a404c5df895954d807c84fa44392616fce06e19 [file] [log] [blame]
// 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)
}
}
}