Skip to content

Add XCTExpectFailure #59

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

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test-debug:
|| (echo "$(FAIL) expected $(XCT_FAIL) to be called with $(EXPECTED)" >&2 && exit 1)

test: test-debug
@swift test -c release | grep '⚠︎ Warning: This XCTFail was ignored' || exit 1
@swift test -c release | grep '⚠︎ Warning: This (XCTFail|XCTExpectFailure) was ignored' || exit 1

test-linux: test-debug
@swift test -c release
Expand Down
103 changes: 103 additions & 0 deletions Sources/XCTestDynamicOverlay/XCTExpectFailure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Foundation

#if DEBUG
#if canImport(ObjectiveC)
/// Instructs the test to expect a failure in an upcoming assertion, with options to customize expected failure checking and handling.
/// - Parameters:
/// - failureReason: An optional string that describes why the test expects a failure.
/// - strict: A Boolean value that indicates whether the test reports an error if the expected failure doesn’t occur.
/// - failingBlock: A block of test code and assertions where the test expects a failure.
@_transparent @_disfavoredOverload
public func XCTExpectFailure(
_ failureReason: String? = nil,
strict: Bool = true,
failingBlock: () -> Void
) {
guard
let XCTExpectedFailureOptions = NSClassFromString("XCTExpectedFailureOptions")
as Any as? NSObjectProtocol,
let options = strict
? XCTExpectedFailureOptions
.perform(NSSelectorFromString("alloc"))?.takeUnretainedValue()
.perform(NSSelectorFromString("init"))?.takeUnretainedValue()
: XCTExpectedFailureOptions
.perform(NSSelectorFromString("nonStrictOptions"))?.takeUnretainedValue()
else { return }

guard
let functionBlockPointer = dlsym(
dlopen(nil, RTLD_LAZY), "XCTExpectFailureWithOptionsInBlock")
else {
let errorString =
dlerror().map { charPointer in
String(cString: charPointer)
} ?? "Unknown error"
fatalError(
"Failed to get symbol for XCTExpectFailureWithOptionsInBlock with error: \(errorString).")
}

let XCTExpectFailureWithOptionsInBlock = unsafeBitCast(
functionBlockPointer,
to: (@convention(c) (String?, AnyObject, () -> Void) -> Void).self
)

XCTExpectFailureWithOptionsInBlock(failureReason, options, failingBlock)
}
#elseif canImport(XCTest)
// NB: It seems to be safe to import XCTest on Linux
@_exported import func XCTest.XCTExpectFailure
#else
@_disfavoredOverload
public func XCTExpectFailure(
_ failureReason: String? = nil,
strict: Bool = true,
failingBlock: () -> Void
) {
print(noop(message: failureReason))
}
#endif
#else

/// Instructs the test to expect a failure in an upcoming assertion, with options to customize expected failure checking and handling.
/// - Parameters:
/// - failureReason: An optional string that describes why the test expects a failure.
/// - strict: A Boolean value that indicates whether the test reports an error if the expected failure doesn’t occur.
/// - failingBlock: A block of test code and assertions where the test expects a failure.
@_disfavoredOverload
public func XCTExpectFailure(
_ failureReason: String? = nil,
strict: Bool = true,
failingBlock: () -> Void
) {
print(noop(message: failureReason))
}
#endif

// Rule-of-threes: this is also used in XCTFail.swift. If you need it in a third place, consider refactoring.
private func noop(message: String?, file: StaticString? = nil, line: UInt? = nil) -> String {
let fileAndLine: String
if let file = file, let line = line {
fileAndLine = """
:
┃ \(file):\(line)
┃ …
"""
} else {
fileAndLine = "\n┃ "
}

return """
XCTExpectFailure: \(message ?? "<no message provided>")

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┉┅
┃ ⚠︎ Warning: This XCTExpectFailure was ignored
┃ XCTExpectFailure was invoked in a non-DEBUG environment\(fileAndLine)and so was ignored. Be sure to run tests with
┃ the DEBUG=1 flag set in order to dynamically
┃ load XCTExpectFailure.
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┉┅
▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄
"""
}
1 change: 1 addition & 0 deletions Sources/XCTestDynamicOverlay/XCTFail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ import Foundation
}
#endif

