diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index 6e338309e9..b88a597439 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -36,7 +36,6 @@ Public API: none. Wire: none. - [Media late join](/quest/m1/test-flakes-2/media-late-join.md) - `just test media` late join stays within one GOP, or the regression is fixed - [Shaper virtual time](/quest/m1/test-flakes-2/shaper-virtual-time.md) - `moq-shaper` tests judge seeded decisions on paused time, not on wall-clock delivery under load - [Scoped WARN capture](/quest/m1/test-flakes-2/warn-capture.md) - the drop-unfinished tests count only their own WARNs -- [moq-mux debounce clock](/quest/m1/test-flakes-2/mux-debounce-clock.md) - the TS export debounce test advances on the paused clock - [js/publish audio clock](/quest/m1/test-flakes-2/publish-audio-clock.md) - the audio encoder delay test runs on mock time - [Rejoin after idle](/quest/m1/test-flakes-2/rejoin-idle-race.md) - an IETF rejoin never gets the stale warm cache first, because the copy goes idle before the cancel diff --git a/quest/m1/test-flakes-2/mux-debounce-clock.md b/quest/m1/test-flakes-2/mux-debounce-clock.md deleted file mode 100644 index e29f84934b..0000000000 --- a/quest/m1/test-flakes-2/mux-debounce-clock.md +++ /dev/null @@ -1,15 +0,0 @@ -# [XS] moq-mux debounce test on the paused clock - -## Goal - -moq-mux `container::ts::export_test::debounce_opens_without_a_media_clock` -advances on the paused clock and sleeps no real time. - -## Plan - -It died with SIGTERM once in a combined run. It is marked -`start_paused = true` but sleeps 1.2 s of real time, because the debounce -reads `crate::Clock`, which uses `std::time::Instant`, so the paused tokio -clock never reaches it. Let the test drive `crate::Clock`'s time (a tokio -`Instant` under test, or an injected source) so the window passes on the -paused clock, and drop the real sleep. diff --git a/rs/moq-mux/src/clock.rs b/rs/moq-mux/src/clock.rs index bb233c80a9..fff7552b79 100644 --- a/rs/moq-mux/src/clock.rs +++ b/rs/moq-mux/src/clock.rs @@ -19,6 +19,38 @@ use std::time::{Duration, Instant, SystemTime}; use hang::catalog::{MAX_SAFE_INTEGER, MOQ_EPOCH_UNIX_MILLIS}; +/// Host instant [`Clock`] measures from. +/// +/// This crate's tests use Tokio's clock, so a paused runtime can advance it. +/// Production keeps [`std::time::Instant`], which is what [`Clock::at`] and +/// [`Clock::capture`] take. A paused test in another crate does not move this clock. +#[cfg(not(test))] +type Mono = Instant; +#[cfg(test)] +type Mono = tokio::time::Instant; + +fn mono_now() -> Mono { + #[cfg(not(test))] + { + Instant::now() + } + #[cfg(test)] + { + tokio::time::Instant::now() + } +} + +fn mono_from(instant: Instant) -> Mono { + #[cfg(not(test))] + { + instant + } + #[cfg(test)] + { + tokio::time::Instant::from_std(instant) + } +} + /// The catalog clock for PTS zero at `wall`. fn wall_clock(wall: SystemTime) -> crate::Result { let unix_micros = wall @@ -51,7 +83,7 @@ fn wall_clock(wall: SystemTime) -> crate::Result { #[derive(Clone, Copy, Debug)] pub struct Clock { /// A monotonic instant, and what the clock read then in micros. - instant: Instant, + instant: Mono, reading: u64, wall: hang::catalog::Clock, } @@ -81,7 +113,7 @@ impl Clock { /// sampling. Refuses an unrepresentable wall. pub fn at(epoch: Instant, wall: SystemTime) -> crate::Result { Ok(Self { - instant: epoch, + instant: mono_from(epoch), reading: 0, wall: wall_clock(wall)?, }) @@ -92,7 +124,7 @@ impl Clock { /// Refuses a `since` so large that PTS zero lands before the moq epoch (2020), which the wall /// mapping cannot name. pub(crate) fn arrival(since: Duration) -> crate::Result { - let (instant, now) = (Instant::now(), SystemTime::now()); + let (instant, now) = (mono_now(), SystemTime::now()); let unmappable = || crate::Error::UnmappableTimestamp(format!("{since:?} puts PTS zero before 2020")); let zero = now.checked_sub(since).ok_or_else(unmappable)?; Ok(Self { @@ -115,7 +147,8 @@ impl Clock { /// Refuses an instant ahead of now, which would claim the payload reached the transport before /// it existed, and one before PTS zero, which no timestamp can name. pub fn capture(&self, at: Instant) -> crate::Result { - if at > Instant::now() { + let at = mono_from(at); + if at > mono_now() { return Err(crate::Error::InvalidCapture); } let micros = match at.checked_duration_since(self.instant) { @@ -281,4 +314,14 @@ mod tests { let max = moq_net::Timestamp::from_micros((1u64 << 62) - 1).unwrap(); assert!(clock.wall_clock(max).is_err(), "past the JSON-safe range is refused"); } + + /// The SI debounce reads this clock. A paused runtime has to move it, or the + /// test sleeps real time for a window the runtime will not advance. + #[tokio::test(start_paused = true)] + async fn paused_time_advances_now() { + let clock = Clock::new(); + let start = clock.now().as_micros(); + tokio::time::advance(Duration::from_secs(3)).await; + assert_eq!(clock.now().as_micros() - start, 3_000_000); + } } diff --git a/rs/moq-mux/src/container/ts/export_test.rs b/rs/moq-mux/src/container/ts/export_test.rs index 0bacdf5378..b2032347fc 100644 --- a/rs/moq-mux/src/container/ts/export_test.rs +++ b/rs/moq-mux/src/container/ts/export_test.rs @@ -5016,9 +5016,8 @@ async fn contiguous_same_version_commit_replaces_stale_sections() { } /// The cut debounce runs on the host clock, so a revision publishes even when no -/// media ever advances a PTS (an audio-only or SI-only input). This test spends -/// real wall time on the debounce window; media timestamps stay pinned at zero -/// throughout, which is exactly the case a media-clock debounce wedges on. +/// media ever advances a PTS (an audio-only or SI-only input). Media timestamps +/// stay pinned at zero, which is exactly the case a media-clock debounce wedges on. #[tokio::test(start_paused = true)] async fn debounce_opens_without_a_media_clock() { let sdt = |version: u8, fill: u8| make_long_section(0x42, 1, version, 0, 0, &[fill; 4]); @@ -5038,8 +5037,8 @@ async fn debounce_opens_without_a_media_clock() { .unwrap(); assert_eq!(track.latest(), Some(0), "a revision inside the window is held"); - // ...and publishes once the window passes in *real* time, no finish, no PTS. - std::thread::sleep(Duration::from_millis(1200)); + // ...and publishes once the paused clock passes the one-second window. + tokio::time::advance(Duration::from_secs(1)).await; rig.import .decode(&BytesMut::from(&si_packet_cc(0x0011, &sdt(1, 0xbb), 3)[..])) .unwrap();