//! Playout timing (WAI `extensions/co-watch.md` §15): the committed corpus //! `wai/moqt-conformance/tempo/vectors.json` regenerates byte for byte, every statement in it //! holds, a changed one is caught, and §15.6 lists exactly the codes the module gives. If the //! corpus cannot be read the test fails, it never passes on nothing. use joule_moq_room::tempo::{self, corpus, Reason, Stated}; use wai::co_watch::action; const WAI: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../../wai/"); fn committed() -> String { std::fs::read_to_string(format!("{WAI}moqt-conformance/tempo/vectors.json")).expect("the playout timing corpus") } #[test] fn the_corpus_regenerates() { assert!(corpus::generate() == committed(), "run `JOULE_MOQ_ROOM_WRITE_TEMPO=1 cargo test --test tempo_vectors write`"); } /// Writes the corpus where `JOULE_MOQ_ROOM_WRITE_TEMPO=1`; otherwise does nothing. Never set in CI. #[test] fn write_the_corpus_when_asked() { if std::env::var("JOULE_MOQ_ROOM_WRITE_TEMPO").as_deref() == Ok("1") { std::fs::create_dir_all(format!("{WAI}moqt-conformance/tempo")).expect("the corpus directory"); std::fs::write(format!("{WAI}moqt-conformance/tempo/vectors.json"), corpus::generate()).expect("write vectors.json"); } } #[test] fn every_statement_holds() { let doc: serde_json::Value = serde_json::from_str(&committed()).expect("JSON"); let fails = corpus::verify(&doc); assert!(fails.is_empty(), "{}", fails.join("\n")); } #[test] fn a_changed_statement_is_caught() { let doc: serde_json::Value = serde_json::from_str(&committed()).expect("JSON"); let edits: [&dyn Fn(&mut serde_json::Value); 6] = [ &|d| d["claims"][1]["capture_ms"] = serde_json::json!(d["claims"][1]["capture_ms"].as_u64().unwrap() + 1), &|d| d["claims"][1]["outcome"] = serde_json::json!({"due_us": 2_999_999}), &|d| d["claims"][11]["outcome"] = d["claims"][1]["outcome"].clone(), &|d| d["scripts"][1]["controls"][1][1] = serde_json::json!(3_000_001), &|d| d["changes"][0]["due_after_us"] = d["changes"][0]["due_before_us"].clone(), &|d| d["catalog"][11]["outcome"] = serde_json::json!("accept"), ]; for (i, e) in edits.iter().enumerate() { let mut changed = doc.clone(); e(&mut changed); assert!(!corpus::verify(&changed).is_empty(), "edit {i} went unnoticed"); } } /// The planted fault of the package: a timing claim outside its bound, in both senses a /// claim has one. An instant off the controlled position by the least the draft's units can /// say, and a delay past the track's stated bound, are each refused; the claim a publisher /// stamps is not. #[test] fn a_timing_claim_outside_its_bound_is_refused() { let tl = corpus::timeline(&[(action::PLAY, 1_000_000, 0), (action::PAUSE, 3_000_000, 0), (action::PLAY, 4_500_000, 0)]); let (t0, q, len) = (corpus::T0_US, 2_500_000, 40_000); // Played from 1 s to 3 s, held to 4.5 s: the position reaches 2.5 s at 5 s. let capture = (t0 + 5_000_000) / 1000 - 200; let t = tempo::stamp(&tl, t0, 0, q, len, capture, 2000).expect("the object is due"); let delay = t.delay_ms.expect("a delay"); let claim = |capture_ms, delay_ms, max_delay_ms| tempo::check_claim(&tl, t0, 0, q, len, Stated { capture_ms, delay_ms, max_delay_ms }); assert_eq!(claim(capture, delay, 2000), Ok(5_000_000)); for (c, d, max, want) in [ (capture, delay + 1, 2000, Reason::OffPosition), (capture, delay - 1, 2000, Reason::OffPosition), (capture + 1, delay, 2000, Reason::OffPosition), (capture, delay, delay - 1, Reason::DelayOutOfBound), (capture - 5000, delay + 5000, 2000, Reason::DelayOutOfBound), ] { assert_eq!(claim(c, d, max), Err(want), "capture {c}, delay {d}, bound {max}"); } // The same instant with the delay moved into the capture time is the same claim. assert_eq!(claim(capture - 300, delay + 300, 2000), Ok(5_000_000)); // And a publisher cannot stamp past the bound either. assert_eq!(tempo::stamp(&tl, t0, 0, q, len, capture, delay - 1), Err(Reason::DelayOutOfBound)); } /// TEMPO-timed controls give co-watch's controlled position: over a sweep of scripts, objects /// and delays, the instant a stamped object states is the instant the controlled position /// enters it, and a longer delay made as controls moves the position by exactly the delay. #[test] fn stamped_instants_are_the_controlled_positions() { let mut checked = 0; for (_, script) in corpus::scripts() { let tl = corpus::timeline(&script); for k in 0..400u64 { let (q, len) = (k * 151_000, 33_367); let Some(x) = tempo::due(&tl, 0, q, len) else { continue }; let p = tl.position_at(x); assert!(p >= q && p < q + len, "the position at {x} is {p}, not inside [{q}, {})", q + len); // No earlier instant has the position inside the object. if x > 0 { let before = tl.position_at(x - 1); assert!(before < q || before >= q + len, "the position was already inside at {}", x - 1); } let capture = (corpus::T0_US + x) / 1000 - 777; let t = tempo::stamp(&tl, corpus::T0_US, 0, q, len, capture, 5000).expect("a stamp"); let stated = capture + u64::from(t.delay_ms.unwrap()); assert!(stated * 1000 >= corpus::T0_US + x && stated * 1000 < corpus::T0_US + x + 1000); assert_eq!(tempo::check_claim(&tl, corpus::T0_US, 0, q, len, Stated { capture_ms: capture, delay_ms: t.delay_ms.unwrap(), max_delay_ms: 5000 }), Ok(x)); checked += 1; } } assert!(checked > 500, "{checked}"); } #[test] fn co_watch_lists_the_codes() { let text = std::fs::read_to_string(format!("{WAI}extensions/co-watch.md")).expect("the extension"); let at = text.find("\n### 15.6 ").expect("a §15.6"); let end = text[at + 1..].find("\n#").map_or(text.len(), |e| at + 1 + e); let listed: Vec<&str> = text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1)).collect(); let want: Vec<&str> = Reason::all().iter().map(|r| r.code()).collect(); assert_eq!(listed, want); } /// The catalog member this crate writes, held to the committed sample /// `wai/ietf/catalog_check.py` reads. `JOULE_MOQ_ROOM_WRITE_TEMPO=1` writes it; never set in CI. #[test] fn the_catalog_member_is_the_committed_sample() { let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/catalogs/tempo.json"); let text = corpus::sample_catalog(); if std::env::var("JOULE_MOQ_ROOM_WRITE_TEMPO").as_deref() == Ok("1") { std::fs::create_dir_all(path.parent().expect("a directory")).expect("the directory"); std::fs::write(&path, &text).expect("write the sample"); } assert!(std::fs::read_to_string(&path).expect("the committed sample") == text, "run JOULE_MOQ_ROOM_WRITE_TEMPO=1 cargo test --test tempo_vectors"); let doc: serde_json::Value = serde_json::from_str(&text).expect("JSON"); let read = tempo::CatalogTempo::from_json(&doc["tracks"][0]["wai_tempo"]).expect("the member reads"); assert_eq!((read.max_delay_ms, read.playout_delay_ms), (2000, Some(200))); }