Skip to content

Commit 197f47f

Browse files
Add XCTExpectFailure (#75)
* Add XCTExpectFailure. * Add tests for XCTExpectFailure. * Fix * wip * wip * wip * wip * wip * wip * wip --------- Co-authored-by: Zev Eisenberg <[email protected]>
1 parent fb533d8 commit 197f47f

File tree

8 files changed

+257
-15
lines changed

8 files changed

+257
-15
lines changed

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ test-debug:
1414
test: test-debug
1515
@swift test -c release
1616

17-
test-linux: test-debug
18-
@swift test -c release
17+
test-linux: test
1918

2019
test-linux-docker:
2120
@docker run \
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import Foundation
2+
3+
#if DEBUG && canImport(ObjectiveC)
4+
/// Instructs the test to expect a failure in an upcoming assertion, with options to customize
5+
/// expected failure checking and handling.
6+
///
7+
/// - Parameters:
8+
/// - failureReason: An optional string that describes why the test expects a failure.
9+
/// - enabled: A Boolean value that indicates whether the test checks for the expected failure.
10+
/// - strict: A Boolean value that indicates whether the test reports an error if the expected
11+
/// failure doesn’t occur.
12+
/// - failingBlock: A block of test code and assertions where the test expects a failure.
13+
@_disfavoredOverload
14+
public func XCTExpectFailure<R>(
15+
_ failureReason: String? = nil,
16+
enabled: Bool? = nil,
17+
strict: Bool? = nil,
18+
failingBlock: () throws -> R,
19+
issueMatcher: ((_XCTIssue) -> Bool)? = nil
20+
) rethrows -> R {
21+
guard enabled ?? true
22+
else { return try failingBlock() }
23+
guard
24+
let XCTExpectedFailureOptions = NSClassFromString("XCTExpectedFailureOptions")
25+
as Any as? NSObjectProtocol,
26+
let options = strict ?? true
27+
? XCTExpectedFailureOptions
28+
.perform(NSSelectorFromString("alloc"))?.takeUnretainedValue()
29+
.perform(NSSelectorFromString("init"))?.takeUnretainedValue()
30+
: XCTExpectedFailureOptions
31+
.perform(NSSelectorFromString("nonStrictOptions"))?.takeUnretainedValue(),
32+
let functionBlockPointer = dlsym(dlopen(nil, RTLD_LAZY), "XCTExpectFailureWithOptionsInBlock")
33+
else {
34+
let errorString = dlerror().map { charPointer in String(cString: charPointer) }
35+
?? "Unknown error"
36+
assertionFailure(
37+
"Failed to get symbol for XCTExpectFailureWithOptionsInBlock with error: \(errorString)."
38+
)
39+
return try failingBlock()
40+
}
41+
42+
if let issueMatcher = issueMatcher {
43+
let issueMatcher: @convention(block) (AnyObject) -> Bool = { issue in
44+
issueMatcher(_XCTIssue(issue))
45+
}
46+
options.setValue(issueMatcher, forKey: "issueMatcher")
47+
}
48+
49+
let XCTExpectFailureWithOptionsInBlock = unsafeBitCast(
50+
functionBlockPointer,
51+
to: (@convention(c) (String?, AnyObject, () -> Void) -> Void).self
52+
)
53+
54+
var result: Result<R, Error>!
55+
XCTExpectFailureWithOptionsInBlock(failureReason, options) {
56+
result = Result { try failingBlock() }
57+
}
58+
return try result._rethrowGet()
59+
}
60+
61+
/// Instructs the test to expect a failure in an upcoming assertion, with options to customize
62+
/// expected failure checking and handling.
63+
///
64+
/// - Parameters:
65+
/// - failureReason: An optional string that describes why the test expects a failure.
66+
/// - enabled: A Boolean value that indicates whether the test checks for the expected failure.
67+
/// - strict: A Boolean value that indicates whether the test reports an error if the expected
68+
/// failure doesn’t occur.
69+
@_disfavoredOverload
70+
public func XCTExpectFailure(
71+
_ failureReason: String? = nil,
72+
enabled: Bool? = nil,
73+
strict: Bool? = nil,
74+
issueMatcher: ((_XCTIssue) -> Bool)? = nil
75+
) {
76+
guard enabled ?? true
77+
else { return }
78+
guard
79+
let XCTExpectedFailureOptions = NSClassFromString("XCTExpectedFailureOptions")
80+
as Any as? NSObjectProtocol,
81+
let options = strict ?? true
82+
? XCTExpectedFailureOptions
83+
.perform(NSSelectorFromString("alloc"))?.takeUnretainedValue()
84+
.perform(NSSelectorFromString("init"))?.takeUnretainedValue()
85+
: XCTExpectedFailureOptions
86+
.perform(NSSelectorFromString("nonStrictOptions"))?.takeUnretainedValue(),
87+
let functionBlockPointer = dlsym(dlopen(nil, RTLD_LAZY), "XCTExpectFailureWithOptions")
88+
else {
89+
let errorString = dlerror().map { charPointer in String(cString: charPointer) }
90+
?? "Unknown error"
91+
assertionFailure(
92+
"Failed to get symbol for XCTExpectFailureWithOptionsInBlock with error: \(errorString)."
93+
)
94+
return
95+
}
96+
97+
if let issueMatcher = issueMatcher {
98+
let issueMatcher: @convention(block) (AnyObject) -> Bool = { issue in
99+
issueMatcher(_XCTIssue(issue))
100+
}
101+
options.setValue(issueMatcher, forKey: "issueMatcher")
102+
}
103+
104+
let XCTExpectFailureWithOptions = unsafeBitCast(
105+
functionBlockPointer,
106+
to: (@convention(c) (String?, AnyObject) -> Void).self
107+
)
108+
109+
XCTExpectFailureWithOptions(failureReason, options)
110+
}
111+
112+
public struct _XCTIssue: /*CustomStringConvertible, */Equatable, Hashable {
113+
public var type: IssueType
114+
public var compactDescription: String
115+
public var detailedDescription: String?
116+
117+
// NB: This surface are has been left unimplemented for now. We can consider adopting more of it
118+
// in the future:
119+
//
120+
// var sourceCodeContext: XCTSourceCodeContext
121+
// var associatedError: Error?
122+
// var attachments: [XCTAttachment]
123+
// mutating func add(XCTAttachment)
124+
//
125+
// public var description: String {
126+
// """
127+
// \(self.type.description) \
128+
// at \
129+
// \(self.sourceCodeContext.location.fileURL.lastPathComponent):\
130+
// \(self.sourceCodeContext.location.lineNumber): \
131+
// \(self.compactDescription)
132+
// """
133+
// }
134+
135+
init(_ issue: AnyObject) {
136+
self.type = IssueType(rawValue: issue.value(forKey: "type") as! Int)!
137+
self.compactDescription = issue.value(forKey: "compactDescription") as! String
138+
self.detailedDescription = issue.value(forKey: "detailedDescription") as? String
139+
}
140+
141+
public enum IssueType: Int, Sendable {
142+
case assertionFailure = 0
143+
case performanceRegression = 3
144+
case system = 4
145+
case thrownError = 1
146+
case uncaughtException = 2
147+
case unmatchedExpectedFailure = 5
148+
149+
var description: String {
150+
switch self {
151+
case .assertionFailure:
152+
return "Assertion Failure"
153+
case .performanceRegression:
154+
return "Performance Regression"
155+
case .system:
156+
return "System Error"
157+
case .thrownError:
158+
return "Thrown Error"
159+
case .uncaughtException:
160+
return "Uncaught Exception"
161+
case .unmatchedExpectedFailure:
162+
return "Unmatched ExpectedFailure"
163+
}
164+
}
165+
}
166+
}
167+
168+
@rethrows
169+
private protocol _ErrorMechanism {
170+
associatedtype Output
171+
func get() throws -> Output
172+
}
173+
extension _ErrorMechanism {
174+
func _rethrowError() rethrows -> Never {
175+
_ = try _rethrowGet()
176+
fatalError()
177+
}
178+
@usableFromInline
179+
func _rethrowGet() rethrows -> Output {
180+
return try get()
181+
}
182+
}
183+
extension Result: _ErrorMechanism {}
184+
#endif

Tests/XCTestDynamicOverlayTests/GeneratePlaceholderTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// TODO: https://github.com/apple/swift-corelibs-xctest/issues/438
2-
#if !os(Linux) && !os(Windows)
1+
#if DEBUG && !os(Linux) && !os(Windows)
32
import Foundation
43
import XCTest
54
import XCTestDynamicOverlay

Tests/XCTestDynamicOverlayTests/TestHelpers.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ func MyXCTFail(_ message: String) {
55
XCTFail(message)
66
}
77

8+
#if DEBUG && canImport(ObjectiveC)
9+
func MyXCTExpectFailure(
10+
_ failureReason: String,
11+
enabled: Bool = true,
12+
strict: Bool = true,
13+
failingBlock: () -> Void,
14+
issueMatcher: ((_XCTIssue) -> Bool)? = nil
15+
) {
16+
XCTExpectFailure(
17+
failureReason,
18+
enabled: enabled,
19+
strict: strict,
20+
failingBlock: failingBlock,
21+
issueMatcher: issueMatcher
22+
)
23+
}
24+
25+
func MyXCTExpectFailure(
26+
_ failureReason: String,
27+
enabled: Bool = true,
28+
strict: Bool = true,
29+
issueMatcher: ((_XCTIssue) -> Bool)? = nil
30+
) {
31+
XCTExpectFailure(
32+
failureReason,
33+
enabled: enabled,
34+
strict: strict,
35+
issueMatcher: issueMatcher
36+
)
37+
}
38+
#endif
39+
840
struct Client {
941
var p00: () -> Int
1042
var p01: () throws -> Int

Tests/XCTestDynamicOverlayTests/UnimplementedTests.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// TODO: https://github.com/apple/swift-corelibs-xctest/issues/438
2-
#if !os(Linux) && !os(Windows)
1+
#if DEBUG && !os(Linux) && !os(Windows)
32
import XCTest
43

54
final class UnimplementedTests: XCTestCase {
@@ -11,7 +10,7 @@
1110
Unimplemented: f00 …
1211
1312
Defined at:
14-
XCTestDynamicOverlayTests/TestHelpers.swift:66
13+
XCTestDynamicOverlayTests/TestHelpers.swift:70
1514
"""
1615
}
1716

@@ -22,7 +21,7 @@
2221
Unimplemented: f01 …
2322
2423
Defined at:
25-
XCTestDynamicOverlayTests/TestHelpers.swift:67
24+
XCTestDynamicOverlayTests/TestHelpers.swift:71
2625
2726
Invoked with:
2827
""
@@ -36,7 +35,7 @@
3635
Unimplemented: f02 …
3736
3837
Defined at:
39-
XCTestDynamicOverlayTests/TestHelpers.swift:68
38+
XCTestDynamicOverlayTests/TestHelpers.swift:72
4039
4140
Invoked with:
4241
("", 42)
@@ -50,7 +49,7 @@
5049
Unimplemented: f03 …
5150
5251
Defined at:
53-
XCTestDynamicOverlayTests/TestHelpers.swift:69
52+
XCTestDynamicOverlayTests/TestHelpers.swift:73
5453
5554
Invoked with:
5655
("", 42, 1.2)
@@ -64,7 +63,7 @@
6463
Unimplemented: f04 …
6564
6665
Defined at:
67-
XCTestDynamicOverlayTests/TestHelpers.swift:70
66+
XCTestDynamicOverlayTests/TestHelpers.swift:74
6867
6968
Invoked with:
7069
("", 42, 1.2, [1, 2])
@@ -80,7 +79,7 @@
8079
Unimplemented: f05 …
8180
8281
Defined at:
83-
XCTestDynamicOverlayTests/TestHelpers.swift:71
82+
XCTestDynamicOverlayTests/TestHelpers.swift:75
8483
8584
Invoked with:
8685
("", 42, 1.2, [1, 2], XCTestDynamicOverlayTests.User(id: DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF))

Tests/XCTestDynamicOverlayTests/XCTContextTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// TODO: https://github.com/apple/swift-corelibs-xctest/issues/438
2-
#if !os(Linux) && !os(Windows)
1+
#if DEBUG && !os(Linux) && !os(Windows)
32
import XCTest
43
import XCTestDynamicOverlay
54

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import XCTest
2+
3+
#if DEBUG && canImport(ObjectiveC)
4+
final class XCTExpectFailureTests: XCTestCase {
5+
func testXCTDynamicOverlayWithBlockShouldFail() async throws {
6+
MyXCTExpectFailure("This is expected to pass.", strict: false) {
7+
XCTAssertEqual(42, 42)
8+
}
9+
10+
MyXCTExpectFailure("This is expected to pass.", strict: true) {
11+
XCTAssertEqual(42, 1729)
12+
} issueMatcher: {
13+
$0.compactDescription == #"XCTAssertEqual failed: ("42") is not equal to ("1729")"#
14+
}
15+
16+
if ProcessInfo.processInfo.environment["TEST_FAILURE"] != nil {
17+
MyXCTExpectFailure("This is expected to fail!", strict: true) {
18+
XCTAssertEqual(42, 42)
19+
}
20+
}
21+
}
22+
23+
func testXCTDynamicOverlayShouldFail() async throws {
24+
MyXCTExpectFailure("This is expected to pass.", strict: true) {
25+
$0.compactDescription == #"XCTAssertEqual failed: ("42") is not equal to ("1729")"#
26+
}
27+
XCTAssertEqual(42, 1729)
28+
}
29+
}
30+
#endif

Tests/XCTestDynamicOverlayTests/XCTFailTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import XCTest
22

3-
final class XCTestDynamicOverlayTests: XCTestCase {
3+
final class XCTFailTests: XCTestCase {
44
func testXCTFailShouldFail() async throws {
55
if ProcessInfo.processInfo.environment["TEST_FAILURE"] != nil {
66
MyXCTFail("This is expected to fail!")

0 commit comments

Comments
 (0)