receipt-ledger §5.2 registers kind 43, an origin claim (wai-origin), and kind 44, a marking (wai-marking), each named by its link hash (synthesis §2) and no ledger source's, so a record that names what an output carried by a kind, a data export's record lines among them (data-export §3.2), names them by WAI's numbers rather than a private one. synthesis §2 states the two kinds; kinds 45 to 127 are unassigned. The reference's constants are record_kind::ORIGIN_CLAIM and MARKING, held to §5.2's table by section_5_2_registers_every_kind_once_and_in_order and receipt_ledger_verify.py's kinds_registered. The two records' formats, verifiers and corpora are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
932 lines
64 KiB
Rust
932 lines
64 KiB
Rust
//! `wai.delivery.ledger` (extensions/receipt-ledger.md): the committed corpus
|
||
//! `live-conformance/ledger/vectors.json` regenerates byte for byte from its seeds and the sources
|
||
//! of the corpora it reads, and is the only file there besides its README; every vector gets the
|
||
//! outcome it states; every reason the extension adds refuses a vector, and so does each source
|
||
//! kind's own extension; the accepted vectors are the cases §11 names; under a tariff of version 8,
|
||
//! a holder's storage billed by byte-days from records its snapshots recompute, and every rule of
|
||
//! §4.13 refusing a control; and §10 lists exactly the reasons the reference reports.
|
||
#![cfg(feature = "receipt_ledger")]
|
||
|
||
use std::collections::BTreeSet;
|
||
use std::path::PathBuf;
|
||
use wai::receipt_ledger::{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/ledger/vectors.json")
|
||
}
|
||
|
||
#[test]
|
||
fn the_committed_corpus_regenerates_from_its_seeds_and_sources() {
|
||
let generated = corpus::generate(&wai_dir()).expect("the corpus generates");
|
||
assert!(committed() == generated, "live-conformance/ledger/vectors.json differs from `wai_receipt_ledger_vectors gen`");
|
||
let mut files: Vec<String> = std::fs::read_dir(wai_dir().join("live-conformance/ledger")).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; each source kind
|
||
/// is refused once by its own extension; the accepted invoices are §11's.
|
||
#[test]
|
||
fn the_corpus_covers_every_reason_and_case() {
|
||
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["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();
|
||
// A refusal of each source kind by its own extension, at the source's place.
|
||
for (n, reason, at) in [
|
||
("room_receipt_refused", "bad_signature", serde_json::json!(["source", 0, "receipt"])),
|
||
("statement_refused", "statement_mismatch", serde_json::json!(["source", 1, "statement"])),
|
||
("package_refused", "receipt-index-count", serde_json::json!(["source", 4, "receipt"])),
|
||
("ads_refused", "funds_exceeded", serde_json::json!(["source", 5, "receipt", 1])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
// Each source kind's scope, period and tariff rule refuses a vector of its own.
|
||
for (n, reason, at) in [
|
||
("room_outside_scope", "outside_scope", serde_json::json!(["source", 0])),
|
||
("party_outside_scope", "outside_scope", serde_json::json!(["source", 2])),
|
||
("package_outside_scope", "outside_scope", serde_json::json!(["source", 4])),
|
||
("decider_outside_scope", "outside_scope", serde_json::json!(["source", 5])),
|
||
("room_under_another_tariff", "tariff_conflict", serde_json::json!(["source", 0])),
|
||
("statement_under_another_tariff", "tariff_conflict", serde_json::json!(["source", 1])),
|
||
("room_outside_period", "outside_period", serde_json::json!(["source", 7])),
|
||
("statement_outside_period", "outside_period", serde_json::json!(["source", 0])),
|
||
("ad_break_outside_period", "outside_period", serde_json::json!(["source", 0])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
let a = get("a_day");
|
||
assert_eq!(a["outcome"], "accept");
|
||
let kinds: BTreeSet<&str> = a["lines"].as_array().unwrap().iter().map(|l| l["source_kind"].as_str().unwrap()).collect();
|
||
assert_eq!(kinds, ["ads", "delivery", "package", "room"].into_iter().collect(), "a line of every source kind");
|
||
let units: BTreeSet<&str> = a["lines"].as_array().unwrap().iter().map(|l| l["unit"].as_str().unwrap()).collect();
|
||
assert!(units.contains("presented_impressions") && units.contains("billable_impressions"), "ads under both rules");
|
||
assert!(a["lines"].as_array().unwrap().iter().all(|l| !l["cites"].as_array().unwrap().is_empty()), "every line cites its records");
|
||
let delivery = &a["reports"]["delivery"];
|
||
assert_eq!(delivery["parties"].as_array().unwrap().len(), 3, "two delivering parties and the forwarder");
|
||
assert!(!delivery["overlaps"].as_array().unwrap().is_empty(), "one viewer acknowledged one object from both parties");
|
||
assert!(delivery["parties"].as_array().unwrap().iter().any(|p| p["declared"]["bytes"].as_u64().unwrap() > 0), "declared bytes, never billed");
|
||
assert!(delivery["parties"].as_array().unwrap().iter().any(|p| p["free_bytes"].as_u64().unwrap() > 0), "a retrieval served free");
|
||
let ads = a["reports"]["ads"].as_array().unwrap();
|
||
assert!(ads.iter().any(|r| r["claims_required"] == true && !r["not_counted"].as_object().unwrap().is_empty()), "billed impressions the players' claims did not count");
|
||
let scopes: BTreeSet<&str> = a["energy"].as_array().unwrap().iter().map(|r| r["scope"].as_str().unwrap()).collect();
|
||
assert!(scopes.contains("room_hops") && scopes.contains("delivery_hops") && scopes.contains("ad_decisions"), "{scopes:?}");
|
||
let s = get("streams_stitched_by_others");
|
||
assert_eq!(s["outcome"], "accept");
|
||
assert!(s["lines"].as_array().unwrap().iter().all(|l| l["unit"] == "presented_impressions"), "stitched presentations bill as presented impressions");
|
||
let statements = s["reports"]["stitched"].as_array().unwrap();
|
||
assert_eq!(statements.len(), 3, "one stitched-presentation statement per stitched source");
|
||
assert!(
|
||
statements.iter().flat_map(|st| st["record_facts"].as_array().unwrap()).filter_map(|r| r["ads"].as_array()).flatten().any(|a| a["own_claims"].as_array().unwrap().len() as u64 > a["presentations"].as_u64().unwrap()),
|
||
"an ad whose own claims are not all presented: only presentations bill"
|
||
);
|
||
assert!(
|
||
statements.iter().flat_map(|st| st["record_facts"].as_array().unwrap()).any(|r| r["stated_spend_micros"].is_u64()),
|
||
"a form-1 receipt's own spend stated beside the presented one"
|
||
);
|
||
assert_eq!(get("stitched_source_given_twice")["reason"], "record_duplicate");
|
||
assert_eq!(get("stitcher_outside_scope")["reason"], "quantity_not_recorded");
|
||
let b = get("the_next_invoice");
|
||
assert_eq!(b["outcome"], "accept");
|
||
assert_eq!(b["coverage"]["room_hops"], "partial", "a relay's unmetered hop");
|
||
let m = get("room_hops_of_three_classes");
|
||
let rows: Vec<&str> = m["energy"].as_array().unwrap().iter().filter(|r| r["scope"] == "room_hops").map(|r| r["class"].as_str().unwrap()).collect();
|
||
assert_eq!(rows, ["ModelBased", "Estimator", "OnChipCounter"], "one row per class, never one across them");
|
||
}
|
||
|
||
/// Version 3 (§4.6, §6.9): one capability's encode and decode at their own prices; a re-run billed
|
||
/// in the next invoice of its chain, the output receipt the first invoice holds cited again; one
|
||
/// output under two hops of one invoice, cited once and billed for each; an operation the tariff
|
||
/// does not price billing nothing; a replayed hop receipt and a hop two jobs claim refused; and
|
||
/// version 2 billing an output once, as it did.
|
||
#[test]
|
||
fn version_3_prices_each_operation_and_bills_the_work() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
|
||
let line = |o: &Value, cap: &str, op: &str, unit: &str| -> Value {
|
||
o["lines"]
|
||
.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.find(|l| l["source_kind"] == "job" && l["capability"] == cap && l["operation"] == op && l["unit"] == unit)
|
||
.cloned()
|
||
.unwrap_or_else(|| panic!("no {cap} {op} {unit} line"))
|
||
};
|
||
let cites = |l: &Value, kind: u64| -> Vec<String> { l["cites"].as_array().unwrap().iter().filter(|c| c[0] == kind).map(|c| c[1].as_str().unwrap().to_string()).collect() };
|
||
const IM: &str = "wai.video.int_motion";
|
||
|
||
let k = get("jobs_priced_per_operation");
|
||
assert_eq!(k["outcome"], "accept");
|
||
let (enc, dec) = (line(&k, IM, "media.encode", "subpixels"), line(&k, IM, "media.decode", "subpixels"));
|
||
assert_eq!((enc["price"].as_u64(), dec["price"].as_u64()), (Some(20_000), Some(10_000)), "an encode and a decode at their own prices");
|
||
assert_eq!(enc["quantity"].as_u64().unwrap(), 2 * dec["quantity"].as_u64().unwrap(), "two encodes and one decode of one clip");
|
||
assert_eq!((line(&k, IM, "media.encode", "jobs")["quantity"].as_u64(), line(&k, IM, "media.decode", "jobs")["quantity"].as_u64()), (Some(2), Some(1)));
|
||
assert_eq!(line(&k, "wai.video.av1", "media.encode", "jobs")["quantity"], 1, "the failover, at the fallback's encode price");
|
||
|
||
let r = get("a_rerun_billed");
|
||
assert_eq!(r["outcome"], "accept");
|
||
let rerun = line(&r, IM, "media.encode", "subpixels");
|
||
let output = cites(&rerun, 15);
|
||
assert_eq!(output.len(), 1);
|
||
assert!(cites(&enc, 15).contains(&output[0]), "the re-run's output receipt is one the first invoice billed with job-0001");
|
||
assert!(cites(&rerun, 14).iter().all(|h| !cites(&enc, 14).contains(h)), "the re-run's hop receipt is new");
|
||
|
||
let both = get("one_output_under_two_hops");
|
||
assert_eq!(both["outcome"], "accept");
|
||
let b = line(&both, IM, "media.encode", "subpixels");
|
||
assert_eq!((cites(&b, 14).len(), cites(&b, 15), cites(&b, 16).len()), (2, output.clone(), 2), "two hops and two records, one output cited once");
|
||
assert_eq!(b["quantity"].as_u64().unwrap(), 2 * rerun["quantity"].as_u64().unwrap(), "its subpixels billed for each hop");
|
||
assert_eq!(line(&both, IM, "media.encode", "jobs")["quantity"], 2);
|
||
|
||
let n = get("an_operation_the_tariff_does_not_price");
|
||
assert_eq!(n["outcome"], "accept");
|
||
assert!(n["lines"].as_array().unwrap().iter().all(|l| l["operation"] == "media.encode"), "the decode bills nothing");
|
||
assert_eq!(n["energy"].as_array().unwrap().iter().filter(|e| e["scope"] == "jobs").map(|e| e["figures"].as_u64().unwrap()).sum::<u64>(), 2, "both hops' energy stated");
|
||
|
||
for (name, reason, at) in [
|
||
("a_hop_receipt_replayed", "record_rebilled", json!(["source", 0])),
|
||
("one_hop_under_two_lines", "record_duplicate", json!(["source", 1])),
|
||
("job_prices_unordered_by_operation", "tariff_invalid", json!(["tariff"])),
|
||
("version_2_invoice_under_a_version_3_tariff", "tariff_mismatch", json!(["invoice"])),
|
||
("version_2_bills_an_output_once", "record_rebilled", json!(["source", 0])),
|
||
("output_claimed_by_two_jobs", "record_duplicate", json!(["source", 1])),
|
||
("job_routed_twice", "record_overlap", json!(["source", 6])),
|
||
] {
|
||
let o = get(name);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{name}");
|
||
}
|
||
}
|
||
|
||
/// Jobs routed under the route record's version 2 (§5.1): billed under version 3 at the endpoint
|
||
/// that served, each output receipt of an object its hop states it delivered, and refused
|
||
/// `output_not_delivered` when it is another or given twice; under version 4 a record of
|
||
/// version 1 holds its outputs to none.
|
||
#[test]
|
||
fn a_jobs_output_is_what_its_hop_delivered() {
|
||
use serde_json::json;
|
||
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
|
||
let r = get("version_2_routes_under_version_3");
|
||
assert_eq!(r["outcome"], "accept");
|
||
let jobs: u64 = r["lines"].as_array().unwrap().iter().filter(|l| l["unit"] == "jobs").map(|l| l["quantity"].as_u64().unwrap()).sum();
|
||
assert_eq!(jobs, 4, "an encode, a failover, a decode and a failover whose failed hop states no class; the late and the failed jobs bill nothing");
|
||
assert!(r["energy"].as_array().unwrap().iter().all(|e| e["scope"] == "jobs"), "version 3 states the billed hops' energy alone");
|
||
let record = |n: &str| -> serde_json::Value { serde_json::from_str(doc["sources"][n]["record"].as_str().unwrap()).unwrap() };
|
||
assert_eq!(record("job_v2_encode")["version"], 2, "a route record of version 2");
|
||
assert!(record("job_v2_failover")["candidates"].as_array().unwrap().iter().any(|c| c["failures"].is_object()), "measured failures stated");
|
||
for (n, at) in [
|
||
("an_output_the_hop_did_not_deliver", json!(["source", 0, "outputs", 0])),
|
||
("one_output_given_twice", json!(["source", 0, "outputs", 1])),
|
||
("version_4_an_output_the_hop_did_not_deliver", json!(["source", 0, "outputs", 0])),
|
||
("version_4_an_output_of_a_version_1_record", json!(["source", 0, "outputs", 0])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some("output_not_delivered"), &at), "{n}");
|
||
}
|
||
}
|
||
|
||
/// Version 4 (§4.7, §6.6): a room's bytes out at its price per 10^9 bytes; a package's work
|
||
/// billed once per run that made it, its receipt cited by each, and its storage by byte-days for
|
||
/// each period it is held; every hop's energy, the hops not billed in a scope of their own; and
|
||
/// each control refused at its place.
|
||
#[test]
|
||
fn version_4_bills_the_bytes_the_work_and_the_storage() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
|
||
let lines = |o: &Value, unit: &str| -> Vec<Value> { o["lines"].as_array().unwrap().iter().filter(|l| l["unit"] == unit).cloned().collect() };
|
||
let metering: Value = serde_json::from_str(&read("live-conformance/metering/vectors.json")).unwrap();
|
||
let bytes = metering["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "bytes_out_priced").unwrap()["units"]["bytes_out"].as_u64().unwrap();
|
||
let package = |n: &str| -> Value { serde_json::from_str(doc["sources"][n]["receipt"].as_str().unwrap()).unwrap() };
|
||
let base = package("package_base");
|
||
let cites = |l: &Value, kind: u64| -> Vec<String> { l["cites"].as_array().unwrap().iter().filter(|c| c[0] == kind).map(|c| c[1].as_str().unwrap().to_string()).collect() };
|
||
|
||
let a = get("version_4_by_the_byte_and_the_work");
|
||
assert_eq!(a["outcome"], "accept");
|
||
let out = lines(&a, "bytes_out");
|
||
assert_eq!(out.len(), 1);
|
||
assert_eq!(out[0]["quantity"].as_u64(), Some(bytes), "the receipt's bytes out");
|
||
assert_eq!(out[0]["amount"].as_u64().unwrap(), (bytes as u128 * out[0]["price"].as_u64().unwrap() as u128 / 1_000_000_000) as u64);
|
||
let work_a = lines(&a, "work_units");
|
||
assert_eq!((work_a.len(), &work_a[0]["quantity"], &work_a[0]["source_kind"]), (1, &base["work_units"], &json!("job")), "the run that made the package, by its work units");
|
||
assert_eq!(cites(&work_a[0], 5), vec![base["receipt_hash"].as_str().unwrap().to_string()], "the package cited as the run's output");
|
||
assert!(a["lines"].as_array().unwrap().iter().all(|l| l["source_kind"] != "package"), "a package first listed bills no storage, and a package source no work");
|
||
let scope = |o: &Value, s: &str| -> Vec<Value> { o["energy"].as_array().unwrap().iter().filter(|e| e["scope"] == s).cloned().collect() };
|
||
assert_eq!(scope(&a, "jobs").iter().map(|e| e["figures"].as_u64().unwrap()).sum::<u64>(), 4, "four billed hops, the packaging run's among them");
|
||
assert!(scope(&a, "packaging").is_empty(), "a package's energy is its run's, stated by its hop");
|
||
assert_eq!(scope(&a, "unbilled_hops").iter().map(|e| e["figures"].as_u64().unwrap()).sum::<u64>(), 4, "the failover's, the late hop's and two of the three failed hops' figures");
|
||
assert_eq!((a["unmetered"]["unbilled_hops"].as_u64(), a["coverage"]["unbilled_hops"].as_str()), (Some(1), Some("partial")), "one failed hop unmetered");
|
||
|
||
let b = get("version_4_storage_held_through_a_period");
|
||
assert_eq!(b["outcome"], "accept");
|
||
let days = lines(&b, "byte_days");
|
||
assert_eq!(days.len(), 1);
|
||
let period = b["end"].as_u64().unwrap() - b["start"].as_u64().unwrap();
|
||
assert_eq!(days[0]["quantity"].as_u64().unwrap() as u128, base["stored_bytes"].as_u64().unwrap() as u128 * period as u128 / 86_400_000_000, "its bytes over the invoice's period, in days");
|
||
let work_b = lines(&b, "work_units");
|
||
assert_eq!((work_b.len(), &work_b[0]["quantity"]), (1, &base["work_units"]), "the same package made again, its work billed for the new run");
|
||
assert_eq!(cites(&work_b[0], 5), cites(&work_a[0], 5), "the package the first invoice lists, cited again");
|
||
assert!(cites(&work_b[0], 14).iter().all(|h| !cites(&work_a[0], 14).contains(h)), "a new run's hop receipt");
|
||
|
||
for (n, reason, at) in [
|
||
("version_4_bytes_at_another_price", "tariff_conflict", json!(["source", 0])),
|
||
("version_4_a_receipt_that_prices_no_bytes", "tariff_conflict", json!(["source", 0])),
|
||
("version_4_bytes_out_above_the_receipt", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_4_storage_of_a_package_first_billed", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_4_a_package_run_replayed", "record_rebilled", json!(["source", 2])),
|
||
("version_4_one_package_run_under_two_jobs", "record_duplicate", json!(["source", 1])),
|
||
("version_4_storage_over_the_tariffs_period", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_4_unbilled_hops_left_out", "invoice_mismatch", json!(["invoice"])),
|
||
("version_4_unbilled_hops_in_the_jobs_scope", "invoice_mismatch", json!(["invoice"])),
|
||
("version_4_a_failed_hop_billed_as_a_job", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_4_a_job_line_at_the_routers_price", "invoice_mismatch", json!(["invoice"])),
|
||
("version_4_unbilled_hop_with_no_class", "job_figure_invalid", json!(["source", 0, "receipts", 0])),
|
||
("version_3_invoice_under_a_version_4_tariff", "tariff_mismatch", json!(["invoice"])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
}
|
||
|
||
/// §10 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
|
||
/// §4.9: a third party's usage per endpoint, and what re-derives an upscale and a tone mapping.
|
||
#[test]
|
||
fn version_6_bills_a_usage_per_endpoint_and_what_it_re_derives() {
|
||
use serde_json::Value;
|
||
let text = committed();
|
||
let doc: Value = serde_json::from_str(&text).unwrap();
|
||
let vector = |name: &str| doc["vectors"].as_array().unwrap().iter().find(|v| v["name"] == name).unwrap_or_else(|| panic!("no vector {name}"));
|
||
let line = |o: &Value, endpoint: &str, usage: &str| -> Value { o["lines"].as_array().unwrap().iter().find(|l| l["endpoint"] == endpoint && l["usage_unit"] == usage).cloned().unwrap_or(Value::Null) };
|
||
|
||
let o = &vector("version_6_a_third_partys_usage_per_endpoint")["outcome"];
|
||
assert_eq!(o["outcome"], "accept");
|
||
// Each endpoint at its own price. gen.alpha's usage is the served hop's alone: the failover's
|
||
// failed hop's billed second, and the job given no record, reach no line.
|
||
assert_eq!((&line(o, "gen.alpha", "seconds")["quantity"], &line(o, "gen.alpha", "seconds")["price"]), (&Value::from(8), &Value::from(40_000_000)));
|
||
assert_eq!((&line(o, "gen.beta", "seconds")["quantity"], &line(o, "gen.beta", "seconds")["price"]), (&Value::from(8), &Value::from(25_000_000)));
|
||
// Another unit at that unit's price, and a unit no price names, which bills by the job alone.
|
||
assert_eq!(line(o, "gen.alpha", "images")["quantity"], 2);
|
||
assert_eq!(o["lines"].as_array().unwrap().iter().filter(|l| l["unit"] == "usage").count(), 3);
|
||
let jobs = o["lines"].as_array().unwrap().iter().find(|l| l["unit"] == "jobs").unwrap();
|
||
assert_eq!((&jobs["quantity"], &jobs["endpoint"]), (&Value::from(6), &Value::from("")));
|
||
// A usage line cites its record; a line per job does not need to.
|
||
let cites_kind = |l: &Value, k: u64| l["cites"].as_array().unwrap().iter().filter(|c| c[0] == k).count();
|
||
assert_eq!(cites_kind(&line(o, "gen.beta", "seconds"), 17), 2);
|
||
assert_eq!(cites_kind(jobs, 17), 0);
|
||
|
||
let o = &vector("version_6_an_upscale_and_a_tone_mapping")["outcome"];
|
||
assert_eq!(o["outcome"], "accept");
|
||
let by_capability = |c: &str| o["lines"].as_array().unwrap().iter().find(|l| l["capability"] == c).unwrap().clone();
|
||
// 12×8 and 8×6 pictures, three samples a pixel; three pictures and two pictures of 8×4.
|
||
assert_eq!(by_capability("wai.media.upscale")["quantity"], (12 * 8 + 8 * 6) * 3);
|
||
assert_eq!(by_capability("wai.media.tonemap")["quantity"], 5 * 8 * 4 * 3);
|
||
assert_eq!(cites_kind(&by_capability("wai.media.upscale"), 18), 2);
|
||
assert_eq!(cites_kind(&by_capability("wai.media.tonemap"), 19), 2);
|
||
|
||
// The chain cites an upscale's records again for a new hop's work.
|
||
assert_eq!(vector("version_6_an_upscale_run_again")["outcome"]["outcome"], "accept");
|
||
|
||
for (name, reason) in [
|
||
("version_6_a_price_of_any_endpoint_beside_one", "tariff_invalid"),
|
||
("version_6_prices_out_of_order_by_endpoint", "tariff_invalid"),
|
||
("version_6_a_usage_price_naming_no_unit", "tariff_invalid"),
|
||
("version_6_a_jobs_price_naming_a_unit", "tariff_invalid"),
|
||
("version_5_a_price_of_usage", "tariff_invalid"),
|
||
("version_5_an_invoice_listing_a_third_partys_record", "invoice_invalid"),
|
||
("version_5_invoice_under_a_version_6_tariff", "tariff_mismatch"),
|
||
("version_5_a_job_given_a_third_partys_records", "malformed"),
|
||
("version_6_another_jobs_record", "third_party_mismatch"),
|
||
("version_6_a_failed_hops_usage_billed", "quantity_not_recorded"),
|
||
("version_6_usage_no_record_states", "quantity_not_recorded"),
|
||
("version_6_usage_at_another_endpoints_price", "invoice_mismatch"),
|
||
("version_6_a_record_left_out", "invoice_mismatch"),
|
||
("version_6_a_usage_line_not_citing_its_record", "citation_mismatch"),
|
||
("version_6_a_picture_altered", "output_invalid"),
|
||
("version_6_an_upscale_under_another_prior", "output_invalid"),
|
||
("version_6_a_record_of_another_output", "output_invalid"),
|
||
("version_6_a_picture_its_hop_did_not_deliver", "output_not_delivered"),
|
||
("version_6_an_upscale_output_twice", "output_not_delivered"),
|
||
("version_6_a_rendition_altered", "output_invalid"),
|
||
("version_6_a_tone_mapping_of_another_envelope", "output_invalid"),
|
||
("version_6_a_rendition_its_hop_did_not_deliver", "output_not_delivered"),
|
||
("version_6_a_tone_mapping_output_twice", "output_not_delivered"),
|
||
("version_6_a_tone_mapping_without_its_proofs", "malformed"),
|
||
] {
|
||
assert_eq!(vector(name)["outcome"]["reason"], reason, "{name}");
|
||
}
|
||
}
|
||
|
||
/// §4.10: a month of a broadcaster's transmissions, four invoices chained a week apart, each
|
||
/// entry of a standing schedule billed in octets sent and in pass time at the price of its class
|
||
/// whose ceiling is the least at or above its pass rate, nothing in a unit above every ceiling,
|
||
/// and each declaration's energy once; every amount below is worked by hand from the schedules.
|
||
#[test]
|
||
fn version_7_bills_a_transmission_per_class_unit_and_ceiling() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
|
||
// (bearer, unit, ceiling, quantity, price, amount) of each line, in line order.
|
||
let lines = |o: &Value| -> Vec<(String, String, u64, u64, u64, u64)> {
|
||
o["lines"]
|
||
.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.map(|l| {
|
||
assert_eq!(l["source_kind"], "group");
|
||
let n = |k: &str| l[k].as_u64().unwrap();
|
||
(l["bearer"].as_str().unwrap().into(), l["unit"].as_str().unwrap().into(), n("max_bps"), n("quantity"), n("price"), n("amount"))
|
||
})
|
||
.collect()
|
||
};
|
||
let row = |b: &str, u: &str, c: u64, q: u64, p: u64, a: u64| (b.to_string(), u.to_string(), c, q, p, a);
|
||
let energy = |o: &Value| -> Vec<(String, u64, u64)> {
|
||
o["energy"].as_array().unwrap().iter().map(|r| (r["scope"].as_str().unwrap().to_string() + " " + r["class"].as_str().unwrap(), r["figures"].as_u64().unwrap(), r["joules"].as_u64().unwrap())).collect()
|
||
};
|
||
|
||
// Week 1. The multicast's object 3 at 5,440 bit/s and object 0 at 563,200; the broadcast's
|
||
// three at 9,472, 5,120 and 2,304, all within its pass-time ceiling of 100,000.
|
||
let w1 = get("version_7_week_1");
|
||
assert_eq!(w1["outcome"], "accept");
|
||
assert_eq!(
|
||
lines(&w1),
|
||
[
|
||
row("multicast", "bytes_sent", 50_000, 1_360, 4_000_000, 5),
|
||
row("multicast", "bytes_sent", 2_000_000, 14_080, 6_000_000, 84),
|
||
row("multicast", "pass_us", 2_000_000, 2_200_000, 300, 660),
|
||
row("unidirectional", "bytes_sent", 1_000_000, 23_680 + 3_840 + 1_152, 2_500_000, 71),
|
||
row("unidirectional", "pass_us", 100_000, 30_000_000, 150, 4_500),
|
||
]
|
||
);
|
||
assert_eq!(w1["amount"], 5 + 84 + 660 + 71 + 4_500);
|
||
assert_eq!(energy(&w1), [("transmissions ModelBased".into(), 1, 1_200_000), ("transmissions OnChipCounter".into(), 1, 2_500_000)]);
|
||
assert_eq!((&w1["coverage"]["transmissions"], &w1["unmetered"]["transmissions"]), (&json!("whole"), &json!(0)));
|
||
let cites = |l: &Value, kind: u64| l["cites"].as_array().unwrap().iter().filter(|c| c[0] == kind).count();
|
||
for l in w1["lines"].as_array().unwrap() {
|
||
assert_eq!((cites(l, 20), cites(l, 21), cites(l, 22)), (1, 1, 1), "the group, its instance's declaration and its schedule");
|
||
}
|
||
|
||
// Week 2: the faster broadcast's object 1 at 1,024,000 bit/s is above the octets' ceiling of
|
||
// 1,000,000 and bills no octets; its pass time and its object 0's, at 947,200, at the
|
||
// ceiling of 10,000,000; the first week's group again on a new instance.
|
||
let w2 = get("version_7_week_2");
|
||
assert_eq!(w2["outcome"], "accept");
|
||
assert_eq!(
|
||
lines(&w2),
|
||
[
|
||
row("unidirectional", "bytes_sent", 1_000_000, 14_208 + 2_040, 2_500_000, 40),
|
||
row("unidirectional", "pass_us", 100_000, 3_000_000, 150, 450),
|
||
row("unidirectional", "pass_us", 10_000_000, 140_000, 900, 126),
|
||
]
|
||
);
|
||
let report = &w2["reports"]["transmission"];
|
||
assert_eq!(report.as_array().unwrap().len(), 2, "two groups, one of them the first week's again");
|
||
let late = &report[0]["entries"][1];
|
||
assert_eq!((&late["rate_bps"], &late["bytes_sent_price"], &late["pass_us_price"]), (&json!(1_024_000), &Value::Null, &json!(6)));
|
||
let unhex = |s: &str| -> Vec<u8> { (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() };
|
||
let records = |n: &str| -> Vec<(u8, [u8; 32])> {
|
||
let b = unhex(vectors.iter().find(|v| v["name"] == n).unwrap()["invoice"].as_str().unwrap());
|
||
wai::receipt_ledger::Invoice::parse(&b).unwrap().records
|
||
};
|
||
let (r1, r2) = (records("version_7_week_1"), records("version_7_week_2"));
|
||
let groups = |r: &[(u8, [u8; 32])]| -> Vec<[u8; 32]> { r.iter().filter(|(k, _)| *k == 20).map(|(_, id)| *id).collect() };
|
||
assert!(groups(&r2).iter().any(|g| groups(&r1).contains(g)), "the first week's group receipt listed again");
|
||
assert!(r2.iter().filter(|(k, _)| *k != 20).all(|r| !r1.contains(r)), "and no declaration or schedule twice");
|
||
|
||
// Week 3: the multicast's object 0 at 3,788,800 bit/s is above every ceiling of its class
|
||
// and bills nothing; its object 2, 49,994 octets in 7,999 ms, is at 50,000.25 bit/s, which
|
||
// rounds up past the ceiling of 50,000; its declaration states no figure.
|
||
let w3 = get("version_7_week_3");
|
||
assert_eq!(w3["outcome"], "accept");
|
||
assert_eq!(
|
||
lines(&w3),
|
||
[
|
||
row("multicast", "bytes_sent", 50_000, 864, 4_000_000, 3),
|
||
row("multicast", "bytes_sent", 2_000_000, 49_994, 6_000_000, 299),
|
||
row("multicast", "pass_us", 2_000_000, 1_500_000 + 7_999_000, 300, 2_849),
|
||
]
|
||
);
|
||
let above = &w3["reports"]["transmission"][0]["entries"][0];
|
||
assert_eq!((&above["rate_bps"], &above["bytes_sent_price"], &above["pass_us_price"]), (&json!(3_788_800), &Value::Null, &Value::Null));
|
||
let edge = &w3["reports"]["transmission"][0]["entries"][2];
|
||
assert_eq!((&edge["rate_bps"], &edge["bytes_sent_price"]), (&json!(50_001), &json!(2)), "rounded up, at the next ceiling");
|
||
assert_eq!((&w3["coverage"]["transmissions"], &w3["unmetered"]["transmissions"]), (&json!("partial"), &json!(1)));
|
||
assert!(energy(&w3).is_empty());
|
||
|
||
// Week 4: a relay chain declared and scheduled nowhere bills nothing and states its energy.
|
||
let w4 = get("version_7_week_4");
|
||
assert_eq!(w4["outcome"], "accept");
|
||
assert_eq!(lines(&w4), [row("unidirectional", "bytes_sent", 1_000_000, 4_080, 2_500_000, 10), row("unidirectional", "pass_us", 100_000, 6_000_000, 150, 900)]);
|
||
assert_eq!(energy(&w4), [("transmissions Estimator".into(), 1, 400_000), ("transmissions OnChipCounter".into(), 1, 3_000_000)]);
|
||
// The month: four invoices a week apart, each naming the one before it.
|
||
let month: Vec<(u64, u64)> = ["version_7_week_1", "version_7_week_2", "version_7_week_3", "version_7_week_4"].iter().map(|n| (get(n)["start"].as_u64().unwrap(), get(n)["end"].as_u64().unwrap())).collect();
|
||
assert_eq!((month[0].0, month[3].1), (1_790_812_800_000_000, 1_793_491_200_000_000), "October 2026");
|
||
assert!(month.windows(2).all(|w| w[0].1 == w[1].0));
|
||
|
||
for (n, reason, at) in [
|
||
("version_7_a_line_over_its_ceiling", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_a_line_above_its_least_ceiling", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_an_entry_above_every_ceiling_billed", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_transmissions_energy_left_out", "invoice_mismatch", json!(["invoice"])),
|
||
("version_7_an_unmetered_transmission_left_out", "invoice_mismatch", json!(["invoice"])),
|
||
("version_7_a_line_not_citing_its_declaration", "citation_mismatch", json!(["invoice"])),
|
||
("version_7_a_publisher_outside_scope", "outside_scope", json!(["source", 0])),
|
||
("version_7_a_group_outside_the_period", "outside_period", json!(["source", 0])),
|
||
("version_7_a_group_given_twice", "record_duplicate", json!(["source", 1])),
|
||
("version_7_a_declaration_billed_again", "record_rebilled", json!(["source", 0])),
|
||
("version_7_a_group_under_version_6", "malformed", json!(["source", 0])),
|
||
("version_6_invoice_under_a_version_7_tariff", "tariff_mismatch", json!(["invoice"])),
|
||
("version_6_an_invoice_listing_a_group_receipt", "invoice_invalid", json!(["invoice"])),
|
||
("version_7_transmission_prices_out_of_order", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_price_of_one_ceiling_twice", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_transmission_price_on_a_relay", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_transmission_unit_past_pass_time", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_ceiling_of_zero", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_ceiling_past_2_53", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_price_past_2_53", "tariff_invalid", json!(["tariff"])),
|
||
("version_6_a_publisher_in_scope", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_schedule_contradicted", "schedule_contradicted", json!(["source", 0, "claims", 1])),
|
||
("version_7_a_schedule_not_declared", "bearer_not_declared", json!(["source", 0, "claims", 0])),
|
||
("version_7_a_schedule_on_a_relay", "schedule_on_bidirectional", json!(["source", 0, "claims", 1])),
|
||
("version_7_declarations_contradicted", "bearer_contradicted", json!(["source", 0, "claims", 0])),
|
||
("version_7_a_schedule_of_another_group", "schedule_subject_mismatch", json!(["source", 0, "claims", 1])),
|
||
("version_7_a_chain_not_held", "parent_not_held", json!(["source", 0, "claims", 0])),
|
||
("version_7_a_chain_broken", "chain_break", json!(["source", 0, "claims", 0])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
assert_eq!(get("version_7_a_group_receipt_altered")["at"], json!(["source", 0, "group"]));
|
||
assert_eq!(get("version_7_an_object_of_another_group")["at"], json!(["source", 0, "objects", 0]));
|
||
}
|
||
|
||
/// §4.10 to §4.12: the invoice after the month, every amount worked by hand: a broadcast whose
|
||
/// schedule a later one extends, billed in the last schedule's passes alone; a node's own instance,
|
||
/// an object it scheduled and did not send billing nothing; an aggregated count in its own unit;
|
||
/// and each object an agent's claims name.
|
||
#[test]
|
||
fn version_7_bills_a_node_s_instance_an_aggregated_count_and_a_consumed_object() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
|
||
let w5 = get("version_7_week_5");
|
||
assert_eq!(w5["outcome"], "accept");
|
||
let keys = &doc["keys"];
|
||
let lines = |kind: &str, source: &Value| -> Vec<(String, u64, u64, u64, u64)> {
|
||
let of = |l: &&Value| l["source_kind"] == kind && l["source"] == *source;
|
||
let n = |l: &Value, k: &str| l[k].as_u64().unwrap_or(0);
|
||
w5["lines"].as_array().unwrap().iter().filter(of).map(|l| (l["unit"].as_str().unwrap().to_string(), n(l, "max_bps"), n(l, "quantity"), n(l, "price"), n(l, "amount"))).collect()
|
||
};
|
||
let row = |u: &str, c: u64, q: u64, p: u64, a: u64| (u.to_string(), c, q, p, a);
|
||
// Five passes of 4,736 octets in 4 s, at 9,472 bit/s: the schedule of two it extends adds none.
|
||
assert_eq!(lines("group", &keys["broadcaster"]), [row("bytes_sent", 1_000_000, 23_680, 2_500_000, 59), row("pass_us", 100_000, 20_000_000, 150, 3_000)]);
|
||
// Objects 0 and 2 of the three the node planned: 563,200 and 2,304 bit/s.
|
||
assert_eq!(
|
||
lines("group", &keys["cache_node"]),
|
||
[row("bytes_sent", 50_000, 1_152, 4_000_000, 4), row("bytes_sent", 2_000_000, 14_080, 6_000_000, 84), row("pass_us", 2_000_000, 200_000 + 4_000_000, 300, 1_260)]
|
||
);
|
||
// Fifty reports in each of two receipts, on one line of the collector's.
|
||
assert_eq!(lines("aggregated", &keys["collector"]), [row("aggregated_impressions", 0, 100, 800, 80_000)]);
|
||
// Three objects and two.
|
||
assert_eq!(lines("consumption", &keys["agent"]), [row("consumed_objects", 0, 5, 5_000, 25_000)]);
|
||
assert_eq!(w5["amount"], 59 + 3_000 + 4 + 84 + 1_260 + 80_000 + 25_000);
|
||
let cites = |kind: &str, of: u64| -> usize {
|
||
let l = w5["lines"].as_array().unwrap().iter().find(|l| l["source_kind"] == kind).unwrap();
|
||
l["cites"].as_array().unwrap().iter().filter(|c| c[0] == of).count()
|
||
};
|
||
assert_eq!((cites("aggregated", 24), cites("consumption", 25)), (2, 2), "each receipt and each claim");
|
||
let node = w5["lines"].as_array().unwrap().iter().find(|l| l["source"] == keys["cache_node"]).unwrap();
|
||
let by = |kind: u64| node["cites"].as_array().unwrap().iter().filter(|c| c[0] == kind).count();
|
||
assert_eq!((by(20), by(22), by(23), by(4)), (1, 1, 1, 1), "its group, its schedule, its claim and its settlement declaration");
|
||
let e = &w5["energy"].as_array().unwrap()[0];
|
||
assert_eq!((e["class"].as_str(), e["figures"].as_u64(), e["joules"].as_u64()), (Some("OnChipCounter"), Some(2), Some(2_700_000)), "each instance's figure, the publisher's and the node's");
|
||
let report = w5["reports"]["transmission"].as_array().unwrap();
|
||
assert_eq!((&report[0]["cache"], &report[1]["cache"]), (&json!(false), &json!(true)));
|
||
let leaves: Vec<u64> = report[1]["entries"].as_array().unwrap().iter().map(|x| x["leaf_index"].as_u64().unwrap()).collect();
|
||
assert_eq!(leaves, [0, 2], "the object not sent is billed nowhere");
|
||
|
||
for (n, reason, at) in [
|
||
("version_7_aggregated_billed_as_presented", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_aggregated_billed_as_billable", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_aggregated_over_its_count", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_cache_billed_an_object_not_sent", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_consumption_over_its_objects", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_7_aggregated_outside_scope", "outside_scope", json!(["source", 0])),
|
||
("version_7_consumption_outside_scope", "outside_scope", json!(["source", 0])),
|
||
("version_7_aggregated_batches_overlap", "record_overlap", json!(["source", 1])),
|
||
("version_7_aggregated_billed_again", "outside_period", json!(["source", 0])),
|
||
("version_7_aggregated_billed_two_invoices_on", "outside_period", json!(["source", 0])),
|
||
("version_7_aggregated_overlap_in_the_next_invoice", "outside_period", json!(["source", 0])),
|
||
("version_7_aggregated_before_its_period", "outside_period", json!(["source", 0])),
|
||
("version_7_aggregated_past_its_period", "outside_period", json!(["source", 0])),
|
||
("version_7_aggregated_past_any_instant", "outside_period", json!(["source", 0])),
|
||
("version_7_aggregated_statement_magic", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_version", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_version_at_version_1_length", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_of_version_1_stating_a_count", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_role", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_short", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_trailing_byte", "statement_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_statement_of_the_helper", "wrong_signer", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_batch_of_no_time", "batch_invalid", json!(["source", 0, "leader"])),
|
||
("version_7_aggregated_task_of_no_precision", "task_invalid", json!(["source", 0, "task"])),
|
||
("version_7_aggregated_task_cut_short", "task_invalid", json!(["source", 0, "task"])),
|
||
("version_7_one_schedule_in_two_cache_sources", "record_duplicate", json!(["source", 1])),
|
||
("version_7_one_instance_in_two_cache_sources", "record_duplicate", json!(["source", 1])),
|
||
("version_7_one_instance_in_two_group_sources", "record_duplicate", json!(["source", 1])),
|
||
("version_7_a_node_billing_the_publishers_instance", "bearer_contradicted", json!(["source", 1])),
|
||
("version_7_a_publisher_after_a_node_on_its_instance", "bearer_contradicted", json!(["source", 1])),
|
||
("version_7_two_nodes_on_one_instance", "bearer_contradicted", json!(["source", 1])),
|
||
("version_7_a_node_s_sent_entry_past_the_period", "outside_period", json!(["source", 0])),
|
||
("version_7_consumption_billed_again", "outside_period", json!(["source", 0])),
|
||
("version_7_a_schedule_extended_after_it_was_billed", "outside_period", json!(["source", 0])),
|
||
("version_7_consumption_past_the_period", "outside_period", json!(["source", 0])),
|
||
("version_7_cache_declaration_outside_the_period", "outside_period", json!(["source", 0])),
|
||
("version_7_cache_without_its_declaration", "record_missing", json!(["source", 0, "claims", 0])),
|
||
("version_7_cache_declaration_of_another_claim", "context_mismatch", json!(["source", 0, "declarations", 0])),
|
||
("version_7_cache_declaration_by_another_key", "context_mismatch", json!(["source", 0, "declarations", 0])),
|
||
("version_7_cache_claims_contradicted", "bearer_contradicted", json!(["source", 0, "claims", 0])),
|
||
("version_7_cache_schedule_contradicted", "schedule_contradicted", json!(["source", 0, "claims", 1])),
|
||
("version_7_cache_schedule_not_declared", "bearer_not_declared", json!(["source", 0, "claims", 0])),
|
||
("version_7_cache_claim_by_the_publisher", "cache_transmission_by_publisher", json!(["source", 0, "claims", 0])),
|
||
("version_7_cache_schedule_by_another_key", "context_mismatch", json!(["source", 0, "claims", 1])),
|
||
("version_7_cache_given_a_declaration_of_the_publisher", "context_mismatch", json!(["source", 0, "claims", 0])),
|
||
("version_7_aggregated_count_wrong", "count_mismatch", json!(["source", 0, "result"])),
|
||
("version_7_aggregated_signature", "bad_signature", json!(["source", 0, "receipt"])),
|
||
("version_7_aggregated_truncated", "truncated", json!(["source", 0, "receipt"])),
|
||
("version_7_aggregated_trailing_byte", "malformed", json!(["source", 0, "receipt"])),
|
||
("version_7_aggregated_version", "unknown_version", json!(["source", 0, "receipt"])),
|
||
("version_7_aggregated_magic", "malformed", json!(["source", 0, "receipt"])),
|
||
("version_7_consumption_without_its_parent", "parent_not_held", json!(["source", 0, "claims", 0])),
|
||
("version_7_consumption_after_another_claim", "chain_break", json!(["source", 0, "claims", 0])),
|
||
("version_7_consumption_of_two_agents", "context_mismatch", json!(["source", 0, "claims", 1])),
|
||
("version_7_consumption_given_another_class", "context_mismatch", json!(["source", 0, "claims", 0])),
|
||
("version_7_consumption_of_no_claim", "malformed", json!(["source", 0])),
|
||
("version_6_an_aggregated_source", "malformed", json!(["source", 0])),
|
||
("version_6_a_consumption_source", "malformed", json!(["source", 0])),
|
||
("version_6_a_cache_source", "malformed", json!(["source", 0])),
|
||
("version_7_an_aggregated_price_past_2_53", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_consumption_price_past_2_53", "tariff_invalid", json!(["tariff"])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
// Two sources of one group, each of an instance of its own: its receipt is cited by both, and
|
||
// listed once.
|
||
let two = get("version_7_one_group_on_two_instances");
|
||
assert_eq!(two["outcome"], "accept");
|
||
assert_eq!(two["reports"]["transmission"].as_array().unwrap().len(), 2);
|
||
// Two groups on one broadcast service: an instance of a group is that group's.
|
||
let service = get("version_7_two_groups_on_one_service");
|
||
assert_eq!(service["outcome"], "accept");
|
||
assert_eq!(service["reports"]["transmission"].as_array().unwrap().len(), 2);
|
||
// A batch fills the period that holds it, from its first instant to the end of its last.
|
||
let hour = get("version_7_aggregated_its_batch_is_the_period");
|
||
assert_eq!((hour["outcome"].as_str(), hour["amount"].as_u64()), (Some("accept"), Some(50 * 800)));
|
||
// A leader's statement of version 1 states no rejected count, and its count bills the same.
|
||
let version_1 = get("version_7_aggregated_of_statements_of_version_1");
|
||
assert_eq!((version_1["outcome"].as_str(), version_1["amount"].as_u64()), (Some("accept"), Some(50 * 800)));
|
||
// A plan's entry past the period that the claim does not say was sent is no pass of what the
|
||
// node bills, so it bills what the fifth week's node bills.
|
||
let unsent = get("version_7_a_node_s_unsent_entry_past_the_period");
|
||
assert_eq!((unsent["outcome"].as_str(), unsent["amount"].as_u64()), (Some("accept"), Some(4 + 84 + 1_260)));
|
||
}
|
||
|
||
/// §4.13: a day of a holder's storage, a window and a clip, billed by byte-days from records each
|
||
/// equal to what its snapshots state, rounded down once per record and once per line; the next
|
||
/// day's, chained; a price of 0; a record under a byte-day; and version 7's sources beside. Each
|
||
/// planted fault is refused where §4.13 and §7.2 say: byte-seconds over what the snapshots
|
||
/// support, an interval overlapping the set's previous record in one invoice or the one before
|
||
/// it, a snapshot after the interval, a unit mismatch, and a signature over other bytes.
|
||
#[test]
|
||
fn version_8_bills_storage_held_over_time_by_its_snapshots() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
|
||
let holder = &doc["keys"]["holder"];
|
||
let line = |o: &Value| -> (String, u64, u64, u64, usize) {
|
||
let l = o["lines"].as_array().unwrap().iter().find(|l| l["source_kind"] == "storage").unwrap();
|
||
(l["unit"].as_str().unwrap().into(), l["quantity"].as_u64().unwrap(), l["price"].as_u64().unwrap(), l["amount"].as_u64().unwrap(), l["cites"].as_array().unwrap().len())
|
||
};
|
||
let held = |o: &Value| -> Vec<u64> { o["reports"]["storage"].as_array().unwrap().iter().map(|r| r["byte_seconds"].as_u64().unwrap()).collect() };
|
||
// Day 1: the window's 263,818,360,797.94 byte-seconds rounded down (a snapshot a microsecond
|
||
// after noon), the clip's 3,047,926 bytes for eleven hours; 4,450,419 byte-days at 2,000,000
|
||
// per 10^9. Five snapshots and two records cited.
|
||
let d1 = get("version_8_storage_day_1");
|
||
assert_eq!(d1["outcome"], "accept");
|
||
assert_eq!(held(&d1), [263_818_360_797, 3_047_926 * 39_600]);
|
||
assert_eq!(line(&d1), ("byte_days".into(), 4_450_419, 2_000_000, 8_900, 7));
|
||
assert_eq!(d1["lines"][0]["source"], *holder);
|
||
assert_eq!((d1["records"].as_u64(), d1["amount"].as_u64()), (Some(7), Some(8_900)));
|
||
assert!(d1["energy"].as_array().unwrap().is_empty(), "a snapshot states no energy");
|
||
let report = &d1["reports"]["storage"][0];
|
||
assert_eq!((report["snapshots"].as_u64(), report["end"].as_u64().unwrap() - report["start"].as_u64().unwrap()), (Some(4), 86_400_000_000));
|
||
// Day 2: the window from its snapshot at the day's start, the clip until its deletion at 05:00.
|
||
let d2 = get("version_8_storage_day_2");
|
||
assert_eq!(held(&d2), [481_782_182_400, 3_047_926 * 18_000]);
|
||
assert_eq!(line(&d2), ("byte_days".into(), 6_211_167, 2_000_000, 12_422, 5));
|
||
assert_eq!(line(&get("version_8_storage_at_a_price_of_0")), ("byte_days".into(), 4_450_419, 0, 0, 7), "a price of 0 bills a line of amount 0");
|
||
let moment = get("version_8_storage_under_a_byte_day");
|
||
assert_eq!((moment["outcome"].as_str(), moment["lines"].as_array().unwrap().len(), moment["records"].as_u64()), (Some("accept"), 0, Some(2)), "1,000 byte-seconds: no line, its records listed");
|
||
let beside = get("version_8_bills_what_version_7_bills");
|
||
let kinds: Vec<&str> = beside["lines"].as_array().unwrap().iter().map(|l| l["source_kind"].as_str().unwrap()).collect();
|
||
assert_eq!(kinds, ["aggregated", "consumption", "storage"]);
|
||
assert_eq!(line(&beside).1, 263_818_360_797 / 86_400);
|
||
assert!(beside["reports"]["transmission"].as_array().unwrap().is_empty(), "version 8 states version 7's report");
|
||
|
||
for (n, reason, at) in [
|
||
// Byte-seconds over what the snapshots support, and a line over its records.
|
||
("version_8_storage_over_its_snapshots", "storage_mismatch", json!(["source", 0, "record"])),
|
||
("version_8_storage_line_over_its_records", "quantity_not_recorded", json!(["invoice"])),
|
||
// An interval that overlaps the set's previous record: in one invoice, and in the one before.
|
||
("version_8_storage_overlapping_in_one_invoice", "record_overlap", json!(["source", 1])),
|
||
("version_8_storage_overlapping_the_previous_invoice", "outside_period", json!(["source", 0])),
|
||
("version_8_storage_billed_again", "outside_period", json!(["source", 0])),
|
||
("version_8_storage_outside_the_period", "outside_period", json!(["source", 0])),
|
||
// A snapshot from after the interval, and the interval not covered from its start or in order.
|
||
("version_8_storage_snapshot_after_the_interval", "outside_interval", json!(["source", 0, "snapshots", 4])),
|
||
("version_8_storage_first_snapshot_after_the_start", "outside_interval", json!(["source", 0, "snapshots", 0])),
|
||
("version_8_storage_snapshots_out_of_order", "outside_interval", json!(["source", 0, "snapshots", 2])),
|
||
// A unit mismatch: bytes stated as byte-seconds, bytes billed as byte-days, another unit.
|
||
("version_8_storage_bytes_as_byte_seconds", "storage_mismatch", json!(["source", 0, "record"])),
|
||
("version_8_storage_bytes_billed_as_byte_days", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_8_storage_line_in_another_unit", "invoice_invalid", json!(["invoice"])),
|
||
// A signature over other bytes.
|
||
("version_8_storage_snapshot_signed_over_other_bytes", "bad_signature", json!(["source", 0, "snapshots", 2])),
|
||
("version_8_storage_record_signed_over_other_bytes", "bad_signature", json!(["source", 0, "record"])),
|
||
("version_8_storage_snapshot_signed_as_a_record", "bad_signature", json!(["source", 0, "snapshots", 1])),
|
||
// The snapshots given are the record's.
|
||
("version_8_storage_a_snapshot_left_out", "snapshot_mismatch", json!(["source", 0, "snapshots"])),
|
||
("version_8_storage_another_snapshot_in_its_place", "snapshot_mismatch", json!(["source", 0, "snapshots", 1])),
|
||
("version_8_storage_snapshot_of_another_set", "snapshot_mismatch", json!(["source", 0, "snapshots", 1])),
|
||
("version_8_storage_snapshot_of_another_holder", "snapshot_mismatch", json!(["source", 0, "snapshots", 1])),
|
||
// Their rules and their reading.
|
||
("version_8_storage_record_of_no_time", "storage_invalid", json!(["source", 0, "record"])),
|
||
("version_8_storage_snapshot_stating_bytes_of_no_objects", "storage_invalid", json!(["source", 0, "snapshots", 0])),
|
||
("version_8_storage_record_truncated", "truncated", json!(["source", 0, "record"])),
|
||
("version_8_storage_record_version", "unknown_version", json!(["source", 0, "record"])),
|
||
("version_8_storage_not_a_record", "malformed", json!(["source", 0, "record"])),
|
||
("version_8_storage_snapshot_trailing_byte", "malformed", json!(["source", 0, "snapshots", 1])),
|
||
("version_8_storage_without_its_snapshots", "malformed", json!(["source", 0])),
|
||
("version_8_storage_given_twice", "record_duplicate", json!(["source", 1])),
|
||
("version_8_storage_outside_scope", "outside_scope", json!(["source", 0])),
|
||
("version_8_a_storage_line_not_citing_its_snapshots", "citation_mismatch", json!(["invoice"])),
|
||
("version_8_a_snapshot_left_out_of_the_records", "invoice_mismatch", json!(["invoice"])),
|
||
("version_8_a_storage_price_past_2_53", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_holder_in_scope", "tariff_invalid", json!(["tariff"])),
|
||
("version_7_a_storage_source", "malformed", json!(["source", 0])),
|
||
("version_7_an_invoice_listing_a_storage_record", "invoice_invalid", json!(["invoice"])),
|
||
("version_7_invoice_under_a_version_8_tariff", "tariff_mismatch", json!(["invoice"])),
|
||
] {
|
||
let o = get(n);
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
}
|
||
|
||
/// §4.14: one chain across three tariffs of version 9, each following the one before it, the
|
||
/// package the first day lists billed its storage under each tariff after, beside signer 2's
|
||
/// stream under the tariff that adds its key; a tariff that follows none begins a chain of its
|
||
/// own; and each rule of a following tariff refuses its control at its place.
|
||
#[test]
|
||
fn version_9_continues_a_chain_across_a_key_made_and_retired() {
|
||
use serde_json::json;
|
||
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}")).clone();
|
||
let units = |o: &serde_json::Value| -> Vec<(String, String)> { o["lines"].as_array().unwrap().iter().map(|l| (l["source_kind"].as_str().unwrap().to_string(), l["unit"].as_str().unwrap().to_string())).collect() };
|
||
let first = get("version_9_first_day");
|
||
let made = get("version_9_a_key_made");
|
||
let retired = get("version_9_a_key_retired");
|
||
let alone = get("version_9_a_tariff_following_none");
|
||
assert_eq!(first["outcome"]["follows"], serde_json::Value::Null, "the first follows none");
|
||
assert_eq!(units(&first["outcome"]), [("ads".into(), "presented_impressions".into())], "a package first listed bills no storage");
|
||
assert_eq!(made["previous"], first["invoice"], "the next tariff's first invoice names the first day's");
|
||
assert_eq!(made["outcome"]["follows"], first["outcome"]["tariff"]);
|
||
assert!(units(&made["outcome"]).contains(&("package".into(), "byte_days".into())), "the package held across the tariffs");
|
||
assert_eq!(retired["outcome"]["follows"], made["outcome"]["tariff"]);
|
||
assert_eq!(units(&retired["outcome"]), [("package".into(), "byte_days".into())], "held through a third tariff's period");
|
||
assert!(!units(&alone["outcome"]).contains(&("package".into(), "byte_days".into())), "a chain of its own holds nothing");
|
||
for (n, reason, at) in [
|
||
("version_9_a_chain_restarted", "chain_restarted", json!(["previous"])),
|
||
("version_9_billed_again_across_tariffs", "record_rebilled", json!(["source", 2])),
|
||
("version_9_after_another_tariffs_invoice", "tariff_mismatch", json!(["previous"])),
|
||
("version_9_overlapping_the_chain_it_follows", "invoice_overlap", json!(["previous"])),
|
||
("version_9_a_retired_keys_records_billed", "quantity_not_recorded", json!(["invoice"])),
|
||
("version_8_invoice_under_a_version_9_tariff", "tariff_mismatch", json!(["invoice"])),
|
||
("invoice_version", "unknown_version", json!(["invoice"])),
|
||
] {
|
||
let o = get(n)["outcome"].clone();
|
||
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
|
||
}
|
||
assert!(corpus::month(&committed()).is_empty(), "{:?}", corpus::month(&committed()));
|
||
}
|
||
|
||
/// A changed copy of a storage day is refused, whichever part changed: a byte of its invoice, a
|
||
/// source left out, a snapshot left out, a snapshot's or a record's byte, or the invoice before it.
|
||
#[test]
|
||
fn version_8_a_changed_copy_of_a_storage_day_is_refused() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let flip = |h: &str, at: usize| -> String {
|
||
let mut b: Vec<u8> = (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap()).collect();
|
||
let n = b.len();
|
||
b[at % n] ^= 1;
|
||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||
};
|
||
let outcome = |d: &Value| wai::receipt_ledger::outcome_json(&wai::receipt_ledger::verify(d));
|
||
for name in corpus::STORAGE_VECTORS {
|
||
let v = vectors.iter().find(|v| v["name"] == name).unwrap();
|
||
let whole = corpus::document(&doc, v).unwrap();
|
||
assert_eq!(outcome(&whole)["outcome"], "accept", "{name}");
|
||
let refused = |d: Value, what: &str| assert_eq!(outcome(&d)["outcome"], "refuse", "{name}: {what}");
|
||
let invoice = whole["invoice"].as_str().unwrap();
|
||
for at in [5, 72, 137, 150, invoice.len() / 2 - 70, invoice.len() / 2 - 1] {
|
||
let mut d = whole.clone();
|
||
d["invoice"] = json!(flip(invoice, at));
|
||
refused(d, "a byte of its invoice");
|
||
}
|
||
for k in 0..whole["sources"].as_array().unwrap().len() {
|
||
let mut d = whole.clone();
|
||
d["sources"].as_array_mut().unwrap().remove(k);
|
||
refused(d, "a source left out");
|
||
let snapshots = whole["sources"][k]["snapshots"].as_array().unwrap();
|
||
let mut d = whole.clone();
|
||
d["sources"][k]["snapshots"] = json!(snapshots[..snapshots.len() - 1]);
|
||
refused(d, "its last snapshot left out");
|
||
for at in [40, 72, 80, 100] {
|
||
let mut d = whole.clone();
|
||
d["sources"][k]["snapshots"][0] = json!(flip(snapshots[0].as_str().unwrap(), at));
|
||
refused(d, "a byte of its first snapshot");
|
||
let mut d = whole.clone();
|
||
d["sources"][k]["record"] = json!(flip(whole["sources"][k]["record"].as_str().unwrap(), at));
|
||
refused(d, "a byte of its record");
|
||
}
|
||
}
|
||
if !whole["previous"].is_null() {
|
||
let mut d = whole.clone();
|
||
d["previous"] = Value::Null;
|
||
refused(d, "the invoice before it not given");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// §11: the month's invoices and the two days of storage, made again from their tariff, the
|
||
/// invoice before each and their sources, are the corpus's byte for byte, and every vector of
|
||
/// versions 7 and 8 gets its outcome.
|
||
#[test]
|
||
fn version_7_the_month_recomputes_byte_for_byte() {
|
||
let fails = corpus::month(&committed());
|
||
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
||
}
|
||
|
||
/// A changed copy of the month is refused, whichever part of it changed: any byte of an invoice's
|
||
/// fixed fields or signature, a source left out, a source's claim left out, the invoice before it
|
||
/// not given, or another tariff's bytes.
|
||
#[test]
|
||
fn version_7_a_changed_copy_of_the_month_is_refused() {
|
||
use serde_json::{json, Value};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let vectors = doc["vectors"].as_array().unwrap();
|
||
let flip = |h: &str, at: usize| -> String {
|
||
let mut b: Vec<u8> = (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap()).collect();
|
||
b[at] ^= 1;
|
||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||
};
|
||
let outcome = |d: &Value| wai::receipt_ledger::outcome_json(&wai::receipt_ledger::verify(d));
|
||
for name in corpus::MONTH_VECTORS {
|
||
let v = vectors.iter().find(|v| v["name"] == name).unwrap();
|
||
let whole = corpus::document(&doc, v).unwrap();
|
||
assert_eq!(outcome(&whole)["outcome"], "accept", "{name}");
|
||
let refused = |d: Value, what: &str| assert_eq!(outcome(&d)["outcome"], "refuse", "{name}: {what}");
|
||
let invoice = whole["invoice"].as_str().unwrap();
|
||
for at in [5, 40, 72, 100, 137, 150, invoice.len() / 2 - 40, invoice.len() / 2 - 1] {
|
||
let mut d = whole.clone();
|
||
d["invoice"] = json!(flip(invoice, at));
|
||
refused(d, "a byte of its invoice");
|
||
}
|
||
let mut d = whole.clone();
|
||
d["tariff"] = json!(flip(whole["tariff"].as_str().unwrap(), 12));
|
||
refused(d, "a byte of its tariff");
|
||
for k in 0..whole["sources"].as_array().unwrap().len() {
|
||
let mut d = whole.clone();
|
||
d["sources"].as_array_mut().unwrap().remove(k);
|
||
refused(d, "a source left out");
|
||
if let Some(claims) = whole["sources"][k]["claims"].as_array() {
|
||
let mut d = whole.clone();
|
||
d["sources"][k]["claims"] = json!(claims[..claims.len() - 1]);
|
||
refused(d, "a source's last claim left out");
|
||
}
|
||
}
|
||
if !whole["previous"].is_null() {
|
||
let mut d = whole.clone();
|
||
d["previous"] = Value::Null;
|
||
refused(d, "the invoice before it not given");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// §4.11: the ledger reads an aggregate receipt as data, so the corpus holds what it bills to the
|
||
/// text that verifies it: each receipt is accepted by impression-aggregates §8.1 against its
|
||
/// catalogue, and the line's quantity is the sum of the flight's table's presentations.
|
||
#[cfg(feature = "impression_aggregate")]
|
||
#[test]
|
||
fn each_aggregate_receipt_billed_stands_by_its_own_text() {
|
||
use serde_json::Value;
|
||
let unhex = |v: &Value| -> Vec<u8> {
|
||
let s = v.as_str().unwrap();
|
||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||
};
|
||
let doc: Value = serde_json::from_str(&committed()).unwrap();
|
||
let flights: Value = serde_json::from_str(&read("live-conformance/impressions/vectors.json")).unwrap();
|
||
let catalogue = unhex(&flights["fixtures"]["catalogues"]["flight"]);
|
||
let mut count = 0;
|
||
for name in ["aggregated_r0", "aggregated_r12"] {
|
||
let v = wai::impression_aggregate::verify_receipt(&unhex(&doc["sources"][name]["receipt"]), None, &catalogue).unwrap_or_else(|e| panic!("{name}: {e:?}"));
|
||
assert_eq!(v.receipt.collector.to_vec(), unhex(&doc["keys"]["collector"]), "the collector the tariff names");
|
||
count += v.receipt.report_count;
|
||
}
|
||
let w5 = doc["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "version_7_week_5").unwrap();
|
||
let line = w5["outcome"]["lines"].as_array().unwrap().iter().find(|l| l["source_kind"] == "aggregated").unwrap();
|
||
assert_eq!(line["quantity"].as_u64(), Some(count));
|
||
// What the ledger does not read: a receipt signed again with a wrong share reads here, and
|
||
// its own text refuses it, which is why the buyer is given the receipt.
|
||
let wrong = flights["receipts"].as_array().unwrap().iter().find(|r| r["name"] == "count_mismatch").unwrap();
|
||
assert!(wai::impression_aggregate::verify_receipt(&unhex(&wrong["receipt"]), None, &catalogue).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn the_extension_lists_the_reasons() {
|
||
let text = read("extensions/receipt-ledger.md");
|
||
let at = text.find("\n## 10. Reasons").expect("a §10");
|
||
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::ALL.iter().map(|r| r.code().to_string()).collect();
|
||
assert_eq!(listed, want, "§10's table and Reason::ALL differ");
|
||
}
|
||
|
||
/// §5.2 is the one registry of record kinds: its rows are the kinds 1 to the last, each once and
|
||
/// in order; the sentence after them names the first kind left unassigned; and the reference's
|
||
/// constants are distinct, each a row, the last of them the last row. Several texts assign kinds
|
||
/// at once, each the next free one, so a row added without its constant, or two texts taking one
|
||
/// number, fails here.
|
||
#[test]
|
||
fn section_5_2_registers_every_kind_once_and_in_order() {
|
||
use wai::receipt_ledger::record_kind as k;
|
||
let text = read("extensions/receipt-ledger.md");
|
||
let at = text.find("\n### 5.2 What a source contributes").expect("a §5.2");
|
||
let end = text[at + 1..].find("\n### ").map_or(text.len(), |e| at + 1 + e);
|
||
let section = &text[at..end];
|
||
let rows: Vec<u32> = section
|
||
.lines()
|
||
.filter_map(|l| l.strip_prefix("| "))
|
||
.filter_map(|l| l.split(" |").next())
|
||
.filter_map(|n| n.trim().parse().ok())
|
||
.collect();
|
||
let want: Vec<u32> = (1..=rows.len() as u32).collect();
|
||
assert_eq!(rows, want, "§5.2's rows are not the kinds 1 to the last, each once and in order");
|
||
let last = *rows.last().expect("a row");
|
||
assert!(section.contains(&format!("Kinds {} to 127 are unassigned.", last + 1)), "§5.2 names another first unassigned kind than {}", last + 1);
|
||
let constants = [
|
||
k::SESSION, k::HOP, k::STATEMENT, k::ACK, k::PACKAGE, k::AD_RECEIPT, k::CLAIM, k::POD_INPUT, k::STITCHED_CLAIM,
|
||
k::RUN, k::INVOICE, k::CUE, k::ROOM_POLICY, k::OER_RECEIPT, k::VIDEO_RECEIPT, k::ROUTE_RECORD, k::THIRD_PARTY,
|
||
k::UPSCALE_RECORD, k::RENDITION_RECORD, k::GROUP_RECEIPT, k::TRANSMISSION_CLAIM, k::SCHEDULE_CLAIM,
|
||
k::CACHE_CLAIM, k::AGGREGATE_RECEIPT, k::CONSUMPTION_CLAIM, k::STORAGE_RECORD, k::SNAPSHOT, k::SINK_SESSION,
|
||
k::SINK_SEGMENT, k::SPLIT_CLAIM, k::FACTOR_TABLE, k::CERT_REFUSAL, k::CERT_PUBLICATION, k::AUDIT_ENTRY,
|
||
k::AUDIT_CHECKPOINT, k::DATA_EXPORT, k::DATA_DELETION, k::PRICE_DOCUMENT, k::HLS_WINDOW, k::HLS_CLIP,
|
||
k::OPPORTUNITY_REPORT, k::STATEMENT_CHECK, k::ORIGIN_CLAIM, k::MARKING,
|
||
];
|
||
let distinct: BTreeSet<u8> = constants.iter().copied().collect();
|
||
assert_eq!(distinct.len(), constants.len(), "two constants of record_kind name one kind");
|
||
let have: Vec<u32> = distinct.iter().map(|&c| u32::from(c)).collect();
|
||
assert_eq!(have, want, "record_kind's constants and §5.2's rows differ");
|
||
assert!(u32::from(k::PRIVATE) == 128 && last < 128, "the private kinds begin at 128, above every row");
|
||
}
|