|
1 | 1 | #if DEBUG |
2 | | - #if canImport(ObjectiveC) |
3 | | - import Foundation |
| 2 | + import Foundation |
4 | 3 |
|
| 4 | + #if canImport(ObjectiveC) |
5 | 5 | /// This function generates a failure immediately and unconditionally. |
6 | 6 | /// |
7 | 7 | /// Dynamically creates and records an `XCTIssue` under the hood that captures the source code |
|
12 | 12 | /// results. |
13 | 13 | @_disfavoredOverload |
14 | 14 | public func XCTFail(_ message: String = "") { |
| 15 | + let message = appendHostAppWarningIfNeeded(message) |
15 | 16 | guard |
16 | 17 | let currentTestCase = XCTCurrentTestCase, |
17 | 18 | let XCTIssue = NSClassFromString("XCTIssue") |
|
45 | 46 | /// results. |
46 | 47 | @_disfavoredOverload |
47 | 48 | public func XCTFail(_ message: String = "", file: StaticString, line: UInt) { |
| 49 | + let message = appendHostAppWarningIfNeeded(message) |
48 | 50 | _XCTFailureHandler(nil, true, "\(file)", line, "\(message.isEmpty ? "failed" : message)", nil) |
49 | 51 | } |
50 | 52 |
|
|
64 | 66 | @_disfavoredOverload |
65 | 67 | public func XCTFail(_ message: String = "", file: StaticString, line: UInt) {} |
66 | 68 | #endif |
| 69 | + |
| 70 | + func appendHostAppWarningIfNeeded(_ originalMessage: String) -> String { |
| 71 | + guard _XCTIsTesting else { return originalMessage } |
| 72 | + if Bundle.main.bundleIdentifier == "com.apple.dt.xctest.tool" // Apple platforms |
| 73 | + || Bundle.main.bundleIdentifier == nil // Linux |
| 74 | + { |
| 75 | + // XCTesting is providing a default host app. |
| 76 | + return originalMessage |
| 77 | + } |
| 78 | + |
| 79 | + if Thread.callStackSymbols.contains(where: { $0.range(of: "XCTestCore") != nil }) { |
| 80 | + // We are apparently performing a sync test |
| 81 | + return originalMessage |
| 82 | + } |
| 83 | + |
| 84 | + if testCaseSubclass(callStackSymbols: Thread.callStackSymbols) != nil { |
| 85 | + // We are apparently performing an async test. |
| 86 | + // We're matching a `() -> ()` function that starts with `test`, from a `XCTestCase` subclass |
| 87 | + return originalMessage |
| 88 | + } |
| 89 | + |
| 90 | + let message = """ |
| 91 | + Warning! This failure occurred while running tests hosted by the main app. |
| 92 | +
|
| 93 | + Testing using the main app as a host can lead to false positive test failures created by the \ |
| 94 | + app accessing unimplemented values itself when it is spun up. |
| 95 | +
|
| 96 | + - Test host: \(Bundle.main.bundleIdentifier ?? "Unknown") |
| 97 | +
|
| 98 | + You can find more information and workarounds in the "Testing/Testing Gotchas" section of \ |
| 99 | + Dependencies' documentation at \ |
| 100 | + https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/testing/. |
| 101 | + """ |
| 102 | + |
| 103 | + return [originalMessage, "", message].joined(separator: "\n") |
| 104 | + } |
| 105 | + |
| 106 | + // (?<=\$s): Starts with "$s" (stable mangling); |
| 107 | + // \d{1,3}: Some numbers (the class name length or the module name length); |
| 108 | + // .*: The class name, or module name + class name length + class name; |
| 109 | + // C: The class type identifier; |
| 110 | + // (?=\d{1,3}test.*yy(Ya)?K?F): Followed by the function name length, function that starts with |
| 111 | + // `test`, has no arguments (y), returns Void (y), and is a function (F), potentially async (Ya), |
| 112 | + // throwing (K), or both. |
| 113 | + private let testCaseRegex = #"(?<=\$s)\d{1,3}.*C(?=\d{1,3}test.*yy(Ya)?K?F)"# |
| 114 | + |
| 115 | + func testCaseSubclass(callStackSymbols: [String]) -> Any.Type? { |
| 116 | + for frame in callStackSymbols { |
| 117 | + var startIndex = frame.startIndex |
| 118 | + while startIndex != frame.endIndex { |
| 119 | + if let range = frame.range( |
| 120 | + of: testCaseRegex, |
| 121 | + options: .regularExpression, |
| 122 | + range: startIndex..<frame.endIndex, |
| 123 | + locale: nil |
| 124 | + ) { |
| 125 | + if let testCase = testCase(mangledName: String(frame[range])) { |
| 126 | + return testCase |
| 127 | + } |
| 128 | + startIndex = range.upperBound |
| 129 | + } else { |
| 130 | + break |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + return nil |
| 135 | + } |
| 136 | + |
| 137 | + private func testCase(mangledName: String) -> Any.Type? { |
| 138 | + if let object = _typeByName(mangledName) as? NSObject.Type, |
| 139 | + NSClassFromString("XCTestCase").map(object.isSubclass(of:)) == true |
| 140 | + { |
| 141 | + return object |
| 142 | + } |
| 143 | + return nil |
| 144 | + } |
| 145 | + |
67 | 146 | #else |
68 | 147 | /// This function generates a failure immediately and unconditionally. |
69 | 148 | /// |
|
0 commit comments