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 */,