blob: afe4356172ee2d0c480c771dfea2951cc0a63094 [file] [log] [blame]
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 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
//
//
// XCTestExpectation.swift
//
/// Expectations represent specific conditions in asynchronous testing.
open class XCTestExpectation {
private static var currentMonotonicallyIncreasingToken: UInt64 = 0
private static func queue_nextMonotonicallyIncreasingToken() -> UInt64 {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
currentMonotonicallyIncreasingToken += 1
return currentMonotonicallyIncreasingToken
}
private static func nextMonotonicallyIncreasingToken() -> UInt64 {
return XCTWaiter.subsystemQueue.sync { queue_nextMonotonicallyIncreasingToken() }
}
/*
Rules for properties
====================
XCTestExpectation has many properties, many of which require synchronization on `XCTWaiter.subsystemQueue`.
When adding properties, use the following rules for consistency. The naming guidelines aim to allow
property names to be as short & simple as possible, while maintaining the necessary synchronization.
- If property is constant (`let`), it is immutable so there is no synchronization concern.
- No underscore prefix on name
- No matching `queue_` property
- If it is only used within this file:
- `private` access
- If is is used outside this file but not outside the module:
- `internal` access
- If it is used outside the module:
- `public` or `open` access, depending on desired overridability
- If property is variable (`var`), it is mutable so access to it must be synchronized.
- `private` access
- If it is only used within this file:
- No underscore prefix on name
- No matching `queue_` property
- If is is used outside this file:
- If access outside this file is always on-queue:
- No underscore prefix on name
- Matching internal `queue_` property with `.onQueue` dispatchPreconditions
- If access outside this file is sometimes off-queue
- Underscore prefix on name
- Matching `internal` property with `queue_` prefix and `XCTWaiter.subsystemQueue` dispatchPreconditions
- Matching `internal` or `public` property without underscore prefix but with `XCTWaiter.subsystemQueue` synchronization
*/
private var _expectationDescription: String
internal let creationToken: UInt64
internal let creationSourceLocation: SourceLocation
private var isFulfilled = false
private var fulfillmentToken: UInt64 = 0
private var _fulfillmentSourceLocation: SourceLocation?
private var _expectedFulfillmentCount = 1
private var numberOfFulfillments = 0
private var _isInverted = false
private var _assertForOverFulfill = false
private var _hasBeenWaitedOn = false
private var _didFulfillHandler: (() -> Void)?
/// A human-readable string used to describe the expectation in log output and test reports.
open var expectationDescription: String {
get {
return XCTWaiter.subsystemQueue.sync { queue_expectationDescription }
}
set {
XCTWaiter.subsystemQueue.sync { queue_expectationDescription = newValue }
}
}
/// The number of times `fulfill()` must be called on the expectation in order for it
/// to report complete fulfillment to its waiter. Default is 1.
/// This value must be greater than 0 and is not meaningful if combined with `isInverted`.
open var expectedFulfillmentCount: Int {
get {
return XCTWaiter.subsystemQueue.sync { queue_expectedFulfillmentCount }
}
set {
precondition(newValue > 0, "API violation - fulfillment count must be greater than 0.")
XCTWaiter.subsystemQueue.sync {
precondition(!queue_hasBeenWaitedOn, "API violation - cannot set expectedFulfillmentCount on '\(queue_expectationDescription)' after already waiting on it.")
queue_expectedFulfillmentCount = newValue
}
}
}
/// If an expectation is set to be inverted, then fulfilling it will have a similar effect as
/// failing to fulfill a conventional expectation has, as handled by the waiter and its delegate.
/// Furthermore, waiters that wait on an inverted expectation will allow the full timeout to elapse
/// and not report timeout to the delegate if it is not fulfilled.
open var isInverted: Bool {
get {
return XCTWaiter.subsystemQueue.sync { queue_isInverted }
}
set {
XCTWaiter.subsystemQueue.sync {
precondition(!queue_hasBeenWaitedOn, "API violation - cannot set isInverted on '\(queue_expectationDescription)' after already waiting on it.")
queue_isInverted = newValue
}
}
}
/// If set, calls to fulfill() after the expectation has already been fulfilled - exceeding the fulfillment
/// count - will cause a fatal error and halt process execution. Default is false (disabled).
///
/// - Note: This is the legacy behavior of expectations created through APIs on the ObjC version of XCTestCase
/// because that version raises ObjC exceptions (which may be caught) instead of causing a fatal error.
/// In this version of XCTest, no expectation ever has this property set to true (enabled) by default, it
/// must be opted-in to explicitly.
open var assertForOverFulfill: Bool {
get {
return XCTWaiter.subsystemQueue.sync { _assertForOverFulfill }
}
set {
XCTWaiter.subsystemQueue.sync {
precondition(!queue_hasBeenWaitedOn, "API violation - cannot set assertForOverFulfill on '\(queue_expectationDescription)' after already waiting on it.")
_assertForOverFulfill = newValue
}
}
}
internal var fulfillmentSourceLocation: SourceLocation? {
return XCTWaiter.subsystemQueue.sync { _fulfillmentSourceLocation }
}
internal var hasBeenWaitedOn: Bool {
return XCTWaiter.subsystemQueue.sync { queue_hasBeenWaitedOn }
}
internal var queue_expectationDescription: String {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return _expectationDescription
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
_expectationDescription = newValue
}
}
internal var queue_isFulfilled: Bool {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return isFulfilled
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
isFulfilled = newValue
}
}
internal var queue_fulfillmentToken: UInt64 {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return fulfillmentToken
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
fulfillmentToken = newValue
}
}
internal var queue_expectedFulfillmentCount: Int {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return _expectedFulfillmentCount
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
_expectedFulfillmentCount = newValue
}
}
internal var queue_isInverted: Bool {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return _isInverted
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
_isInverted = newValue
}
}
internal var queue_hasBeenWaitedOn: Bool {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return _hasBeenWaitedOn
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
_hasBeenWaitedOn = newValue
if _hasBeenWaitedOn {
didBeginWaiting()
}
}
}
internal var queue_didFulfillHandler: (() -> Void)? {
get {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
return _didFulfillHandler
}
set {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
_didFulfillHandler = newValue
}
}
/// Initializes a new expectation with a description of the condition it is checking.
///
/// - Parameter description: A human-readable string used to describe the condition the expectation is checking.
public init(description: String = "no description provided", file: StaticString = #file, line: Int = #line) {
_expectationDescription = description
creationToken = XCTestExpectation.nextMonotonicallyIncreasingToken()
creationSourceLocation = SourceLocation(file: file, line: line)
}
/// Marks an expectation as having been met. It's an error to call this
/// method on an expectation that has already been fulfilled, or when the
/// test case that vended the expectation has already completed.
///
/// - 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.
///
/// - Note: Whereas Objective-C XCTest determines the file and line
/// number the expectation was fulfilled 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`.
open func fulfill(_ file: StaticString = #file, line: Int = #line) {
let sourceLocation = SourceLocation(file: file, line: line)
let didFulfillHandler: (() -> Void)? = XCTWaiter.subsystemQueue.sync {
// FIXME: Objective-C XCTest emits failures when expectations are
// fulfilled after the test cases that generated those
// expectations have completed. Similarly, this should cause an
// error as well.
if queue_isFulfilled {
// FIXME: No regression tests exist for this feature. We may break it
// without ever realizing (similar to `continueAfterFailure`).
if _assertForOverFulfill {
fatalError("API violation - multiple calls made to fulfill() for \(queue_expectationDescription)")
}
if let testCase = XCTCurrentTestCase {
testCase.recordFailure(
description: "API violation - multiple calls made to XCTestExpectation.fulfill() for \(queue_expectationDescription).",
at: sourceLocation,
expected: false)
}
return nil
}
if queue_fulfill(sourceLocation: sourceLocation) {
return queue_didFulfillHandler
} else {
return nil
}
}
didFulfillHandler?()
}
private func queue_fulfill(sourceLocation: SourceLocation) -> Bool {
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
numberOfFulfillments += 1
if numberOfFulfillments == queue_expectedFulfillmentCount {
queue_isFulfilled = true
_fulfillmentSourceLocation = sourceLocation
queue_fulfillmentToken = XCTestExpectation.queue_nextMonotonicallyIncreasingToken()
return true
} else {
return false
}
}
internal func didBeginWaiting() {
// Override point for subclasses
}
internal func cleanUp() {
// Override point for subclasses
}
}
extension XCTestExpectation: Equatable {
public static func == (lhs: XCTestExpectation, rhs: XCTestExpectation) -> Bool {
return lhs === rhs
}
}
extension XCTestExpectation: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
extension XCTestExpectation: CustomStringConvertible {
public var description: String {
return expectationDescription
}
}