Skip to content

Commit

Permalink
Merge pull request swiftlang#279 from stmontgomery/XCTUnwrap
Browse files Browse the repository at this point in the history
XCTUnwrap API
  • Loading branch information
stmontgomery committed Jul 22, 2019
2 parents bd8ed8d + aff2ab1 commit 3675a01
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 8 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ add_swift_library(XCTest
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
Expand Down
25 changes: 25 additions & 0 deletions Sources/XCTest/Private/IgnoredErrors.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) 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]
}
}
43 changes: 43 additions & 0 deletions Sources/XCTest/Public/XCTAssert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private enum _XCTAssertion {
case notEqualWithAccuracy
case `nil`
case notNil
case unwrap
case `true`
case `false`
case fail
Expand All @@ -39,6 +40,7 @@ private enum _XCTAssertion {
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"
Expand Down Expand Up @@ -289,6 +291,47 @@ public func XCTAssertNotNil(_ expression: @autoclosure () throws -> Any?, _ mess
}
}

/// 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()
Expand Down
18 changes: 13 additions & 5 deletions Sources/XCTest/Public/XCTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,19 @@ open class XCTestCase: XCTest {
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()
}
Expand Down
160 changes: 157 additions & 3 deletions Tests/Functional/ErrorHandling/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ class ErrorHandling: XCTestCase {
// 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),
]
}()

Expand All @@ -38,6 +52,7 @@ class ErrorHandling: XCTestCase {

enum SomeError: Swift.Error {
case anError(String)
case shouldNotBeReached
}

func functionThatDoesThrowError() throws {
Expand All @@ -63,6 +78,8 @@ class ErrorHandling: XCTestCase {
switch thrownError {
case .anError(let message):
XCTAssertEqual(message, "an error message")
default:
XCTFail("Unexpected error: \(thrownError)")
}
}
}
Expand All @@ -80,6 +97,8 @@ class ErrorHandling: XCTestCase {
switch thrownError {
case .anError(let message):
XCTAssertEqual(message, "")
default:
XCTFail("Unexpected error: \(thrownError)")
}
}
}
Expand Down Expand Up @@ -121,14 +140,149 @@ class ErrorHandling: XCTestCase {
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
5 changes: 5 additions & 0 deletions XCTest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */

Expand Down Expand Up @@ -123,6 +125,7 @@
AE2FE0E81CFE86A5003EF0D7 /* Private */ = {
isa = PBXGroup;
children = (
E1495C7F224276A600CDEB7D /* IgnoredErrors.swift */,
AE2FE10C1CFE86E6003EF0D7 /* ArgumentParser.swift */,
AE2FE10D1CFE86E6003EF0D7 /* ObjectWrapper.swift */,
AE2FE10E1CFE86E6003EF0D7 /* PerformanceMeter.swift */,
Expand Down Expand Up @@ -296,6 +299,7 @@
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
Expand Down Expand Up @@ -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 */,
Expand Down

0 comments on commit 3675a01

Please sign in to comment.