Skip to content

Commit 5dce136

Browse files
committed
Merge pull request #69 from briancroom/xctestobservation
XCTestObservation and XCTestObservationCenter
2 parents 264d26b + 2638459 commit 5dce136

File tree

7 files changed

+248
-9
lines changed

7 files changed

+248
-9
lines changed

Sources/XCTest/ObjectWrapper.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
// ObjectWrapper.swift
11+
// Utility type for adapting implementors of a `class` protocol to Hashable
12+
//
13+
14+
/// A `Hashable` representation of an object and its ObjectIdentifier. This is
15+
/// useful because Swift classes aren't implicitly hashable based on identity.
16+
internal struct ObjectWrapper<T>: Hashable {
17+
let object: T
18+
let objectIdentifier: ObjectIdentifier
19+
20+
var hashValue: Int { return objectIdentifier.hashValue }
21+
}
22+
23+
internal func ==<T>(lhs: ObjectWrapper<T>, rhs: ObjectWrapper<T>) -> Bool {
24+
return lhs.objectIdentifier == rhs.objectIdentifier
25+
}

Sources/XCTest/XCTestCase.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(
2626

2727
public class XCTestCase {
2828

29+
/// The name of the test case, consisting of its class name and the method name it will run.
30+
/// - Note: FIXME: This property should be readonly, but currently has to be publicly settable due to a
31+
/// toolchain bug on Linux. To ensure compatibility of tests between
32+
/// swift-corelibs-xctest and Apple XCTest, this property should not be modified.
33+
public var name: String
34+
2935
public required init() {
36+
name = "\(self.dynamicType).<unknown>"
3037
}
3138

3239
public func setUp() {
@@ -72,25 +79,34 @@ extension XCTestCase {
7279
}
7380

7481
internal static func invokeTests(tests: [(String, XCTestCase throws -> Void)]) {
82+
let observationCenter = XCTestObservationCenter.sharedTestObservationCenter()
83+
7584
var totalDuration = 0.0
7685
var totalFailures = 0
7786
var unexpectedFailures = 0
7887
let overallDuration = measureTimeExecutingBlock {
7988
for (name, test) in tests {
8089
let testCase = self.init()
81-
let fullName = "\(testCase.dynamicType).\(name)"
90+
testCase.name = "\(testCase.dynamicType).\(name)"
8291

8392
var failures = [XCTFailure]()
8493
XCTFailureHandler = { failure in
94+
observationCenter.testCase(testCase,
95+
didFailWithDescription: failure.failureMessage,
96+
inFile: String(failure.file),
97+
atLine: failure.line)
98+
8599
if !testCase.continueAfterFailure {
86-
failure.emit(fullName)
100+
failure.emit(testCase.name)
87101
fatalError("Terminating execution due to test failure", file: failure.file, line: failure.line)
88102
} else {
89103
failures.append(failure)
90104
}
91105
}
92106

93-
XCTPrint("Test Case '\(fullName)' started.")
107+
XCTPrint("Test Case '\(testCase.name)' started.")
108+
109+
observationCenter.testCaseWillStart(testCase)
94110

95111
testCase.setUp()
96112

@@ -107,20 +123,22 @@ extension XCTestCase {
107123
testCase.failIfExpectationsNotWaitedFor(XCTAllExpectations)
108124
XCTAllExpectations = []
109125

126+
observationCenter.testCaseDidFinish(testCase)
127+
110128
totalDuration += duration
111129

112130
var result = "passed"
113131
for failure in failures {
114-
failure.emit(fullName)
132+
failure.emit(testCase.name)
115133
totalFailures += 1
116134
if !failure.expected {
117135
unexpectedFailures += 1
118136
}
119137
result = failures.count > 0 ? "failed" : "passed"
120138
}
121139

122-
XCTPrint("Test Case '\(fullName)' \(result) (\(printableStringForTimeInterval(duration)) seconds).")
123-
XCTAllRuns.append(XCTRun(duration: duration, method: fullName, passed: failures.count == 0, failures: failures))
140+
XCTPrint("Test Case '\(testCase.name)' \(result) (\(printableStringForTimeInterval(duration)) seconds).")
141+
XCTAllRuns.append(XCTRun(duration: duration, method: testCase.name, passed: failures.count == 0, failures: failures))
124142
XCTFailureHandler = nil
125143
}
126144
}

Sources/XCTest/XCTestMain.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ struct XCTFailure {
3131
var expected: Bool
3232
var file: StaticString
3333
var line: UInt
34-
34+
35+
var failureMessage: String { return "\(failureDescription) - \(message)" }
36+
3537
func emit(method: String) {
36-
XCTPrint("\(file):\(line): \(expected ? "" : "unexpected ")error: \(method) : \(failureDescription) - \(message)")
38+
XCTPrint("\(file):\(line): \(expected ? "" : "unexpected ")error: \(method) : \(failureMessage)")
3739
}
3840
}
3941

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
// XCTestObservation.swift
11+
// Hooks for being notified about progress during a test run.
12+
//
13+
14+
/// `XCTestObservation` provides hooks for being notified about progress during a
15+
/// test run.
16+
/// - seealso: `XCTestObservationCenter`
17+
public protocol XCTestObservation: class {
18+
/// Called just before a test begins executing.
19+
/// - Parameter testCase: The test case that is about to start. Its `name`
20+
/// property can be used to identify it.
21+
func testCaseWillStart(testCase: XCTestCase)
22+
23+
/// Called when a test failure is reported.
24+
/// - Parameter testCase: The test case that failed. Its `name` property
25+
/// can be used to identify it.
26+
/// - Parameter description: Details about the cause of the test failure.
27+
/// - Parameter filePath: The path to the source file where the failure
28+
/// was reported, if available.
29+
/// - Parameter lineNumber: The line number in the source file where the
30+
/// failure was reported.
31+
func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt)
32+
33+
/// Called just after a test finishes executing.
34+
/// - Parameter testCase: The test case that finished. Its `name` property
35+
/// can be used to identify it.
36+
func testCaseDidFinish(testCase: XCTestCase)
37+
}
38+
39+
// All `XCTestObservation` methods are optional, so empty default implementations are provided
40+
public extension XCTestObservation {
41+
func testCaseWillStart(testCase: XCTestCase) {}
42+
func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {}
43+
func testCaseDidFinish(testCase: XCTestCase) {}
44+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
// XCTestObservationCenter.swift
11+
// Notification center for test run progress events.
12+
//
13+
14+
/// Provides a registry for objects wishing to be informed about progress
15+
/// during the course of a test run. Observers must implement the
16+
/// `XCTestObservation` protocol
17+
/// - seealso: `XCTestObservation`
18+
public class XCTestObservationCenter {
19+
20+
private static var center = XCTestObservationCenter()
21+
private var observers = Set<ObjectWrapper<XCTestObservation>>()
22+
23+
/// Registration should be performed on this shared instance
24+
public class func sharedTestObservationCenter() -> XCTestObservationCenter {
25+
return center
26+
}
27+
28+
/// Register an observer to receive future events during a test run. The order
29+
/// in which individual observers are notified about events is undefined.
30+
public func addTestObserver(testObserver: XCTestObservation) {
31+
observers.insert(testObserver.wrapper)
32+
}
33+
34+
/// Remove a previously-registered observer so that it will no longer receive
35+
/// event callbacks.
36+
public func removeTestObserver(testObserver: XCTestObservation) {
37+
observers.remove(testObserver.wrapper)
38+
}
39+
40+
41+
internal func testCaseWillStart(testCase: XCTestCase) {
42+
forEachObserver { $0.testCaseWillStart(testCase) }
43+
}
44+
45+
internal func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {
46+
forEachObserver { $0.testCase(testCase, didFailWithDescription: description, inFile: filePath, atLine: lineNumber) }
47+
}
48+
49+
internal func testCaseDidFinish(testCase: XCTestCase) {
50+
forEachObserver { $0.testCaseDidFinish(testCase) }
51+
}
52+
53+
private func forEachObserver(@noescape body: XCTestObservation -> Void) {
54+
for observer in observers {
55+
body(observer.object)
56+
}
57+
}
58+
}
59+
60+
private extension XCTestObservation {
61+
var wrapper: ObjectWrapper<XCTestObservation> {
62+
return ObjectWrapper(object: self, objectIdentifier: ObjectIdentifier(self))
63+
}
64+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// RUN: %{swiftc} %s -o %{built_tests_dir}/Observation
2+
// RUN: %{built_tests_dir}/Observation > %t || true
3+
// RUN: %{xctest_checker} %t %s
4+
5+
#if os(Linux) || os(FreeBSD)
6+
import XCTest
7+
#else
8+
import SwiftXCTest
9+
#endif
10+
11+
class Observer: XCTestObservation {
12+
var startedTestCaseNames = [String]()
13+
var failureDescriptions = [String]()
14+
var finishedTestCaseNames = [String]()
15+
16+
func testCaseWillStart(testCase: XCTestCase) {
17+
startedTestCaseNames.append(testCase.name)
18+
}
19+
20+
func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {
21+
failureDescriptions.append(description)
22+
}
23+
24+
func testCaseDidFinish(testCase: XCTestCase) {
25+
finishedTestCaseNames.append(testCase.name)
26+
}
27+
}
28+
29+
let observer = Observer()
30+
31+
class Observation: XCTestCase {
32+
static var allTests: [(String, Observation -> () throws -> Void)] {
33+
return [
34+
("test_one", test_one),
35+
("test_two", test_two),
36+
("test_three", test_three),
37+
]
38+
}
39+
40+
// CHECK: Test Case 'Observation.test_one' started.
41+
// CHECK: Test Case 'Observation.test_one' passed \(\d+\.\d+ seconds\).
42+
func test_one() {
43+
XCTAssertEqual(observer.startedTestCaseNames, [])
44+
XCTAssertEqual(observer.failureDescriptions, [])
45+
XCTAssertEqual(observer.finishedTestCaseNames, [])
46+
47+
XCTestObservationCenter.sharedTestObservationCenter().addTestObserver(observer)
48+
}
49+
50+
// CHECK: Test Case 'Observation.test_two' started.
51+
// CHECK: .*/Observation/main.swift:\d+: error: Observation.test_two : failed - fail!
52+
// CHECK: Test Case 'Observation.test_two' failed \(\d+\.\d+ seconds\).
53+
func test_two() {
54+
XCTAssertEqual(observer.startedTestCaseNames, ["Observation.test_two"])
55+
XCTAssertEqual(observer.finishedTestCaseNames,["Observation.test_one"])
56+
57+
XCTFail("fail!")
58+
XCTAssertEqual(observer.failureDescriptions, ["failed - fail!"])
59+
60+
XCTestObservationCenter.sharedTestObservationCenter().removeTestObserver(observer)
61+
}
62+
63+
// CHECK: Test Case 'Observation.test_three' started.
64+
// CHECK: Test Case 'Observation.test_three' passed \(\d+\.\d+ seconds\).
65+
func test_three() {
66+
XCTAssertEqual(observer.startedTestCaseNames, ["Observation.test_two"])
67+
XCTAssertEqual(observer.finishedTestCaseNames,["Observation.test_one"])
68+
}
69+
}
70+
71+
XCTMain([testCase(Observation.allTests)])
72+
73+
// CHECK: Executed 3 tests, with 1 failure \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
74+
// CHECK: Total executed 3 tests, with 1 failure \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds

XCTest.xcodeproj/project.pbxproj

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
/* Begin PBXBuildFile section */
1010
AE7DD6091C8E81A0006FC722 /* ArgumentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */; };
1111
AE7DD60A1C8E81A0006FC722 /* TestFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */; };
12+
AE7DD60C1C8F0513006FC722 /* XCTestObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD60B1C8F0513006FC722 /* XCTestObservation.swift */; };
13+
AE9596DF1C96911F001A9EF0 /* ObjectWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE9596DE1C96911F001A9EF0 /* ObjectWrapper.swift */; };
14+
AE9596E11C9692B8001A9EF0 /* XCTestObservationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE9596E01C9692B8001A9EF0 /* XCTestObservationCenter.swift */; };
1215
C265F66F1C3AEB6A00520CF9 /* XCTAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F6691C3AEB6A00520CF9 /* XCTAssert.swift */; };
1316
C265F6701C3AEB6A00520CF9 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F66A1C3AEB6A00520CF9 /* XCTestCase.swift */; };
1417
C265F6721C3AEB6A00520CF9 /* XCTestMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */; };
@@ -33,6 +36,9 @@
3336
AE7DD6061C8DC6C0006FC722 /* Functional */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Functional; sourceTree = "<group>"; };
3437
AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentParser.swift; sourceTree = "<group>"; };
3538
AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestFiltering.swift; sourceTree = "<group>"; };
39+
AE7DD60B1C8F0513006FC722 /* XCTestObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestObservation.swift; sourceTree = "<group>"; };
40+
AE9596DE1C96911F001A9EF0 /* ObjectWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectWrapper.swift; sourceTree = "<group>"; };
41+
AE9596E01C9692B8001A9EF0 /* XCTestObservationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestObservationCenter.swift; sourceTree = "<group>"; };
3642
B1384A411C1B3E8700EDF031 /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
3743
B1384A421C1B3E8700EDF031 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
3844
B1384A431C1B3E8700EDF031 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -102,11 +108,14 @@
102108
isa = PBXGroup;
103109
children = (
104110
AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */,
111+
AE9596DE1C96911F001A9EF0 /* ObjectWrapper.swift */,
105112
AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */,
106113
C265F6691C3AEB6A00520CF9 /* XCTAssert.swift */,
107114
C265F66A1C3AEB6A00520CF9 /* XCTestCase.swift */,
108-
C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */,
109115
DADB979B1C51BDA2005E68B6 /* XCTestExpectation.swift */,
116+
C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */,
117+
AE7DD60B1C8F0513006FC722 /* XCTestObservation.swift */,
118+
AE9596E01C9692B8001A9EF0 /* XCTestObservationCenter.swift */,
110119
C265F66D1C3AEB6A00520CF9 /* XCTimeUtilities.swift */,
111120
DACC94411C8B87B900EC85F5 /* XCWaitCompletionHandler.swift */,
112121
);
@@ -241,11 +250,14 @@
241250
files = (
242251
DACC94421C8B87B900EC85F5 /* XCWaitCompletionHandler.swift in Sources */,
243252
C265F6731C3AEB6A00520CF9 /* XCTimeUtilities.swift in Sources */,
253+
AE7DD60C1C8F0513006FC722 /* XCTestObservation.swift in Sources */,
244254
C265F6701C3AEB6A00520CF9 /* XCTestCase.swift in Sources */,
245255
DADB979C1C51BDA2005E68B6 /* XCTestExpectation.swift in Sources */,
246256
AE7DD60A1C8E81A0006FC722 /* TestFiltering.swift in Sources */,
247257
AE7DD6091C8E81A0006FC722 /* ArgumentParser.swift in Sources */,
258+
AE9596E11C9692B8001A9EF0 /* XCTestObservationCenter.swift in Sources */,
248259
C265F66F1C3AEB6A00520CF9 /* XCTAssert.swift in Sources */,
260+
AE9596DF1C96911F001A9EF0 /* ObjectWrapper.swift in Sources */,
249261
C265F6721C3AEB6A00520CF9 /* XCTestMain.swift in Sources */,
250262
);
251263
runOnlyForDeploymentPostprocessing = 0;

0 commit comments

Comments
 (0)