Skip to content

Commit 5720199

Browse files
authored
Merge pull request #297 from stmontgomery/XCTSkip
Introduce XCTSkip and related APIs for skipping tests
2 parents cd95abc + 930bab2 commit 5720199

File tree

13 files changed

+455
-27
lines changed

13 files changed

+455
-27
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_library(XCTest
4141
Sources/XCTest/Public/XCTestObservationCenter.swift
4242
Sources/XCTest/Public/XCTestCase+Performance.swift
4343
Sources/XCTest/Public/XCTAssert.swift
44+
Sources/XCTest/Public/XCTSkip.swift
4445
Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift
4546
Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift
4647
Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift

Sources/XCTest/Private/IgnoredErrors.swift

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,58 @@
1010
// IgnoredErrors.swift
1111
//
1212

13-
/// The user info key used by errors so that they are ignored by the XCTest library.
14-
internal let XCTestErrorUserInfoKeyShouldIgnore = "XCTestErrorUserInfoKeyShouldIgnore"
13+
protocol XCTCustomErrorHandling: Error {
1514

16-
/// The error type thrown by `XCTUnwrap` on assertion failure.
17-
internal struct XCTestErrorWhileUnwrappingOptional: Error, CustomNSError {
18-
static var errorDomain: String = XCTestErrorDomain
15+
/// Whether this error should be recorded as a test failure when it is caught. Default: true.
16+
var shouldRecordAsTestFailure: Bool { get }
17+
18+
/// Whether this error should cause the test invocation to be skipped when it is caught during a throwing setUp method. Default: true.
19+
var shouldSkipTestInvocation: Bool { get }
1920

20-
var errorCode: Int = 105
21+
/// Whether this error should be recorded as a test skip when it is caught during a test invocation. Default: false.
22+
var shouldRecordAsTestSkip: Bool { get }
23+
24+
}
25+
26+
extension XCTCustomErrorHandling {
27+
28+
var shouldRecordAsTestFailure: Bool {
29+
true
30+
}
2131

22-
var errorUserInfo: [String : Any] {
23-
return [XCTestErrorUserInfoKeyShouldIgnore: true]
32+
var shouldSkipTestInvocation: Bool {
33+
true
2434
}
35+
36+
var shouldRecordAsTestSkip: Bool {
37+
false
38+
}
39+
40+
}
41+
42+
extension Error {
43+
44+
var xct_shouldRecordAsTestFailure: Bool {
45+
(self as? XCTCustomErrorHandling)?.shouldRecordAsTestFailure ?? true
46+
}
47+
48+
var xct_shouldSkipTestInvocation: Bool {
49+
(self as? XCTCustomErrorHandling)?.shouldSkipTestInvocation ?? true
50+
}
51+
52+
var xct_shouldRecordAsTestSkip: Bool {
53+
(self as? XCTCustomErrorHandling)?.shouldRecordAsTestSkip ?? false
54+
}
55+
56+
}
57+
58+
/// The error type thrown by `XCTUnwrap` on assertion failure.
59+
internal struct XCTestErrorWhileUnwrappingOptional: Error, XCTCustomErrorHandling {
60+
61+
var shouldRecordAsTestFailure: Bool {
62+
// Don't record this error as a test failure, because XCTUnwrap
63+
// internally records the failure before throwing this error
64+
false
65+
}
66+
2567
}

Sources/XCTest/Private/PrintObserver.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,18 @@ internal class PrintObserver: XCTestObservation {
3131

3232
func testCaseDidFinish(_ testCase: XCTestCase) {
3333
let testRun = testCase.testRun!
34-
let verb = testRun.hasSucceeded ? "passed" : "failed"
34+
35+
let verb: String
36+
if testRun.hasSucceeded {
37+
if testRun.hasBeenSkipped {
38+
verb = "skipped"
39+
} else {
40+
verb = "passed"
41+
}
42+
} else {
43+
verb = "failed"
44+
}
45+
3546
printAndFlush("Test Case '\(testCase.name)' \(verb) (\(formatTimeInterval(testRun.totalDuration)) seconds)")
3647
}
3748

@@ -41,11 +52,16 @@ internal class PrintObserver: XCTestObservation {
4152
printAndFlush("Test Suite '\(testSuite.name)' \(verb) at \(dateFormatter.string(from: testRun.stopDate!))")
4253

4354
let tests = testRun.executionCount == 1 ? "test" : "tests"
55+
let skipped = testRun.skipCount > 0 ? "\(testRun.skipCount) test\(testRun.skipCount != 1 ? "s" : "") skipped and " : ""
4456
let failures = testRun.totalFailureCount == 1 ? "failure" : "failures"
45-
printAndFlush(
46-
"\t Executed \(testRun.executionCount) \(tests), " +
47-
"with \(testRun.totalFailureCount) \(failures) (\(testRun.unexpectedExceptionCount) unexpected) " +
48-
"in \(formatTimeInterval(testRun.testDuration)) (\(formatTimeInterval(testRun.totalDuration))) seconds"
57+
58+
printAndFlush("""
59+
\t Executed \(testRun.executionCount) \(tests), \
60+
with \(skipped)\
61+
\(testRun.totalFailureCount) \(failures) \
62+
(\(testRun.unexpectedExceptionCount) unexpected) \
63+
in \(formatTimeInterval(testRun.testDuration)) (\(formatTimeInterval(testRun.totalDuration))) seconds
64+
"""
4965
)
5066
}
5167

@@ -70,6 +86,12 @@ internal class PrintObserver: XCTestObservation {
7086
}
7187

7288
extension PrintObserver: XCTestInternalObservation {
89+
func testCase(_ testCase: XCTestCase, wasSkippedWithDescription description: String, at sourceLocation: SourceLocation?) {
90+
let file = sourceLocation?.file ?? "<unknown>"
91+
let line = sourceLocation?.line ?? 0
92+
printAndFlush("\(file):\(line): \(testCase.name) : \(description)")
93+
}
94+
7395
func testCase(_ testCase: XCTestCase, didMeasurePerformanceResults results: String, file: StaticString, line: Int) {
7496
printAndFlush("\(file):\(line): Test Case '\(testCase.name)' measured \(results)")
7597
}

Sources/XCTest/Private/XCTestInternalObservation.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
/// Expanded version of `XCTestObservation` used internally to respond to
1616
/// additional events not publicly exposed.
1717
internal protocol XCTestInternalObservation: XCTestObservation {
18+
func testCase(_ testCase: XCTestCase, wasSkippedWithDescription description: String, at sourceLocation: SourceLocation?)
19+
1820
/// Called when a test case finishes measuring performance and has results
1921
/// to report
2022
/// - Parameter testCase: The test case that did the measurements.
@@ -28,5 +30,6 @@ internal protocol XCTestInternalObservation: XCTestObservation {
2830

2931
// All `XCInternalTestObservation` methods are optional, so empty default implementations are provided
3032
internal extension XCTestInternalObservation {
33+
func testCase(_ testCase: XCTestCase, wasSkippedWithDescription description: String, at sourceLocation: SourceLocation?) {}
3134
func testCase(_ testCase: XCTestCase, didMeasurePerformanceResults results: String, file: StaticString, line: Int) {}
3235
}

Sources/XCTest/Public/XCAbstractTest.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ open class XCTest {
5252
perform(testRun!)
5353
}
5454

55+
/// Setup method called before the invocation of `setUp` and the test method
56+
/// for each test method in the class.
57+
open func setUpWithError() throws {}
58+
5559
/// Setup method called before the invocation of each test method in the
5660
/// class.
5761
open func setUp() {}
@@ -60,6 +64,10 @@ open class XCTest {
6064
/// class.
6165
open func tearDown() {}
6266

67+
/// Teardown method called after the invocation of the test method and `tearDown`
68+
/// for each test method in the class.
69+
open func tearDownWithError() throws {}
70+
6371
// FIXME: This initializer is required due to a Swift compiler bug on Linux.
6472
// It should be removed once the bug is fixed.
6573
public init() {}

Sources/XCTest/Public/XCTSkip.swift

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2020 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+
// XCTSkip.swift
11+
// APIs for skipping tests
12+
//
13+
14+
/// An error which causes the current test to cease executing
15+
/// and be marked as skipped when it is thrown.
16+
public struct XCTSkip: Error {
17+
18+
/// The user-supplied message related to this skip, if specified.
19+
public let message: String?
20+
21+
/// A complete description of the skip. Includes the string-ified expression and user-supplied message when possible.
22+
let summary: String
23+
24+
/// An explanation of why the skip has occurred.
25+
///
26+
/// - Note: May be nil if the skip was unconditional.
27+
private let explanation: String?
28+
29+
/// The source code location where the skip occurred.
30+
let sourceLocation: SourceLocation?
31+
32+
private init(explanation: String?, message: String?, sourceLocation: SourceLocation?) {
33+
self.explanation = explanation
34+
self.message = message
35+
self.sourceLocation = sourceLocation
36+
37+
var summary = "Test skipped"
38+
if let explanation = explanation {
39+
summary += ": \(explanation)"
40+
}
41+
if let message = message, !message.isEmpty {
42+
summary += " - \(message)"
43+
}
44+
self.summary = summary
45+
}
46+
47+
public init(_ message: @autoclosure () -> String? = nil, file: StaticString = #file, line: UInt = #line) {
48+
self.init(explanation: nil, message: message(), sourceLocation: SourceLocation(file: file, line: line))
49+
}
50+
51+
fileprivate init(expectedValue: Bool, message: String?, file: StaticString, line: UInt) {
52+
let explanation = expectedValue
53+
? "required true value but got false"
54+
: "required false value but got true"
55+
self.init(explanation: explanation, message: message, sourceLocation: SourceLocation(file: file, line: line))
56+
}
57+
58+
internal init(error: Error, message: String?, sourceLocation: SourceLocation?) {
59+
let explanation = #"threw error "\#(error)""#
60+
self.init(explanation: explanation, message: message, sourceLocation: sourceLocation)
61+
}
62+
63+
}
64+
65+
extension XCTSkip: XCTCustomErrorHandling {
66+
67+
var shouldRecordAsTestFailure: Bool {
68+
// Don't record this error as a test failure since it's a test skip
69+
false
70+
}
71+
72+
var shouldRecordAsTestSkip: Bool {
73+
true
74+
}
75+
76+
}
77+
78+
/// Evaluates a boolean expression and, if it is true, throws an error which
79+
/// causes the current test to cease executing and be marked as skipped.
80+
public func XCTSkipIf(
81+
_ expression: @autoclosure () throws -> Bool,
82+
_ message: @autoclosure () -> String? = nil,
83+
file: StaticString = #file, line: UInt = #line
84+
) throws {
85+
try skipIfEqual(expression(), true, message(), file: file, line: line)
86+
}
87+
88+
/// Evaluates a boolean expression and, if it is false, throws an error which
89+
/// causes the current test to cease executing and be marked as skipped.
90+
public func XCTSkipUnless(
91+
_ expression: @autoclosure () throws -> Bool,
92+
_ message: @autoclosure () -> String? = nil,
93+
file: StaticString = #file, line: UInt = #line
94+
) throws {
95+
try skipIfEqual(expression(), false, message(), file: file, line: line)
96+
}
97+
98+
private func skipIfEqual(
99+
_ expression: @autoclosure () throws -> Bool,
100+
_ expectedValue: Bool,
101+
_ message: @autoclosure () -> String?,
102+
file: StaticString, line: UInt
103+
) throws {
104+
let expressionValue: Bool
105+
106+
do {
107+
// evaluate the expression exactly once
108+
expressionValue = try expression()
109+
} catch {
110+
throw XCTSkip(error: error, message: message(), sourceLocation: SourceLocation(file: file, line: line))
111+
}
112+
113+
if expressionValue == expectedValue {
114+
throw XCTSkip(expectedValue: expectedValue, message: message(), file: file, line: line)
115+
}
116+
}

Sources/XCTest/Public/XCTestCase.swift

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ internal var XCTCurrentTestCase: XCTestCase?
3434
open class XCTestCase: XCTest {
3535
private let testClosure: XCTestCaseClosure
3636

37+
private var skip: XCTSkip?
38+
3739
/// The name of the test case, consisting of its class name and the method
3840
/// name it will run.
3941
open override var name: String {
@@ -114,26 +116,31 @@ open class XCTestCase: XCTest {
114116
/// Invoking a test performs its setUp, invocation, and tearDown. In
115117
/// general this should not be called directly.
116118
open func invokeTest() {
117-
setUp()
119+
performSetUpSequence()
120+
118121
do {
119-
try testClosure(self)
122+
if skip == nil {
123+
try testClosure(self)
124+
}
120125
} catch {
121-
var shouldIgnore = false
122-
if let userInfo = (error as? CustomNSError)?.errorUserInfo,
123-
let shouldIgnoreValue = userInfo[XCTestErrorUserInfoKeyShouldIgnore] as? NSNumber {
124-
shouldIgnore = shouldIgnoreValue.boolValue
126+
if error.xct_shouldRecordAsTestFailure {
127+
recordFailure(for: error)
125128
}
126129

127-
if !shouldIgnore {
128-
recordFailure(
129-
withDescription: "threw error \"\(error)\"",
130-
inFile: "<EXPR>",
131-
atLine: 0,
132-
expected: false)
130+
if error.xct_shouldRecordAsTestSkip {
131+
if let skip = error as? XCTSkip {
132+
self.skip = skip
133+
} else {
134+
self.skip = XCTSkip(error: error, message: nil, sourceLocation: nil)
135+
}
133136
}
134137
}
135-
runTeardownBlocks()
136-
tearDown()
138+
139+
if let skip = skip {
140+
testRun?.recordSkip(description: skip.summary, sourceLocation: skip.sourceLocation)
141+
}
142+
143+
performTearDownSequence()
137144
}
138145

139146
/// Records a failure in the execution of the test and is used by all test
@@ -171,6 +178,15 @@ open class XCTestCase: XCTest {
171178
recordFailure(withDescription: description, inFile: sourceLocation.file, atLine: Int(sourceLocation.line), expected: expected)
172179
}
173180

181+
// Convenience for recording a failure for a caught Error
182+
private func recordFailure(for error: Error) {
183+
recordFailure(
184+
withDescription: "threw error \"\(error)\"",
185+
inFile: "<EXPR>",
186+
atLine: 0,
187+
expected: false)
188+
}
189+
174190
/// Setup method called before the invocation of any test method in the
175191
/// class.
176192
open class func setUp() {}
@@ -192,6 +208,40 @@ open class XCTestCase: XCTest {
192208
}
193209
}
194210

211+
private func performSetUpSequence() {
212+
do {
213+
try setUpWithError()
214+
} catch {
215+
if error.xct_shouldRecordAsTestFailure {
216+
recordFailure(for: error)
217+
}
218+
219+
if error.xct_shouldSkipTestInvocation {
220+
if let skip = error as? XCTSkip {
221+
self.skip = skip
222+
} else {
223+
self.skip = XCTSkip(error: error, message: nil, sourceLocation: nil)
224+
}
225+
}
226+
}
227+
228+
setUp()
229+
}
230+
231+
private func performTearDownSequence() {
232+
runTeardownBlocks()
233+
234+
tearDown()
235+
236+
do {
237+
try tearDownWithError()
238+
} catch {
239+
if error.xct_shouldRecordAsTestFailure {
240+
recordFailure(for: error)
241+
}
242+
}
243+
}
244+
195245
private func runTeardownBlocks() {
196246
let blocks = teardownBlocksQueue.sync { () -> [() -> Void] in
197247
self.teardownBlocksDequeued = true

0 commit comments

Comments
 (0)