cargo test -p jcode-tui --lib does not finish on Windows at the default
thread count. It is not slow, it deadlocks: every worker blocks and the run
hangs indefinitely, so every test after the wedge is silently never executed.
This is on current master (verified at e65e47c31), not a fork-specific
problem. It matters beyond Windows because a hung run reports nothing, so the
suite has been quietly under-reporting rather than failing loudly.
Reproduce
cargo test -p jcode-tui --lib # hangs forever on Windows
cargo test -p jcode-tui --lib -- --test-threads=2 # completes
Exactly --test-threads many workers wedge. 1 and 2 are clean; 4, 8 and 16 all hang.
Cause
Two process-global test mutexes:
jcode_base::storage::lock_test_env() (guards JCODE_HOME / env vars)
jcode_tui::tui::ui::render_state_test_lock() (guards global render state)
create_test_app takes the render lock internally, so the de-facto order is
env then render, and six tests follow it. Two take them in the opposite
order, which is a classic AB-BA cycle:
-
crates/jcode-tui/src/tui/app/tests/scroll_copy_02/part_02.rs:86
test_alt_shift_i_toggles_inline_images_and_persists takes render, then env.
-
crates/jcode-tui/src/tui/app/tests/smoothness_benchmark.rs:52
smoothness_benchmark_simulated_streaming_turn_stays_within_budget takes
render, then env indirectly via with_reasoning_current_home.
The second is the one that makes this hard to spot: the env acquisition is
inside a helper, so the call site does not look like it touches a lock at all.
A note on reading the thread dump
An earlier pass at this concluded a guard was being leaked from a dead thread,
because a gdb dump shows every worker blocked with no thread holding either
lock. That reading is wrong, and it is an easy trap: holding a mutex leaves no
stack frame, so a thread that owns one lock while blocking on the other is
indistinguishable from a thread that owns nothing. "No visible holder" is
exactly what an AB-BA cycle looks like.
Both locks also recover from poisoning, so a panicking test cannot leak them.
Fix
Reducing the repro to just the two conflicting tests wedges within ~5 runs and
passes in 0.2s otherwise, which is why this never reproduced on demand.
Making both follow the env -> render order:
|
before |
after |
| full suite, default 16 threads |
never completes |
~50s, 5/5 runs |
| the 2-test repro at 2 threads |
hangs within ~5 runs |
20/20 clean |
| serial results |
2255 passed / 15 failed |
2255 passed / 15 failed |
Identical pass/fail counts: this removes a hang, not a test.
The second test does not need the explicit render lock at all, since
create_test_app already takes it.
I also added a source-scanning test that asserts the ordering and names the
file, line and offending call, counting the three env-taking helpers
(with_temp_jcode_home, with_reasoning_current_home,
with_ssh_remote_test_home) as env acquisitions so the indirect form is
caught. It is static rather than runtime because the race is timing-dependent,
so a green run proves very little. Verified it fails by reintroducing the
original inversion.
Availability
The fix is one commit on top of master, on a fork:
YalmutairiAisc/jcode @ fix/windows-overload-and-tests.
I could not open a PR ("An owner of this repository has limited the ability to
open a pull request to users that are collaborators on this repository"), so
filing here instead. Happy to send it as a standalone one-commit PR if you
would like to enable that, or feel free to just take the change directly, it is
a three-line reorder plus the guard test.
cargo test -p jcode-tui --libdoes not finish on Windows at the defaultthread count. It is not slow, it deadlocks: every worker blocks and the run
hangs indefinitely, so every test after the wedge is silently never executed.
This is on current
master(verified ate65e47c31), not a fork-specificproblem. It matters beyond Windows because a hung run reports nothing, so the
suite has been quietly under-reporting rather than failing loudly.
Reproduce
Exactly
--test-threadsmany workers wedge. 1 and 2 are clean; 4, 8 and 16 all hang.Cause
Two process-global test mutexes:
jcode_base::storage::lock_test_env()(guardsJCODE_HOME/ env vars)jcode_tui::tui::ui::render_state_test_lock()(guards global render state)create_test_apptakes the render lock internally, so the de-facto order isenv then render, and six tests follow it. Two take them in the opposite
order, which is a classic AB-BA cycle:
crates/jcode-tui/src/tui/app/tests/scroll_copy_02/part_02.rs:86test_alt_shift_i_toggles_inline_images_and_persiststakes render, then env.crates/jcode-tui/src/tui/app/tests/smoothness_benchmark.rs:52smoothness_benchmark_simulated_streaming_turn_stays_within_budgettakesrender, then env indirectly via
with_reasoning_current_home.The second is the one that makes this hard to spot: the env acquisition is
inside a helper, so the call site does not look like it touches a lock at all.
A note on reading the thread dump
An earlier pass at this concluded a guard was being leaked from a dead thread,
because a gdb dump shows every worker blocked with no thread holding either
lock. That reading is wrong, and it is an easy trap: holding a mutex leaves no
stack frame, so a thread that owns one lock while blocking on the other is
indistinguishable from a thread that owns nothing. "No visible holder" is
exactly what an AB-BA cycle looks like.
Both locks also recover from poisoning, so a panicking test cannot leak them.
Fix
Reducing the repro to just the two conflicting tests wedges within ~5 runs and
passes in 0.2s otherwise, which is why this never reproduced on demand.
Making both follow the env -> render order:
Identical pass/fail counts: this removes a hang, not a test.
The second test does not need the explicit render lock at all, since
create_test_appalready takes it.I also added a source-scanning test that asserts the ordering and names the
file, line and offending call, counting the three env-taking helpers
(
with_temp_jcode_home,with_reasoning_current_home,with_ssh_remote_test_home) as env acquisitions so the indirect form iscaught. It is static rather than runtime because the race is timing-dependent,
so a green run proves very little. Verified it fails by reintroducing the
original inversion.
Availability
The fix is one commit on top of
master, on a fork:YalmutairiAisc/jcode@fix/windows-overload-and-tests.I could not open a PR ("An owner of this repository has limited the ability to
open a pull request to users that are collaborators on this repository"), so
filing here instead. Happy to send it as a standalone one-commit PR if you
would like to enable that, or feel free to just take the change directly, it is
a three-line reorder plus the guard test.