diff --git a/quest/m1/mock-clock-tests/README.md b/quest/m1/mock-clock-tests/README.md index f1df1479e8a..f85d34e2cbb 100644 --- a/quest/m1/mock-clock-tests/README.md +++ b/quest/m1/mock-clock-tests/README.md @@ -40,7 +40,7 @@ before starting, since tests land and move. - [js/net tests run on a mocked clock](/quest/m1/mock-clock-tests/js-net.md) - about 120 js/net tests stop waiting on real time, through their shared helpers - [Other JS tests run on a mocked clock](/quest/m1/mock-clock-tests/js-rest.md) - hang's `settle(20)`, publish's fanout helpers, and the rest of JS -- [moq-relay tests run on a mocked clock](/quest/m1/mock-clock-tests/rs-relay.md) - the relay's `timeout(..).is_err()` session proofs and settle sleeps +- [moq-relay's mesh tests run on a mocked clock](/quest/m1/mock-clock-tests/rs-relay.md) - the relay's multi-relay quiet windows and settle sleeps - [Transport crate tests run on a mocked clock](/quest/m1/mock-clock-tests/rs-transport.md) - moq-tokio, moq-quic, moq-sock, and moq-uring - [Media crate tests run on a mocked clock](/quest/m1/mock-clock-tests/rs-media.md) - moq-audio, moq-video, moq-hls, moq-mux, and the bridges - [Binding and CLI tests run on a mocked clock](/quest/m1/mock-clock-tests/rs-bindings.md) - moq-ffi, moq-c, moq-cli, and kio diff --git a/quest/m1/mock-clock-tests/rs-relay.md b/quest/m1/mock-clock-tests/rs-relay.md index 2488dd3ebf8..1b0cecc3d06 100644 --- a/quest/m1/mock-clock-tests/rs-relay.md +++ b/quest/m1/mock-clock-tests/rs-relay.md @@ -1,21 +1,55 @@ -# [M] moq-relay tests run on a mocked clock +# [M] moq-relay's mesh tests run on a mocked clock ## Goal -moq-relay's tests stop proving a session stayed open with -`timeout(200ms, session.closed()).is_err()` on the real clock, and stop -sleeping to settle. See the [epic](/quest/m1/mock-clock-tests/README.md) -for the rules. +moq-relay's multi-relay and process-signal tests stop proving that nothing +happened by waiting out a quiet window, and stop sleeping to settle. See the +[epic](/quest/m1/mock-clock-tests/README.md) for the rules. ## Plan -As of 2026-11-06, 28 tests: `tests/integration/auth_lifetime.rs` 9, -`session_revalidate.rs` 4, `public_rules.rs` 3, `goaway_cluster.rs` 2, -`runtime_uring.rs` 2, `tenant_matrix.rs` 2, `web.rs` 2, and one each in -`cluster_unknown.rs`, `socket_auth.rs`, `src/auth.rs`, and `src/session.rs`. -Many run over real sockets: assert on the event the relay emits (a -revalidation, a close reason), or drive the deadline with the -[simulator or a `Manual` clock](/quest/m0/test-flakes-2/README.md), rather -than waiting out a window. +The single-relay tests are done: a session that stays open is proven by the +`end` its lease reports for the client's own close, an absent broadcast by a +listing read to `Synced`, and the GOAWAY handover by a manual clock. What is +left, as of 2026-11-06: -Public API: none. Wire: none. +- `cluster_unknown.rs`: `assert_unknown_publisher_stays_announced` watches for + a flap for `SETTLE` (1s) after the frame reaches D. +- `goaway_cluster.rs`: `cluster_migrates_on_upstream_goaway` and + `cluster_continues_a_group_split_by_goaway` end on a 500ms window with no + retraction, and the second also on a 500ms window with no downstream GOAWAY. +- `cluster_drain.rs`: `pass` moves a manual clock in steps and sleeps `SETTLE` + on tokio's clock after each, so real sockets catch up. +- `shutdown_signal.rs`: `an_embedder_owns_the_signals` waits `DRAIN_TIMEOUT / 2` + for a GOAWAY that must not come. + +Each crosses a mesh of relays over loopback, where no event marks convergence: +`Synced` is per hop, origin cursors coalesce across prefixes, and retractions +are held for `update_hold`. A quiet window is the only proof that fits today. +Only the simulator's `idle` proves a mesh has settled, and the cluster dials +peers only through `moq_tokio` sockets. + +Tests that only wait for progress, or bound real-socket timing from below, stay +on the real clock: the `runtime_uring.rs` and listing poll loops, `web.rs`'s +`start.elapsed() >= timeout`, `bitrate_caps.rs`'s `LEAST`, and +`shutdown_signal.rs`'s drain-window lower bounds. + +Decided 2026-11-06 (maintainer, on +[moq-dev/moq#8087](https://github.com/moq-dev/moq/pull/8087)): + +- ✅ **A dial hook on `cluster::Options` that an in-memory link can fill**, so + the four tests above run on `kio::clock::sim` and prove convergence with + `sim::idle` instead of a quiet window. Not chosen: keeping them as documented + real-clock exceptions opted out of the [Rust guard](/quest/m0/clock-guard/rust.md). + +Guidance: + +- The hook is public API whose only consumer today is these tests; name it by + role (how a peer is reached), not by the test transport. +- The mesh tests build whole relays with `Relay::load` over loopback, so the + simulator needs a relay that accepts in-memory links too, as + `tests/integration/memory.rs` already does for one relay. +- `shutdown_signal`'s window waits on a process signal, not a mesh: its relay + must also take that signal from a source the simulator can drive. + +Public API: a new `cluster::Options` dial hook. Wire: none. diff --git a/rs/moq-relay/src/auth.rs b/rs/moq-relay/src/auth.rs index 720f3cbff8b..5e0bb770b67 100644 --- a/rs/moq-relay/src/auth.rs +++ b/rs/moq-relay/src/auth.rs @@ -1856,14 +1856,12 @@ mod tests { #[tokio::test] async fn dropping_the_last_auth_ends_admissions() { + use futures::FutureExt; + let (auth, mut admissions) = Auth::embedded("relay-1"); let clone = auth.clone(); drop(auth); - assert!( - kio::clock::timeout(std::time::Duration::ZERO, admissions.next()) - .await - .is_err() - ); + assert!(admissions.next().now_or_never().is_none(), "a clone still admits"); drop(clone); assert!(admissions.next().await.is_none()); } diff --git a/rs/moq-relay/src/session.rs b/rs/moq-relay/src/session.rs index f3f4878d4e0..e76ea793502 100644 --- a/rs/moq-relay/src/session.rs +++ b/rs/moq-relay/src/session.rs @@ -590,6 +590,8 @@ mod tests { #[tokio::test] async fn a_push_wakes_the_registration_once() { + use futures::FutureExt; + let registry = Registry::new(); let request = request("abc", "/room", "127.0.0.1:1"); let reg = registry.register(request); @@ -598,11 +600,6 @@ mod tests { kio::clock::timeout(Duration::from_secs(1), reg.nudged()) .await .expect("the registration woke"); - assert!( - kio::clock::timeout(std::time::Duration::from_millis(20), reg.nudged()) - .await - .is_err(), - "a single push wakes once" - ); + assert!(reg.nudged().now_or_never().is_none(), "a single push wakes once"); } } diff --git a/rs/moq-relay/tests/integration/auth_lifetime.rs b/rs/moq-relay/tests/integration/auth_lifetime.rs index 6feab4c60f3..b729f37ed18 100644 --- a/rs/moq-relay/tests/integration/auth_lifetime.rs +++ b/rs/moq-relay/tests/integration/auth_lifetime.rs @@ -54,8 +54,8 @@ struct Script { seen: Arc>>, /// Answers each `end` only once a permit is added, when [`Self::hold_ends`] set it. end_gate: Arc>>>, - /// Notified as each `end` arrives. - end_arrived: Arc, + /// Bumped as each request arrives, so a test awaits one instead of sleeping. + arrived: Arc>, } impl Script { @@ -66,7 +66,7 @@ impl Script { revalidate_http: Arc::new(Mutex::new(None)), seen: Arc::new(Mutex::new(Vec::new())), end_gate: Arc::default(), - end_arrived: Arc::default(), + arrived: Arc::new(tokio::sync::watch::Sender::new(())), } } @@ -94,13 +94,37 @@ impl Script { .lock() .unwrap() .iter() - .filter(|r| matches!(r.event, Event::End { .. })) + .filter(|r| is_end(r)) .cloned() .collect() } + /// Wait until the requests seen so far satisfy `done`, and return them. + async fn until(&self, what: &str, done: impl Fn(&[Request]) -> bool) -> Vec { + let mut arrived = self.arrived.subscribe(); + let waited = kio::clock::timeout(TIMEOUT, async { + loop { + let seen = self.seen.lock().unwrap().clone(); + if done(&seen) { + return seen; + } + arrived.changed().await.expect("the script outlives its waiters"); + } + }) + .await; + waited.unwrap_or_else(|_| panic!("{what}: got {:?}", self.seen.lock().unwrap())) + } + + /// Wait for at least `n` `end`s, and return every one. + async fn ends_at_least(&self, n: usize, what: &str) -> Vec { + self.until(what, |seen| seen.iter().filter(|r| is_end(r)).count() >= n) + .await; + self.ends() + } + async fn handle(State(script): State