test: G-4 unix accept loop + G-5 unnamed fail-loud guards
- G-4: drive UnixListenerHalves::accept_loop — bind_unix + temp path,
the loop exits on the queue-closed handler error after one unix
client accept (the unix parity twin of the TCP accept-loop test;
closes the largest single uncovered block, local/mod.rs 591-609).
- G-5: pin ADR-008 §3's two fail-loud sentences — bind_unix("") is
the kernel autobind (no filesystem path) so local_addr fails loud
(578-583), and a socket2 autobind client against a NAMED listener
is refused by accept_pair's unnamed-peer guard (626-633; Linux —
abstract addresses/autobind are Linux-shaped, mio maps any
abstract-namespace peer to unnamed).
Verification: cargo test --features local green (162 tests);
clippy --all-targets -D warnings clean (native + wasm32-unknown-
unknown); cargo fmt --check clean.
This commit is contained in:
1 parent
7991bf6ad0
commit
98afb092ce
3 files changed
+156
-2
No files matched your search
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: tunnels/review-002-coverage-gaps
|
||||
name: "G-4 + G-5 — close the remaining cheap coverage gaps (unix accept-loop test, unnamed unix fail-loud guards)"
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
@@ -87,4 +87,48 @@ promise.
|
||||
|
||||
## Notes
|
||||
|
||||
> Agent fills during implementation.
|
||||
> Agent fills during implementation.
|
||||
|
||||
### Implementation (2026-09-20)
|
||||
|
||||
**G-4** — `unix_accept_loop_stops_feeding_after_close_and_maps_the_drop`
|
||||
in `tests/producer_listen.rs` (mirroring the TCP twin above it):
|
||||
`bind_unix` + a temp path under the test's own pid-scoped dir, the
|
||||
listener's `accept_loop` spawned against a pre-closed `AcceptQueue`,
|
||||
one unix client connect, and the loop's `handler_error` exit asserted
|
||||
within a 5s bound. Covers the full 591-609 range (the unix
|
||||
accept-first loop was previously never driven).
|
||||
|
||||
**G-5** — `unix_unnamed_bind_and_unnamed_peer_fail_loud` in
|
||||
`tests/local_bindfirst.rs` (`#[cfg(target_os = "linux")]` — abstract
|
||||
addresses + autobind are Linux-shaped). Both fail-loud arms pinned via
|
||||
the crate's own public API (no private-constructor seam needed):
|
||||
|
||||
- **Unnamed bind**: `bind_unix("")` — tokio passes the empty path
|
||||
through to `bind(2)`, which the kernel autobinds (addlen ==
|
||||
`offsetof(sun_path)`, an abstract name, no filesystem path) —
|
||||
`local_addr` maps it to the "no filesystem path" handler error
|
||||
(the 578-583 arm). Probed on this host: the bound addr reports
|
||||
`as_pathname() == None`.
|
||||
- **Unnamed peer**: a socket2 client bound to the empty abstract
|
||||
sockaddr (the autobind shape — verified `Ok` on this kernel)
|
||||
connects to a NAMED `bind_unix` listener; `accept_pair` refuses
|
||||
with the unnamed-peer handler error (the 626-633 arm). A plain
|
||||
unbound client behaves identically: mio's UDS accept maps ANY
|
||||
abstract-namespace peer (`sun_path[0] == 0`, which includes the
|
||||
autobind assignment) to an unnamed addr, so the fail-loud guard
|
||||
catches both client shapes through tokio's accept.
|
||||
|
||||
`bind_unix_first` itself stays untested for the unnamed-bind arm at
|
||||
the establisher level (it composes `bind_unix` + `local_addr` — the
|
||||
arm is the same code path the test drives directly); the local_addr
|
||||
assertion covers the composition's error leg.
|
||||
|
||||
**Verification** (all green):
|
||||
|
||||
- `cargo test` (135) + `cargo test --features local` (162 passed,
|
||||
0 failed)
|
||||
- `cargo clippy --all-targets -- -D warnings` (default + `local`)
|
||||
- `cargo fmt --check`
|
||||
- `cargo check` + `cargo clippy -D warnings --target
|
||||
wasm32-unknown-unknown` (the default crate stays wasm-clean)
|
||||
@@ -400,3 +400,75 @@ async fn accept_pair_loop_downcast_miss_fails_loud() {
|
||||
"the downcast miss is handler_error-class: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The unnamed-peer/unnamed-bind fail-loud arms (G-5 — ADR-008 §3's
|
||||
/// two pinned sentences: a producer that cannot determine the peer
|
||||
/// does not serve a bind-first listener over that substrate):
|
||||
///
|
||||
/// - **Unnamed bind** — an empty-path `bind_unix` is the kernel's
|
||||
/// autobind (the kernel assigns an abstract name, addlen ==
|
||||
/// offsetof(sun_path)); the bound addr has no filesystem path, so
|
||||
/// `local_addr` (and thus `bind_unix_first`) fails loud with the
|
||||
/// "no filesystem path" handler error instead of serving an
|
||||
/// unrepresentable `bound`.
|
||||
/// - **Unnamed peer** — an addressless client (socket2 autobind
|
||||
/// here; a plain unbound client behaves identically through tokio
|
||||
/// accept — mio maps any abstract-namespace peer to unnamed) is
|
||||
/// accepted by a NAMED listener; `accept_pair` fails loud because
|
||||
/// the forwarded op's `peer` param is required on the wire.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
async fn unix_unnamed_bind_and_unnamed_peer_fail_loud() {
|
||||
// Arm 1: the autobind listener — `bind_unix("")`. tokio passes the
|
||||
// empty path through to `bind(2)` with addlen == offsetof(sun_path):
|
||||
// the kernel autobinds an abstract name, no filesystem path exists.
|
||||
let halves = alktunnels::local::bind_unix("")
|
||||
.expect("autobind bind succeeds (the kernel assigns the abstract name)");
|
||||
let err = halves
|
||||
.local_addr()
|
||||
.expect_err("an autobound listener has no filesystem path");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
alktunnels::producer::TunnelEstablishError::HandlerError(ref m)
|
||||
if m.contains("no filesystem path"),
|
||||
),
|
||||
"unnamed bind maps to the handler error: {err:?}"
|
||||
);
|
||||
|
||||
// Arm 2: the unnamed peer — a socket2 autobind client connects to a
|
||||
// NAMED listener; `accept_pair` must refuse (the forwarded `peer`
|
||||
// param requires an address).
|
||||
let dir = std::env::temp_dir().join(format!("alktunnels-g5-{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let listen_path = dir.join("named.sock");
|
||||
let _ = std::fs::remove_file(&listen_path);
|
||||
|
||||
let named =
|
||||
alktunnels::local::bind_unix(&listen_path.to_string_lossy()).expect("bind named listener");
|
||||
assert!(named.local_addr().is_ok(), "the named listener has a path");
|
||||
|
||||
let client = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)
|
||||
.expect("socket2 client");
|
||||
client
|
||||
.bind(&socket2::SockAddr::unix("").expect("empty sockaddr (the autobind shape)"))
|
||||
.expect("autobind client bind");
|
||||
client
|
||||
.connect(&socket2::SockAddr::unix(&listen_path).expect("listen sockaddr"))
|
||||
.expect("autobind client dials");
|
||||
|
||||
let err = named
|
||||
.accept_pair()
|
||||
.await
|
||||
.expect_err("an unnamed peer cannot be represented in the forwarded op");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
alktunnels::producer::TunnelEstablishError::HandlerError(ref m)
|
||||
if m.contains("unnamed peer"),
|
||||
),
|
||||
"unnamed peer maps to the handler error: {err:?}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&listen_path);
|
||||
}
|
||||
@@ -384,6 +384,44 @@ async fn accept_loop_stops_feeding_after_close_and_maps_the_drop() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The unix accept loop's queue-closed mapping (G-4 — the unix parity
|
||||
/// twin of the TCP test above): the same loop shape over
|
||||
/// `UnixListenerHalves` — a `push` after close errors the loop with
|
||||
/// the handler error, the loop exits on the mapped error, and the
|
||||
/// unaccepted far end is dropped.
|
||||
#[tokio::test]
|
||||
#[cfg(feature = "local")]
|
||||
async fn unix_accept_loop_stops_feeding_after_close_and_maps_the_drop() {
|
||||
let queue = AcceptQueue::new();
|
||||
queue.close().await;
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("alktunnels-unixaccept-{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join("accept-loop.sock");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let listen_str = path.to_string_lossy().into_owned();
|
||||
|
||||
let listener = alktunnels::local::bind_unix(&listen_str).expect("bind");
|
||||
let loop_queue = queue.clone();
|
||||
let outcome = tokio::spawn(async move { listener.accept_loop(loop_queue).await });
|
||||
let _ = tokio::net::UnixStream::connect(&path)
|
||||
.await
|
||||
.expect("connect");
|
||||
let err = tokio::time::timeout(std::time::Duration::from_secs(5), outcome)
|
||||
.await
|
||||
.expect("accept loop timed out (did it keep feeding a closed queue?)")
|
||||
.expect("accept loop task")
|
||||
.expect_err("closed queue must end the accept loop");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
alktunnels::producer::TunnelEstablishError::HandlerError(_)
|
||||
),
|
||||
"queue-closed push maps to the handler error, got {err:?}"
|
||||
);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// The lost-wakeup regression (review 001 C-1): a `push` landing in
|
||||
/// the wall-clock window between a parked `pop`'s state check and its
|
||||
/// `Notified` registration must still wake the pop. The window only
|
||||
|
||||
Reference in new issue
Block a user