open-standards/joule-code/joule-code-rs/crates/joule-moq/tests/adaptation_vectors.rs
Transaction Science 34ae495b54 Track switching and probe tracks as library modules of joule-moq, each with a corpus and a second implementation
Two drafts read whole at the revision in the IETF archive on 10 November 2026, with
draft-gurel-moq-track-switching-01 beside the first. Neither module opens a stream or
is on a session's path: each is what a subscriber, a publisher and a relay compute.

- Subscriber-side track switching (draft-ugur-moq-fast-switching-00). joule-moq's
  switching module reads a Switch Set from an MSF catalog's altGroup and
  keyframeSpacing, with its GoP Ladders and its Alignment Unit, and refuses a set in
  which a spacing is not a multiple of that unit. It gives the Frame Number, the
  nearest Switch Point and its Distance, the track chosen under the draft's full
  policy and its simpler one, and the End Group in both of its cases. SWITCH_FROM is
  an unmerged pull request and is not in draft-ietf-moq-transport-23, and neither are
  track-switching-01's altTrackGroup and Switch Track Alias: none is written. A
  SwitchPlan states the switch as the two requests -23 has, a SUBSCRIBE of the target
  with an absolute-start Location Filter at {targetGroup, 0} and a REQUEST_UPDATE
  that ends the current subscription at the End Group, and the module's text says
  what the drafts' parameters would add. The release buffer keys both tracks' objects
  by Frame Number and presents each once, in order. A switch that would start inside
  a Group, and one at a Group the target never publishes, are refused, each with its
  own code.
- Probe tracks (draft-gurel-moq-probing-00). joule-moq's probe module parses Dynamic
  Probe Names by the draft's grammar with every invalid case of its section 7.1 under
  its own reason, resolves a probe as Figure 4 does in exact integer arithmetic, and
  lists its objects: Location, payload size, release time, the lowest publisher
  priority, LOC's Timestamp property and a payload of zeros. It holds the per-probe,
  session and relay-wide limits with Table 5's defaults as a configuration and clamps
  nothing, maps every outcome to its error code in both directions with its Retry
  Interval, writes and reads the probe catalog (which has no member a limit could be
  carried in), reads the session-scoped namespaces and the empty-namespace form, and
  measures a receiver's throughput and the trend of its arrival gaps against the send
  gaps.

Writing the tests found one thing in the first draft: where the spacings of the
current and the target track do not divide one another, the End Group its section 7
computes can end before the Switch Point, so frames between them would never be
sent. The module computes the End Group as the draft writes it and refuses to plan
such a switch.

Corpora: wai/moqt-conformance/switching/ and wai/moqt-conformance/probe/, each one
vectors.json written by the module's corpus generator, with the drafts' own worked
examples in them (Frame Number 28 and Table 1, both End Groups; Tables 4, 5 and 6
and Figure 5). switching_verify.py and probe_verify.py are the second
implementations, standard library only, sharing no code with the crate. Planted
faults, each refused with its own reason by both: 34 for switching, 70 for probes.
Each verifier changes copies of its corpus and requires every change to be caught,
then leaves out each of its own checks in turn and requires the corpus to catch it.

joule-code-conformance.yml runs both verifiers; wai conform --independent lists
them as moq-switching and moq-probe (moqt-mappings.sh).

Co-Authored-By: Claude <[email protected]>
2026-11-10 23:31:32 -05:00

156 lines
9.4 KiB
Rust

