blob: 1aaf8febc28e8d5c4a438ee71d787962444087f5 [file] [log] [blame]
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 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.swift
// Base class for test cases
//
#if os(Linux) || os(FreeBSD)
import Foundation
#else
import SwiftFoundation
#endif
/// This is a compound type used by `XCTMain` to represent tests to run. It combines an
/// `XCTestCase` subclass type with the list of test case methods to invoke on the class.
/// This type is intended to be produced by the `testCase` helper function.
/// - seealso: `testCase`
/// - seealso: `XCTMain`
public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(String, XCTestCase throws -> Void)])
// A global pointer to the currently running test case. This is required in
// order for XCTAssert functions to report failures.
internal var XCTCurrentTestCase: XCTestCase?
/// An instance of this class represents an individual test case which can be
/// run by the framework. This class is normally subclassed and extended with
/// methods containing the tests to run.
/// - seealso: `XCTMain`
public class XCTestCase: XCTest {
private let testClosure: XCTestCase throws -> Void
/// The name of the test case, consisting of its class name and the method
/// name it will run.
public override var name: String {
return _name
}
/// A private setter for the name of this test case.
/// - Note: FIXME: This property should be readonly, but currently has to
/// be publicly settable due to a Swift compiler bug on Linux. To ensure
/// compatibility of tests between swift-corelibs-xctest and Apple XCTest,
/// this property should not be modified. See
/// https://bugs.swift.org/browse/SR-1129 for details.
public var _name: String
public override var testCaseCount: UInt {
return 1
}
/// The set of expectations made upon this test case.
/// - Note: FIXME: This is meant to be a `private var`, but is marked as
/// `public` here to work around a Swift compiler bug on Linux. To ensure
/// compatibility of tests between swift-corelibs-xctest and Apple XCTest,
/// this property should not be modified. See
/// https://bugs.swift.org/browse/SR-1129 for details.
public var _allExpectations = [XCTestExpectation]()
public override var testRunClass: AnyClass? {
return XCTestCaseRun.self
}
public override func perform(_ run: XCTestRun) {
guard let testRun = run as? XCTestCaseRun else {
fatalError("Wrong XCTestRun class.")
}
XCTCurrentTestCase = self
testRun.start()
invokeTest()
failIfExpectationsNotWaitedFor(_allExpectations)
testRun.stop()
XCTCurrentTestCase = nil
}
/// The designated initializer for SwiftXCTest's XCTestCase.
/// - Note: Like the designated initializer for Apple XCTest's XCTestCase,
/// `-[XCTestCase initWithInvocation:]`, it's rare for anyone outside of
/// XCTest itself to call this initializer.
public required init(name: String, testClosure: XCTestCase throws -> Void) {
_name = "\(self.dynamicType).\(name)"
self.testClosure = testClosure
}
/// Invoking a test performs its setUp, invocation, and tearDown. In
/// general this should not be called directly.
public func invokeTest() {
setUp()
do {
try testClosure(self)
} catch {
recordFailure(
withDescription: "threw error \"\(error)\"",
inFile: "<EXPR>",
atLine: 0,
expected: false)
}
tearDown()
}
/// Records a failure in the execution of the test and is used by all test
/// assertions.
/// - Parameter description: The description of the failure being reported.
/// - Parameter filePath: The file path to the source file where the failure
/// being reported was encountered.
/// - Parameter lineNumber: The line number in the source file at filePath
/// where the failure being reported was encountered.
/// - Parameter expected: `true` if the failure being reported was the
/// result of a failed assertion, `false` if it was the result of an
/// uncaught exception.
public func recordFailure(withDescription description: String, inFile filePath: String, atLine lineNumber: UInt, expected: Bool) {
testRun?.recordFailure(
withDescription: description,
inFile: filePath,
atLine: lineNumber,
expected: expected)
// FIXME: Apple XCTest does not throw a fatal error and crash the test
// process, it merely prevents the remainder of a testClosure
// from execting after it's been determined that it has already
// failed. The following behavior is incorrect.
// FIXME: No regression tests exist for this feature. We may break it
// without ever realizing.
if !continueAfterFailure {
fatalError("Terminating execution due to test failure")
}
}
/// Setup method called before the invocation of any test method in the
/// class.
public class func setUp() {}
/// Teardown method called after the invocation of every test method in the
/// class.
public class func tearDown() {}
}
/// Wrapper function allowing an array of static test case methods to fit
/// the signature required by `XCTMain`
/// - seealso: `XCTMain`
public func testCase<T: XCTestCase>(_ allTests: [(String, T -> () throws -> Void)]) -> XCTestCaseEntry {
let tests: [(String, XCTestCase throws -> Void)] = allTests.map { ($0.0, test($0.1)) }
return (T.self, tests)
}
private func test<T: XCTestCase>(_ testFunc: T -> () throws -> Void) -> XCTestCase throws -> Void {
return { testCaseType in
guard let testCase = testCaseType as? T else {
fatalError("Attempt to invoke test on class \(T.self) with incompatible instance type \(testCaseType.dynamicType)")
}
try testFunc(testCase)()
}
}
extension XCTestCase {
public var continueAfterFailure: Bool {
get {
return true
}
set {
// TODO: When using the Objective-C runtime, XCTest is able to throw an exception from an assert and then catch it at the frame above the test method.
// This enables the framework to effectively stop all execution in the current test.
// There is no such facility in Swift. Until we figure out how to get a compatible behavior,
// we have decided to hard-code the value of 'true' for continue after failure.
}
}
/// 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.
private func failIfExpectationsNotWaitedFor(_ expectations: [XCTestExpectation]) {
if expectations.count > 0 {
recordFailure(
withDescription: "Failed due to unwaited expectations.",
inFile: String(expectations.last!.file),
atLine: expectations.last!.line,
expected: false)
}
}
/// 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`.
public func expectation(withDescription description: String, file: StaticString = #file, line: UInt = #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`.
public func waitForExpectations(withTimeout timeout: NSTimeInterval, file: StaticString = #file, line: UInt = #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(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 = NSRunLoop.currentRunLoop()
let timeoutDate = NSDate(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.runUntilDate(NSDate(timeIntervalSinceNow: 0.01))
} while NSDate().compare(timeoutDate) == NSComparisonResult.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(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: XCTestErrorCode.timeoutWhileWaiting.rawValue,
userInfo: [:])
}
completionHandler(error)
}
}
/// Creates and returns an expectation for a notification.
///
/// - Parameter notificationName: The name of the notification the
/// expectation observes.
/// - Parameter object: The object whose notifications the expectation will
/// receive; that is, only notifications with this object are observed by
/// the test case. If you pass nil, the expectation doesn't use
/// a notification's object to decide whether it is fulfilled.
/// - Parameter handler: If provided, the handler will be invoked when the
/// notification is observed. It will not be invoked on timeout. Use the
/// handler to further investigate if the notification fulfills the
/// expectation.
public func expectation(forNotification notificationName: String, object objectToObserve: AnyObject?, handler: XCNotificationExpectationHandler? = nil) -> XCTestExpectation {
let objectDescription = objectToObserve == nil ? "any object" : "\(objectToObserve!)"
let expectation = self.expectation(withDescription: "Expect notification '\(notificationName)' from " + objectDescription)
// Start observing the notification with specified name and object.
var observer: NSObjectProtocol? = nil
func removeObserver() {
if let observer = observer as? AnyObject {
NSNotificationCenter.defaultCenter().removeObserver(observer)
}
}
weak var weakExpectation = expectation
observer = NSNotificationCenter
.defaultCenter()
.addObserverForName(notificationName,
object: objectToObserve,
queue: nil,
usingBlock: {
notification in
guard let expectation = weakExpectation else {
removeObserver()
return
}
// If the handler is invoked, the test will
// only pass if true is returned.
if let handler = handler {
if handler(notification) {
expectation.fulfill()
removeObserver()
}
} else {
expectation.fulfill()
removeObserver()
}
})
return expectation
}
}