blob: 0b33b0f57f556149303e25f5a0c1806974be3a27 [file] [log] [blame]
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2016 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
//
//
// XCTestCase+Asynchronous.swift
// Methods on XCTestCase for testing asynchronous operations
//
public extension XCTestCase {
/// Creates and returns an expectation associated with the test case.
///
/// - Parameter description: This string will be displayed in the test log
/// to help diagnose failures.
/// - Parameter file: The file name to use in the error message if
/// this expectation is not waited for. 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
/// this expectation is not waited for. 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 expectations that are created by 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 func expectation(description: String, file: StaticString = #file, line: Int = #line) -> XCTestExpectation {
let expectation = XCTestExpectation(
description: description,
file: file,
line: line,
testCase: self)
_allExpectations.append(expectation)
return expectation
}
/// Creates a point of synchronization in the flow of a test. Only one
/// "wait" can be active at any given time, but multiple discrete sequences
/// of { expectations -> wait } can be chained together.
///
/// - Parameter timeout: The amount of time within which all expectation
/// must be fulfilled.
/// - Parameter file: The file name to use in the error message if
/// expectations are not met 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 met 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.
/// - Parameter handler: If provided, the handler will be invoked both on
/// timeout or fulfillment of all expectations. Timeout is always treated
/// as a test failure.
///
/// - 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`.
func waitForExpectations(timeout: TimeInterval, file: StaticString = #file, line: Int = #line, handler: XCWaitCompletionHandler? = nil) {
// Mirror Objective-C XCTest behavior; display an unexpected test
// failure when users wait without having first set expectations.
// FIXME: Objective-C XCTest raises an exception for most "API
// violation" failures, including this one. Normally this causes
// the test to stop cold. swift-corelibs-xctest does not stop,
// and executes the rest of the test. This discrepancy should be
// fixed.
if _allExpectations.count == 0 {
recordFailure(
withDescription: "API violation - call made to wait without any expectations having been set.",
inFile: String(describing: file),
atLine: line,
expected: false)
return
}
// Objective-C XCTest outputs the descriptions of every unfulfilled
// expectation. We gather them into this array, which is also used
// to determine failure--a non-empty array meets expectations weren't
// met.
var unfulfilledDescriptions = [String]()
// We continue checking whether expectations have been fulfilled until
// the specified timeout has been reached.
// FIXME: Instead of polling the expectations to check whether they've
// been fulfilled, it would be more efficient to use a runloop
// source that can be signaled to wake up when an expectation is
// fulfilled.
let runLoop = RunLoop.current
let timeoutDate = Date(timeIntervalSinceNow: timeout)
repeat {
unfulfilledDescriptions = []
for expectation in _allExpectations {
if !expectation.isFulfilled {
unfulfilledDescriptions.append(expectation.description)
}
}
// If we've met all expectations, then break out of the specified
// timeout loop early.
if unfulfilledDescriptions.count == 0 {
break
}
// Otherwise, wait another fraction of a second.
runLoop.run(until: Date(timeIntervalSinceNow: 0.01))
} while Date().compare(timeoutDate) == ComparisonResult.orderedAscending
if unfulfilledDescriptions.count > 0 {
// Not all expectations were fulfilled. Append a failure
// to the array of expectation-based failures.
let descriptions = unfulfilledDescriptions.joined(separator: ", ")
recordFailure(
withDescription: "Asynchronous wait failed - Exceeded timeout of \(timeout) seconds, with unfulfilled expectations: \(descriptions)",
inFile: String(describing: file),
atLine: line,
expected: true)
}
// We've recorded all the failures; clear the expectations that
// were set for this test case.
_allExpectations = []
// The handler is invoked regardless of whether the test passed.
if let completionHandler = handler {
var error: NSError? = nil
if unfulfilledDescriptions.count > 0 {
// If the test failed, send an error object.
error = NSError(
domain: XCTestErrorDomain,
code: XCTestError.Code.timeoutWhileWaiting.rawValue,
userInfo: [:])
}
completionHandler(error)
}
}
}
internal extension XCTestCase {
/// It is an API violation to create expectations but not wait for them to
/// be completed. Notify the user of a mistake via a test failure.
func failIfExpectationsNotWaitedFor(_ expectations: [XCTestExpectation]) {
if expectations.count > 0 {
recordFailure(
withDescription: "Failed due to unwaited expectations.",
inFile: String(describing: expectations.last!.file),
atLine: expectations.last!.line,
expected: false)
}
}
}