From 980a603143c53d8e1a402cff74b0c0a1ada90702 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:24:44 +0000 Subject: [PATCH 1/4] test(cmux-next): an XCTest wait in swift test's XCTest phase must return swift test runs the bundle a second time in xctest after the Swift Testing tests. From 2026-11-02 that phase hung for about 10 minutes and died on signal 6 on CmuxNext suite steps. The hung stack (aws-m4pro-6, feat 305434be55a1): XCTestDriver waits on its start-up future with a turn of the main run loop, and the run-loop guard's block, which never returns, runs in that turn. This runs an XCTest probe that waits the same way in a child xctest of this bundle and needs it to pass and exit 0 within 120 s. Red on the base on every host; the fix follows. --- .../CmuxNextAppTests/XCTestPhaseTests.swift | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift new file mode 100644 index 000000000000..0125a1bfa2f5 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift @@ -0,0 +1,94 @@ +import CmuxNextTestExitTrace +import Darwin +import Foundation +import Testing +import XCTest + +/// `swift test` runs this bundle twice: its Swift Testing tests in `swiftpm-testing-helper`, then +/// its XCTest phase in Xcode's `xctest` tool, which loads the same bundle. From 2026-11-02 the +/// XCTest phase of CmuxNext suite steps hung for about 10 minutes and then died on signal 6, after +/// every Swift Testing test had passed (aws-m4pro-1, -3, -4, -6, -9, Blacksmith macOS 26). The stack +/// of the hung xctest (aws-m4pro-6, feat 305434be55a1): +/// +/// _XCTestMain -> -[XCTestDriver _runTests] -> -[XCTFuture value] +/// -> +[XCTWaiter _synchronouslyWaitForTimeInterval:] -> CFRunLoopRun -> __CFRunLoopDoBlocks +/// -> the run-loop guard's block -> guard_hold_the_main_loop -> CFRunLoopRun (for good) +/// +/// XCTest waits for its start-up work with a turn of the main run loop whenever that work is not +/// done yet (there, xctest's own Contacts lookup failing for 22 s). The run-loop guard +/// (Tests/Support/CmuxNextTestExitTrace) had queued a block that never returns, so that one turn +/// took xctest's main thread and XCTest never ran its suite. +struct XCTestPhaseTests { + /// An XCTest wait in `xctest` must come back: the probe below waits the way XCTest's own + /// start-up does, in a child `xctest` of this same bundle. On the base the guard held the + /// child's main thread, and it was killed here at the limit. + @Test(.timeLimit(.minutes(3))) + func anXCTestWaitInTheXCTestPhaseReturns() async throws { + let bundle = try #require(Self.testBundle(), "the test bundle that holds this test") + let output = FileManager.default.temporaryDirectory + .appendingPathComponent("xctest-phase-\(UUID().uuidString).log") + #expect(FileManager.default.createFile(atPath: output.path, contents: nil)) + defer { try? FileManager.default.removeItem(at: output) } + let log = try FileHandle(forWritingTo: output) + defer { try? log.close() } + + let child = Process() + child.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + child.arguments = ["xctest", "-XCTest", "CmuxNextAppTests.XCTestPhaseRunLoopProbe", bundle.path] + child.standardInput = FileHandle.nullDevice + child.standardOutput = log + child.standardError = log + let ended = AsyncStream.makeStream() + child.terminationHandler = { finished in + ended.continuation.yield(finished.terminationStatus) + ended.continuation.finish() + } + try child.run() + let pid = child.processIdentifier + + let status: Int32? = await withTaskGroup(of: Int32?.self) { group in + group.addTask { + for await status in ended.stream { return status } + return nil + } + group.addTask { + try? await Task.sleep(for: .seconds(120)) + return nil + } + let first = await group.next() ?? nil + group.cancelAll() + return first + } + if status == nil { kill(pid, SIGKILL) } + + let text = (try? String(contentsOf: output, encoding: .utf8)) ?? "" + let tail = String(text.suffix(4000)) + #expect(status == 0, "xctest: \(status.map { "exit \($0)" } ?? "still running after 120 s, killed"); its output ends: \(tail)") + #expect(text.contains("Executed 1 test, with 0 failures"), "the probe did not run and pass; output ends: \(tail)") + #expect(!text.contains("holding the main run loop open"), "the run-loop guard held xctest's main thread") + } + + /// `…/CmuxNextPackageTests.xctest`, from the image that holds the guard's own code. + private static func testBundle() -> URL? { + let installed: @convention(c) () -> Int32 = cmux_next_test_runloop_guard_installed + var info = Dl_info() + guard dladdr(unsafeBitCast(installed, to: UnsafeRawPointer.self), &info) != 0, let name = info.dli_fname else { + return nil + } + // .xctest/Contents/MacOS/ + let bundle = URL(fileURLWithPath: String(cString: name)) + .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + return bundle.pathExtension == "xctest" ? bundle : nil + } +} + +/// Run by ``XCTestPhaseTests`` in a child `xctest` only (no Swift Testing suite and no `--filter` +/// in the lanes names it). It waits on an expectation the way XCTest's start-up does: the wait +/// turns the main run loop until the expectation is met. +final class XCTestPhaseRunLoopProbe: XCTestCase { + func testAnXCTestWaitReturns() { + let turned = expectation(description: "the main run loop turned") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { turned.fulfill() } + wait(for: [turned], timeout: 10) + } +} From 03c6725c19fcb6f2ccb653ae4882d9fbdc64e326 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:25:49 +0000 Subject: [PATCH 2/4] fix(cmux-next tests): the run-loop guard holds only the Swift Testing helper, never xctest The guard (#25861) queues a block on the main run loop that never returns. That is right only in swiftpm-testing-helper, whose main is swift_task_asyncMainDrainQueue. swift test's XCTest phase loads the same bundle in xctest, where XCTestDriver waits on its start-up future with a turn of the main run loop: the guard's block ran in that turn, XCTest never ran its suite, and the process died on signal 6 about 10 minutes later (stack from aws-m4pro-6 at feat 305434be55a1). Every CmuxNext suite step was red after its Swift Testing tests passed. The guard now installs nothing in a process that is not swiftpm-testing-helper. Its own regression test (TestProcessExitTests.aStopOfTheOutermostRunLoopDoesNotEndTheTestRun) runs in the helper and is unchanged. --- .../Tests/CmuxNextAppTests/XCTestPhaseTests.swift | 15 +++++++++++---- .../CmuxNextTestRunLoopGuard.c | 15 +++++++++++++++ .../include/CmuxNextTestExitTrace.h | 4 ++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift index 0125a1bfa2f5..93ba77a39a74 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift @@ -15,10 +15,17 @@ import XCTest /// -> the run-loop guard's block -> guard_hold_the_main_loop -> CFRunLoopRun (for good) /// /// XCTest waits for its start-up work with a turn of the main run loop whenever that work is not -/// done yet (there, xctest's own Contacts lookup failing for 22 s). The run-loop guard +/// done yet (there, a Contacts store lookup in xctest failed for 22 s first; on aws-m4pro-1 the +/// same hang came with no Contacts line at all). The run-loop guard /// (Tests/Support/CmuxNextTestExitTrace) had queued a block that never returns, so that one turn /// took xctest's main thread and XCTest never ran its suite. struct XCTestPhaseTests { + @Test func theRunLoopGuardHoldsOnlyTheSwiftTestingHelper() { + #expect(cmux_next_test_runloop_guard_holds_in("swiftpm-testing-helper") == 1) + #expect(cmux_next_test_runloop_guard_holds_in("xctest") == 0) + #expect(cmux_next_test_runloop_guard_holds_in(nil) == 0) + } + /// An XCTest wait in `xctest` must come back: the probe below waits the way XCTest's own /// start-up does, in a child `xctest` of this same bundle. On the base the guard held the /// child's main thread, and it was killed here at the limit. @@ -82,9 +89,9 @@ struct XCTestPhaseTests { } } -/// Run by ``XCTestPhaseTests`` in a child `xctest` only (no Swift Testing suite and no `--filter` -/// in the lanes names it). It waits on an expectation the way XCTest's start-up does: the wait -/// turns the main run loop until the expectation is met. +/// Run by ``XCTestPhaseTests`` in a child `xctest`, and by the XCTest phase of a whole-package +/// `swift test`, where it passes in about 50 ms. It waits on an expectation the way XCTest's +/// start-up does: the wait turns the main run loop until the expectation is met. final class XCTestPhaseRunLoopProbe: XCTestCase { func testAnXCTestWaitReturns() { let turned = expectation(description: "the main run loop turned") diff --git a/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/CmuxNextTestRunLoopGuard.c b/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/CmuxNextTestRunLoopGuard.c index 4495517f5886..6ebf9ab2a71f 100644 --- a/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/CmuxNextTestRunLoopGuard.c +++ b/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/CmuxNextTestRunLoopGuard.c @@ -146,10 +146,25 @@ static void probe_fire(CFRunLoopTimerRef timer, void *info) { /// Defined beside this file: a run that lists its tests rather than running any. int cmux_next_test_lists_tests(void); +/// Only `swift test`'s Swift Testing helper has the main thread this guard protects: its `main` is +/// `swift_task_asyncMainDrainQueue`, which exits when its run of the main run loop returns. Every +/// other host of this bundle runs its own main, and the block below would take that main thread for +/// good the first time anything turns the main run loop. That is how `swift test`'s XCTest phase +/// (the `xctest` tool, which loads the same bundle) hung for about 10 minutes and then died on +/// signal 6 after every Swift Testing test had passed (2026-11-02, five fleet Macs and Blacksmith): +/// `-[XCTestDriver _runTests]` waits on its start-up future with a turn of the main run loop +/// (`+[XCTWaiter _synchronouslyWaitForTimeInterval:]`) whenever that work is not done yet, the +/// guard's block ran in that turn, and XCTest never ran its suite (XCTestPhaseTests). +int cmux_next_test_runloop_guard_holds_in(const char *process_name) { + return process_name != NULL && strcmp(process_name, "swiftpm-testing-helper") == 0; +} + __attribute__((constructor)) static void cmux_next_test_runloop_guard_install(void) { // A listing run reports its tests and ends; holding its run loop open could only hang it, and // nothing in a listing run needs the depth count either. if (cmux_next_test_lists_tests()) return; + // Nothing at all in another host (xctest, an Xcode test host): no observer, no source, no block. + if (!cmux_next_test_runloop_guard_holds_in(getprogname())) return; const char *setting = getenv("CMUX_NEXT_TEST_RUNLOOP_GUARD"); int hold = !(setting && strcmp(setting, "0") == 0); CFRunLoopRef main_loop = CFRunLoopGetMain(); diff --git a/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h b/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h index 7b911a2e40ad..167909d2c32b 100644 --- a/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h +++ b/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h @@ -12,6 +12,10 @@ int cmux_next_test_backtrace_kept_from_children(void); /// 1 once the main run loop survives being stopped at its outermost run (installed with the trace). int cmux_next_test_runloop_guard_installed(void); +/// 1 when the guard holds the main run loop in a process of this name: only `swift test`'s Swift +/// Testing helper (`swiftpm-testing-helper`), never `xctest` or another test host. +int cmux_next_test_runloop_guard_holds_in(const char *process_name); + /// How many times the guard has entered the main run loop again after it was stopped. int cmux_next_test_runloop_guard_reentries(void); From 435913a25e9aae991a7b81f66a3494dc5191028f Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:40:49 +0000 Subject: [PATCH 3/4] test(cmux-next): start the XCTest-phase probe as SwiftPM starts that phase Review of #33013: the child ran through the xcrun shim (SIP-protected, so the DYLD_* paths SwiftPM gives the phase were dropped) and without SWIFT_TESTING_ENABLED=0 (so XCTest could run Swift Testing tests in it, this test included). It now finds xctest with xcrun --find, runs it directly with the helper's environment and SWIFT_TESTING_ENABLED=0, as run_swift_test_bundle.py does. Comments in the header and Package.swift say the guard is in swiftpm-testing-helper only. --- Packages/macOS/CmuxNext/Package.swift | 7 +-- .../CmuxNextAppTests/XCTestPhaseTests.swift | 54 ++++++++++++------- .../include/CmuxNextTestExitTrace.h | 4 +- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index c99e5a97b477..d2826eeb0498 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -1690,9 +1690,10 @@ let packageTargets: [Target] = uiLayerPreviewTargets + [ dependencies: ["CmuxNextActions", "CmuxNextCompat", "CmuxNextDaemon", "CmuxNextDaemonService", "CmuxNextPages", "CmuxNextSettings"], swiftSettings: uiSwiftSettings ), - // Test-only: every exit() of the test process prints its stack, and the main run loop - // survives being stopped at its outermost run, whose return would end the whole test run - // through swift_task_asyncMainDrainQueue's exit(0) (TestProcessExitTests). + // Test-only: every exit() of the test process prints its stack, and in swiftpm-testing-helper + // the main run loop survives being stopped at its outermost run, whose return would end the + // whole test run through swift_task_asyncMainDrainQueue's exit(0) (TestProcessExitTests). + // Never in xctest, which runs its own main (XCTestPhaseTests). .target( name: "CmuxNextTestExitTrace", path: "Tests/Support/CmuxNextTestExitTrace", diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift index 93ba77a39a74..d09747ac0170 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift @@ -27,21 +27,43 @@ struct XCTestPhaseTests { } /// An XCTest wait in `xctest` must come back: the probe below waits the way XCTest's own - /// start-up does, in a child `xctest` of this same bundle. On the base the guard held the - /// child's main thread, and it was killed here at the limit. + /// start-up does, in a child `xctest` of this same bundle started as SwiftPM starts its XCTest + /// phase (xctest itself, not through the xcrun shim, which is SIP-protected and drops the + /// DYLD_* paths; SWIFT_TESTING_ENABLED=0, so XCTest runs no Swift Testing test in it). On the + /// base the guard held the child's main thread, and it was killed here at the limit. @Test(.timeLimit(.minutes(3))) func anXCTestWaitInTheXCTestPhaseReturns() async throws { let bundle = try #require(Self.testBundle(), "the test bundle that holds this test") - let output = FileManager.default.temporaryDirectory - .appendingPathComponent("xctest-phase-\(UUID().uuidString).log") - #expect(FileManager.default.createFile(atPath: output.path, contents: nil)) - defer { try? FileManager.default.removeItem(at: output) } - let log = try FileHandle(forWritingTo: output) - defer { try? log.close() } + let find = await Self.run("/usr/bin/xcrun", ["--find", "xctest"], environment: ProcessInfo.processInfo.environment, + limit: .seconds(60)) + let xctest = find.output.trimmingCharacters(in: .whitespacesAndNewlines) + try #require(find.status == 0 && xctest.hasPrefix("/"), "xcrun --find xctest: \(find.output)") + var environment = ProcessInfo.processInfo.environment + environment["SWIFT_TESTING_ENABLED"] = "0" + let phase = await Self.run(xctest, ["-XCTest", "CmuxNextAppTests.XCTestPhaseRunLoopProbe", bundle.path], + environment: environment, limit: .seconds(120)) + let tail = String(phase.output.suffix(4000)) + #expect(phase.status == 0, "xctest: \(phase.status.map { "exit \($0)" } ?? "still running after 120 s, killed"); its output ends: \(tail)") + #expect(phase.output.contains("Executed 1 test, with 0 failures"), "the probe did not run and pass; output ends: \(tail)") + #expect(!phase.output.contains("holding the main run loop open"), "the run-loop guard held xctest's main thread") + } + + /// Runs a child to its end or to `limit` (then SIGKILL, and a nil status). Its output goes to a + /// file, not a pipe, so a child that prints a lot can never block on a full pipe. + private static func run(_ executable: String, _ arguments: [String], environment: [String: String], + limit: Duration) async -> (status: Int32?, output: String) { + let output = FileManager.default.temporaryDirectory.appendingPathComponent("xctest-phase-\(UUID().uuidString).log") + guard FileManager.default.createFile(atPath: output.path, contents: nil), + let log = try? FileHandle(forWritingTo: output) else { return (nil, "cannot create \(output.path)") } + defer { + try? log.close() + try? FileManager.default.removeItem(at: output) + } let child = Process() - child.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") - child.arguments = ["xctest", "-XCTest", "CmuxNextAppTests.XCTestPhaseRunLoopProbe", bundle.path] + child.executableURL = URL(fileURLWithPath: executable) + child.arguments = arguments + child.environment = environment child.standardInput = FileHandle.nullDevice child.standardOutput = log child.standardError = log @@ -50,16 +72,15 @@ struct XCTestPhaseTests { ended.continuation.yield(finished.terminationStatus) ended.continuation.finish() } - try child.run() + do { try child.run() } catch { return (nil, "\(executable): \(error)") } let pid = child.processIdentifier - let status: Int32? = await withTaskGroup(of: Int32?.self) { group in group.addTask { for await status in ended.stream { return status } return nil } group.addTask { - try? await Task.sleep(for: .seconds(120)) + try? await Task.sleep(for: limit) return nil } let first = await group.next() ?? nil @@ -67,12 +88,7 @@ struct XCTestPhaseTests { return first } if status == nil { kill(pid, SIGKILL) } - - let text = (try? String(contentsOf: output, encoding: .utf8)) ?? "" - let tail = String(text.suffix(4000)) - #expect(status == 0, "xctest: \(status.map { "exit \($0)" } ?? "still running after 120 s, killed"); its output ends: \(tail)") - #expect(text.contains("Executed 1 test, with 0 failures"), "the probe did not run and pass; output ends: \(tail)") - #expect(!text.contains("holding the main run loop open"), "the run-loop guard held xctest's main thread") + return (status, (try? String(contentsOf: output, encoding: .utf8)) ?? "") } /// `…/CmuxNextPackageTests.xctest`, from the image that holds the guard's own code. diff --git a/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h b/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h index 167909d2c32b..c54e5d3267c2 100644 --- a/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h +++ b/Packages/macOS/CmuxNext/Tests/Support/CmuxNextTestExitTrace/include/CmuxNextTestExitTrace.h @@ -9,7 +9,8 @@ int cmux_next_test_exit_trace_installed(void); /// this process keeps its backtracer and its children (exit tests, privileged tools) do not get one. int cmux_next_test_backtrace_kept_from_children(void); -/// 1 once the main run loop survives being stopped at its outermost run (installed with the trace). +/// 1 once the main run loop survives being stopped at its outermost run (installed when the bundle +/// loads, in swiftpm-testing-helper only). int cmux_next_test_runloop_guard_installed(void); /// 1 when the guard holds the main run loop in a process of this name: only `swift test`'s Swift @@ -20,6 +21,7 @@ int cmux_next_test_runloop_guard_holds_in(const char *process_name); int cmux_next_test_runloop_guard_reentries(void); /// Runs of the main run loop in the common modes, outermost included: 1 when no nested pass is open. +/// Counted in swiftpm-testing-helper only; 0 in any other process. int cmux_next_test_runloop_guard_depth(void); /// Stops the run a stray stop would be fatal to, `times` times, each on a turn when that run is From bc800c77fbe0c5f3af6130ea44fe66bd8c54a1b4 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:44:01 +0000 Subject: [PATCH 4/4] test(cmux-next): the XCTest-phase test and its XCTestCase probe are nonisolated This target defaults to the main actor; an XCTestCase subclass there does not compile (its initializers override nonisolated ones), and the test's file reads need not run on the main thread. --- .../CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift index d09747ac0170..5b431a7d51ee 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/XCTestPhaseTests.swift @@ -19,7 +19,7 @@ import XCTest /// same hang came with no Contacts line at all). The run-loop guard /// (Tests/Support/CmuxNextTestExitTrace) had queued a block that never returns, so that one turn /// took xctest's main thread and XCTest never ran its suite. -struct XCTestPhaseTests { +nonisolated struct XCTestPhaseTests { @Test func theRunLoopGuardHoldsOnlyTheSwiftTestingHelper() { #expect(cmux_next_test_runloop_guard_holds_in("swiftpm-testing-helper") == 1) #expect(cmux_next_test_runloop_guard_holds_in("xctest") == 0) @@ -107,8 +107,9 @@ struct XCTestPhaseTests { /// Run by ``XCTestPhaseTests`` in a child `xctest`, and by the XCTest phase of a whole-package /// `swift test`, where it passes in about 50 ms. It waits on an expectation the way XCTest's -/// start-up does: the wait turns the main run loop until the expectation is met. -final class XCTestPhaseRunLoopProbe: XCTestCase { +/// start-up does: the wait turns the main run loop until the expectation is met. `nonisolated`: +/// this target defaults to the main actor, and XCTestCase's initializers are not isolated. +nonisolated final class XCTestPhaseRunLoopProbe: XCTestCase { func testAnXCTestWaitReturns() { let turned = expectation(description: "the main run loop turned") DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { turned.fulfill() }