Skip to content

Commit 6493cbb

Browse files
committed
Use XCTestRun to execute tests and report results
A major goal for swift-corelibs-xctest is API parity with Apple XCTest. This adds the largest missing API in swift-corelibs-xctest: `XCTestRun`. In Apple XCTest, `XCTestRun` is responsible for keeping track of the result of a test run. It's an integral part of how Apple XCTest works. swift-corelibs-xctest, on the other hand, used a global array of `XCTRun` structs to keep track of how many tests passed/failed. While it may have been possible to tack on `XCTestRun` to the swift-corelibs-xctest mechanism for failure reporting, this commit instead fully integrates it. As a result, the changes are widespread: gone is `XCTFailureHandler`, `XCTRun`, and other internal structures. In their place, welcome the Apple XCTest public APIs: the `XCTest` abstract class, `XCTestRun`, and its subclasses `XCTestCaseRun` and `XCTestSuiteRun`. In conjunction with the new `XCTestSuite`-related observation methods from swiftlang#84, test reporting is now done exclusively through `XCTestObservation`. As a result, test output is now nearly identical to Apple XCTest.
1 parent f935137 commit 6493cbb

File tree

26 files changed

+824
-389
lines changed

26 files changed

+824
-389
lines changed

Sources/XCTest/PrintObserver.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 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+
// PrintObserver.swift
11+
// Prints test progress to stdout.
12+
//
13+
14+
#if os(Linux) || os(FreeBSD)
15+
import Foundation
16+
#else
17+
import SwiftFoundation
18+
#endif
19+
20+
/// Prints textual representations of each XCTestObservation event to stdout.
21+
/// Mirrors the Apple XCTest output exactly.
22+
internal class PrintObserver: XCTestObservation {
23+
func testBundleWillStart(testBundle: NSBundle) {}
24+
25+
func testSuiteWillStart(testSuite: XCTestSuite) {
26+
printAndFlush("Test Suite '\(testSuite.name)' started at \(dateFormatter.stringFromDate(testSuite.testRun!.startDate!))")
27+
}
28+
29+
func testCaseWillStart(testCase: XCTestCase) {
30+
printAndFlush("Test Case '\(testCase.name)' started at \(dateFormatter.stringFromDate(testCase.testRun!.startDate!))")
31+
}
32+
33+
func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {
34+
let file = filePath ?? "<unknown>"
35+
printAndFlush("\(file):\(lineNumber): error: \(testCase.name) : \(description)")
36+
}
37+
38+
func testCaseDidFinish(testCase: XCTestCase) {
39+
let testRun = testCase.testRun!
40+
let verb = testRun.hasSucceeded ? "passed" : "failed"
41+
// FIXME: Apple XCTest does not print a period after "(N seconds)".
42+
// The trailing period here should be removed and the functional
43+
// test suite should be updated.
44+
printAndFlush("Test Case '\(testCase.name)' \(verb) (\(formatTimeInterval(testRun.totalDuration)) seconds).")
45+
}
46+
47+
func testSuiteDidFinish(testSuite: XCTestSuite) {
48+
let testRun = testSuite.testRun!
49+
let verb = testRun.hasSucceeded ? "passed" : "failed"
50+
printAndFlush("Test Suite '\(testSuite.name)' \(verb) at \(dateFormatter.stringFromDate(testRun.stopDate!))")
51+
52+
let tests = testRun.executionCount == 1 ? "test" : "tests"
53+
let failures = testRun.totalFailureCount == 1 ? "failure" : "failures"
54+
printAndFlush(
55+
"\t Executed \(testRun.executionCount) \(tests), " +
56+
"with \(testRun.totalFailureCount) \(failures) (\(testRun.unexpectedExceptionCount) unexpected) " +
57+
"in \(formatTimeInterval(testRun.testDuration)) (\(formatTimeInterval(testRun.totalDuration))) seconds"
58+
)
59+
}
60+
61+
func testBundleDidFinish(testBundle: NSBundle) {}
62+
63+
private lazy var dateFormatter: NSDateFormatter = {
64+
let formatter = NSDateFormatter()
65+
formatter.dateFormat = "HH:mm:ss.SSS"
66+
return formatter
67+
}()
68+
69+
private func printAndFlush(message: String) {
70+
print(message)
71+
fflush(stdout)
72+
}
73+
74+
private func formatTimeInterval(timeInterval: NSTimeInterval) -> String {
75+
return String(round(timeInterval * 1000.0) / 1000.0)
76+
}
77+
}

