extensions/conformance-results.md specifies the record a conformance run signs: its members (§2) and corpus digest (§2.1), its signing input (the domain `wai:conform-result` and 0x01, then the record without `sig` in RFC 8785 form) and Ed25519 signature under SPEC §7's strict rule (§3), what a verifier checks and in what order (§4: it reads, its version, its members, its signature, its summary, its verdict) and the codes of each refusal (§5). conform-result/1 is registered as a document kind (registry export and REGISTRY.md regenerated); SPEC §7 points to it. wai-rs gains the module conform_result (feature conform_result, in quickstart): sign, check and verdict, the corpus digest, and the corpus of §7, whose outcomes are stated from the text and asserted before anything is written. `wai conform` signs with it and `wai verify` checks with it. conform-result-conformance/ holds accepted records and refused ones (a case flipped, the corpus digest changed, the summary alone changed, a bad signature, S past L, a small-order key, a summary or verdict that disagrees though signed, an unknown version, members missing, added or named twice, a fractional count, a digest out of form, bytes that are not JSON); wai_conform_result_vectors generates and verifies it, and conform_result_verify.py, written from the text and sharing no code with wai-rs, reaches every outcome and fails the corpus with each check of §4 left out. `wai conform` gains the verifier suite conform-result. `wai breaking` leaves out an explicit list of corpus files that record a measured run (the format probes' external decodes, the relay's recorded metering run), each with its reason, and says so in a note; the vectors built from them stay compared. A command `wai conform` runs is put in a process group of its own, so a timed-out command's children are stopped too. Co-Authored-By: Claude Opus 5.5 <[email protected]>
85 lines
4 KiB
Rust
85 lines
4 KiB
Rust
//! Conformance results (extensions/conformance-results.md): the committed corpus
|
|
//! (`conform-result-conformance/vectors.json`) regenerates byte for byte, wai-rs gives every
|
|
//! vector its stated outcome, every reason of §5 refuses a vector, §5 lists exactly the
|
|
//! reference's codes, and a record survives no change after it is signed.
|
|
#![cfg(feature = "conform_result")]
|
|
|
|
use serde_json::{Value, json};
|
|
use wai::conform_result::{Reason, check, corpus_document, sign, verdict};
|
|
|
|
fn wai_dir() -> std::path::PathBuf {
|
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf()
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_corpus_regenerates_byte_for_byte() {
|
|
let committed = std::fs::read_to_string(wai_dir().join("conform-result-conformance/vectors.json")).expect("the corpus is committed");
|
|
assert!(committed == corpus_document(), "conform-result-conformance/vectors.json differs from what wai_conform_result_vectors gen writes");
|
|
}
|
|
|
|
#[test]
|
|
fn every_vector_reaches_its_stated_outcome_and_every_reason_is_raised() {
|
|
let doc: Value = serde_json::from_str(&corpus_document()).unwrap();
|
|
let vs = doc["vectors"].as_array().unwrap();
|
|
let mut seen = std::collections::BTreeSet::new();
|
|
for v in vs {
|
|
let got = verdict(v["record"].as_str().unwrap().as_bytes());
|
|
assert_eq!(got, v["expected"].as_str().unwrap(), "{}", v["name"]);
|
|
if let Some(r) = got.split("reason=").nth(1) {
|
|
seen.insert(r.to_string());
|
|
}
|
|
}
|
|
for r in Reason::ALL {
|
|
assert!(seen.contains(r.code()), "no vector is refused with {}", r.code());
|
|
}
|
|
assert!(vs.iter().any(|v| v["expected"] == "accept conformant=true") && vs.iter().any(|v| v["expected"] == "accept conformant=false"));
|
|
}
|
|
|
|
#[test]
|
|
fn section_5_lists_exactly_the_reference_codes() {
|
|
let text = std::fs::read_to_string(wai_dir().join("extensions/conformance-results.md")).unwrap();
|
|
let section = text.split("\n## 5. Reasons").nth(1).expect("§5").split("\n## ").next().unwrap();
|
|
let listed: std::collections::BTreeSet<String> =
|
|
section.lines().filter_map(|l| l.strip_prefix("| `")).filter_map(|l| l.split('`').next()).map(String::from).collect();
|
|
let codes: std::collections::BTreeSet<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
|
|
assert_eq!(listed, codes);
|
|
}
|
|
|
|
/// A record signed, then each leaf of it changed in turn: every change is refused, at the
|
|
/// signature or before it.
|
|
#[test]
|
|
fn no_change_after_signing_survives() {
|
|
let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
|
|
let doc: Value = serde_json::from_str(&corpus_document()).unwrap();
|
|
let valid: Value = serde_json::from_str(doc["vectors"][0]["record"].as_str().unwrap()).unwrap();
|
|
let mut r = valid.as_object().unwrap().clone();
|
|
r.remove("sig");
|
|
let r = sign(r, &key).unwrap();
|
|
let text = serde_json::to_string(&Value::Object(r.clone())).unwrap();
|
|
assert!(check(text.as_bytes()).is_ok());
|
|
fn leaves(v: &Value, path: Vec<String>, out: &mut Vec<Vec<String>>) {
|
|
match v {
|
|
Value::Object(m) => m.iter().for_each(|(k, x)| leaves(x, [path.clone(), vec![k.clone()]].concat(), out)),
|
|
Value::Array(a) => a.iter().enumerate().for_each(|(i, x)| leaves(x, [path.clone(), vec![i.to_string()]].concat(), out)),
|
|
_ => out.push(path),
|
|
}
|
|
}
|
|
let mut paths = Vec::new();
|
|
leaves(&Value::Object(r.clone()), vec![], &mut paths);
|
|
for p in paths.iter().filter(|p| p[0] != "sig") {
|
|
let mut t = Value::Object(r.clone());
|
|
let mut at = &mut t;
|
|
for k in p {
|
|
at = if let Ok(i) = k.parse::<usize>() { &mut at[i] } else { &mut at[k.as_str()] };
|
|
}
|
|
*at = match at.clone() {
|
|
Value::String(s) => json!(format!("{s}0")),
|
|
Value::Bool(b) => json!(!b),
|
|
Value::Number(n) => json!(n.as_u64().unwrap() + 1),
|
|
Value::Null => json!("x"),
|
|
other => other,
|
|
};
|
|
let got = check(serde_json::to_string(&t).unwrap().as_bytes());
|
|
assert!(got.as_ref().is_err_and(|e| e.step <= 4), "{p:?} changed: {got:?}");
|
|
}
|
|
}
|