Skip to content

Commit df734de

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`. In order to implement the asynchronous APIs, swift-corelibs-xctest now has a dependency upon swift-corelibs-foundation. Bump minimum deployment targets for XCTest to the same required by Foundation.
1 parent 96772ca commit df734de

File tree

11 files changed

+607
-22
lines changed

11 files changed

+607
-22
lines changed

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,58 @@ The rest of this document will focus on how this version of XCTest differs from
2424

2525
## Working on XCTest
2626

27+
### On Linux
28+
2729
XCTest can be built as part of the overall Swift package. When following [the instructions for building Swift](http://www.github.com/apple/swift), pass the `--xctest` option to the build script:
2830

2931
```sh
3032
swift/utils/build-script --xctest
3133
```
3234

33-
If you want to build just XCTest, use the `build_script.py` script at the root of the project. The `master` version of XCTest must be built with the `master` version of Swift.
35+
If you want to build just XCTest, use the `build_script.py` script at the root of the project. The `master` version of XCTest must be built with the `master` version of Swift. XCTest has a dependency upon Foundation, so you must have built the `master` version of that as well.
3436

3537
If your install of Swift is located at `/swift` and you wish to install XCTest into that same location, here is a sample invocation of the build script:
3638

3739
```sh
3840
./build_script.py \
3941
--swiftc="/swift/usr/bin/swiftc" \
4042
--build-dir="/tmp/XCTest_build" \
43+
--foundation-build-dir "/swift//usr/lib/swift/linux" \
4144
--library-install-path="/swift/usr/lib/swift/linux" \
4245
--module-install-path="/swift/usr/lib/swift/linux/x86_64"
4346
```
4447

4548
To run the tests on Linux, use the `--test` option:
4649

4750
```sh
48-
./build_script.py --swiftc="/swift/usr/bin/swiftc" --test
51+
./build_script.py \
52+
--swiftc="/swift/usr/bin/swiftc" \
53+
--foundation-build-dir "/swift/usr/lib/swift/linux" \
54+
--test
55+
```
56+
57+
You may add tests for XCTest by including them in the `Tests/Functional/` directory. For an example, see `Tests/Functional/SingleFailingTestCase`.
58+
59+
### On OS X
60+
61+
You may build XCTest via the "SwiftXCTest" scheme in `XCTest.xcworkspace`. The workspace assumes that Foundation and XCTest are checked out from GitHub in sibling directories. For example:
62+
4963
```
64+
% cd Development
65+
% ls
66+
swift-corelibs-foundation swift-corelibs-xctest
67+
%
68+
```
69+
70+
Unlike on Linux, you do not need to build Foundation prior to building XCTest. The "SwiftXCTest" Xcode scheme takes care of that for you.
5071

5172
To run the tests on OS X, build and run the `SwiftXCTestFunctionalTests` target in the Xcode workspace. You may also run them via the command line:
5273

5374
```
5475
xcodebuild -workspace XCTest.xcworkspace -scheme SwiftXCTestFunctionalTests
5576
```
5677

57-
You may add tests for XCTest by including them in the `Tests/Functional/` directory. For an example, see `Tests/Functional/SingleFailingTestCase`.
78+
When adding tests to the `Tests/Functional` directory, make sure they can be opened in the `XCTest.xcworkspace` by adding references to them, but do not add them to any of the targets.
5879

5980
### Additional Considerations for Swift on Linux
6081

Sources/XCTest/XCTestCase.swift

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
// Base class for test cases
1212
//
1313

14+
#if os(Linux) || os(FreeBSD)
15+
import Foundation
16+
#else
17+
import SwiftFoundation
18+
#endif
19+
1420
/// This is a compound type used by `XCTMain` to represent tests to run. It combines an
1521
/// `XCTestCase` subclass type with the list of test methods to invoke on the test case.
1622
/// This type is intended to be produced by the `testCase` helper function.
@@ -48,6 +54,12 @@ private func test<T: XCTestCase>(testFunc: T -> () throws -> Void) -> XCTestCase
4854
}
4955
}
5056

57+
// FIXME: Expectations should be stored in an instance variable defined on
58+
// XCTestCase, but when so defined Linux tests fail with "hidden symbol
59+
// isn't defined". Use a global for the time being, as this seems to
60+
// appease the Linux compiler.
61+
private var XCTAllExpectations = [XCTestExpectation]()
62+
5163
extension XCTestCase {
5264

5365
public var continueAfterFailure: Bool {
@@ -92,6 +104,8 @@ extension XCTestCase {
92104
}
93105

94106
testCase.tearDown()
107+
testCase.failIfExpectationsNotWaitedFor(XCTAllExpectations)
108+
XCTAllExpectations = []
95109

96110
totalDuration += duration
97111

@@ -122,4 +136,161 @@ extension XCTestCase {
122136

123137
XCTPrint("Executed \(tests.count) test\(testCountSuffix), with \(totalFailures) failure\(failureSuffix) (\(unexpectedFailures) unexpected) in \(printableStringForTimeInterval(totalDuration)) (\(printableStringForTimeInterval(overallDuration))) seconds")
124138
}
139+
140+
/// It is an API violation to create expectations but not wait for them to
141+
/// be completed. Notify the user of a mistake via a test failure.
142+
private func failIfExpectationsNotWaitedFor(expectations: [XCTestExpectation]) {
143+
if expectations.count > 0 {
144+
let failure = XCTFailure(
145+
message: "Failed due to unwaited expectations.",
146+
failureDescription: "",
147+
expected: false,
148+
file: expectations.last!.file,
149+
line: expectations.last!.line)
150+
if let failureHandler = XCTFailureHandler {
151+
failureHandler(failure)
152+
}
153+
}
154+
}
155+
156+
/// Creates and returns an expectation associated with the test case.
157+
///
158+
/// - Parameter description: This string will be displayed in the test log
159+
/// to help diagnose failures.
160+
/// - Parameter file: The file name to use in the error message if
161+
/// this expectation is not waited for. Default is the file
162+
/// containing the call to this method. It is rare to provide this
163+
/// parameter when calling this method.
164+
/// - Parameter line: The line number to use in the error message if the
165+
/// this expectation is not waited for. Default is the line
166+
/// number of the call to this method in the calling file. It is rare to
167+
/// provide this parameter when calling this method.
168+
///
169+
/// - Note: Whereas Objective-C XCTest determines the file and line
170+
/// number of expectations that are created by using symbolication, this
171+
/// implementation opts to take `file` and `line` as parameters instead.
172+
/// As a result, the interface to these methods are not exactly identical
173+
/// between these environments. To ensure compatibility of tests between
174+
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
175+
/// explicit values for `file` and `line`.
176+
public func expectationWithDescription(description: String, file: StaticString = #file, line: UInt = #line) -> XCTestExpectation {
177+
let expectation = XCTestExpectation(
178+
description: description,
179+
file: file,
180+
line: line,
181+
testCase: self)
182+
XCTAllExpectations.append(expectation)
183+
return expectation
184+
}
185+
186+
/// Creates a point of synchronization in the flow of a test. Only one
187+
/// "wait" can be active at any given time, but multiple discrete sequences
188+
/// of { expectations -> wait } can be chained together.
189+
///
190+
/// - Parameter timeout: The amount of time within which all expectation
191+
/// must be fulfilled.
192+
/// - Parameter file: The file name to use in the error message if
193+
/// expectations are not met before the given timeout. Default is the file
194+
/// containing the call to this method. It is rare to provide this
195+
/// parameter when calling this method.
196+
/// - Parameter line: The line number to use in the error message if the
197+
/// expectations are not met before the given timeout. Default is the line
198+
/// number of the call to this method in the calling file. It is rare to
199+
/// provide this parameter when calling this method.
200+
/// - Parameter handler: If provided, the handler will be invoked both on
201+
/// timeout or fulfillment of all expectations. Timeout is always treated
202+
/// as a test failure.
203+
///
204+
/// - Note: Whereas Objective-C XCTest determines the file and line
205+
/// number of the "wait" call using symbolication, this implementation
206+
/// opts to take `file` and `line` as parameters instead. As a result,
207+
/// the interface to these methods are not exactly identical between
208+
/// these environments. To ensure compatibility of tests between
209+
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
210+
/// explicit values for `file` and `line`.
211+
public func waitForExpectationsWithTimeout(timeout: NSTimeInterval, file: StaticString = #file, line: UInt = #line, handler: XCWaitCompletionHandler?) {
212+
// Mirror Objective-C XCTest behavior; display an unexpected test
213+
// failure when users wait without having first set expectations.
214+
// FIXME: Objective-C XCTest raises an exception for most "API
215+
// violation" failures, including this one. Normally this causes
216+
// the test to stop cold. swift-corelibs-xctest does not stop,
217+
// and executes the rest of the test. This discrepancy should be
218+
// fixed.
219+
if XCTAllExpectations.count == 0 {
220+
let failure = XCTFailure(
221+
message: "call made to wait without any expectations having been set.",
222+
failureDescription: "API violation",
223+
expected: false,
224+
file: file,
225+
line: line)
226+
if let failureHandler = XCTFailureHandler {
227+
failureHandler(failure)
228+
}
229+
return
230+
}
231+
232+
// Objective-C XCTest outputs the descriptions of every unfulfilled
233+
// expectation. We gather them into this array, which is also used
234+
// to determine failure--a non-empty array meets expectations weren't
235+
// met.
236+
var unfulfilledDescriptions = [String]()
237+
238+
// We continue checking whether expectations have been fulfilled until
239+
// the specified timeout has been reached.
240+
// FIXME: Instead of polling the expectations to check whether they've
241+
// been fulfilled, it would be more efficient to use a runloop
242+
// source that can be signaled to wake up when an expectation is
243+
// fulfilled.
244+
let runLoop = NSRunLoop.currentRunLoop()
245+
let timeoutDate = NSDate(timeIntervalSinceNow: timeout)
246+
repeat {
247+
unfulfilledDescriptions = []
248+
for expectation in XCTAllExpectations {
249+
if !expectation.fulfilled {
250+
unfulfilledDescriptions.append(expectation.description)
251+
}
252+
}
253+
254+
// If we've met all expectations, then break out of the specified
255+
// timeout loop early.
256+
if unfulfilledDescriptions.count == 0 {
257+
break
258+
}
259+
260+
// Otherwise, wait another fraction of a second.
261+
runLoop.runUntilDate(NSDate(timeIntervalSinceNow: 0.01))
262+
} while NSDate().compare(timeoutDate) == NSComparisonResult.OrderedAscending
263+
264+
if unfulfilledDescriptions.count > 0 {
265+
// Not all expectations were fulfilled. Append a failure
266+
// to the array of expectation-based failures.
267+
let descriptions = unfulfilledDescriptions.joinWithSeparator(", ")
268+
let failure = XCTFailure(
269+
message: "Exceeded timeout of \(timeout) seconds, with unfulfilled expectations: \(descriptions)",
270+
failureDescription: "Asynchronous wait failed",
271+
expected: true,
272+
file: file,
273+
line: line)
274+
if let failureHandler = XCTFailureHandler {
275+
failureHandler(failure)
276+
}
277+
}
278+
279+
// We've recorded all the failures; clear the expectations that
280+
// were set for this test case.
281+
XCTAllExpectations = []
282+
283+
// The handler is invoked regardless of whether the test passed.
284+
if let completionHandler = handler {
285+
var error: NSError? = nil
286+
if unfulfilledDescriptions.count > 0 {
287+
// If the test failed, send an error object.
288+
error = NSError(
289+
domain: "org.swift.XCTestErrorDomain",
290+
code: 0,
291+
userInfo: [:])
292+
}
293+
completionHandler(error)
294+
}
295+
}
125296
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 let description: String
17+
internal let file: StaticString
18+
internal let line: UInt
19+
20+
internal var fulfilled = false
21+
internal weak var testCase: XCTestCase?
22+
23+
internal init(description: String, file: StaticString, line: UInt, testCase: XCTestCase) {
24+
self.description = description
25+
self.file = file
26+
self.line = line
27+
self.testCase = testCase
28+
}
29+
30+
/// Marks an expectation as having been met. It's an error to call this
31+
/// method on an expectation that has already been fulfilled, or when the
32+
/// test case that vended the expectation has already completed.
33+
///
34+
/// - Parameter file: The file name to use in the error message if
35+
/// expectations are not met before the given timeout. Default is the file
36+
/// containing the call to this method. It is rare to provide this
37+
/// parameter when calling this method.
38+
/// - Parameter line: The line number to use in the error message if the
39+
/// expectations are not met before the given timeout. Default is the line
40+
/// number of the call to this method in the calling file. It is rare to
41+
/// provide this parameter when calling this method.
42+
///
43+
/// - Note: Whereas Objective-C XCTest determines the file and line
44+
/// number the expectation was fulfilled using symbolication, this
45+
/// implementation opts to take `file` and `line` as parameters instead.
46+
/// As a result, the interface to these methods are not exactly identical
47+
/// between these environments. To ensure compatibility of tests between
48+
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
49+
/// explicit values for `file` and `line`.
50+
public func fulfill(file: StaticString = #file, line: UInt = #line) {
51+
// FIXME: Objective-C XCTest emits failures when expectations are
52+
// fulfilled after the test cases that generated those
53+
// expectations have completed. Similarly, this should cause an
54+
// error as well.
55+
if fulfilled {
56+
// Mirror Objective-C XCTest behavior: treat multiple calls to
57+
// fulfill() as an unexpected failure.
58+
let failure = XCTFailure(
59+
message: "multiple calls made to XCTestExpectation.fulfill() for \(description).",
60+
failureDescription: "API violation",
61+
expected: false,
62+
file: file,
63+
line: line)
64+
if let failureHandler = XCTFailureHandler {
65+
failureHandler(failure)
66+
}
67+
} else {
68+
fulfilled = true
69+
}
70+
}
71+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2015 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 closure invoked by XCTestCase when a wait for expectations to be
12+
// fulfilled times out.
13+
//
14+
15+
#if os(Linux) || os(FreeBSD)
16+
import Foundation
17+
#else
18+
import SwiftFoundation
19+
#endif
20+
21+
/// A block to be invoked when a call to wait times out or has had all
22+
/// associated expectations fulfilled.
23+
///
24+
/// - Parameter error: If the wait timed out or a failure was raised while
25+
/// waiting, the error's code will specify the type of failure. Otherwise
26+
/// error will be nil.
27+
public typealias XCWaitCompletionHandler = (NSError?) -> ()

0 commit comments

Comments
 (0)