Skip to content

Commit a823a2a

Browse files
authored
test: exit TestRunner upon a crash instead of waiting a timeout (#373)
testRuntime() previously called wait(for:timeout:) with a single 300s budget. If the runtime crashed (EXC_BAD_ACCESS or similar) the HTTP POST that fulfills runtimeUnitTestsExpectation never arrived, so the test would sit out the full 5 minutes before reporting a generic timeout — masking crashes as slow tests in CI. Add a 0.5s polling Timer that watches XCUIApplication.state and fulfills the expectation when the app reaches .notRunning, recording the exit via a didCrash flag. The Timer is registered on RunLoop.main in .common modes so it keeps firing while XCTWaiter spins the run loop. After the wait resolves, didCrash drives a specific XCTFail pointing reviewers at ~/Library/Logs/DiagnosticReports/TestRunner-*.ips, while a true timeout still surfaces as the original "exceeded N seconds" failure. Switching the expectation from self.expectation(...) to a standalone XCTestExpectation is required because XCTestCase tracks the former and requires waitForExpectations; driving it through XCTWaiter directly avoids that constraint.
1 parent 5555935 commit a823a2a

1 file changed

Lines changed: 40 additions & 3 deletions

File tree

TestRunnerTests/TestRunnerTests.swift

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ class TestRunnerTests: XCTestCase {
99
override func setUp() {
1010
continueAfterFailure = false
1111

12-
runtimeUnitTestsExpectation = self.expectation(description: "Jasmine tests")
12+
// Standalone (not via self.expectation(...)) so we can drive it through
13+
// XCTWaiter alongside the crash watchdog without tripping the
14+
// XCTestCase "must waitForExpectations" rule.
15+
runtimeUnitTestsExpectation = XCTestExpectation(description: "Jasmine tests")
1316

1417
loop = try! SelectorEventLoop(selector: try! KqueueSelector())
1518
self.server = DefaultHTTPServer(eventLoop: loop!, port: port) {
@@ -58,11 +61,45 @@ class TestRunnerTests: XCTestCase {
5861
loop.stop()
5962
}
6063

61-
func testRuntime() {
64+
func testRuntime() {
65+
let jasmineTestsTimeout: TimeInterval = 300
66+
6267
let app = XCUIApplication()
6368
app.launchEnvironment["REPORT_BASEURL"] = "http://[::1]:\(port)/junit_report"
6469
app.launch()
6570

66-
wait(for: [runtimeUnitTestsExpectation], timeout: 300.0, enforceOrder: true)
71+
// Watchdog: if the runtime crashes (e.g. EXC_BAD_ACCESS) it never
72+
// POSTs results, and a plain `wait(for:)` would sit out the full
73+
// timeout. Fulfill the same expectation from the watchdog when the
74+
// app process leaves the running state, and track the crash via a
75+
// flag so we can still distinguish the two outcomes after the wait.
76+
var didCrash = false
77+
let crashWatchdog = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
78+
if app.state == .notRunning {
79+
didCrash = true
80+
self.runtimeUnitTestsExpectation.fulfill()
81+
}
82+
}
83+
// The XCUITest run loop spins in default mode during wait(for:); add
84+
// the timer to common modes too in case anything switches it.
85+
RunLoop.main.add(crashWatchdog, forMode: .common)
86+
87+
let result = XCTWaiter().wait(
88+
for: [runtimeUnitTestsExpectation],
89+
timeout: jasmineTestsTimeout
90+
)
91+
crashWatchdog.invalidate()
92+
93+
switch result {
94+
case .completed:
95+
if didCrash {
96+
XCTFail("TestRunner exited before reporting Jasmine results (likely crashed). Check ~/Library/Logs/DiagnosticReports/TestRunner-*.ips for the stack.")
97+
}
98+
return
99+
case .timedOut:
100+
XCTFail("Asynchronous wait failed: exceeded \(Int(jasmineTestsTimeout)) seconds with unfulfilled \"Jasmine tests\" expectation")
101+
default:
102+
XCTFail("Unexpected XCTWaiter result: \(result.rawValue)")
103+
}
67104
}
68105
}

0 commit comments

Comments
 (0)