Skip to content

Commit 51dec01

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 e6f3120 commit 51dec01

File tree

9 files changed

+458
-21
lines changed

9 files changed

+458
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
22
xcuserdata
3+
*.xcscmblueprint
34
.build/
45
Output/

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,40 +24,64 @@ 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
38-
./build_script.py --swiftc="/swift/usr/bin/swiftc" --build-dir="/tmp/XCTest_build" --swift-build-dir="/swift/usr" --library-install-path="/swift/usr/lib/swift/linux" --module-install-path="/swift/usr/lib/swift/linux/x86_64"
40+
./build_script.py \
41+
--swiftc "/swift/usr/bin/swiftc" \
42+
--build-dir "/tmp/XCTest_build" \
43+
--swift-build-dir "/swift/usr" \
44+
--foundation-build-dir "/swift//usr/lib/swift/linux" \
45+
--library-install-path "/swift/usr/lib/swift/linux" \
46+
--module-install-path "/swift/usr/lib/swift/linux/x86_64"
3947
```
4048

4149
To run the tests on Linux, pass the `--test` option in combination with options to
4250
install XCTest in your active version of Swift:
4351

4452
```sh
4553
./build_script.py \
46-
--swiftc="/swift/usr/bin/swiftc" \
47-
--build-dir="/tmp/XCTest_build" \
48-
--swift-build-dir="/swift/usr" \
49-
--library-install-path="/swift/usr/lib/swift/linux" \
50-
--module-install-path="/swift/usr/lib/swift/linux/x86_64" \
54+
--swiftc "/swift/usr/bin/swiftc" \
55+
--build-dir "/tmp/XCTest_build" \
56+
--swift-build-dir "/swift/usr" \
57+
--foundation-build-dir "/swift//usr/lib/swift/linux" \
58+
--library-install-path "/swift/usr/lib/swift/linux" \
59+
--module-install-path "/swift/usr/lib/swift/linux/x86_64" \
5160
--test
5261
```
5362

63+
You may add tests for XCTest by including them in the `Tests/Functional/` directory. For an example, see `Tests/Functional/SingleFailingTestCase`.
64+
65+
### On OS X
66+
67+
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:
68+
69+
```
70+
% cd Development
71+
% ls
72+
swift-corelibs-foundation swift-corelibs-xctest
73+
%
74+
```
75+
76+
Unlike on Linux, you do not need to build Foundation prior to building XCTest. The "SwiftXCTest" Xcode project scheme takes care of that for you.
77+
5478
To run the tests on OS X, build and run the `SwiftXCTestFunctionalTests` target in the Xcode project. You may also run them via the command line:
5579

5680
```
5781
xcodebuild -project XCTest.xcodeproj -scheme SwiftXCTestFunctionalTests
5882
```
5983

60-
You may add tests for XCTest by including them in the `Tests/Functional/` directory. For an example, see `Tests/Functional/SingleFailingTestCase`.
84+
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.
6185

6286
### Additional Considerations for Swift on Linux
6387

Sources/XCTest/XCTestCase.swift

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

14+
#if os(Linux) || os(FreeBSD)
15+
import Foundation
16+
#else
17+
import SwiftFoundation
18+
#endif
19+
1420
public protocol XCTestCase : XCTestCaseProvider {
1521
func setUp()
1622
func tearDown()
1723
}
1824

25+
/// A block to be invoked when a call to wait times out or has had all
26+
/// associated expectations fulfilled.
27+
///
28+
/// - Parameter error: If the wait timed out or a failure was raised while
29+
/// waiting, the error's code will specify the type of failure. Otherwise
30+
/// error will be nil.
31+
public typealias XCWaitCompletionHandler = (NSError?) -> ()
32+
1933
extension XCTestCase {
2034

2135
public var continueAfterFailure: Bool {
@@ -61,6 +75,23 @@ extension XCTestCase {
6175

6276
tearDown()
6377

78+
// It is an API violation to create expectations but not wait
79+
// for them to be completed. Notify the user of a mistake via
80+
// a test failure.
81+
if XCTAllExpectations.count > 0 {
82+
let failure = XCTFailure(
83+
message: "Failed due to unwaited expectations.",
84+
failureDescription: "",
85+
expected: true,
86+
file: XCTLatestExpectationLocation.file,
87+
line: XCTLatestExpectationLocation.line)
88+
XCTAllExpectationFailures.append(failure)
89+
}
90+
91+
failures += XCTAllExpectationFailures
92+
XCTLatestExpectationLocation = (file: "", line: 0)
93+
XCTAllExpectationFailures = []
94+
6495
totalDuration += duration
6596

6697
var result = "passed"
@@ -98,4 +129,137 @@ extension XCTestCase {
98129
public func tearDown() {
99130

100131
}
132+
133+
/// Creates and returns an expectation associated with the test case.
134+
///
135+
/// - Parameter description: This string will be displayed in the test log
136+
/// to help diagnose failures.
137+
/// - Parameter file: The file name to use in the error message if
138+
/// this expectation is not waited for. Default is the file
139+
/// containing the call to this method. It is rare to provide this
140+
/// parameter when calling this method.
141+
/// - Parameter line: The line number to use in the error message if the
142+
/// this expectation is not waited for. Default is the line
143+
/// number of the call to this method in the calling file. It is rare to
144+
/// provide this parameter when calling this method.
145+
///
146+
/// - Note: Whereas Objective-C XCTest determines the file and line
147+
/// number of expectations that are created by using symbolication, this
148+
/// implementation opts to take `file` and `line` as parameters instead.
149+
/// As a result, the interface to these methods are not exactly identical
150+
/// between these environments.
151+
public func expectationWithDescription(description: String, file: StaticString = __FILE__, line: UInt = __LINE__) -> XCTestExpectation {
152+
let expectation = XCTestExpectation(description: description)
153+
XCTAllExpectations.append(expectation)
154+
XCTLatestExpectationLocation = (file: file, line: line)
155+
return expectation
156+
}
157+
158+
/// Creates a point of synchronization in the flow of a test. Only one
159+
/// "wait" can be active at any given time, but multiple discrete sequences
160+
/// of { expectations -> wait } can be chained together.
161+
///
162+
/// - Parameter timeout: The amount of time within which all expectation
163+
/// must be fulfilled.
164+
/// - Parameter file: The file name to use in the error message if
165+
/// expectations are not met before the given timeout. Default is the file
166+
/// containing the call to this method. It is rare to provide this
167+
/// parameter when calling this method.
168+
/// - Parameter line: The line number to use in the error message if the
169+
/// expectations are not met before the given timeout. Default is the line
170+
/// number of the call to this method in the calling file. It is rare to
171+
/// provide this parameter when calling this method.
172+
/// - Parameter handler: If provided, the handler will be invoked both on
173+
/// timeout or fulfillment of all expectations. Timeout is always treated
174+
/// as a test failure.
175+
///
176+
/// - Note: Whereas Objective-C XCTest determines the file and line
177+
/// number of the "wait" call using symbolication, this implementation
178+
/// opts to take `file` and `line` as parameters instead. As a result,
179+
/// the interface to these methods are not exactly identical between
180+
/// these environments.
181+
public func waitForExpectationsWithTimeout(timeout: Double, file: StaticString = __FILE__, line: UInt = __LINE__, handler: XCWaitCompletionHandler?) {
182+
// Mirror Objective-C XCTest behavior; display a test failure when
183+
// users wait without having first set expectations.
184+
if XCTAllExpectations.count == 0 {
185+
let failure = XCTFailure(
186+
message: "call made to wait without any expectations having been set",
187+
failureDescription: "API violation",
188+
expected: false,
189+
file: file,
190+
line: line)
191+
XCTAllExpectationFailures.append(failure)
192+
return
193+
}
194+
195+
// Objective-C XCTest outputs the descriptions of every unfulfilled
196+
// expectation. We gather them into this array, which is also used
197+
// to determine failure--a non-empty array meets expectations weren't
198+
// met.
199+
var unfulfilledDescriptions = [String]()
200+
201+
// We continue checking whether expectations have been fulfilled until
202+
// the specified timeout has been reached.
203+
let runLoop = NSRunLoop.currentRunLoop()
204+
let timeoutDate = NSDate(timeIntervalSinceNow: timeout)
205+
while NSDate().compare(timeoutDate) == NSComparisonResult.OrderedAscending {
206+
unfulfilledDescriptions = []
207+
for expectation in XCTAllExpectations {
208+
if !expectation.fulfilled {
209+
unfulfilledDescriptions.append(expectation.description)
210+
}
211+
}
212+
213+
// If we've met all expectations, then break out of the specified
214+
// timeout loop early.
215+
if unfulfilledDescriptions.count == 0 {
216+
break
217+
}
218+
219+
// Otherwise, wait another fraction of a second.
220+
runLoop.runUntilDate(NSDate(timeIntervalSinceNow: 0.01))
221+
}
222+
223+
if unfulfilledDescriptions.count > 0 {
224+
// Not all expectations were fulfilled. Append a failure
225+
// to the array of expectation-based failures.
226+
let descriptions = unfulfilledDescriptions.joinWithSeparator(", ")
227+
let failure = XCTFailure(
228+
message: "Exceeded timeout of \(timeout) seconds, with unfulfilled expectations: \(descriptions)",
229+
failureDescription: "Asynchronous wait failed",
230+
expected: true,
231+
file: file,
232+
line: line)
233+
// FIXME: This should be an instance variable on the test case,
234+
// not a global.
235+
XCTAllExpectationFailures.append(failure)
236+
}
237+
238+
// We've recorded all the failures; clear the expectations that
239+
// were set for this test case, in order to prepare the next test
240+
// case to be run.
241+
// FIXME: This should be an instance variable on the test case.
242+
// Once this is the case, there is no longer any need
243+
// to reset it to an empty array here.
244+
XCTAllExpectations = []
245+
246+
// The handler is invoked regardless of whether the test passed.
247+
if let completionHandler = handler {
248+
var error: NSError? = nil
249+
if unfulfilledDescriptions.count > 0 {
250+
// If the test failed, send an error object.
251+
error = NSError(
252+
domain: "org.swift.XCTestErrorDomain",
253+
code: 0,
254+
userInfo: [:])
255+
}
256+
completionHandler(error)
257+
}
258+
}
101259
}
260+
261+
// FIXME: These should be instance variables on XCTestCase;
262+
// see: https://github.com/apple/swift-corelibs-xctest/pull/40
263+
private var XCTLatestExpectationLocation: (file: StaticString, line: UInt) = ("", 0)
264+
private var XCTAllExpectations = [XCTestExpectation]()
265+
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+
}

0 commit comments

Comments
 (0)