test+fix: recover MockGateway-deletion coverage; fix async cancel deadlock#482
Merged
Conversation
…dlock
After PR 5 deleted client/{sync,async}/tests.rs, several files dropped
in coverage because the deleted MockGateway tests were the only callers
of certain trivial getters and MessageBus-trait impl methods.
Adds:
- src/client/{sync,async}_tests.rs: stubbed-Client tests covering
is_connected, client_id, server_version, time_zone, connection_time,
next_request_id, next_order_id, set_next_order_id, decoder_context,
market_data, order, send_*, create_order_update_subscription,
check_server_version (10 tests, sync+async).
- transport/sync/tests.rs: 5 tests against the real TcpMessageBus via
the MessageBus trait — cancel_subscription, cancel_order_subscription,
cancel_shared_subscription, send_message,
create_order_update_subscription.
- transport/async_tests.rs: 5 mirroring tests on AsyncMessageBus.
Fixes (transport/async.rs):
- AsyncTcpMessageBus::cancel_subscription and cancel_order_subscription
acquired a read guard, then awaited a write guard on the same RwLock
while the read guard was still alive (Rust shadowing doesn't drop
the prior binding). Self-deadlock on the same task. The new
cancel_subscription test was the first caller exercising this path
on the real bus and exposed the hang. Fix is a single write guard.
Coverage delta (cargo llvm-cov --all-features --summary-only):
TOTAL region 82.65% -> 83.11%, line 87.21% -> 87.67%
client/sync.rs region 52.74% -> 60.27%, line 61.54% -> 70.19%
client/async.rs region 62.59% -> 70.07%, line 73.45% -> 81.42%
transport/async.rs region 44.26% -> 51.83%, line 46.14% -> 53.88%
transport/sync/mod.rs region 59.02% -> 65.46%, line 62.06% -> 68.04%
Remaining gaps are concentrated in production-TCP paths (Client::connect,
AsyncTcpSocket, TcpMessageBus<TcpSocket> monomorphization) that cannot
be unit-tested without restoring socket scaffolding.
Adds minimal one-shot TCP listener helpers (sync + async, ~50 LOC each) that bind to 127.0.0.1:0, accept once, replay scripted handshake frames, and drain further writes until the client closes. No per-API-call mock surface — used only at the Client::connect* seam. Tests added per side (sync + async, 3 each): - connect_handshakes_against_real_socket: verify Client::connect succeeds and metadata is populated. - connect_with_callback_receives_unsolicited_messages: inject an OpenOrder mid-handshake; assert the callback receives it. - connect_with_options_applies_tcp_no_delay: verify connect_with_options(tcp_no_delay=true) returns Ok. Coverage delta from this PR's combined work (cargo llvm-cov --all-features --summary-only): TOTAL region 82.65% -> 83.80%, line 87.21% -> 88.21% client/sync.rs region 52.74% -> 90.41%, line 61.54% -> 93.27% client/async.rs region 62.59% -> 95.92%, line 73.45% -> 97.35% transport/async_io.rs region 0.00% -> 57.89%, line 0.00% -> 71.05% connection/sync.rs region 84.08% -> 88.57%, line 92.14% -> 95.00% connection/async.rs region 66.67% -> 73.64%, line 72.14% -> 77.86% transport/sync/mod.rs region 59.02% -> 69.81%, line 62.06% -> 72.52% transport/async.rs region 44.26% -> 51.83%, line 46.14% -> 53.88% Remaining gaps (e.g. AsyncTcpSocket::reconnect, dispatcher-loop branches) require more sophisticated test harnesses; deferred.
After PRs #473-#480 eliminated MockGateway and PR #482 introduced the handshake-replay listener, the testing docs were stale. - testing-patterns.md: full rewrite. Describes the three-fixture stratification (MessageBusStub for domain logic, MemoryStream for transport/connection, spawn_handshake_listener for Client::connect*). - build-and-test.md: replace MockGateway integration test snippet with MessageBusStub example; defer to testing-patterns.md for the full story. - troubleshooting.md: drop the obsolete "MockGateway tests failing" section. - CLAUDE.md: update doc index label.
5 tasks
wboayue
added a commit
that referenced
this pull request
May 2, 2026
) * fix(async): self-deadlock in cancel_subscription/cancel_order_subscription AsyncTcpMessageBus::cancel_subscription and cancel_order_subscription acquired a read guard, then awaited a write guard on the same RwLock while the read guard was still alive. Rust shadowing doesn't drop the prior binding before the new RHS evaluates, so the task self-deadlocks. Collapse to a single write guard. Backport of #482 (main). v2-stable bug is latent: production cancel flow routes through Subscription::cancel().await -> send_message, not through cancel_subscription, but the trait method is part of the pub(crate) AsyncMessageBus surface and any future caller would hang. Production fix only; the accompanying tests on main depend on the MemoryStream infrastructure that didn't land on v2-stable. Closes #483 * bump version to 2.11.4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After PR #480 deleted the MockGateway-driven test files, coverage dropped in a handful of files. This PR recovers it via two phases:
MessageBusStub-driven) for trivial Client accessors andMessageBus-trait impl methods on the realTcpMessageBus/AsyncTcpMessageBus.spawn_handshake_listenerper side, ~50 LOC each) that bind to127.0.0.1:0and letClient::connect*exercise the production TCP paths without restoring MockGateway's per-API-call surface.Also fixes a real production deadlock found while writing the trait-level cancel tests.
Coverage delta
cargo llvm-cov --all-features --summary-only:client/sync.rsclient/async.rstransport/async_io.rsconnection/async.rsconnection/sync.rstransport/sync/mod.rstransport/async.rsWhat's added
Phase 1 —
MessageBusStub-driven unit testssrc/client/{sync,async}_tests.rs— sibling test files exercisingClient::stubbed-built clients: accessor round-trip,check_server_versionbranches, builder factories,send_*helpers,create_order_update_subscriptionuniqueness.transport/sync/tests.rs— 5 tests callingMessageBus-trait methods on the realTcpMessageBus<MemoryStream>:cancel_subscription,cancel_order_subscription,cancel_shared_subscription,send_message,create_order_update_subscription.transport/async_tests.rs— 5 mirroring tests on the realAsyncTcpMessageBus<MemoryStream>plus anis_connectedreflection check.Phase 2 — Handshake-replay TCP listener
src/transport/sync/test_listener.rs+src/transport/async_test_listener.rs—spawn_handshake_listener(frames) -> (SocketAddr, JoinHandle). Binds127.0.0.1:0, accepts once, consumes theAPI\0magic + version range + start_api writes, replays length-prefixed frames, then drains further writes until the client closes.client/{sync,async}_tests.rs:connect_handshakes_against_real_socket—Client::connectsucceeds, metadata populated.connect_with_callback_receives_unsolicited_messages— injects an OpenOrder mid-handshake; asserts the callback fires.connect_with_options_applies_tcp_no_delay—connect_with_options(tcp_no_delay=true)returns Ok.What's fixed
AsyncTcpMessageBus::cancel_subscriptionandcancel_order_subscriptionself-deadlocked: each acquired a read guard, then awaited a write guard on the sameRwLockwhile the read guard was still alive (Rust shadowing doesn't drop the prior binding before the new RHS evaluates).The bug was latent because the production cancel flow goes through
Subscription::cancel().await → message_bus.send_message(...), not throughcancel_subscription. The newtest_cancel_subscription_writes_and_clears_channelwas the first caller to hit the path on the real bus and hung. Fix collapses to a single write guard.Backport tracked in #483 (v2-stable has the identical bug).
Remaining gaps (intentionally accepted)
transport/async.rs51.83% —process_messagestask body branches (timeout-loop, connection-error reconnect) need more sophisticated test harnesses; deferred.transport/async_io.rs57.89% —AsyncTcpSocket::reconnectandsleep(the listener accepts once, can't drive reconnect cleanly without listener swap).transport/routing.rs72.33% — routing decision branches; orthogonal to MockGateway deletion.These are covered by the live-Gateway integration tests under
tests/.Test plan
cargo fmt --checkcargo clippy --all-targets -- -D warningscargo clippy --all-targets --features sync -- -D warningscargo clippy --all-featurescargo test(default): 877 passed (+13 from main)cargo test --no-default-features --features sync: 881 passed (+13)cargo test --all-features: 1058 passed (+26)