Some checks failed
joule-moq-agent AI participants / joule-moq-agent · does this change touch it (push) Successful in 1m20s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 1m12s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 1m26s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m7s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 3m30s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m55s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 3m44s
wai-ledger service / does this change touch it (push) Failing after 59s
wai-ledger service / service · linux (push) Has been skipped
wai-ledger service / service · macos (push) Has been skipped
wai-quantum studio and desktop / does this change touch it (push) Failing after 5s
wai-sink-android native sink library / does this change touch it (push) Failing after 6s
wai-pack / wai-pack · does this change touch it (push) Failing after 50s
wai-quantum studio and desktop / studio · browser (push) Has been skipped
wai-quantum studio and desktop / desktop app · macos (push) Has been skipped
wai-quantum studio and desktop / desktop app · linux (push) Has been skipped
wai-quantum studio and desktop / desktop app · windows (push) Has been skipped
wai-sink-swift native sink package / does this change touch it (push) Failing after 5s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been skipped
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been skipped
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been skipped
WAI operation catalogue / does this change touch it (push) Successful in 2m40s
wai-pack / wai-pack · linux (push) Has been skipped
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m30s
wai-pack / wai-pack · linux-arm64 (push) Has been skipped
wai-pack / wai-pack · macos (push) Has been skipped
wai-pack / wai-pack · independent verifier (Python) (push) Has been skipped
wai-pack / wai-pack · the floor through a media prober (push) Has been skipped
wai-fleet service / does this change touch it (push) Successful in 2m27s
wai-pack / wai-pack · dependency licences (push) Has been skipped
WAI Studio / does this change touch it (push) Failing after 8s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been skipped
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been skipped
WAI Studio / Studio in WebKit · linux (push) Has been skipped
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 4m6s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 8s
WAI examples / WAI examples · does this change touch them (push) Successful in 11s
wai web player plugins / the browser run's room compiles · linux (push) Successful in 58s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 32s
wai tv web application / the tv application, built to its baseline and checked · linux (push) Successful in 5m12s
wai web player plugins / the plugins' core under Node · linux (push) Successful in 6m56s
wai-audit collector and player integration / collector · linux (push) Successful in 3m3s
wai-fleet service / service · linux (push) Successful in 2m22s
wai-quantum for JavaScript / javascript · linux (push) Successful in 6m54s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 16m49s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m25s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 8m44s
joule-code conformance gate / joule-moq-ingest, every target (push) Failing after 6m56s
joule-code conformance gate / joule-moq-relay, every target (push) Failing after 7m31s
WAI examples / WAI examples · portable · linux (push) Successful in 24m14s
joule-moq-agent AI participants / joule-moq-agent, every target (push) Successful in 5m45s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 6m55s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 5m28s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 16m10s
joule-moq-room room service / joule-moq-room, every target (push) Failing after 15m19s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 51s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 52s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m39s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m47s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Failing after 16m25s
wai-fleet service / service · macos (push) Successful in 1m25s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 20m13s
wai tv web application / the tv application in Chromium, Firefox and WebKit · macos (push) Successful in 21m9s
joule-moq-http in browsers / the egress and the protected room in Chromium, Firefox and WebKit · macos (push) Successful in 50m56s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 20m9s
wai web player plugins / the three players in Chromium, Firefox and WebKit · macos (push) Failing after 34m1s
waiTransport's wt §50.2 and §50.4. An egress signs a per-hop record of role 4 (live-metering §3.12): what it received at the relay and what it answered 200 over HTTP, no subscriber time, no DVR window, its ledger by the grant each request carried; key-log authority live/egress. A room-session receipt of version 2 counts its bytes out beside the relays', so receipt-ledger tariff version 4 bills them at the room's price for bytes out: no new source kind and no new tariff version. Its session at a relay is a subscriber session under its grant, not a hop link. energy-methods places its figure in delivery_relay. The gateway signs its record (Gateway::hop_record), held to its own trace in its test. Metering corpus: room lesson_egress, vector `egress` and seven planted faults; live_metering_verify.py checks them and each of its five egress checks, dropped, is caught. Co-Authored-By: Claude Opus 5.5 <[email protected]>
171 lines
9.6 KiB
Rust
171 lines
9.6 KiB
Rust
//! `wai.live.metering` (extensions/live-metering.md): the committed corpus
|
|
//! `live-conformance/metering/vectors.json` regenerates byte for byte from its fixed seeds and
|
|
//! the reference relay's recorded record, every vector in it gets the outcome it states, every
|
|
//! reason the extension adds refuses a vector, the large audience's relay record is the size of
|
|
//! the lesson's, a relay keeping a DVR window signs a version 2 record and every other record
|
|
//! is version 1, and the extension's §9 lists exactly the reasons the reference reports.
|
|
#![cfg(feature = "live_metering")]
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::PathBuf;
|
|
use wai::live_metering::{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/metering/vectors.json")
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_corpus_regenerates_from_its_seeds() {
|
|
assert!(committed() == corpus::generate(), "live-conformance/metering/vectors.json differs from `wai_live_metering_vectors gen`");
|
|
}
|
|
|
|
#[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 refuses a vector; the accepted vectors are the ones §10
|
|
/// names; the large audience's relay record is the lesson's size, within the same bound; the
|
|
/// mixed vector states one total per class; the ceilings vector names two grants past; the
|
|
/// DVR relay's record is version 2, 48 bytes longer than the lesson relay's under a bound 48
|
|
/// bytes higher, with every DVR figure above 0, and every other accepted record is version 1.
|
|
#[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["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| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"));
|
|
for n in ["lesson", "signed_by_authority", "mixed_classes", "ceilings_past", "large_audience", "relay_recorded", "dvr_relay", "egress"] {
|
|
assert_eq!(get(n)["outcome"], "accept", "{n}");
|
|
}
|
|
let relay_record = |n: &str| {
|
|
let v = get(n);
|
|
let relay = &doc["principals"]["relay"];
|
|
let i = v["hops"].as_array().unwrap().iter().position(|h| h["record"].as_str().unwrap()[74..138] == relay.as_str().unwrap()[..]).unwrap();
|
|
let rec = v["hops"][i]["record"].as_str().unwrap().len() / 2;
|
|
v["records"].as_array().unwrap().iter().find(|r| r["size"] == rec).unwrap().clone()
|
|
};
|
|
let (lesson, large) = (relay_record("lesson"), relay_record("large_audience"));
|
|
assert_eq!(lesson["size"], large["size"], "the record does not grow with the audience");
|
|
assert_eq!(lesson["bound"], large["bound"]);
|
|
assert!(large["size"].as_u64() <= large["bound"].as_u64());
|
|
assert!(get("large_audience")["units"]["sessions"].as_u64().unwrap() > 10_000);
|
|
assert_eq!(get("large_audience")["qoe_cell"]["suppressed"], false);
|
|
assert_eq!(get("lesson")["qoe_cell"]["suppressed"], true);
|
|
assert_eq!(get("lesson")["coverage"], "whole");
|
|
assert_eq!(get("mixed_classes")["totals"].as_array().unwrap().len(), 3);
|
|
assert_eq!(get("relay_recorded")["coverage"], "partial");
|
|
let past = get("ceilings_past")["past"].as_array().unwrap();
|
|
assert_eq!(past.iter().map(|p| p["flags"].as_u64().unwrap()).collect::<BTreeSet<_>>(), BTreeSet::from([1, 2]));
|
|
let dvr = relay_record("dvr_relay");
|
|
assert_eq!(dvr["version"], 2);
|
|
assert_eq!(dvr["size"].as_u64().unwrap(), lesson["size"].as_u64().unwrap() + 48);
|
|
assert_eq!(dvr["bound"].as_u64().unwrap(), lesson["bound"].as_u64().unwrap() + 48);
|
|
for k in ["dvr_objects_out", "dvr_bytes_out", "dvr_objects_stored", "dvr_bytes_stored", "dvr_byte_s", "dvr_dropped"] {
|
|
assert!(dvr[k].as_u64().unwrap() > 0, "{k}");
|
|
}
|
|
for v in vectors.iter().filter(|v| v["outcome"] == "accept" && v["name"] != "dvr_relay") {
|
|
for r in v["records"].as_array().unwrap() {
|
|
assert_eq!(r["version"], 1, "{}", v["name"]);
|
|
assert!(r.get("dvr_byte_s").is_none());
|
|
}
|
|
}
|
|
for (n, why) in [("dvr_out_exceeds", "record_invalid"), ("dvr_stored_exceeds", "record_invalid"), ("record_unknown_version", "unknown_version"), ("dvr_record_too_large", "record_too_large")] {
|
|
assert_eq!(get(n)["reason"], why, "{n}");
|
|
}
|
|
}
|
|
|
|
/// A receipt of version 2 (§4.1, §4.4, §4.6): its bytes out are its hops' ledgers' bytes out,
|
|
/// the sum of its grants'; a grant's funds count its bytes at their price; every receipt of
|
|
/// version 1 states no bytes; and the controls of version 2 are refused at their places.
|
|
#[test]
|
|
fn a_receipt_of_version_2_states_and_prices_the_bytes_out() {
|
|
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}"));
|
|
let v2 = get("bytes_out_priced");
|
|
assert_eq!(v2["outcome"], "accept");
|
|
assert_eq!(&v2["receipt"].as_str().unwrap()[8..10], "02", "a receipt of version 2");
|
|
let bytes = v2["units"]["bytes_out"].as_u64().unwrap();
|
|
assert!(bytes > 0);
|
|
let by_grant: u64 = v2["consumption"].as_array().unwrap().iter().map(|c| c["bytes_out"].as_u64().unwrap()).sum();
|
|
assert_eq!(by_grant, bytes, "the grants' bytes out sum to the receipt's");
|
|
let lesson = get("lesson");
|
|
let mut without = v2["units"].clone();
|
|
without.as_object_mut().unwrap().remove("bytes_out");
|
|
assert_eq!(without, lesson["units"], "every other unit is version 1's");
|
|
assert!(
|
|
v2["consumption"].as_array().unwrap().iter().zip(lesson["consumption"].as_array().unwrap()).any(|(a, b)| a["funds"].as_u64() > b["funds"].as_u64()),
|
|
"a grant's funds count its bytes"
|
|
);
|
|
for v in vectors.iter().filter(|v| v["outcome"] == "accept" && v["name"] != "bytes_out_priced" && v["name"] != "egress") {
|
|
assert!(v["units"].get("bytes_out").is_none(), "{}", v["name"]);
|
|
}
|
|
for (n, why, at) in [
|
|
("bytes_out_mismatch", "unit_mismatch", serde_json::json!(["units"])),
|
|
("bytes_out_left_out_of_the_consumption", "ceiling_mismatch", serde_json::json!(["ceilings"])),
|
|
("bytes_priced_with_no_tariff", "session_invalid", serde_json::json!(["receipt"])),
|
|
("receipt_unknown_version", "unknown_version", serde_json::json!(["receipt"])),
|
|
] {
|
|
assert_eq!((&get(n)["reason"], &get(n)["at"]), (&serde_json::json!(why), &at), "{n}");
|
|
}
|
|
}
|
|
|
|
/// An egress (§3.12): a hop of role 4 whose bytes out the receipt of version 2 counts beside
|
|
/// the relay's, the lesson's session otherwise; and each control of its record refused at the
|
|
/// egress's entry.
|
|
#[test]
|
|
fn an_egress_is_a_hop_whose_bytes_the_receipt_counts() {
|
|
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}"));
|
|
let (egress, lesson) = (get("egress"), get("bytes_out_priced"));
|
|
assert_eq!(egress["outcome"], "accept");
|
|
// The egress's ledger: every body it answered, 66 932 bytes over its out entries.
|
|
let more = egress["units"]["bytes_out"].as_u64().unwrap() - lesson["units"]["bytes_out"].as_u64().unwrap();
|
|
assert_eq!(more, 4_992_000 + 47_840_000 + 14_100_000, "the egress's bytes out, beside the relay's");
|
|
let (mut a, mut b) = (egress["units"].clone(), lesson["units"].clone());
|
|
a.as_object_mut().unwrap().remove("bytes_out");
|
|
b.as_object_mut().unwrap().remove("bytes_out");
|
|
assert_eq!(a, b, "no subscriber time and no session of the egress's");
|
|
let role_4 = |v: &serde_json::Value| v["hops"].as_array().unwrap().iter().filter(|h| h["record"].as_str().unwrap()[138..140] == *"04").count();
|
|
assert_eq!(role_4(egress), 1, "one record of role 4");
|
|
for (n, why) in [
|
|
("egress_states_sessions", "record_invalid"),
|
|
("egress_states_a_dvr_window", "record_invalid"),
|
|
("egress_states_drops_out", "record_invalid"),
|
|
("egress_ledger_sessions", "ledger_mismatch"),
|
|
("egress_ledger_bytes_in", "ledger_mismatch"),
|
|
("egress_not_authorised", "hop_not_authorised"),
|
|
] {
|
|
let v = get(n);
|
|
assert_eq!(v["reason"], why, "{n}");
|
|
assert_eq!(v["at"][0], "hop", "{n}: refused at a hop's record");
|
|
// The relay's key signs a record of role 4 in one of them: the record is still of role 4.
|
|
assert_eq!(role_4(v), 1, "{n}");
|
|
}
|
|
assert_eq!((&get("egress_bytes_left_out")["reason"], &get("egress_bytes_left_out")["at"]), (&serde_json::json!("unit_mismatch"), &serde_json::json!(["units"])));
|
|
}
|
|
|
|
/// §9 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
|
|
#[test]
|
|
fn the_extension_lists_the_reasons() {
|
|
let text = read("extensions/live-metering.md");
|
|
let at = text.find("\n## 9. Reasons").expect("a §9");
|
|
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, "§9's table and Reason::ALL differ");
|
|
}
|