//! Track switching and probe tracks (`draft-ugur-moq-fast-switching-00`,
//! `draft-gurel-moq-probing-00`): the committed corpora
//! `wai/moqt-conformance/switching/vectors.json` and `wai/moqt-conformance/probe/vectors.json`
//! regenerate byte for byte, every case and refusal in them holds, the drafts' own worked
//! examples are in them with the drafts' numbers, and a corpus changed by one value is
//! caught. The corpora live beside this crate in the monorepo; if one cannot be read the test
//! fails, it never passes on nothing.
use joule_moq::{probe, switching};
use serde_json::{json, Value};
const WAI: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../../wai/");
fn committed(corpus: &str) -> (String, Value) {
let text = std::fs::read_to_string(format!("{WAI}moqt-conformance/{corpus}/vectors.json")).unwrap_or_else(|_| panic!("the {corpus} corpus"));
let doc = serde_json::from_str(&text).expect("JSON");
(text, doc)
}
fn cases<'a>(doc: &'a Value, section: &str) -> &'a Vec<Value> {
doc[section].as_array().unwrap_or_else(|| panic!("{section} of the corpus"))
}
#[test]
fn the_switching_corpus_regenerates_and_holds() {
let (text, doc) = committed("switching");
assert!(switching::corpus::generate() == text, "run `cargo run -p joule-moq --example adaptation_vectors -- gen`");
assert_eq!(switching::corpus::verify(&doc), Vec::<String>::new());
}
#[test]
fn the_probe_corpus_regenerates_and_holds() {
let (text, doc) = committed("probe");
assert!(probe::corpus::generate() == text, "run `cargo run -p joule-moq --example adaptation_vectors -- gen`");
assert_eq!(probe::corpus::verify(&doc), Vec::<String>::new());
}
/// The numbers the draft prints, held to the corpus as literals: §6.4's Frame Number 28 and
/// Table 1, and §7's two End Groups.
#[test]
fn the_switching_corpus_holds_the_drafts_worked_examples() {
let (_, doc) = committed("switching");
let frame = cases(&doc, "frames").iter().find(|c| c["track"] == "720p-long" && c["at"] == json!([13, 3])).expect("§6.4's object");
assert_eq!(frame["frame"], 28);
let table: Vec<(String, u64, u64)> = cases(&doc, "switch_points")
.iter()
.filter(|c| c["catalog"] == "figure-1" && c["frame"] == 28)
.map(|c| (c["target"].as_str().expect("a target").to_string(), c["target_group"].as_u64().expect("a group"), c["distance"].as_u64().expect("a distance")))
.collect();
assert_eq!(table, vec![("1080p-short".to_string(), 15, 1), ("1080p-medium".to_string(), 15, 1), ("1080p-long".to_string(), 17, 5)]);
let pick = cases(&doc, "selections").iter().find(|c| c["current"] == "720p-long" && c["to"] == "1080p-short" && c["policy"] == "full").expect("§6.4's selection");
assert_eq!(pick["chosen"], "1080p-medium");
let plans = cases(&doc, "plans");
let first = plans.iter().find(|c| c["catalog"] == "section-7").expect("§7's first example");
assert_eq!((first["frame"].as_u64(), first["switch_frame"].as_u64(), first["end_group"].as_u64()), (Some(9), Some(13), Some(5)));
let second = plans.iter().find(|c| c["current"] == "720p-long" && c["target"] == "1080p-medium").expect("§7's second example");
assert_eq!((second["target_group"].as_u64(), second["end_group"].as_u64()), (Some(15), Some(13)));
assert_eq!(second["subscribe"], json!({"filter_type": 2, "start": [15, 0]}));
assert_eq!(second["update"], json!({"filter_type": 3, "start": [5, 0], "end_group_delta": 8}));
// Every handoff presents consecutive Frame Numbers: no gap and no frame twice.
for h in cases(&doc, "handoffs") {
let frames: Vec<u64> = h["released"].as_array().expect("released").iter().map(|r| r[3].as_u64().expect("a frame")).collect();
assert!(frames.len() > 4 && frames.windows(2).all(|w| w[1] == w[0] + 1), "{}", h["what"]);
}
}
/// §6's Table 4, and §7.3's Table 6, as the draft prints them.
#[test]
fn the_probe_corpus_holds_the_drafts_tables() {
let (_, doc) = committed("probe");
let probe = |name: &str| cases(&doc, "probes").iter().find(|c| c["name"] == name).unwrap_or_else(|| panic!("{name}")).clone();
let paced = probe("d2000-r300000");
assert_eq!((&paced["shape"], &paced["f"], &paced["s"], &paced["objects"], &paced["b"], &paced["groups"]), (&json!("paced"), &json!([50, 1]), &json!(750), &json!(100), &json!(75_000), &json!(1)));
let greedy = probe("b500000");
assert_eq!((&greedy["shape"], &greedy["s"], &greedy["objects"], &greedy["groups"], &greedy["bytes"]), (&json!("greedy"), &json!(16_384), &json!(31), &json!(1), &json!(500_000)));
assert_eq!(greedy["sample"][2], json!({"index": 30, "group": 0, "object": 30, "len": 8480, "offset_us": null}));
let video = probe("d2000-f30-g30-k5-r3000000");
assert_eq!((&video["s"], &video["key_object_bytes"], &video["objects"], &video["groups"]), (&json!(11_029), &json!(55_145), &json!(60), &json!(2)));
let row = |outcome: &str| cases(&doc, "errors").iter().find(|r| r["outcome"] == outcome).unwrap_or_else(|| panic!("{outcome}")).clone();
let codes = |outcome: &str| (row(outcome)["downlink"]["name"].as_str().map(str::to_string), row(outcome)["uplink"]["name"].as_str().map(str::to_string));
let named = |d: &str, u: &str| (Some(d.to_string()), Some(u.to_string()));
assert_eq!(codes("invalid"), named("DOES_NOT_EXIST", "UNINTERESTED"));
assert_eq!(codes("forward_state"), (None, Some("UNINTERESTED".to_string())));
assert_eq!(codes("per_probe_limit"), named("DOES_NOT_EXIST", "UNINTERESTED"));
assert_eq!(codes("load_limit"), named("EXCESSIVE_LOAD", "EXCESSIVE_LOAD"));
assert_eq!(codes("unauthorized"), named("UNAUTHORIZED", "UNAUTHORIZED"));
assert_eq!(codes("other_request"), named("NOT_SUPPORTED", "NOT_SUPPORTED"));
// Table 5.
assert_eq!(doc["limits"]["max_duration_ms"], 5000);
assert_eq!(doc["limits"]["max_bytes"], 10_000_000);
assert_eq!(doc["limits"]["max_rate_bps"], 50_000_000);
assert_eq!(doc["limits"]["max_object_bytes"], 1_048_576);
assert_eq!((&doc["limits"]["session_concurrent"], &doc["limits"]["min_gap_ms"]), (&json!(1), &json!(1000)));
assert_eq!((&doc["limits"]["session_bytes"], &doc["limits"]["session_window_ms"]), (&json!(50_000_000), &json!(60_000)));
// Every object of every schedule is at the lowest priority with one property, the Timestamp.
for s in cases(&doc, "schedules") {
for o in s["sent"].as_array().expect("sent") {
assert_eq!(o["priority"], 255);
assert!(o["properties"].as_str().is_some_and(|p| p.starts_with("10")), "{}", s["what"]);
}
}
}
/// The planted faults: a corpus one value away from the committed one is caught, whichever
/// side of a case the value is on.
#[test]
fn a_changed_corpus_is_caught() {
type Edit = (&'static str, fn(&mut Value));
let (_, doc) = committed("switching");
let edits: [Edit; 8] = [
("a Frame Number", |d| d["frames"][0]["frame"] = json!(27)),
("a target group", |d| d["switch_points"][2]["target_group"] = json!(15)),
("the track chosen", |d| d["selections"][0]["chosen"] = json!("1080p-short")),
("an End Group", |d| d["plans"][0]["end_group"] = json!(9)),
("a frame presented twice", |d| {
let again = d["handoffs"][0]["released"][3].clone();
d["handoffs"][0]["released"].as_array_mut().expect("released").insert(4, again);
}),
("a spacing made a multiple", |d| d["faults"][0]["catalog"]["tracks"][1]["keyframeSpacing"] = json!(8)),
("a mid-group start moved to its Group's first object", |d| {
let at = d["faults"].as_array().expect("faults").iter().position(|f| f["reason"] == "switch_start_mid_group").expect("the fault");
d["faults"][at]["at"][1] = json!(0);
}),
("one reason for another", |d| {
let at = d["faults"].as_array().expect("faults").iter().position(|f| f["reason"] == "switch_start_not_switch_point").expect("the fault");
d["faults"][at]["reason"] = json!("switch_start_mid_group");
}),
];
for (what, edit) in edits {
let mut changed = doc.clone();
edit(&mut changed);
assert!(!switching::corpus::verify(&changed).is_empty(), "a change went unnoticed: {what}");
}
let (_, doc) = committed("probe");
let edits: [Edit; 8] = [
("an object size", |d| d["probes"][0]["s"] = json!(751)),
("an object count", |d| d["probes"][1]["objects"] = json!(30)),
("the cut object's length", |d| d["probes"][1]["sample"][2]["len"] = json!(16_384)),
("a payload that is not zeros", |d| d["schedules"][0]["sent"][1]["payload_sha256"] = json!("00")),
("an error code", |d| d["errors"][0]["downlink"]["code"] = json!(0x20)),
("a limit in the catalog", |d| d["catalogs"][0]["catalog"]["maxRate"] = json!(1)),
("a refused request made acceptable", |d| {
let at = d["faults"].as_array().expect("faults").iter().position(|f| f["reason"] == "probe_duration_over_limit").expect("the fault");
d["faults"][at]["name"] = json!("d5000-r300000");
}),
("a Retry Interval", |d| {
let at = d["faults"].as_array().expect("faults").iter().position(|f| f["reason"] == "probe_session_gap").expect("the fault");
d["faults"][at]["retry_interval"] = json!(0);
}),
];
for (what, edit) in edits {
let mut changed = doc.clone();
edit(&mut changed);
assert!(!probe::corpus::verify(&changed).is_empty(), "a change went unnoticed: {what}");
}
}