open-standards/wai/wai-rs/tests/opportunity_report.rs
Transaction Science 372f063bcb wai: the opportunity report and the statement check (wai.delivery.opportunity)
extensions/opportunity-report.md: the ad breaks a channel's own signed records
prove, per owner and period (WOR1): the cues of a room a share contract names,
made into breaks by live-ads and their SCTE 35 sections read for slots, and the
program-as-run's ad entries no cue covers, each attributed by its start to the
programme on air; a documented JSON and CSV form for a platform's reported
figures, never evidence; and the check (WCK1) setting them side by side, with
the operator's signed revenue-share statement where given.

wai-rs: the opportunity_report module (feature opportunity_report), its SCTE 35
subset reader, the reported statement's reader, the corpus generator
wai_opportunity_vectors and tests/opportunity_report.rs with the worked example.
revenue_share's the_runs and asset_at become pub(crate), unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:12:30 -04:00

173 lines
9.9 KiB
Rust

//! `wai.delivery.opportunity` (extensions/opportunity-report.md): the committed corpus
//! `live-conformance/opportunity/vectors.json` regenerates byte for byte from its seeds and is the
//! only file there besides its README; every vector gets the outcome it states; every reason the
//! extension adds and every code it reuses refuses a vector; §13 lists exactly the reasons the
//! reference reports; and the worked example of §14: a channel's day of two owners, signed cues and
//! program-as-run, a reported statement that under-reports one owner's breaks, and the check that
//! shows it, computed and then recomputed from the public records alone.
#![cfg(feature = "opportunity_report")]
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use serde_json::{Value, json};
use wai::opportunity_report::{self as opp, Reason, corpus, scope};
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/opportunity/vectors.json")
}
fn doc() -> Value {
serde_json::from_str(&committed()).expect("the corpus is JSON")
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
let generated = corpus::generate().expect("the corpus generates");
assert!(committed() == generated, "live-conformance/opportunity/vectors.json differs from `wai_opportunity_vectors gen`");
let mut files: Vec<String> = std::fs::read_dir(wai_dir().join("live-conformance/opportunity")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
files.sort();
assert_eq!(files, ["README.md", "vectors.json"]);
}
#[test]
fn every_vector_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// Every reason this extension adds and every code it reuses refuses a vector, and the texts it
/// delegates to refuse one each at their own places.
#[test]
fn the_corpus_covers_every_reason_and_case() {
let d = doc();
let vectors = d["vectors"].as_array().unwrap();
let seen: BTreeSet<&str> = vectors.iter().filter_map(|v| v["outcome"]["reason"].as_str()).collect();
for r in Reason::ALL {
assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}");
}
for r in Reason::REUSED {
assert!(seen.contains(r), "nothing in the corpus is refused {r}");
}
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
assert_eq!(get("contract_offered")["reason"], "not_countersigned");
assert_eq!(get("run_aired_misstated")["at"], json!(["run", 0]));
assert_eq!(get("room_event_signature")["at"].as_array().unwrap()[..3], [json!("room"), json!(0), json!("room")]);
assert_eq!(get("check_report_refused")["at"], json!(["report", "report"]));
assert_eq!(get("check_statement_refused")["at"], json!(["statement", "statement"]));
for n in ["owner_a_day", "owner_b_day", "operator_day", "owner_a_runs_alone", "owner_b_two_rooms", "check_owner_b_under_reported", "check_owner_a_csv", "check_owner_b_without_statement", "check_owner_b_reports_a_programme"] {
assert_eq!(get(n)["outcome"], "accept", "{n}");
}
}
/// §13's table names exactly the codes the reference adds, in its order.
#[test]
fn section_13_lists_exactly_the_reference_codes() {
let text = read("extensions/opportunity-report.md");
let at = text.find("\n## 13. Reasons").expect("§13");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<&str> = text[at..end].lines().filter_map(|l| l.strip_prefix("| `")).filter_map(|l| l.split('`').next()).collect();
let codes: Vec<&str> = Reason::ALL.iter().map(|r| r.code()).collect();
assert_eq!(listed, codes);
}
fn line<'a>(o: &'a Value, scope_: &str, key: &str) -> &'a Value {
o["lines"].as_array().unwrap().iter().find(|l| l["scope"] == scope_ && l["key"] == key).unwrap_or_else(|| panic!("no line {scope_} {key}"))
}
/// The worked example (§14). Owner B computes and signs its report of the day from the channel's
/// two windows and the room's cues; its platform reports B's impressions per programme, and all
/// but leaves out B's second documentary; B checks the report against it and against the
/// operator's signed statement of the day. A party holding only the public records receives the
/// check's document as text, verifies it, and recomputes the opportunities of every line from
/// the attributed breaks and the contract alone.
#[test]
fn the_worked_example_shows_the_under_report_from_the_public_records() {
let w = corpus::world().expect("the world");
let b = &w.reports["owner_b"];
// B's report, computed from the documents without it, is the corpus's, byte for byte.
let (rep, report_doc) = opp::create(&w.pools.report_document(b, false), &corpus::key("owner-b"), corpus::pk("owner-b"), corpus::DAY16, corpus::DAY17).expect("B's report");
assert_eq!(rep.to_bytes(), b.report);
// Four cued breaks in B's documentaries: 360 s, twelve slots (four, two, four, two).
assert_eq!((rep.breaks, rep.break_ms, rep.slots), (4, 360_000, 12));
// The check of the platform's figures, beside the signed statement.
let inputs = w.pools.check_document(b, "b_programmes", Some(&w.statements["owner_b"]), None);
let (_, document) = opp::create_check(&inputs, &corpus::key("owner-b"), opp::FLOOR_PPM, opp::CEILING_PPM).expect("the check");
// A party holding only the public records: the document, as text.
let text = serde_json::to_string(&document).unwrap();
let got: Value = serde_json::from_str(&text).unwrap();
let a = opp::verify_check(&got).expect("the check verifies");
let o = opp::check_json(&a);
let period = line(&o, "period", "");
// Fourteen impressions reported against twenty-four the signed records prove, and revenue
// below B's signed part.
assert_eq!((period["impressions"].as_u64(), period["signed"]["impressions"].as_u64()), (Some(14), Some(24)));
assert_eq!(period["fill_ppm"], 1_166_666);
assert_eq!(period["flags"], json!(["impressions_below_signed", "revenue_below_signed"]));
// The first documentary is in step with the day; the second, as many slots and two
// impressions, falls far below it.
let doc_1 = line(&o, "programme", "b/doc-1");
let doc_2 = line(&o, "programme", "b/doc-2");
assert_eq!((doc_1["slots"].as_u64(), doc_1["flags"].clone()), (Some(6), json!([])));
assert_eq!((doc_2["slots"].as_u64(), doc_2["fill_ppm"].as_u64(), doc_2["flags"].clone()), (Some(6), Some(333_333), json!(["below"])));
// Recomputed from the report's attributed breaks and the contract: each programme's breaks,
// seconds and slots, over the breaks of the splits B shares in.
let report = opp::verify(&report_doc).expect("the report verifies");
let r = opp::accepted_json(&report);
let b_splits: BTreeSet<u64> = r["lines"].as_array().unwrap().iter().map(|l| l["split"].as_u64().unwrap()).collect();
let mut per: BTreeMap<String, (u64, u64, u64)> = BTreeMap::new();
for x in r["breaks"].as_array().unwrap() {
if !b_splits.contains(&x["split"].as_u64().unwrap()) {
continue;
}
let e = per.entry(x["asset"].as_str().unwrap_or("").to_string()).or_default();
e.0 += 1;
e.1 += x["duration_ms"].as_u64().unwrap();
e.2 += x["slots"].as_u64().unwrap_or(0);
}
for p in ["b/doc-1", "b/doc-2"] {
let l = line(&o, "programme", p);
assert_eq!(per[p], (l["breaks"].as_u64().unwrap(), l["break_ms"].as_u64().unwrap(), l["slots"].as_u64().unwrap()), "{p}");
}
// The signed figures, from the statement's own lines.
let st = &a.statement.as_ref().expect("the statement").statement;
assert_eq!(st.lines.iter().map(|l| l.impressions).sum::<u64>(), 24);
assert_eq!(period["signed"]["revenue"].as_u64(), Some(st.total));
// The check's lines are the only figures that change with the reported statement: the same
// report and statement beside owner A's fair CSV flag nothing.
let d = doc();
let v = d["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "check_owner_a_csv").expect("A's check vector");
let fair = opp::verify_check(&corpus::document(&d, v).expect("it reads")).expect("A's check");
assert!(fair.check.lines.iter().all(|l| l.flags == 0), "A's reported statement is what its records prove");
assert_eq!(fair.check.lines[0].scope, scope::PERIOD);
}
/// Owner A's day holds every kind of break §4 makes: a break inside the ad slot after A's first
/// episode counts as cued and the slot is not counted again; the slot no cue covers is scheduled
/// and unslotted; slots come from a section or from the policy.
#[test]
fn owner_a_holds_every_kind_of_break() {
let w = corpus::world().expect("the world");
let a = opp::verify(&w.pools.report_document(&w.reports["owner_a"], true)).expect("A's report");
let mine: Vec<&opp::Break> = a.counted().collect();
let cued: Vec<&&opp::Break> = mine.iter().filter(|b| b.class == opp::class::CUED).collect();
let scheduled: Vec<&&opp::Break> = mine.iter().filter(|b| b.class == opp::class::SCHEDULED).collect();
assert_eq!(cued.len(), 4);
assert_eq!(scheduled.len(), 1);
assert_eq!((scheduled[0].slots, scheduled[0].asset.as_deref()), (None, Some("a/ep-2")), "the uncued ad slot, by the rule preceding");
let sources: Vec<Option<&str>> = cued.iter().map(|b| b.slot_source).collect();
assert_eq!(sources, [Some("policy"), Some("section"), Some("section"), Some("section")]);
assert_eq!(cued[2].asset.as_deref(), Some("a/ep-1"), "the break inside the ad slot, by the rule preceding");
let runs_alone = opp::verify(&w.pools.report_document(&w.reports["owner_a_runs_alone"], true)).expect("from the runs alone");
assert_eq!(runs_alone.counted().filter(|b| b.class == opp::class::SCHEDULED).count(), 2, "with no room, both ad slots are scheduled");
}