Skip to content

Commit dce290d

Browse files
committed
[XCTestCase] Add asynchronous testing API
Mirror the Objective-C XCTest API to add asynchronous testing support, primarily via two methods: - `XCTestCase.expectationWithDescription()` - `XCTestCase.waitForExpectationsWithTimeout()` These methods' APIs are identical to their Objective-C SDK overlay counterparts, with the following exceptions: 1. If expectations are created but not waited for, Objective-C XCTest generates a test failure. In order to display a failure on the exact line of the last expectation that was created, Objective-C XCTest symbolicates the call stack. It does so using a private framework called `CoreSymbolication.framework`. We don't have this at our disposal for swift-corelibs-xctest, so instead we take `file` and `line` as parameters, defaulting to `__FILE__` and `__LINE__`. By doing so, we provide the same failure highlighting functionality while also maintaining a (nearly) identical API. 2. If `waitForExpectationsWithTimeout()` fails, Objective-C XCTest generates a test failure on the exact line that method was called, using the same technique from (1). For the same reason, this method also takes `file` and `line` parameters in swift-corelibs-xctest. 3. `waitForExpectationsWithTimeout()` takes a handler callback, which in Objective-C XCTest is passed an instance of `NSError`. swift-corelibs-xctest doesn't depend upon swift-corelibs-foundation, and so does not have access to `NSError`. Instead, it defines a shim, `XCTError`.
1 parent 0970216 commit dce290d

File tree

5 files changed

+397
-0
lines changed

5 files changed

+397
-0
lines changed

Sources/XCTest/XCTestCase.swift

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
// Base protocol (and extension with default methods) for test cases
1212
//
1313

