open-standards/joule-code/joule-code-rs/crates/joule-moq-interop/tests/moq_test_relay.rs
Transaction Science e84e3af10f
All checks were successful
wai-fleet service / does this change touch it (push) Successful in 19s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 21s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 12s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 58s
wai-fleet service / service · linux (push) Successful in 2m44s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 5m22s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 5m24s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 5m31s
wai-app and Create / wai-app · does this change touch it (push) Successful in 5m31s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 5m45s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 12m7s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 11m7s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 12m37s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 33s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m49s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m54s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m16s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 8m54s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 1m15s
wai-fleet service / service · macos (push) Successful in 1m2s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m3s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 9m43s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 24m20s
MoQ test tracks: draft-afrind-moq-test-01 through the relay, and the namespace of draft-marzot-moq-test-tracks-00
joule-moq gains moq_test (the sixteen tuple fields, a publisher's limits, the object
schedule, the test extensions and a subscriber's checker with a reason for each fault) and
test_tracks (the grammar, shortest namespace, tracks, ladders, bitrates and timing of the
media test tracks; no media). joule-moq-interop gains the moq-test test: a publisher that
serves the track a SUBSCRIBE or FETCH names, a subscriber that checks every object, planted
faults by --fault, run against joule-moq-relay at -23, -22, -18 and -16. Two corpora under
wai/moqt-conformance, each with a second implementation in Python. decisions D19.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-11 08:43:23 -05:00

135 lines
6.1 KiB
Rust

//! `moq-test` (the test tracks of `draft-afrind-moq-test-01`) through `joule-moq-relay`, on
//! loopback: the interop client's publisher and subscriber at each draft the relay speaks,
//! by PUBLISH_NAMESPACE and by PUBLISH, over native QUIC and WebTransport, and across
//! drafts; then each planted fault, which the subscriber must refuse with its own reason.
//!
//! The relay runs in this process on a port the system chooses; the client is the built
//! binary, run once a session pair, under a guard that kills and reaps it.
use joule_moq_relay::{RelayConfig, RelayServer};
use serde_json::Value;
use std::io::Read;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
/// A client run that cannot outlive its test: killed and reaped when dropped.
struct Guard(Child);
impl Drop for Guard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
/// Run the client's `moq-test` against `url`; its exit status and its TAP.
fn client(url: &str, args: &[&str]) -> (i32, String) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_joule-moq-interop"));
cmd.args(["--relay", url, "--tls-disable-verify", "--test", "moq-test"]).args(args).stdout(Stdio::piped()).stderr(Stdio::null());
let mut g = Guard(cmd.spawn().expect("the client starts"));
let mut stdout = g.0.stdout.take().expect("stdout");
let reader = std::thread::spawn(move || {
let mut s = String::new();
let _ = stdout.read_to_string(&mut s);
s
});
let deadline = Instant::now() + Duration::from_secs(120);
let status = loop {
match g.0.try_wait().expect("the client's status") {
Some(s) => break s.code().unwrap_or(-1),
None if Instant::now() > deadline => break -2,
None => std::thread::sleep(Duration::from_millis(20)),
}
};
drop(g);
(status, reader.join().unwrap_or_default())
}
/// A YAML line of the test point's diagnostics, as JSON.
fn diag(tap: &str, key: &str) -> Value {
let prefix = format!(" {key}: ");
tap.lines().find_map(|l| l.strip_prefix(&prefix)).and_then(|v| serde_json::from_str(v).ok()).unwrap_or(Value::Null)
}
async fn relay() -> (RelayServer, String) {
let server = RelayServer::start(RelayConfig::loopback()).await.expect("the relay starts");
let addr = server.quic_addr.expect("a UDP endpoint");
let path = server.relay().config().path.clone();
(server, format!("{addr}{path}"))
}
async fn run(url: String, args: Vec<String>) -> (i32, String) {
tokio::task::spawn_blocking(move || client(&url, &args.iter().map(String::as_str).collect::<Vec<_>>())).await.expect("the client's task")
}
/// Every case of a passing run: every scheduled object accepted, and the whole track served
/// again by FETCH.
fn whole(tap: &str, what: &str) {
let cases = diag(tap, "cases");
let cases = cases.as_array().unwrap_or_else(|| panic!("{what}: no cases\n{tap}"));
assert_eq!(cases.len(), 6, "{what}\n{tap}");
for c in cases {
assert_eq!(c["objects_accepted"], c["objects_scheduled"], "{what}: {}\n{tap}", c["what"]);
assert_eq!(c["fetch"]["objects"], c["fetch"]["payload_objects_scheduled"], "{what}: FETCH of {}\n{tap}", c["what"]);
if !c["delivery_timeout_carried"].is_null() {
assert_eq!(c["delivery_timeout_carried"], true, "{what}: {}\n{tap}", c["what"]);
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_test_tracks_pass_the_relay_at_every_draft() {
let (_server, at) = relay().await;
for draft in ["23", "22", "18", "16"] {
for flow in ["namespace", "publish"] {
let what = format!("draft {draft}, flow {flow}");
let (status, tap) = run(format!("moqt://{at}"), vec!["--draft".into(), draft.into(), "--flow".into(), flow.into()]).await;
assert_eq!(status, 0, "{what}\n{tap}");
assert!(tap.contains("\nok 1 - moq-test\n"), "{what}\n{tap}");
whole(&tap, &what);
if flow == "namespace" {
assert_eq!(diag(&tap, "refusals").as_array().map(Vec::len), Some(3), "{what}\n{tap}");
}
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_test_tracks_pass_over_webtransport_and_across_drafts() {
let (_server, at) = relay().await;
for draft in ["23", "16"] {
let what = format!("draft {draft} over WebTransport");
let (status, tap) = run(format!("https://{at}"), vec!["--draft".into(), draft.into()]).await;
assert_eq!(status, 0, "{what}\n{tap}");
whole(&tap, &what);
}
for (publisher, subscriber) in [("23", "22"), ("23", "16"), ("16", "23"), ("18", "23"), ("22", "18")] {
let what = format!("publisher at {publisher}, subscriber at {subscriber}");
let (status, tap) = run(format!("moqt://{at}"), vec!["--draft".into(), publisher.into(), "--sub-draft".into(), subscriber.into()]).await;
assert_eq!(status, 0, "{what}\n{tap}");
whole(&tap, &what);
}
}
/// The planted faults: a publisher that sends a track wrongly is refused by the subscriber,
/// each fault with its own reason, at the current draft and at the oldest.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_track_sent_wrongly_is_refused_with_its_reason() {
let (_server, at) = relay().await;
for draft in ["23", "16"] {
for (fault, reason) in [
("sequence", "moq_test_object_unscheduled"),
("pattern", "moq_test_payload_pattern"),
("size", "moq_test_payload_size"),
("subgroup", "moq_test_subgroup"),
("end-of-group", "moq_test_end_of_group_missing"),
("extension", "moq_test_extension_absent"),
] {
let what = format!("fault {fault} at draft {draft}");
let (status, tap) = run(format!("moqt://{at}"), vec!["--draft".into(), draft.into(), "--fault".into(), fault.into()]).await;
assert_eq!(status, 1, "{what}: the client's own verdict, a test that failed\n{tap}");
assert!(tap.contains("\nnot ok 1 - moq-test\n"), "{what}\n{tap}");
assert_eq!(diag(&tap, "refused"), reason, "{what}\n{tap}");
}
}
}