Skip to content

Commit f821dcb

Browse files
authored
Add XCTUnimplemented (#16)
1 parent 0cfca55 commit f821dcb

File tree

9 files changed

+700
-69
lines changed

9 files changed

+700
-69
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
# NB: We can't rely on `XCTExpectFailure` because it doesn't exist in `swift-corelibs-foundation`
12
PASS = \033[1;7;32m PASS \033[0m
23
FAIL = \033[1;7;31m FAIL \033[0m
34
XCT_FAIL = \033[34mXCTFail\033[0m
45
EXPECTED_STRING = This is expected to fail!
56
EXPECTED = \033[31m\"$(EXPECTED_STRING)\"\033[0m
67

78
test:
8-
@swift test --enable-test-discovery 2>&1 | grep '$(EXPECTED_STRING)' > /dev/null \
9+
@swift test 2>&1 | grep '$(EXPECTED_STRING)' > /dev/null \
910
&& (echo "$(PASS) $(XCT_FAIL) was called with $(EXPECTED)" && exit) \
1011
|| (echo "$(FAIL) expected $(XCT_FAIL) to be called with $(EXPECTED)" >&2 && exit 1)
1112

@@ -14,7 +15,7 @@ test-linux:
1415
--rm \
1516
-v "$(PWD):$(PWD)" \
1617
-w "$(PWD)" \
17-
swift:5.3 \
18+
swift:5.6.2-focal \
1819
bash -c "make test"
1920

2021
format:

Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
// swift-tools-version:5.1
1+
// swift-tools-version:5.5
22

33
import PackageDescription
44

55
let package = Package(
66
name: "xctest-dynamic-overlay",
7+
platforms: [
8+
.iOS(.v13),
9+
.macOS(.v10_15),
10+
.tvOS(.v13),
11+
.watchOS(.v6),
12+
],
713
products: [
814
.library(name: "XCTestDynamicOverlay", targets: ["XCTestDynamicOverlay"])
915
],

Sources/XCTestDynamicOverlay/Internal/Breakpoint.swift

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#if DEBUG && canImport(os)
2+
import os
3+
4+
// NB: Xcode runtime warnings offer a much better experience than traditional assertions and
5+
// breakpoints, but Apple provides no means of creating custom runtime warnings ourselves.
6+
// To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead.
7+
//
8+
// Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc
9+
private let rw = (
10+
dso: { () -> UnsafeMutableRawPointer in
11+
let count = _dyld_image_count()
12+
for i in 0..<count {
13+
if let name = _dyld_get_image_name(i) {
14+
let swiftString = String(cString: name)
15+
if swiftString.hasSuffix("/SwiftUI") {
16+
if let header = _dyld_get_image_header(i) {
17+
return UnsafeMutableRawPointer(mutating: UnsafeRawPointer(header))
18+
}
19+
}
20+
}
21+
}
22+
return UnsafeMutableRawPointer(mutating: #dsohandle)
23+
}(),
24+
log: OSLog(subsystem: "com.apple.runtime-issues", category: "XCTestDynamicOverlay")
25+
)
26+
#endif
27+
28+
@_transparent
29+
@inline(__always)
30+
func runtimeWarning(
31+
_ message: @autoclosure () -> StaticString,
32+
_ args: @autoclosure () -> [CVarArg] = []
33+
) {
34+
#if DEBUG && canImport(os)
35+
let message = message()
36+
if !_XCTIsTesting {
37+
unsafeBitCast(
38+
os_log as (OSLogType, UnsafeRawPointer, OSLog, StaticString, CVarArg...) -> Void,
39+
to: ((OSLogType, UnsafeRawPointer, OSLog, StaticString, [CVarArg]) -> Void).self
40+
)(.fault, rw.dso, rw.log, message, args())
41+
}
42+
XCTFail(String(format: "\(message)", arguments: args()))
43+
#endif
44+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#if DEBUG
2+
#if canImport(ObjectiveC)
3+
import Foundation
4+
5+
var XCTCurrentTestCase: AnyObject? {
6+
guard
7+
let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter")
8+
as Any as? NSObjectProtocol,
9+
String(describing: XCTestObservationCenter) != "<null>",
10+
let shared = XCTestObservationCenter.perform(Selector(("sharedTestObservationCenter")))?
11+
.takeUnretainedValue(),
12+
let observers = shared.perform(Selector(("observers")))?
13+
.takeUnretainedValue() as? [AnyObject],
14+
let observer =
15+
observers
16+
.first(where: { NSStringFromClass(type(of: $0)) == "XCTestMisuseObserver" }),
17+
let currentTestCase = observer.perform(Selector(("currentTestCase")))?
18+
.takeUnretainedValue()
19+
else { return nil }
20+
return currentTestCase
21+
}
22+
#else
23+
var XCTCurrentTestCase: AnyObject? {
24+
nil
25+
}
26+
#endif
27+
#else
28+
var XCTCurrentTestCase: AnyObject? {
29+
nil
30+
}
31+
#endif

Sources/XCTestDynamicOverlay/XCTFail.swift

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,7 @@
1212
/// results.
1313
public func XCTFail(_ message: String = "") {
1414
guard
15-
let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter")
16-
as Any as? NSObjectProtocol,
17-
String(describing: XCTestObservationCenter) != "<null>",
18-
let shared = XCTestObservationCenter.perform(Selector(("sharedTestObservationCenter")))?
19-
.takeUnretainedValue(),
20-
let observers = shared.perform(Selector(("observers")))?
21-
.takeUnretainedValue() as? [AnyObject],
22-
let observer =
23-
observers
24-
.first(where: { NSStringFromClass(type(of: $0)) == "XCTestMisuseObserver" }),
25-
let currentTestCase = observer.perform(Selector(("currentTestCase")))?
26-
.takeUnretainedValue(),
15+
let currentTestCase = XCTCurrentTestCase,
2716
let XCTIssue = NSClassFromString("XCTIssue")
2817
as Any as? NSObjectProtocol,
2918
let alloc = XCTIssue.perform(NSSelectorFromString("alloc"))?
@@ -37,28 +26,23 @@
3726
)?
3827
.takeUnretainedValue()
3928
else {
40-
#if canImport(Darwin)
41-
let indentedMessage = message.split(separator: "\n", omittingEmptySubsequences: false)
42-
.map { " \($0)" }
43-
.joined(separator: "\n")
29+
runtimeWarning(
30+
"""
31+
"XCTFail" was invoked outside of a test.
4432
45-
breakpoint(
46-
"""
47-
---
48-
Warning: "XCTestDynamicOverlay.XCTFail" has been invoked outside of tests\
49-
\(message.isEmpty ? "." : "with the message:\n\n\(indentedMessage)")
33+
Message:
34+
%@
5035
51-
This function should only be invoked during an XCTest run, and is a no-op when run in \
52-
application code. If you or a library you depend on is using "XCTFail" for \
53-
test-specific code paths, ensure that these same paths are not called in your \
54-
application.
55-
---
56-
"""
57-
)
58-
#endif
36+
This function should only be invoked during an XCTest run, and is a no-op when run in \
37+
application code. If you or a library you depend on is using "XCTFail" for test-specific \
38+
code paths, ensure that these same paths are not called in your application.
39+
""",
40+
[
41+
message.isEmpty ? "(none)" : message
42+
]
43+
)
5944
return
6045
}
61-
6246
_ = currentTestCase.perform(Selector(("recordIssue:")), with: issue)
6347
}
6448

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
public let _XCTIsTesting: Bool = {
4+
guard let path = ProcessInfo.processInfo.arguments.first
5+
else { return false }
6+
7+
let url = URL(fileURLWithPath: path)
8+
return url.lastPathComponent == "xctest" || url.pathExtension == "xctest"
9+
}()

0 commit comments

Comments
 (0)