14+
#if os(Linux) || os(FreeBSD)
15+
// FIXME: Import swift-corelibs-foundation or swift-corelibs-libdispatch
16+
#else
17+
import Foundation
18+
#endif
19+
1420
public protocol XCTestCase : XCTestCaseProvider {
1521
func setUp()
1622
func tearDown()
@@ -61,6 +67,23 @@ extension XCTestCase {
6167

6268
tearDown()
6369

70+
// It is an API violation to create expectations but not wait
71+
// for them to be completed. Notify the user of a mistake via
72+
// a test failure.
73+
if XCTAllExpectations.count > 0 {
74+
let failure = XCTFailure(
75+
message: "Failed due to unwaited expectations.",
76+
failureDescription: "",
77+
expected: true,
78+
file: XCTLatestExpectationLocation.file,
79+
line: XCTLatestExpectationLocation.line)
80+
XCTAllExpectationFailures.append(failure)
81+
}
82+
83+
failures += XCTAllExpectationFailures
84+
XCTLatestExpectationLocation = (file: "", line: 0)
85+
XCTAllExpectationFailures = []
86+
6487
totalDuration += duration
6588

6689
var result = "passed"
@@ -98,4 +121,132 @@ extension XCTestCase {
98121
public func tearDown() {
99122

100123
}
124+
125+
/// Creates and returns an expectation associated with the test case.
126+
///
127+
/// - Parameter description: This string will be displayed in the test log
128+
/// to help diagnose failures.
129+
/// - Parameter file: The file name to use in the error message if
130+
/// this expectation is not waited for. Default is the file
131+
/// containing the call to this method. It is rare to provide this
132+
/// parameter when calling this method.
133+
/// - Parameter line: The line number to use in the error message if the
134+
/// this expectation is not waited for. Default is the line
135+
/// number of the call to this method in the calling file. It is rare to
136+
/// provide this parameter when calling this method.
137+
///
138+
/// - Note: Whereas Objective-C XCTest determines the file and line
139+
/// number of expectations that are created by using symbolication, this
140+
/// implementation opts to take `file` and `line` as parameters instead.
141+
/// As a result, the interface to these methods are not exactly identical
142+
/// between these environments.
143+
public func expectationWithDescription(description: String, file: StaticString = __FILE__, line: UInt = __LINE__) -> XCTestExpectation {
144+
let expectation = XCTestExpectation(description: description)
145+
XCTAllExpectations.append(expectation)
146+
XCTLatestExpectationLocation = (file: file, line: line)
147+
return expectation
148+
}
149+
150+
/// Creates a point of synchronization in the flow of a test. Only one
151+
/// "wait" can be active at any given time, but multiple discrete sequences
152+
/// of { expectations -> wait } can be chained together.
153+
///
154+
/// - Parameter timeout: The amount of time within which all expectation
155+
/// must be fulfilled.
156+
/// - Parameter file: The file name to use in the error message if
157+
/// expectations are not met before the given timeout. Default is the file
158+
/// containing the call to this method. It is rare to provide this
159+
/// parameter when calling this method.
160+
/// - Parameter line: The line number to use in the error message if the
161+
/// expectations are not met before the given timeout. Default is the line
162+
/// number of the call to this method in the calling file. It is rare to
163+
/// provide this parameter when calling this method.
164+
/// - Parameter handler: If provided, the handler will be invoked both on
165+
/// timeout or fulfillment of all expectations. Timeout is always treated
166+
/// as a test failure.
167+
///
168+
/// - Note: Whereas Objective-C XCTest determines the file and line
169+
/// number of the "wait" call using symbolication, this implementation
170+
/// opts to take `file` and `line` as parameters instead. As a result,
171+
/// the interface to these methods are not exactly identical between
172+
/// these environments.
173+
public func waitForExpectationsWithTimeout(timeout: Double, file: StaticString = __FILE__, line: UInt = __LINE__, handler: XCWaitCompletionHandler?) {
174+
// Mirror Objective-C XCTest behavior; display a test failure when
175+
// users wait without having first set expectations.
176+
if XCTAllExpectations.count == 0 {
177+
let failure = XCTFailure(
178+
message: "call made to wait without any expectations having been set",
179+
failureDescription: "API violation",
180+
expected: false,
181+
file: file,
182+
line: line)
183+
XCTAllExpectationFailures.append(failure)
184+
return
185+
}
186+
187+
// Objective-C XCTest outputs the descriptions of every unfulfilled
188+
// expectation. We gather them into this array, which is also used
189+
// to determine failure--a non-empty array meets expectations weren't
190+
// met.
191+
var unfulfilledDescriptions = [String]()
192+
193+
// FIXME: Ideally we'd poll the expectations frequently and stop
194+
// blocking as soon as they've been met. For the time being,
195+
// however, we wait the full timeout period every time.
196+
let semaphore = dispatch_semaphore_create(0)
197+
let when = dispatch_time(
198+
DISPATCH_TIME_NOW,
199+
Int64(timeout * Double(NSEC_PER_SEC)))
200+
dispatch_after(when, dispatch_get_global_queue(0, 0)) {
201+
for expectation in XCTAllExpectations {
202+
if !expectation.fulfilled {
203+
unfulfilledDescriptions.append(expectation.description)
204+
}
205+
}
206+
207+
if unfulfilledDescriptions.count > 0 {
208+
// Not all expectations were fulfilled. Append a failure
209+
// to the array of expectation-based failures.
210+
let descriptions = unfulfilledDescriptions.joinWithSeparator(", ")
211+
let failure = XCTFailure(
212+
message: "Exceeded timeout of \(timeout) seconds, with unfulfilled expectations: \(descriptions)",
213+
failureDescription: "Asynchronous wait failed",
214+
expected: true,
215+
file: file,
216+
line: line)
217+
// FIXME: This should be an instance variable on the test case,
218+
// not a global.
219+
XCTAllExpectationFailures.append(failure)
220+
}
221+
222+
// We've recorded all the failures; clear the expectations that
223+
// were set for this test case, in order to prepare the next test
224+
// case to be run.
225+
// FIXME: This should be an instance variable on the test case.
226+
// Once this is the case, there is no longer any need
227+
// to reset it to an empty array here.
228+
XCTAllExpectations = []
229+
dispatch_semaphore_signal(semaphore)
230+
}
231+
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
232+
233+
// The handler is invoked regardless of whether the test passed.
234+
if let completionHandler = handler {
235+
var error: XCTError? = nil
236+
if unfulfilledDescriptions.count > 0 {
237+
// If the test failed, send an error object.
238+
error = XCTError(
239+
domain: "org.swift.XCTestErrorDomain",
240+
code: 0,
241+
userInfo: [:])
242+
}
243+
completionHandler(error)
244+
}
245+
}
101246
}
247+
248+
// FIXME: These should be instance variables on XCTestCase;
249+
// see: https://github.com/apple/swift-corelibs-xctest/pull/40
250+
private var XCTLatestExpectationLocation: (file: StaticString, line: UInt) = ("", 0)
251+
private var XCTAllExpectations = [XCTestExpectation]()
252+
private var XCTAllExpectationFailures = [XCTFailure]()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// XCTestExpectation.swift
11+
// Expectations represent specific conditions in asynchronous testing.
12+
//
13+
14+
/// Expectations represent specific conditions in asynchronous testing.
15+
public class XCTestExpectation {
16+
internal var fulfilled = false
17+
internal let description: String
18+
19+
internal init(description: String) {
20+
self.description = description
21+
}
22+
23+
/// Marks an expectation as having been met. It's an error to call this
24+
/// method on an expectation that has already been fulfilled, or when the
25+
/// test case that vended the expectation has already completed.
26+
public func fulfill() {
27+
fulfilled = true
28+
}
29+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// XCWaitCompletionHandler.swift
11+
// A block to be invoked when a call to wait times out or has had all
12+
// associated expectations fulfilled.
13+
//
14+
15+
#if os(Linux) || os(FreeBSD)
16+
/// On platforms where NSError is not available, this class steps in as a
17+
/// similar, but not identical, interface.
18+
public class XCTError {
19+
/// The domain and code define the error. Domains are described by names
20+
/// that are arbitrary strings used to differentiate groups of codes;
21+
/// for custom domain using reverse-DNS naming will help avoid
22+
/// conflicts. Codes are domain-specific.
23+
public let domain: String
24+
public let code: Int
25+
26+
/// Additional info which may be used to describe the error further.
27+
public let userInfo: [String: String]
28+
29+
// An internal initializer that mirrors the Objective-C Foundation
30+
// API. This and the typealias below allow us to instantiate an
31+
// XCTError easily on any platform.
32+
internal init(domain: String, code: Int, userInfo: [String: String]) {
33+
self.domain = domain
34+
self.code = code
35+
self.userInfo = userInfo
36+
}
37+
}
38+
39+
/// A block to be invoked when a call to wait times out or has had all
40+
/// associated expectations fulfilled.
41+
///
42+
/// - Parameter error: If the wait timed out or a failure was raised while
43+
/// waiting, the error's code will specify the type of failure. Otherwise
44+
/// error will be nil.
45+
public typealias XCWaitCompletionHandler = (XCTError?) -> ()
46+
#else
47+
import Foundation
48+
49+
/// A block to be invoked when a call to wait times out or has had all
50+
/// associated expectations fulfilled.
51+
///
52+
/// - Parameter error: If the wait timed out or a failure was raised while
53+
/// waiting, the error's code will specify the type of failure. Otherwise
54+
/// error will be nil.
55+
public typealias XCWaitCompletionHandler = (NSError?) -> ()
56+
57+
// An alias that allows us to reference an XCTError easily on any platform.
58+
internal typealias XCTError = NSError
59+
#endif

0 commit comments

Comments
 (0)