Sources/XCTest/XCAbstractTest.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,46 @@ public class XCTest {
2626
fatalError("Must be overridden by subclasses.")
2727
}
2828

29+
/// The `XCTestRun` subclass that will be instantiated when the test is run
30+
/// to hold the test's results. Must be overridden by subclasses.
31+
public var testRunClass: AnyClass? {
32+
fatalError("Must be overridden by subclasses.")
33+
}
34+
35+
/// The test run object that executed the test, an instance of
36+
/// testRunClass. If the test has not yet been run, this will be nil.
37+
/// - Note: FIXME: This property is meant to be `private(set)`. It is
38+
/// publicly settable for now due to a Swift compiler bug on Linux. To
39+
/// ensure compatibility of tests between swift-corelibs-xctest and Apple
40+
/// XCTest, you should not set this property. See
41+
/// https://bugs.swift.org/browse/SR-1129 for details.
42+
public public(set) var testRun: XCTestRun? = nil
43+
44+
/// The method through which tests are executed. Must be overridden by
45+
/// subclasses.
46+
public func perform(run: XCTestRun) {
47+
fatalError("Must be overridden by subclasses.")
48+
}
49+
50+
/// Creates an instance of the `testRunClass` and passes it as a parameter
51+
/// to `perform()`.
52+
public func run() {
53+
guard let testRunType = testRunClass as? XCTestRun.Type else {
54+
fatalError("XCTest.testRunClass must be a kind of XCTestRun.")
55+
}
56+
testRun = testRunType.init(test: self)
57+
perform(testRun!)
58+
}
59+
2960
/// Setup method called before the invocation of each test method in the
3061
/// class.
3162
public func setUp() {}
3263

3364
/// Teardown method called after the invocation of each test method in the
3465
/// class.
3566
public func tearDown() {}
67+
68+
// FIXME: This initializer is required due to a Swift compiler bug on Linux.
69+
// It should be removed once the bug is fixed.
70+
public init() {}
3671
}

Sources/XCTest/XCTAssert.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,12 @@ private func _XCTEvaluateAssertion(assertion: _XCTAssertion, @autoclosure messag
8787
case .success:
8888
return
8989
default:
90-
if let handler = XCTFailureHandler {
91-
handler(XCTFailure(message: message(), failureDescription: result.failureDescription(assertion), expected: result.isExpected, file: file, line: line))
90+
if let currentTestCase = XCTCurrentTestCase {
91+
currentTestCase.recordFailure(
92+
withDescription: "\(result.failureDescription(assertion)) - \(message())",
93+
inFile: String(file),
94+
atLine: line,
95+
expected: result.isExpected)
9296
}
9397
}
9498
}

Sources/XCTest/XCTestCase.swift

Lines changed: 92 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
/// - seealso: `XCTMain`
2525
public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(String, XCTestCase throws -> Void)])
2626

