Skip to content

XCTestObservation and XCTestObservationCenter #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Sources/XCTest/ObjectWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//
// ObjectWrapper.swift
// Utility type for adapting implementors of a `class` protocol to Hashable
//

/// A `Hashable` representation of an object and its ObjectIdentifier. This is
/// useful because Swift classes aren't implicitly hashable based on identity.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great way of putting it - I wonder whether there'd be value in opening a thread about changing that on Swift evolution. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem worth discussing. I was a bit disappointed that I had to jump through such hoops to make Set work out in this situation. I'm not sure how soon I'd have time to do a proper write-up though, so please feel free to open a thread if you feel so inclined!

internal struct ObjectWrapper<T>: Hashable {
let object: T
let objectIdentifier: ObjectIdentifier

var hashValue: Int { return objectIdentifier.hashValue }
}

internal func ==<T>(lhs: ObjectWrapper<T>, rhs: ObjectWrapper<T>) -> Bool {
return lhs.objectIdentifier == rhs.objectIdentifier
}
30 changes: 24 additions & 6 deletions Sources/XCTest/XCTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(

public class XCTestCase {

/// The name of the test case, consisting of its class name and the method name it will run.
/// - Note: FIXME: This property should be readonly, but currently has to be publicly settable due to a
/// toolchain bug on Linux. To ensure compatibility of tests between
/// swift-corelibs-xctest and Apple XCTest, this property should not be modified.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, interesting... It would be great to reference a JIRA issue here. ☝️

public var name: String

public required init() {
name = "\(self.dynamicType).<unknown>"
}

public func setUp() {
Expand Down Expand Up @@ -72,25 +79,34 @@ extension XCTestCase {
}

internal static func invokeTests(tests: [(String, XCTestCase throws -> Void)]) {
let observationCenter = XCTestObservationCenter.sharedTestObservationCenter()

var totalDuration = 0.0
var totalFailures = 0
var unexpectedFailures = 0
let overallDuration = measureTimeExecutingBlock {
for (name, test) in tests {
let testCase = self.init()
let fullName = "\(testCase.dynamicType).\(name)"
testCase.name = "\(testCase.dynamicType).\(name)"

var failures = [XCTFailure]()
XCTFailureHandler = { failure in
observationCenter.testCase(testCase,
didFailWithDescription: failure.failureMessage,
inFile: String(failure.file),
atLine: failure.line)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description we pass to the observers is different from the description that's printed by XCTFailure.emit(). Is this intentional? It seems like it opens us up to some maintenance dilemmas down the line... for example, @mike-ferris-apple mentioned looking into removing trailing whitespace from failure messages. This code would necessitate us to modify two different call paths.

Could we have XCTFailure.emit() return a string, which we then pass to observers? Or is the description passed to observers supposed to be different here? I'll try checking to see what Apple XCTest does myself to make sure.

Of course, this could be something we take care of later. Let me know if you'd rather we address this in later commits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've cleaned this up now 👍


if !testCase.continueAfterFailure {
failure.emit(fullName)
failure.emit(testCase.name)
fatalError("Terminating execution due to test failure", file: failure.file, line: failure.line)
} else {
failures.append(failure)
}
}

XCTPrint("Test Case '\(fullName)' started.")
XCTPrint("Test Case '\(testCase.name)' started.")

observationCenter.testCaseWillStart(testCase)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a crazy idea: could we define an XCTestObserver that we use internally to call XCTPrint()? That is, could we use our own observation infrastructure to print the default test output?

I'm not sure off the top of my head whether Apple XCTest does this, but I think it'd be wild! 😎

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it turns out, Apple XCTest does exactly that (well, not exactly, because the logging uses the legacy XCTestObserver protocol, but close enough. I learned all this while working through Quick/Quick#486)

I would like to make that refactor, yes. In fact, one reason why I wrote the observer in the functional test to keep track of state in properties instead of just printing was to facilitate this, since XCTestObservationCenter doesn't provide any guarantees of the order in which observers are called.


testCase.setUp()

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

observationCenter.testCaseDidFinish(testCase)

totalDuration += duration

var result = "passed"
for failure in failures {
failure.emit(fullName)
failure.emit(testCase.name)
totalFailures += 1
if !failure.expected {
unexpectedFailures += 1
}
result = failures.count > 0 ? "failed" : "passed"
}

XCTPrint("Test Case '\(fullName)' \(result) (\(printableStringForTimeInterval(duration)) seconds).")
XCTAllRuns.append(XCTRun(duration: duration, method: fullName, passed: failures.count == 0, failures: failures))
XCTPrint("Test Case '\(testCase.name)' \(result) (\(printableStringForTimeInterval(duration)) seconds).")
XCTAllRuns.append(XCTRun(duration: duration, method: testCase.name, passed: failures.count == 0, failures: failures))
XCTFailureHandler = nil
}
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/XCTest/XCTestMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ struct XCTFailure {
var expected: Bool
var file: StaticString
var line: UInt


var failureMessage: String { return "\(failureDescription) - \(message)" }

func emit(method: String) {
XCTPrint("\(file):\(line): \(expected ? "" : "unexpected ")error: \(method) : \(failureDescription) - \(message)")
XCTPrint("\(file):\(line): \(expected ? "" : "unexpected ")error: \(method) : \(failureMessage)")
}
}

Expand Down
44 changes: 44 additions & 0 deletions Sources/XCTest/XCTestObservation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//
// XCTestObservation.swift
// Hooks for being notified about progress during a test run.
//

/// `XCTestObservation` provides hooks for being notified about progress during a
/// test run.
/// - seealso: `XCTestObservationCenter`
public protocol XCTestObservation: class {
/// Called just before a test begins executing.
/// - Parameter testCase: The test case that is about to start. Its `name`
/// property can be used to identify it.
func testCaseWillStart(testCase: XCTestCase)

/// Called when a test failure is reported.
/// - Parameter testCase: The test case that failed. Its `name` property
/// can be used to identify it.
/// - Parameter description: Details about the cause of the test failure.
/// - Parameter filePath: The path to the source file where the failure
/// was reported, if available.
/// - Parameter lineNumber: The line number in the source file where the
/// failure was reported.
func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt)

/// Called just after a test finishes executing.
/// - Parameter testCase: The test case that finished. Its `name` property
/// can be used to identify it.
func testCaseDidFinish(testCase: XCTestCase)
}

// All `XCTestObservation` methods are optional, so empty default implementations are provided
public extension XCTestObservation {
func testCaseWillStart(testCase: XCTestCase) {}
func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {}
func testCaseDidFinish(testCase: XCTestCase) {}
}
64 changes: 64 additions & 0 deletions Sources/XCTest/XCTestObservationCenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//
// XCTestObservationCenter.swift
// Notification center for test run progress events.
//

/// Provides a registry for objects wishing to be informed about progress
/// during the course of a test run. Observers must implement the
/// `XCTestObservation` protocol
/// - seealso: `XCTestObservation`
public class XCTestObservationCenter {

private static var center = XCTestObservationCenter()
private var observers = Set<ObjectWrapper<XCTestObservation>>()

/// Registration should be performed on this shared instance
public class func sharedTestObservationCenter() -> XCTestObservationCenter {
return center
}

/// Register an observer to receive future events during a test run. The order
/// in which individual observers are notified about events is undefined.
public func addTestObserver(testObserver: XCTestObservation) {
observers.insert(testObserver.wrapper)
}

/// Remove a previously-registered observer so that it will no longer receive
/// event callbacks.
public func removeTestObserver(testObserver: XCTestObservation) {
observers.remove(testObserver.wrapper)
}


internal func testCaseWillStart(testCase: XCTestCase) {
forEachObserver { $0.testCaseWillStart(testCase) }
}

internal func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {
forEachObserver { $0.testCase(testCase, didFailWithDescription: description, inFile: filePath, atLine: lineNumber) }
}

internal func testCaseDidFinish(testCase: XCTestCase) {
forEachObserver { $0.testCaseDidFinish(testCase) }
}

private func forEachObserver(@noescape body: XCTestObservation -> Void) {
for observer in observers {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also follow the function's name 😊
observers.forEach { body($0.object) }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's true! I remember considering that but decided to do the traditional for loop instead. I seem to remember reading a comment from Chris Lattner at some point about forEach having a small performance overhead and for being perfectly idiomatic Swift, though either way would be perfectly valid here I think.

body(observer.object)
}
}
}

private extension XCTestObservation {
var wrapper: ObjectWrapper<XCTestObservation> {
return ObjectWrapper(object: self, objectIdentifier: ObjectIdentifier(self))
}
}
74 changes: 74 additions & 0 deletions Tests/Functional/Observation/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// RUN: %{swiftc} %s -o %{built_tests_dir}/Observation
// RUN: %{built_tests_dir}/Observation > %t || true
// RUN: %{xctest_checker} %t %s

#if os(Linux) || os(FreeBSD)
import XCTest
#else
import SwiftXCTest
#endif

class Observer: XCTestObservation {
var startedTestCaseNames = [String]()
var failureDescriptions = [String]()
var finishedTestCaseNames = [String]()

func testCaseWillStart(testCase: XCTestCase) {
startedTestCaseNames.append(testCase.name)
}

func testCase(testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: UInt) {
failureDescriptions.append(description)
}

func testCaseDidFinish(testCase: XCTestCase) {
finishedTestCaseNames.append(testCase.name)
}
}

let observer = Observer()

class Observation: XCTestCase {
static var allTests: [(String, Observation -> () throws -> Void)] {
return [
("test_one", test_one),
("test_two", test_two),
("test_three", test_three),
]
}

// CHECK: Test Case 'Observation.test_one' started.
// CHECK: Test Case 'Observation.test_one' passed \(\d+\.\d+ seconds\).
func test_one() {
XCTAssertEqual(observer.startedTestCaseNames, [])
XCTAssertEqual(observer.failureDescriptions, [])
XCTAssertEqual(observer.finishedTestCaseNames, [])

XCTestObservationCenter.sharedTestObservationCenter().addTestObserver(observer)
}

// CHECK: Test Case 'Observation.test_two' started.
// CHECK: .*/Observation/main.swift:\d+: error: Observation.test_two : failed - fail!
// CHECK: Test Case 'Observation.test_two' failed \(\d+\.\d+ seconds\).
func test_two() {
XCTAssertEqual(observer.startedTestCaseNames, ["Observation.test_two"])
XCTAssertEqual(observer.finishedTestCaseNames,["Observation.test_one"])

XCTFail("fail!")
XCTAssertEqual(observer.failureDescriptions, ["failed - fail!"])

XCTestObservationCenter.sharedTestObservationCenter().removeTestObserver(observer)
}

// CHECK: Test Case 'Observation.test_three' started.
// CHECK: Test Case 'Observation.test_three' passed \(\d+\.\d+ seconds\).
func test_three() {
XCTAssertEqual(observer.startedTestCaseNames, ["Observation.test_two"])
XCTAssertEqual(observer.finishedTestCaseNames,["Observation.test_one"])
}
}

XCTMain([testCase(Observation.allTests)])

// CHECK: Executed 3 tests, with 1 failure \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
// CHECK: Total executed 3 tests, with 1 failure \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
14 changes: 13 additions & 1 deletion XCTest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
/* Begin PBXBuildFile section */
AE7DD6091C8E81A0006FC722 /* ArgumentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */; };
AE7DD60A1C8E81A0006FC722 /* TestFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */; };
AE7DD60C1C8F0513006FC722 /* XCTestObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD60B1C8F0513006FC722 /* XCTestObservation.swift */; };
AE9596DF1C96911F001A9EF0 /* ObjectWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE9596DE1C96911F001A9EF0 /* ObjectWrapper.swift */; };
AE9596E11C9692B8001A9EF0 /* XCTestObservationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE9596E01C9692B8001A9EF0 /* XCTestObservationCenter.swift */; };
C265F66F1C3AEB6A00520CF9 /* XCTAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F6691C3AEB6A00520CF9 /* XCTAssert.swift */; };
C265F6701C3AEB6A00520CF9 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F66A1C3AEB6A00520CF9 /* XCTestCase.swift */; };
C265F6721C3AEB6A00520CF9 /* XCTestMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */; };
Expand All @@ -33,6 +36,9 @@
AE7DD6061C8DC6C0006FC722 /* Functional */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Functional; sourceTree = "<group>"; };
AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentParser.swift; sourceTree = "<group>"; };
AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestFiltering.swift; sourceTree = "<group>"; };
AE7DD60B1C8F0513006FC722 /* XCTestObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestObservation.swift; sourceTree = "<group>"; };
AE9596DE1C96911F001A9EF0 /* ObjectWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectWrapper.swift; sourceTree = "<group>"; };
AE9596E01C9692B8001A9EF0 /* XCTestObservationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestObservationCenter.swift; sourceTree = "<group>"; };
B1384A411C1B3E8700EDF031 /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
B1384A421C1B3E8700EDF031 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
B1384A431C1B3E8700EDF031 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,11 +108,14 @@
isa = PBXGroup;
children = (
AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */,
AE9596DE1C96911F001A9EF0 /* ObjectWrapper.swift */,
AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */,
C265F6691C3AEB6A00520CF9 /* XCTAssert.swift */,
C265F66A1C3AEB6A00520CF9 /* XCTestCase.swift */,
C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */,
DADB979B1C51BDA2005E68B6 /* XCTestExpectation.swift */,
C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */,
AE7DD60B1C8F0513006FC722 /* XCTestObservation.swift */,
AE9596E01C9692B8001A9EF0 /* XCTestObservationCenter.swift */,
C265F66D1C3AEB6A00520CF9 /* XCTimeUtilities.swift */,
DACC94411C8B87B900EC85F5 /* XCWaitCompletionHandler.swift */,
);
Expand Down Expand Up @@ -241,11 +250,14 @@
files = (
DACC94421C8B87B900EC85F5 /* XCWaitCompletionHandler.swift in Sources */,
C265F6731C3AEB6A00520CF9 /* XCTimeUtilities.swift in Sources */,
AE7DD60C1C8F0513006FC722 /* XCTestObservation.swift in Sources */,
C265F6701C3AEB6A00520CF9 /* XCTestCase.swift in Sources */,
DADB979C1C51BDA2005E68B6 /* XCTestExpectation.swift in Sources */,
AE7DD60A1C8E81A0006FC722 /* TestFiltering.swift in Sources */,
AE7DD6091C8E81A0006FC722 /* ArgumentParser.swift in Sources */,
AE9596E11C9692B8001A9EF0 /* XCTestObservationCenter.swift in Sources */,
C265F66F1C3AEB6A00520CF9 /* XCTAssert.swift in Sources */,
AE9596DF1C96911F001A9EF0 /* ObjectWrapper.swift in Sources */,
C265F6721C3AEB6A00520CF9 /* XCTestMain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down