From 95b1cda25de4c13ed385e79fdaa2d4710cae9be9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 17:22:23 -0700 Subject: [PATCH 1/2] claim quest/m1/test-flakes-2/import-catalog-finish From 78c63821beee41b044d62c99537f911d9b664fcb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 17:49:52 -0700 Subject: [PATCH 2/2] test(cli): fail the import EOF test on the publisher's exit, not a deadline Each wait while `moq import` still holds stdin open now races the child's exit, so a publisher that dies reports its exit status at once instead of surfacing ten seconds later as "announce timed out". The announce wait expects the publisher's Start directly, since the `Live` marker it skipped past no longer exists. The reported 8.8 s run and the loaded "announce timed out" did not reproduce on main, on the quest's base, or on the base the failure was seen on, so the quest closes with this hardening. Co-Authored-By: Claude Opus 5.5 --- quest/m1/test-flakes-2/README.md | 1 - .../m1/test-flakes-2/import-catalog-finish.md | 22 ------------ rs/moq-cli/tests/import.rs | 34 ++++++++++++------- 3 files changed, 22 insertions(+), 35 deletions(-) delete mode 100644 quest/m1/test-flakes-2/import-catalog-finish.md diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index b0fc70f158..1b43c6790c 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -33,7 +33,6 @@ Public API: none. Wire: none. ## Required - [Media late join](/quest/m1/test-flakes-2/media-late-join.md) - a late joiner shows video promptly and catches up to live, and the check asserts that under load -- [Import catalog finish](/quest/m1/test-flakes-2/import-catalog-finish.md) - `moq-cli`'s subprocess EOF catalog-finish test holds up under load with event-based fixture coordination - [Relay restart rebind](/quest/m1/test-flakes-2/relay-restart-rebind.md) - the crash drill restarts on its original UDP address under concurrent load - [Impaired handshake](/quest/m1/test-flakes-2/impaired-handshake.md) - the impaired cluster drills' clients never time out while connecting - [Media audio tone](/quest/m1/media-audio-tone.md) - the `just test media` audio-tone check passes under load, fixed at its cause diff --git a/quest/m1/test-flakes-2/import-catalog-finish.md b/quest/m1/test-flakes-2/import-catalog-finish.md deleted file mode 100644 index c7494c4d1e..0000000000 --- a/quest/m1/test-flakes-2/import-catalog-finish.md +++ /dev/null @@ -1,22 +0,0 @@ -# [XS] Import catalog finish - -## Goal - -`moq-cli`'s `import_delivers_the_catalog_finish_at_eof` -(`rs/moq-cli/tests/import.rs`) passes under a loaded `just check`. It runs -8.8 s alone against a 10 s timeout and fails with "announce timed out" under -load. - -## Plan - -Preserve the real `moq` subprocess coverage: closing stdin must finish the -catalog and let the process exit successfully while the subscriber still -receives a clean finish. Pausing the parent's Tokio clock cannot control the -child process, and the real QUIC sockets can race an auto-advancing clock. -Reproduce the slowdown, fix its cause, and coordinate the fixture through -observable readiness and completion events rather than elapsed-time -assumptions. Use an in-memory fixture for separable protocol assertions if -helpful, while retaining the subprocess EOF regression. Never raise the -timeout or add a retry. - -Public API: none. Wire: none. diff --git a/rs/moq-cli/tests/import.rs b/rs/moq-cli/tests/import.rs index 6cf401b75d..69bd6c3cbe 100644 --- a/rs/moq-cli/tests/import.rs +++ b/rs/moq-cli/tests/import.rs @@ -5,10 +5,23 @@ use std::process::Stdio; use std::time::Duration; use tokio::io::AsyncWriteExt; +use tokio::process::Child; const TIMEOUT: Duration = Duration::from_secs(10); const BBB: &[u8] = include_bytes!("../../moq-mux/src/container/ts/test_data/bbb.ts"); +/// Await `step` while `moq` still holds stdin open, failing as soon as the process exits. +/// +/// Until stdin ends, an exit is the publisher failing, so report its status rather than +/// waiting out `TIMEOUT` and blaming the step. +async fn running(child: &mut Child, what: &str, step: impl Future) -> T { + tokio::select! { + out = step => out, + status = child.wait() => panic!("moq exited with {} before {what}", status.expect("wait for moq")), + _ = tokio::time::sleep(TIMEOUT) => panic!("{what} timed out"), + } +} + #[tokio::test] async fn import_delivers_the_catalog_finish_at_eof() { let _ = moq_tokio::crypto::install_default(); @@ -58,17 +71,15 @@ async fn import_delivers_the_catalog_finish_at_eof() { .await .expect("subscriber connects"); - // `Live` can come first while the relay has yet to learn the publisher's route. - while !matches!( - tokio::time::timeout(TIMEOUT, announced.next()) - .await - .expect("announce timed out") - .expect("origin closed"), - moq_tokio::moq_net::announce::Event::Start(_) - ) {} - let broadcast = tokio::time::timeout(TIMEOUT, consumer.request_broadcast("demo", None)) + let event = running(&mut child, "the announce", announced.next()) + .await + .expect("origin closed"); + let moq_tokio::moq_net::announce::Event::Start(announce) = event else { + panic!("the first announce event is {event:?}, not a start"); + }; + assert_eq!(announce.prefix.as_str(), "demo"); + let broadcast = running(&mut child, "the broadcast", consumer.request_broadcast("demo", None)) .await - .expect("request timed out") .expect("announced broadcast resolves"); let mut catalogs = hang::catalog::Catalog::<()>::subscribe(&broadcast) .await @@ -76,9 +87,8 @@ async fn import_delivers_the_catalog_finish_at_eof() { // The relay is serving the catalog before stdin ends, so the finish is queued on a // live subscription when the process exits. - tokio::time::timeout(TIMEOUT, catalogs.next()) + running(&mut child, "the first catalog", catalogs.next()) .await - .expect("catalog timed out") .expect("catalog read") .expect("a catalog");