27+
// A global pointer to the currently running test case. This is required in
28+
// order for XCTAssert functions to report failures.
29+
internal var XCTCurrentTestCase: XCTestCase?
30+
2731
public class XCTestCase: XCTest {
32+
private let testClosure: XCTestCase throws -> Void
2833

2934
/// The name of the test case, consisting of its class name and the method
3035
/// name it will run.
@@ -39,6 +44,10 @@ public class XCTestCase: XCTest {
3944
/// https://bugs.swift.org/browse/SR-1129 for details.
4045
public var _name: String
4146

47+
public override var testCaseCount: UInt {
48+
return 1
49+
}
50+
4251
/// The set of expectations made upon this test case.
4352
/// - Note: FIXME: This is meant to be a `private var`, but is marked as
4453
/// `public` here to work around a Swift compiler bug on Linux. To ensure
@@ -47,8 +56,74 @@ public class XCTestCase: XCTest {
4756
/// https://bugs.swift.org/browse/SR-1129 for details.
4857
public var _allExpectations = [XCTestExpectation]()
4958

50-
public required override init() {
51-
_name = "\(self.dynamicType).<unknown>"
59+
public override var testRunClass: AnyClass? {
60+
return XCTestCaseRun.self
61+
}
62+
63+
public override func perform(run: XCTestRun) {
64+
guard let testRun = run as? XCTestCaseRun else {
65+
fatalError("Wrong XCTestRun class.")
66+
}
67+
68+
XCTCurrentTestCase = self
69+
testRun.start()
70+
invokeTest()
71+
failIfExpectationsNotWaitedFor(_allExpectations)
72+
testRun.stop()
73+
XCTCurrentTestCase = nil
74+
}
75+
76+
/// The designated initializer for SwiftXCTest's XCTestCase.
77+
/// - Note: Like the designated initializer for Apple XCTest's XCTestCase,
78+
/// `-[XCTestCase initWithInvocation:]`, it's rare for anyone outside of
79+
/// XCTest itself to call this initializer.
80+
public required init(name: String, testClosure: XCTestCase throws -> Void) {
81+
_name = "\(self.dynamicType).\(name)"
82+
self.testClosure = testClosure
83+
}
84+
85+
/// Invoking a test performs its setUp, invocation, and tearDown. In
86+
/// general this should not be called directly.
87+
public func invokeTest() {
88+
setUp()
89+
do {
90+
try testClosure(self)
91+
} catch {
92+
recordFailure(
93+
withDescription: "threw error \"\(error)\"",
94+
inFile: "<EXPR>",
95+
atLine: 0,
96+
expected: false)
97+
}
98+
tearDown()
99+
}
100+
101+
/// Records a failure in the execution of the test and is used by all test
102+
/// assertions.
103+
/// - Parameter description: The description of the failure being reported.
104+
/// - Parameter filePath: The file path to the source file where the failure
105+
/// being reported was encountered.
106+
/// - Parameter lineNumber: The line number in the source file at filePath
107+
/// where the failure being reported was encountered.
108+
/// - Parameter expected: `true` if the failure being reported was the
109+
/// result of a failed assertion, `false` if it was the result of an
110+
/// uncaught exception.
111+
public func recordFailure(withDescription description: String, inFile filePath: String, atLine lineNumber: UInt, expected: Bool) {
112+
testRun?.recordFailure(
113+
withDescription: description,
114+
inFile: filePath,
115+
atLine: lineNumber,
116+
expected: expected)
117+
118+
// FIXME: Apple XCTest does not throw a fatal error and crash the test
119+
// process, it merely prevents the remainder of a testClosure
120+
// from execting after it's been determined that it has already
121+
// failed. The following behavior is incorrect.
122+
// FIXME: No regression tests exist for this feature. We may break it
123+
// without ever realizing.
124+
if !continueAfterFailure {
125+
fatalError("Terminating execution due to test failure")
126+
}
52127
}
53128
}
54129

@@ -84,89 +159,15 @@ extension XCTestCase {
84159
}
85160
}
86161

87-
internal static func invokeTests(tests: [(String, XCTestCase throws -> Void)]) {
88-
let observationCenter = XCTestObservationCenter.shared()
89-
90-
var totalDuration = 0.0
91-
var totalFailures = 0
92-
var unexpectedFailures = 0
93-
let overallDuration = measureTimeExecutingBlock {
94-
for (name, test) in tests {
95-
let testCase = self.init()
96-
testCase._name = "\(testCase.dynamicType).\(name)"
97-
98-
var failures = [XCTFailure]()
99-
XCTFailureHandler = { failure in
100-
observationCenter.testCase(testCase,
101-
didFailWithDescription: failure.failureMessage,
102-
inFile: String(failure.file),
103-
atLine: failure.line)
104-
105-
if !testCase.continueAfterFailure {
106-
failure.emit(testCase.name)
107-
fatalError("Terminating execution due to test failure", file: failure.file, line: failure.line)
108-
} else {
109-
failures.append(failure)
110-
}
111-
}
112-
113-
XCTPrint("Test Case '\(testCase.name)' started.")
114-
115-
observationCenter.testCaseWillStart(testCase)
116-
117-
testCase.setUp()
118-
119-
let duration = measureTimeExecutingBlock {
120-
do {
121-
try test(testCase)
122-
} catch {
123-
let unexpectedFailure = XCTFailure(message: "", failureDescription: "threw error \"\(error)\"", expected: false, file: "<EXPR>", line: 0)
124-
XCTFailureHandler!(unexpectedFailure)
125-
}
126-
}
127-
128-
testCase.tearDown()
129-
testCase.failIfExpectationsNotWaitedFor(testCase._allExpectations)
130-
131-
observationCenter.testCaseDidFinish(testCase)
132-
133-
totalDuration += duration
134-
135-
var result = "passed"
136-
for failure in failures {
137-
failure.emit(testCase.name)
138-
totalFailures += 1
139-
if !failure.expected {
140-
unexpectedFailures += 1
141-
}
142-
result = failures.count > 0 ? "failed" : "passed"
143-
}
144-
145-
XCTPrint("Test Case '\(testCase.name)' \(result) (\(printableStringForTimeInterval(duration)) seconds).")
146-
XCTAllRuns.append(XCTRun(duration: duration, method: testCase.name, passed: failures.count == 0, failures: failures))
147-
XCTFailureHandler = nil
148-
}
149-
}
150-
151-
let testCountSuffix = (tests.count == 1) ? "" : "s"
152-
let failureSuffix = (totalFailures == 1) ? "" : "s"
153-
154-
XCTPrint("Executed \(tests.count) test\(testCountSuffix), with \(totalFailures) failure\(failureSuffix) (\(unexpectedFailures) unexpected) in \(printableStringForTimeInterval(totalDuration)) (\(printableStringForTimeInterval(overallDuration))) seconds")
155-
}
156-
157162
/// It is an API violation to create expectations but not wait for them to
158163
/// be completed. Notify the user of a mistake via a test failure.
159164
private func failIfExpectationsNotWaitedFor(expectations: [XCTestExpectation]) {
160165
if expectations.count > 0 {
161-
let failure = XCTFailure(
162-
message: "Failed due to unwaited expectations.",
163-
failureDescription: "",
164-
expected: false,
165-
file: expectations.last!.file,
166-
line: expectations.last!.line)
167-
if let failureHandler = XCTFailureHandler {
168-
failureHandler(failure)
169-
}
166+
recordFailure(
167+
withDescription: "Failed due to unwaited expectations.",
168+
inFile: String(expectations.last!.file),
169+
atLine: expectations.last!.line,
170+
expected: false)
170171
}
171172
}
172173

@@ -234,15 +235,11 @@ extension XCTestCase {
234235
// and executes the rest of the test. This discrepancy should be
235236
// fixed.
236237
if _allExpectations.count == 0 {
237-
let failure = XCTFailure(
238-
message: "call made to wait without any expectations having been set.",
239-
failureDescription: "API violation",
240-
expected: false,
241-
file: file,
242-
line: line)
243-
if let failureHandler = XCTFailureHandler {
244-
failureHandler(failure)
245-
}
238+
recordFailure(
239+
withDescription: "API violation - call made to wait without any expectations having been set.",
240+
inFile: String(file),
241+
atLine: line,
242+
expected: false)
246243
return
247244
}
248245

@@ -282,15 +279,11 @@ extension XCTestCase {
282279
// Not all expectations were fulfilled. Append a failure
283280
// to the array of expectation-based failures.
284281
let descriptions = unfulfilledDescriptions.joined(separator: ", ")
285-
let failure = XCTFailure(
286-
message: "Exceeded timeout of \(timeout) seconds, with unfulfilled expectations: \(descriptions)",
287-
failureDescription: "Asynchronous wait failed",
288-
expected: true,
289-
file: file,
290-
line: line)
291-
if let failureHandler = XCTFailureHandler {
292-
failureHandler(failure)
293-
}
282+
recordFailure(
283+
withDescription: "Asynchronous wait failed - Exceeded timeout of \(timeout) seconds, with unfulfilled expectations: \(descriptions)",
284+
inFile: String(file),
285+
atLine: line,
286+
expected: true)
294287
}
295288

296289
// We've recorded all the failures; clear the expectations that

0 commit comments

Comments
 (0)