|
1 | | -#if DEBUG |
2 | | - import Foundation |
| 1 | +import Foundation |
3 | 2 |
|
| 3 | +#if DEBUG |
4 | 4 | #if canImport(ObjectiveC) |
5 | 5 | /// This function generates a failure immediately and unconditionally. |
6 | 6 | /// |
|
12 | 12 | /// results. |
13 | 13 | @_disfavoredOverload |
14 | 14 | public func XCTFail(_ message: String = "") { |
15 | | - let message = appendHostAppWarningIfNeeded(message) |
| 15 | + var message = message |
| 16 | + attachHostApplicationWarningIfNeeded(&message) |
16 | 17 | guard |
17 | 18 | let currentTestCase = XCTCurrentTestCase, |
18 | 19 | let XCTIssue = NSClassFromString("XCTIssue") |
|
46 | 47 | /// results. |
47 | 48 | @_disfavoredOverload |
48 | 49 | public func XCTFail(_ message: String = "", file: StaticString, line: UInt) { |
49 | | - let message = appendHostAppWarningIfNeeded(message) |
| 50 | + var message = message |
| 51 | + attachHostApplicationWarningIfNeeded(&message) |
50 | 52 | _XCTFailureHandler(nil, true, "\(file)", line, "\(message.isEmpty ? "failed" : message)", nil) |
51 | 53 | } |
52 | 54 |
|
|
57 | 59 | dlsym(dlopen(nil, RTLD_LAZY), "_XCTFailureHandler"), |
58 | 60 | to: XCTFailureHandler.self |
59 | 61 | ) |
| 62 | + |
| 63 | + private func attachHostApplicationWarningIfNeeded(_ message: inout String) { |
| 64 | + guard |
| 65 | + _XCTIsTesting, |
| 66 | + Bundle.main.bundleIdentifier != "com.apple.dt.xctest.tool" |
| 67 | + else { return } |
| 68 | + |
| 69 | + let callStack = Thread.callStackSymbols |
| 70 | + |
| 71 | + // Detect when synchronous test exists in stack. |
| 72 | + guard callStack.allSatisfy({ frame in !frame.contains(" XCTestCore ") }) |
| 73 | + else { return } |
| 74 | + |
| 75 | + // Detect when asynchronous test exists in stack. |
| 76 | + guard callStack.allSatisfy({ frame in !isTestFrame(frame) }) |
| 77 | + else { return } |
| 78 | + |
| 79 | + let displayName = |
| 80 | + Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String |
| 81 | + ?? Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String |
| 82 | + ?? "Unknown host application" |
| 83 | + |
| 84 | + let bundleIdentifier = Bundle.main.bundleIdentifier ?? "Unknown bundle identifier" |
| 85 | + |
| 86 | + if !message.contains(where: \.isNewline) { |
| 87 | + message.append(" …") |
| 88 | + } |
| 89 | + |
| 90 | + message.append(""" |
| 91 | +
|
| 92 | +
|
| 93 | + ┏━━━━━━━━━━━━━━━━━┉┅ |
| 94 | + ┃ ⚠︎ Warning: |
| 95 | + ┃ |
| 96 | + ┃ This failure was emitted from a host application outside the test stack. |
| 97 | + ┃ |
| 98 | + ┃ Host application: |
| 99 | + ┃ \(displayName) (\(bundleIdentifier)) |
| 100 | + ┃ |
| 101 | + ┃ The host application may have emitted this failure when it first launched, |
| 102 | + ┃ outside this current test that happens to be running. |
| 103 | + ┃ |
| 104 | + ┃ Consider setting the test target's host application to "None," or prevent |
| 105 | + ┃ the host application from performing the code path that emits failure. |
| 106 | + ┗━━━━━━━━━━━━━━━━━┉┅ |
| 107 | + ▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄ |
| 108 | +
|
| 109 | + For more information (and workarounds), see "Testing gotchas": |
| 110 | +
|
| 111 | + https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/testing#testing-gotchas |
| 112 | + """ |
| 113 | + ) |
| 114 | + } |
| 115 | + |
| 116 | + func isTestFrame(_ frame: String) -> Bool { |
| 117 | + // Regular expression to detect and demangle an XCTest case frame: |
| 118 | + // |
| 119 | + // 1. `(?<=\$s)`: Starts with "$s" (stable mangling) |
| 120 | + // 2. `\d{1,3}`: Some numbers (the class name length or the module name length) |
| 121 | + // 3. `.*`: The class name, or module name + class name length + class name |
| 122 | + // 4. `C`: The class type identifier |
| 123 | + // 5. `(?=\d{1,3}test.*yy(Ya)?K?F)`: The function name length, a function that starts with |
| 124 | + // `test`, has no arguments (`y`), returns Void (`y`), and is a function (`F`), potentially |
| 125 | + // async (`Ya`), throwing (`K`), or both. |
| 126 | + let mangledTestFrame = #"(?<=\$s)\d{1,3}.*C(?=\d{1,3}test.*yy(Ya)?K?F)"# |
| 127 | + |
| 128 | + guard let XCTestCase = NSClassFromString("XCTestCase") |
| 129 | + else { return false } |
| 130 | + |
| 131 | + return frame.range(of: mangledTestFrame, options: .regularExpression) |
| 132 | + .map { (_typeByName(String(frame[$0])) as? NSObject.Type)?.isSubclass(of: XCTestCase) ?? false } |
| 133 | + ?? false |
| 134 | + } |
60 | 135 | #elseif canImport(XCTest) |
61 | 136 | // NB: It seems to be safe to import XCTest on Linux |
62 | 137 | @_exported import func XCTest.XCTFail |
|
66 | 141 | @_disfavoredOverload |
67 | 142 | public func XCTFail(_ message: String = "", file: StaticString, line: UInt) {} |
68 | 143 | #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 | | - |
146 | 144 | #else |
147 | 145 | /// This function generates a failure immediately and unconditionally. |
148 | 146 | /// |
|
0 commit comments