Merge pull request #279 from stmontgomery/XCTUnwrap
XCTUnwrap API
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a402d2f..8c9800f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -70,6 +70,7 @@
Sources/XCTest/Private/ArgumentParser.swift
Sources/XCTest/Private/SourceLocation.swift
Sources/XCTest/Private/WaiterManager.swift
+ Sources/XCTest/Private/IgnoredErrors.swift
Sources/XCTest/Public/XCTestRun.swift
Sources/XCTest/Public/XCTestMain.swift
Sources/XCTest/Public/XCTestCase.swift
diff --git a/Sources/XCTest/Private/IgnoredErrors.swift b/Sources/XCTest/Private/IgnoredErrors.swift
new file mode 100644
index 0000000..360076e
--- /dev/null
+++ b/Sources/XCTest/Private/IgnoredErrors.swift
@@ -0,0 +1,25 @@
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2019 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
+//
+//
+// IgnoredErrors.swift
+//
+
+/// The user info key used by errors so that they are ignored by the XCTest library.
+internal let XCTestErrorUserInfoKeyShouldIgnore = "XCTestErrorUserInfoKeyShouldIgnore"
+
+/// The error type thrown by `XCTUnwrap` on assertion failure.
+internal struct XCTestErrorWhileUnwrappingOptional: Error, CustomNSError {
+ static var errorDomain: String = XCTestErrorDomain
+
+ var errorCode: Int = 105
+
+ var errorUserInfo: [String : Any] {
+ return [XCTestErrorUserInfoKeyShouldIgnore: true]
+ }
+}
diff --git a/Sources/XCTest/Public/XCTAssert.swift b/Sources/XCTest/Public/XCTAssert.swift
index 4006723..dc81cb9 100644
--- a/Sources/XCTest/Public/XCTAssert.swift
+++ b/Sources/XCTest/Public/XCTAssert.swift
@@ -21,6 +21,7 @@
case notEqualWithAccuracy
case `nil`
case notNil
+ case unwrap
case `true`
case `false`
case fail
@@ -39,6 +40,7 @@
case .notEqualWithAccuracy: return "XCTAssertNotEqual"
case .`nil`: return "XCTAssertNil"
case .notNil: return "XCTAssertNotNil"
+ case .unwrap: return "XCTUnwrap"
case .`true`: return "XCTAssertTrue"
case .`false`: return "XCTAssertFalse"
case .throwsError: return "XCTAssertThrowsError"
@@ -289,6 +291,47 @@
}
}
+/// Asserts that an expression is not `nil`, and returns its unwrapped value.
+///
+/// Generates a failure if `expression` returns `nil`.
+///
+/// - Parameters:
+/// - expression: An expression of type `T?` to compare against `nil`. Its type will determine the type of the
+/// returned value.
+/// - message: An optional description of the failure.
+/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was
+/// called.
+/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
+/// - Returns: A value of type `T`, the result of evaluating and unwrapping the given `expression`.
+/// - Throws: An error if `expression` returns `nil`. If `expression` throws an error, then that error will be rethrown instead.
+public func XCTUnwrap<T>(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) throws -> T {
+ var value: T?
+ var caughtErrorOptional: Swift.Error?
+
+ _XCTEvaluateAssertion(.unwrap, message: message(), file: file, line: line) {
+ do {
+ value = try expression()
+ } catch {
+ caughtErrorOptional = error
+ return .unexpectedFailure(error)
+ }
+
+ if value != nil {
+ return .success
+ } else {
+ return .expectedFailure("expected non-nil value of type \"\(T.self)\"")
+ }
+ }
+
+ if let unwrappedValue = value {
+ return unwrappedValue
+ } else if let error = caughtErrorOptional {
+ throw error
+ } else {
+ throw XCTestErrorWhileUnwrappingOptional()
+ }
+}
+
public func XCTAssertTrue(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) {
_XCTEvaluateAssertion(.`true`, message: message(), file: file, line: line) {
let value = try expression()
diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift
index baff3fb..ce52d31 100644
--- a/Sources/XCTest/Public/XCTestCase.swift
+++ b/Sources/XCTest/Public/XCTestCase.swift
@@ -118,11 +118,19 @@
do {
try testClosure(self)
} catch {
- recordFailure(
- withDescription: "threw error \"\(error)\"",
- inFile: "<EXPR>",
- atLine: 0,
- expected: false)
+ var shouldIgnore = false
+ if let userInfo = (error as? CustomNSError)?.errorUserInfo,
+ let shouldIgnoreValue = userInfo[XCTestErrorUserInfoKeyShouldIgnore] as? NSNumber {
+ shouldIgnore = shouldIgnoreValue.boolValue
+ }
+
+ if !shouldIgnore {
+ recordFailure(
+ withDescription: "threw error \"\(error)\"",
+ inFile: "<EXPR>",
+ atLine: 0,
+ expected: false)
+ }
}
tearDown()
}
diff --git a/Tests/Functional/ErrorHandling/main.swift b/Tests/Functional/ErrorHandling/main.swift
index b130c0c..8463674 100644
--- a/Tests/Functional/ErrorHandling/main.swift
+++ b/Tests/Functional/ErrorHandling/main.swift
@@ -30,6 +30,20 @@
// Tests for XCTAssertNoThrow
("test_shouldNotThrowErrorDefiningSuccess", test_shouldNotThrowErrorDefiningSuccess),
("test_shouldThrowErrorDefiningFailure", test_shouldThrowErrorDefiningFailure),
+
+ // Tests for XCTUnwrap
+ ("test_shouldNotThrowErrorOnUnwrapSuccess", test_shouldNotThrowErrorOnUnwrapSuccess),
+ ("test_shouldThrowErrorOnUnwrapFailure", test_shouldThrowErrorOnUnwrapFailure),
+ ("test_shouldThrowErrorOnEvaluationFailure", test_shouldThrowErrorOnEvaluationFailure),
+ ("test_implicitlyUnwrappedOptional_notNil", test_implicitlyUnwrappedOptional_notNil),
+ ("test_implicitlyUnwrappedOptional_nil", test_implicitlyUnwrappedOptional_nil),
+ ("test_unwrapAnyOptional_notNil", test_unwrapAnyOptional_notNil),
+ ("test_unwrapAnyOptional_nil", test_unwrapAnyOptional_nil),
+ ("test_shouldReportFailureOnUnwrapFailure_catchUnwrapFailure", test_shouldReportFailureOnUnwrapFailure_catchUnwrapFailure),
+ ("test_shouldReportFailureOnUnwrapFailure_catchExpressionFailure", test_shouldReportFailureOnUnwrapFailure_catchExpressionFailure),
+ ("test_shouldReportCorrectTypeOnUnwrapFailure", test_shouldReportCorrectTypeOnUnwrapFailure),
+ ("test_shouldReportCustomFileLineLocation", test_shouldReportCustomFileLineLocation),
+ ("test_shouldReportFailureNotOnMainThread", test_shouldReportFailureNotOnMainThread),
]
}()
@@ -38,6 +52,7 @@
enum SomeError: Swift.Error {
case anError(String)
+ case shouldNotBeReached
}
func functionThatDoesThrowError() throws {
@@ -63,6 +78,8 @@
switch thrownError {
case .anError(let message):
XCTAssertEqual(message, "an error message")
+ default:
+ XCTFail("Unexpected error: \(thrownError)")
}
}
}
@@ -80,6 +97,8 @@
switch thrownError {
case .anError(let message):
XCTAssertEqual(message, "")
+ default:
+ XCTFail("Unexpected error: \(thrownError)")
}
}
}
@@ -121,14 +140,149 @@
func test_shouldThrowErrorDefiningFailure() {
XCTAssertNoThrow(try functionThatDoesThrowError())
}
+
+ func functionShouldReturnOptionalButThrows() throws -> String? {
+ throw SomeError.anError("an error message")
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldNotThrowErrorOnUnwrapSuccess' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: Test Case 'ErrorHandling.test_shouldNotThrowErrorOnUnwrapSuccess' passed \(\d+\.\d+ seconds\)
+ func test_shouldNotThrowErrorOnUnwrapSuccess() throws {
+ let optional: String? = "is not nil"
+
+ let unwrapped = try XCTUnwrap(optional)
+ XCTAssertEqual(unwrapped, optional)
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorOnUnwrapFailure' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+4]]: error: ErrorHandling.test_shouldThrowErrorOnUnwrapFailure : XCTUnwrap failed: expected non-nil value of type "String" -
+// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorOnUnwrapFailure' failed \(\d+\.\d+ seconds\)
+ func test_shouldThrowErrorOnUnwrapFailure() throws {
+ let optional: String? = nil
+ _ = try XCTUnwrap(optional)
+
+ // Should not be reached:
+ throw SomeError.shouldNotBeReached
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorOnEvaluationFailure' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+4]]: error: ErrorHandling.test_shouldThrowErrorOnEvaluationFailure : XCTUnwrap threw error "anError\("an error message"\)" - Failure error message
+// CHECK: \<EXPR\>:0: error: ErrorHandling.test_shouldThrowErrorOnEvaluationFailure : threw error "anError\("an error message"\)"
+// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorOnEvaluationFailure' failed \(\d+\.\d+ seconds\)
+ func test_shouldThrowErrorOnEvaluationFailure() throws {
+ _ = try XCTUnwrap(functionShouldReturnOptionalButThrows(), "Failure error message")
+
+ // Should not be reached:
+ throw SomeError.shouldNotBeReached
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_implicitlyUnwrappedOptional_notNil' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: Test Case 'ErrorHandling.test_implicitlyUnwrappedOptional_notNil' passed \(\d+\.\d+ seconds\)
+ func test_implicitlyUnwrappedOptional_notNil() throws {
+ let implicitlyUnwrappedOptional: String! = "is not nil"
+
+ let unwrapped = try XCTUnwrap(implicitlyUnwrappedOptional)
+ XCTAssertEqual(unwrapped, implicitlyUnwrappedOptional)
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_implicitlyUnwrappedOptional_nil' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+4]]: error: ErrorHandling.test_implicitlyUnwrappedOptional_nil : XCTUnwrap failed: expected non-nil value of type "String" - Failure error message
+// CHECK: Test Case 'ErrorHandling.test_implicitlyUnwrappedOptional_nil' failed \(\d+\.\d+ seconds\)
+ func test_implicitlyUnwrappedOptional_nil() throws {
+ let implicitlyUnwrappedOptional: String! = nil
+ _ = try XCTUnwrap(implicitlyUnwrappedOptional, "Failure error message")
+
+ // Should not be reached:
+ throw SomeError.shouldNotBeReached
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_unwrapAnyOptional_notNil' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: Test Case 'ErrorHandling.test_unwrapAnyOptional_notNil' passed \(\d+\.\d+ seconds\)
+ func test_unwrapAnyOptional_notNil() throws {
+ let anyOptional: Any? = "is not nil"
+
+ let unwrapped = try XCTUnwrap(anyOptional)
+ XCTAssertEqual(unwrapped as! String, anyOptional as! String)
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_unwrapAnyOptional_nil' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+4]]: error: ErrorHandling.test_unwrapAnyOptional_nil : XCTUnwrap failed: expected non-nil value of type "Any" - Failure error message
+// CHECK: Test Case 'ErrorHandling.test_unwrapAnyOptional_nil' failed \(\d+\.\d+ seconds\)
+ func test_unwrapAnyOptional_nil() throws {
+ let anyOptional: Any? = nil
+ _ = try XCTUnwrap(anyOptional, "Failure error message")
+
+ // Should not be reached:
+ throw SomeError.shouldNotBeReached
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldReportFailureOnUnwrapFailure_catchUnwrapFailure' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+5]]: error: ErrorHandling.test_shouldReportFailureOnUnwrapFailure_catchUnwrapFailure : XCTUnwrap failed: expected non-nil value of type "String" -
+// CHECK: Test Case 'ErrorHandling.test_shouldReportFailureOnUnwrapFailure_catchUnwrapFailure' failed \(\d+\.\d+ seconds\)
+ func test_shouldReportFailureOnUnwrapFailure_catchUnwrapFailure() {
+ do {
+ let optional: String? = nil
+ _ = try XCTUnwrap(optional)
+ } catch {}
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldReportFailureOnUnwrapFailure_catchExpressionFailure' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+4]]: error: ErrorHandling.test_shouldReportFailureOnUnwrapFailure_catchExpressionFailure : XCTUnwrap threw error "anError\("an error message"\)" -
+// CHECK: Test Case 'ErrorHandling.test_shouldReportFailureOnUnwrapFailure_catchExpressionFailure' failed \(\d+\.\d+ seconds\)
+ func test_shouldReportFailureOnUnwrapFailure_catchExpressionFailure() {
+ do {
+ _ = try XCTUnwrap(functionShouldReturnOptionalButThrows())
+ } catch {}
+ }
+
+ struct CustomType {
+ var name: String
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldReportCorrectTypeOnUnwrapFailure' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+4]]: error: ErrorHandling.test_shouldReportCorrectTypeOnUnwrapFailure : XCTUnwrap failed: expected non-nil value of type "CustomType" -
+// CHECK: Test Case 'ErrorHandling.test_shouldReportCorrectTypeOnUnwrapFailure' failed \(\d+\.\d+ seconds\)
+ func test_shouldReportCorrectTypeOnUnwrapFailure() throws {
+ let customTypeOptional: CustomType? = nil
+ _ = try XCTUnwrap(customTypeOptional)
+
+ // Should not be reached:
+ throw SomeError.shouldNotBeReached
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldReportCustomFileLineLocation' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: custom_file.swift:1234: error: ErrorHandling.test_shouldReportCustomFileLineLocation : XCTUnwrap failed: expected non-nil value of type "CustomType" -
+// CHECK: Test Case 'ErrorHandling.test_shouldReportCustomFileLineLocation' failed \(\d+\.\d+ seconds\)
+ func test_shouldReportCustomFileLineLocation() throws {
+ let customTypeOptional: CustomType? = nil
+ _ = try XCTUnwrap(customTypeOptional, file: "custom_file.swift", line: 1234)
+
+ // Should not be reached:
+ throw SomeError.shouldNotBeReached
+ }
+
+// CHECK: Test Case 'ErrorHandling.test_shouldReportFailureNotOnMainThread' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
+// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+7]]: error: ErrorHandling.test_shouldReportFailureNotOnMainThread : XCTUnwrap failed: expected non-nil value of type "CustomType" -
+// CHECK: Test Case 'ErrorHandling.test_shouldReportFailureNotOnMainThread' failed \(\d+\.\d+ seconds\)
+ func test_shouldReportFailureNotOnMainThread() throws {
+ let queue = DispatchQueue(label: "Test")
+ let semaphore = DispatchSemaphore(value: 0)
+ queue.async {
+ let customTypeOptional: CustomType? = nil
+ _ = try? XCTUnwrap(customTypeOptional)
+ semaphore.signal()
+ }
+
+ semaphore.wait()
+ }
}
// CHECK: Test Suite 'ErrorHandling' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
-// CHECK: \t Executed \d+ tests, with \d+ failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
+// CHECK: \t Executed \d+ tests, with \d+ failures \(5 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
XCTMain([testCase(ErrorHandling.allTests)])
// CHECK: Test Suite '.*\.xctest' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
-// CHECK: \t Executed \d+ tests, with \d+ failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
+// CHECK: \t Executed \d+ tests, with \d+ failures \(5 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
// CHECK: Test Suite 'All tests' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
-// CHECK: \t Executed \d+ tests, with \d+ failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
+// CHECK: \t Executed \d+ tests, with \d+ failures \(5 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
diff --git a/XCTest.xcodeproj/project.pbxproj b/XCTest.xcodeproj/project.pbxproj
index 3e8f99a..df57a5f 100644
--- a/XCTest.xcodeproj/project.pbxproj
+++ b/XCTest.xcodeproj/project.pbxproj
@@ -36,6 +36,7 @@
AE63767E1D01ED17002C0EA8 /* TestListing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE63767D1D01ED17002C0EA8 /* TestListing.swift */; };
DA9D441B1D920A3500108768 /* XCTestCase+Asynchronous.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9D44161D920A3500108768 /* XCTestCase+Asynchronous.swift */; };
DA9D441C1D920A3500108768 /* XCTestExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9D44171D920A3500108768 /* XCTestExpectation.swift */; };
+ E1495C80224276A600CDEB7D /* IgnoredErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1495C7F224276A600CDEB7D /* IgnoredErrors.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -84,6 +85,7 @@
DA7805F91C6704A2003C6636 /* SwiftFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftFoundation.framework; path = "../swift-corelibs-foundation/build/Debug/SwiftFoundation.framework"; sourceTree = "<group>"; };
DA9D44161D920A3500108768 /* XCTestCase+Asynchronous.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Asynchronous.swift"; sourceTree = "<group>"; };
DA9D44171D920A3500108768 /* XCTestExpectation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestExpectation.swift; sourceTree = "<group>"; };
+ E1495C7F224276A600CDEB7D /* IgnoredErrors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IgnoredErrors.swift; sourceTree = "<group>"; };
EA3E74BB1BF2B6D500635A73 /* build_script.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = build_script.py; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -123,6 +125,7 @@
AE2FE0E81CFE86A5003EF0D7 /* Private */ = {
isa = PBXGroup;
children = (
+ E1495C7F224276A600CDEB7D /* IgnoredErrors.swift */,
AE2FE10C1CFE86E6003EF0D7 /* ArgumentParser.swift */,
AE2FE10D1CFE86E6003EF0D7 /* ObjectWrapper.swift */,
AE2FE10E1CFE86E6003EF0D7 /* PerformanceMeter.swift */,
@@ -296,6 +299,7 @@
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
+ English,
en,
Base,
);
@@ -326,6 +330,7 @@
buildActionMask = 2147483647;
files = (
AE2FE1071CFE86DB003EF0D7 /* XCTestObservationCenter.swift in Sources */,
+ E1495C80224276A600CDEB7D /* IgnoredErrors.swift in Sources */,
DA9D441C1D920A3500108768 /* XCTestExpectation.swift in Sources */,
AE2FE1011CFE86DB003EF0D7 /* XCTestCase+Performance.swift in Sources */,
DA9D441B1D920A3500108768 /* XCTestCase+Asynchronous.swift in Sources */,