Repository navigation
Stop a blocking wait helper from shadowing the pumping ones - #8725
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe blocking condition polling helper is renamed to clarify its behavior, with expanded documentation. Three synchronization points in the Codex timeout regression test now call the renamed helper. ChangesCodex timeout regression test synchronization
Estimated code review effort: 1 (Trivial) | ~5 minutes 🚥 Pre-merge checks | ✅ 25✅ Passed checks (25 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
1160ce8 to
47439d4
Compare
47439d4 to
29e33cd
Compare
Greptile SummaryThis PR fixes three flaky tests caused by Swift overload resolution silently binding
Confidence Score: 5/5Safe to merge — all changes are confined to test files with no production code touched. The change is a targeted rename in two test-support files. The three updated call sites all wait on a socket accumulator filled by background threads, which is the explicitly documented correct use of the blocking form. The rename itself becomes a compile-time guard against future misuse: any new waitForCondition(timeout:) in a file without a pumping helper will fail to compile rather than silently stalling the main thread. No production logic is modified. Files Needing Attention: No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
subgraph Before
A[waitForCondition timeout 2.0] --> B{Swift overload resolution}
B -->|fewer defaults needed| C[module-scope waitForCondition Thread.sleep blocks main]
B -.->|more defaults needed| D[file-private waitForCondition XCTWaiter pumps main queue]
C --> E[Main thread starved - condition never true]
end
subgraph After
F[waitForConditionBlocking timeout 2.0] --> G[module-scope waitForConditionBlocking Thread.sleep explicit]
H[waitForCondition timeout 2.0] --> I[file-private waitForCondition XCTWaiter pumps main queue]
J[waitForCondition timeout 2.0 in file without pumping helper] --> K[Compile error - no matching overload]
G --> L[Socket/file conditions filled off main thread]
I --> M[Main-actor work can complete]
end
Reviews (3): Last reviewed commit: "cmuxTests: stop a blocking wait helper f..." | Re-trigger Greptile |
29e33cd to
c5bd939
Compare
Two test files define a file-private `waitForCondition` that polls by hopping
through DispatchQueue.main under XCTWaiter, so the main queue keeps draining while
a test waits. The test target also has a module-scope one that polls with
Thread.sleep and runs no run loop at all.
Swift prefers the overload that fills in fewer defaulted parameters, so
`waitForCondition(timeout: X) { ... }` binds to the module-scope blocking helper --
it only defaults pollInterval, where the file-private one would also default file
and line. A call with no timeout: argument cannot bind to it and gets the pumping
helper. So adding a timeout silently changed which helper ran, and blocked the main
thread for the whole budget.
That is fatal for anything waiting on main-actor work. Three call sites pass an
explicit timeout and all three are red on main:
- testRemoteSplitSkipsInitialGitMetadataProbe and
testUnrelatedDefaultsChangeDoesNotRestartGitMetadataRefreshes wait for the
initial sidebar git probe to drain. That probe is registered synchronously at
schedule time and only clears once the ladder task, the snapshot, and its
MainActor.run apply get main-actor turns, so a blocking wait denies the very work
it waits for. Both fail on their first assertion, before reaching what they mean
to check, with the run wedged long enough that the crash reporter logged an ANR.
The product is correct in both cases.
- testFocusedPanelTitleRefreshesAutoWorkspaceTitleInSplitWorkspace waits for a
panel title to propagate. The .ghosttyDidSetTitle observer is registered with
queue: .main, so it runs as a queued main-queue operation rather than inline with
the post, and the apply is deferred again by the panel title coalescer's default
1/30s delay. A second of Thread.sleep starves both hops. Unlike the other two it
fails at the behavior the test is named after, with earlier assertions passing.
Renaming the blocking helper to waitForConditionBlocking makes all three resolve to
their file-private helpers again, with the same budgets and poll intervals. Its
three existing callers wait on a socket accumulator filled off the main thread, so
they keep the blocking form and are unaffected. A future waitForCondition(timeout:)
in a file without a pumping helper now fails to compile instead of quietly
blocking.
c5bd939 to
b008c6c
Compare
Three tests fail on
mainbecause adding atimeout:argument to a wait silently changes which helperruns. Two are in the sidebar git probe suites and one is in
TabManagerWorkspaceOwnershipTests, andthe product is correct in all three cases.
Why the wait cannot succeed
TabManagerUnitTests.swiftandWorkspacePullRequestSidebarTests.swifteach define a file-privatewaitForConditionthat polls by hopping throughDispatchQueue.mainunderXCTWaiter, so the mainqueue keeps draining while a test waits. The test target also has a module-scope
waitForCondition(timeout:pollInterval:_:)inCLICodexHookTimeoutRegressionTestSupport.swiftthatpolls with
Thread.sleepand runs no run loop at all.Swift prefers the overload that fills in fewer defaulted parameters.
waitForCondition(timeout: 12.0) { … }leaves onlypollIntervaldefaulted on the module-scope one, but would leavepollInterval,fileandlinedefaulted on the file-private one, so it binds to the blocking version. A call withno
timeout:argument cannot bind to the module-scope one at all and gets the pumping version. So thewaits that pass a timeout block the main thread for their entire budget, and every neighbouring wait in
the same file does not.
That is fatal for these tests, because they wait on main-actor work. The two probe tests fail on their
very first assertion, before reaching what they mean to check. An initial sidebar git probe is
registered synchronously at schedule time — the state map is written when the ladder task is installed,
and the accessor unions both maps, so a probe reports as active before it has run a single attempt.
Clearing it needs the ladder task's first hop, the detached snapshot, and its
await MainActor.runapply. Twelve seconds of
Thread.sleepon the main thread denies all three, so the set cannot drainand the condition can never become true. The logs show a
workspace.gitProbe.schedule … reason=initialline, then 16.5 seconds of silence with no apply, and the crash reporter logging an ANR partway
through.
None of the three failures carries the "Timed out waiting for condition" message that both
file-private helpers emit on timeout, which is independent confirmation that the blocking overload ran
in each case.
The product was right in all three cases
For the remote-split test, the panel id in the failure is the workspace's original local panel, not the
split. The split panel has no
gitProbe.scheduleline at all, so the skip guard did fire, and the twoassertions that check the panel is remote both passed.
ProbeSchedulingTests.swiftin CmuxSidebarGitpins the same behavior deterministically with a fake host and a manual clock, and it passes.
For the defaults test, a non-repository directory does not leak a probe: the snapshot resolves to "not
found", the refresh stops on the first attempt, and both map entries are removed.
testTrackedWorkspaceGitMetadataPollCandidatesExcludeDirectoriesWithoutResolvedGitMetadatawaits onthe same accessor for a plain temp directory and passes in 0.077 seconds, using the pumping helper.
The title test differs from the other two in one way worth noting: its earlier assertions pass and it
fails at the behavior it is named after, rather than before reaching it. Delivery there is genuinely
asynchronous: the
.ghosttyDidSetTitleobserver is registeredwith
queue: .main, so it runs as a queued main-queue operation rather than inline with the post, andthe apply is deferred again by the panel title coalescer's default 1/30s delay. A second of
Thread.sleepon the main thread starves both hops.The change
Rename the blocking helper to
waitForConditionBlockingand document what it is for. The three callsthen resolve to their file-private helpers again with the same budgets and poll intervals — no
assertion, timeout, or interval is touched. Its three existing callers wait on a socket accumulator
filled off the main thread, which is what the blocking form is appropriate for, so they keep it.
Renaming rather than shadow-proofing the call sites is what makes this stay fixed: a future
waitForCondition(timeout:)written in a file without a pumping helper now fails to compile instead ofquietly blocking the main thread.
Test plan
Measured on
4253cc2884, one GUI test host at a time, same checkout and DerivedData for both arms:mainunchangedThe three tests that flip are
testRemoteSplitSkipsInitialGitMetadataProbe,testUnrelatedDefaultsChangeDoesNotRestartGitMetadataRefreshesandtestFocusedPanelTitleRefreshesAutoWorkspaceTitleInSplitWorkspace.CLICodexHookTimeoutRegressionTests,which owns the renamed helper, reports
Test run with 8 tests in 1 suite passedin both arms, so itsthree callers are unaffected. No host restarts in either arm.
Pull-request CI on this repo runs review bots and security scanners, not the test suite, so the arms
above are the only test evidence this carries.
The six tests still red on this branch fail for reasons unrelated to waits: two git-index fixtures that
describe a state git cannot produce, a scoped socket report sent to a manager AppDelegate cannot
resolve, an async test using the pumping helper from a main-actor job, a stub that watches a subprocess
the product no longer spawns, and a product bug in
TabManager.closeWorkspace.Summary by CodeRabbit