open-standards/wai/wai-rs/tests/stream_termination.rs
Transaction Science ea745e32f3 wai: termination corpus — display records both ways, a repeated observation, a rendition kept above a shrunk panel
Two mutations of the reference survived the corpus: ignoring a display
record's declaration, and restarting a signal's grace at a repeated
observation. Each now changes a log's outcome.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 13:40:43 -04:00

97 lines
4.7 KiB
Rust

//! `wai.live.termination` (extensions/stream-termination.md) and the display match of
//! capability-constraints §4: the committed corpus `live-conformance/termination/vectors.json`
//! regenerates byte for byte from its fixed seeds, every match and log in it gets the outcome it
//! states, every reason the extension adds refuses a log, the accepted logs are the cases §8
//! names, and the extension's §7 lists exactly the reasons the reference reports.
#![cfg(feature = "stream_termination")]
use std::collections::BTreeSet;
use std::path::PathBuf;
use wai::stream_termination::{corpus, Reason};
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn read(p: &str) -> String {
std::fs::read_to_string(wai_dir().join(p)).unwrap_or_else(|e| panic!("{p}: {e}"))
}
fn committed() -> String {
read("live-conformance/termination/vectors.json")
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
assert!(committed() == corpus::generate(), "live-conformance/termination/vectors.json differs from `wai_stream_termination_vectors gen`");
}
#[test]
fn every_match_and_log_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// Every reason refuses a log; the accepted logs are §8's; and the facts that make the
/// invisible seen are what the cases were written to show.
#[test]
fn the_corpus_covers_every_reason_and_case() {
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
let logs = doc["logs"].as_array().unwrap();
let seen: BTreeSet<&str> = logs.iter().filter_map(|v| v["reason"].as_str()).collect();
for r in Reason::ALL {
assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}");
}
let get = |n: &str| logs.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no log {n}"));
for (n, why) in [
("hidden", "hidden"),
("occluded", "occluded"),
("display_off", "display_off"),
("idle", "idle"),
("autoplay_unattended", "autoplay_unattended"),
("user_stop", "user"),
("audio_hidden", "ended"),
("none_within", "ended"),
("display_change", "ended"),
("hidden_repeated", "hidden"),
("hdr", "user"),
] {
assert_eq!(get(n)["outcome"], "accept", "{n}");
assert_eq!(get(n)["terminated"], why, "{n}");
}
assert_eq!(get("open")["terminated"], serde_json::Value::Null);
// The hidden log: one segment past the grace is unwatched; its hops are per class, the
// origin unmetered, the relay absent.
let h = get("hidden");
assert_eq!(h["unwatched"]["segments"], 1);
assert_eq!(h["unseen_us"], 10_800_000);
assert_eq!(h["absent"], serde_json::json!(["relay"]));
assert_eq!(h["unmetered"], serde_json::json!([{ "role": "origin", "count": 4 }]));
// Two classes of one hop, two totals.
let classes: Vec<&str> = get("user_stop")["totals"].as_array().unwrap().iter().filter(|t| t["role"] == "sink_decode").map(|t| t["class"].as_str().unwrap()).collect();
assert_eq!(classes, ["OnChipCounter", "CalibratedInstrument"]);
assert_eq!(get("none_within")["panel"], serde_json::json!({ "within": 0, "none_within": 2 }));
assert_eq!(get("audio_hidden")["unwatched"], serde_json::Value::Null);
assert_eq!(get("segment_after_termination")["reason"], "after_termination");
assert_eq!(get("segment_after_termination")["at"], 7);
}
/// §7 of the extension lists exactly the codes `Reason::OWN` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {
let text = read("extensions/stream-termination.md");
let at = text.find("\n## 7. Reasons").expect("a §7");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<String> = text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
let want: Vec<String> = Reason::OWN.iter().map(|r| r.code().to_string()).collect();
assert_eq!(listed, want, "§7's table and Reason::OWN differ");
}
/// capability-constraints §4.2's match table names the bits the reference sets, in order.
#[test]
fn capability_constraints_lists_the_match_bits() {
let text = read("extensions/capability-constraints.md");
let at = text.find("\n### 4.2 The match").expect("a §4.2");
let names: Vec<String> = text[at..].lines().skip_while(|l| !l.starts_with("| bit")).filter(|l| l.starts_with("| ") && !l.starts_with("| bit") && !l.starts_with("|---")).take_while(|l| l.starts_with("| ")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
assert_eq!(names, ["resolution", "refresh", "bit_depth", "dynamic_range"]);
}