// Rule-of-threes: this is also used in XCTExpectFailure.swift. If you need it in a third place, consider refactoring.
private func noop(message: String, file: StaticString? = nil, line: UInt? = nil) -> String {
let fileAndLine: String
if let file = file, let line = line {
Expand Down
4 changes: 4 additions & 0 deletions Tests/XCTestDynamicOverlayTests/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ func MyXCTFail(_ message: String) {
XCTFail(message)
}

func MyXCTExpectFailure(strict: Bool, message: String, failingBlock: () -> Void) {
XCTExpectFailure(message, strict: strict, failingBlock: failingBlock)
}

struct Client {
var p00: () -> Int
var p01: () throws -> Int
Expand Down
12 changes: 6 additions & 6 deletions Tests/XCTestDynamicOverlayTests/UnimplementedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Unimplemented: f00 …

Defined at:
XCTestDynamicOverlayTests/TestHelpers.swift:66
XCTestDynamicOverlayTests/TestHelpers.swift:70
"""
}

Expand All @@ -21,7 +21,7 @@
Unimplemented: f01 …

Defined at:
XCTestDynamicOverlayTests/TestHelpers.swift:67
XCTestDynamicOverlayTests/TestHelpers.swift:71

Invoked with:
""
Expand All @@ -35,7 +35,7 @@
Unimplemented: f02 …

Defined at:
XCTestDynamicOverlayTests/TestHelpers.swift:68
XCTestDynamicOverlayTests/TestHelpers.swift:72

Invoked with:
("", 42)
Expand All @@ -49,7 +49,7 @@
Unimplemented: f03 …

Defined at:
XCTestDynamicOverlayTests/TestHelpers.swift:69
XCTestDynamicOverlayTests/TestHelpers.swift:73

Invoked with:
("", 42, 1.2)
Expand All @@ -63,7 +63,7 @@
Unimplemented: f04 …

Defined at:
XCTestDynamicOverlayTests/TestHelpers.swift:70
XCTestDynamicOverlayTests/TestHelpers.swift:74

Invoked with:
("", 42, 1.2, [1, 2])
Expand All @@ -79,7 +79,7 @@
Unimplemented: f05 …

Defined at:
XCTestDynamicOverlayTests/TestHelpers.swift:71
XCTestDynamicOverlayTests/TestHelpers.swift:75

Invoked with:
("", 42, 1.2, [1, 2], XCTestDynamicOverlayTests.User(id: DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF))
Expand Down
19 changes: 19 additions & 0 deletions Tests/XCTestDynamicOverlayTests/XCTExpectFailureTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import XCTest

final class XCTExpectFailureTests: XCTestCase {
func testXCTDynamicOverlayShouldFail() async throws {
MyXCTExpectFailure(strict: false, message: "This is expected to pass.") {
XCTAssertEqual(42, 42)
}

MyXCTExpectFailure(strict: true, message: "This is expected to pass.") {
XCTAssertEqual(42, 1729)
}

if ProcessInfo.processInfo.environment["TEST_FAILURE"] != nil {
MyXCTExpectFailure(strict: true, message: "This is expected to fail!") {
XCTAssertEqual(42, 42)
}
}
}
}
2 changes: 1 addition & 1 deletion Tests/XCTestDynamicOverlayTests/XCTFailTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import XCTest

final class XCTestDynamicOverlayTests: XCTestCase {
final class XCTFailTests: XCTestCase {
func testXCTFailShouldFail() async throws {
if ProcessInfo.processInfo.environment["TEST_FAILURE"] != nil {
MyXCTFail("This is expected to fail!")
Expand Down