joule-code does not compile against main. `jcp-schema`'s Grant gained
`budget_funds` (dual ceilings) and `caveats` (§4.9), and three Grant literals in
joule-code-jcp and joule-code-conformance were never updated, so the whole
workspace fails with E0063.
This is the SAME break already repaired in sandbox-jcp, in a second workspace I
missed. The cause is structural, not incidental: `jcp-schema` is path-depended on
from at least three OTHER cargo workspaces —
sandbox/sandbox-rs → ../../../../jcp/jcp-rs/crates/jcp-schema
joule-code/… → ../../../../jcp/jcp-rs/crates/jcp-schema
jouledesk/… → ../../../../jcp/jcp-rs/crates/jcp-schema
— and none of them is in jcp's workspace, so `cargo test --workspace` inside
jcp/jcp-rs is green while three sibling workspaces are broken. Adding a required
field to a shared schema type silently breaks every consumer outside the
workspace that CI and local verification actually build.
Surveyed all four jcp-consuming workspaces rather than guessing: joule-code was
broken (2 errors), jouledesk imports the types but constructs no Grant literal,
sandbox was already repaired, jcp itself green. Only joule-code needed the fix.
Two of the three literals took the patch mechanically; the third used FIELD
SHORTHAND (`budget_uj,` with no colon) and escaped a `budget_uj:` pattern — the
same shorthand that escaped this same class of patch twice earlier. Worth
remembering: shorthand initializers are invisible to `field:`-shaped searches.
39 joule-code suites green; all four workspaces now compile.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
585 lines
20 KiB
Rust
585 lines
20 KiB
Rust
//! Joule Code conformance harness (SPEC §8).
|
|
//!
|
|
//! Loads the published wire vectors and runs the four suites:
|
|
//! C1 content addressing, C2 signing, C3 receipt, C4 cascade. Each vector is
|
|
//! reported PASS / FAIL / SKIP. SKIP marks a positive crypto vector whose
|
|
//! derived field is a placeholder pending regeneration against this library
|
|
//! (see `conformance/v1/regeneration.md`); the structural and negative checks,
|
|
//! the real Ed25519 known-answer vectors, and the entire cascade suite run for
|
|
//! real. Any FAIL makes the run non-zero (CI-ready).
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
|
use joule_code_cargo::{Anchor, CargoError, CodingOp, Edit};
|
|
use joule_code_receipt::{Energy, Provenance, Receipt, ReceiptError};
|
|
use joule_code_cascade::Cascade;
|
|
use joule_code_schema::{ContentId, Tier};
|
|
use serde::Deserialize;
|
|
|
|
/// The outcome of a run over one or more suites.
|
|
#[derive(Default, Debug)]
|
|
pub struct Report {
|
|
pub pass: u32,
|
|
pub fail: u32,
|
|
pub skip: u32,
|
|
pub lines: Vec<String>,
|
|
}
|
|
|
|
impl Report {
|
|
fn pass(&mut self, name: &str) {
|
|
self.pass += 1;
|
|
self.lines.push(format!(" PASS {name}"));
|
|
}
|
|
fn fail(&mut self, name: &str, why: &str) {
|
|
self.fail += 1;
|
|
self.lines.push(format!(" FAIL {name}: {why}"));
|
|
}
|
|
fn skip(&mut self, name: &str, why: &str) {
|
|
self.skip += 1;
|
|
self.lines.push(format!(" SKIP {name}: {why}"));
|
|
}
|
|
/// Merge another report into this one.
|
|
pub fn absorb(&mut self, other: Report) {
|
|
self.pass += other.pass;
|
|
self.fail += other.fail;
|
|
self.skip += other.skip;
|
|
self.lines.extend(other.lines);
|
|
}
|
|
}
|
|
|
|
fn is_placeholder(s: &str) -> bool {
|
|
s.contains("PLACEHOLDER")
|
|
}
|
|
|
|
// ---------- JSON mirrors of the vector files ----------
|
|
|
|
#[derive(Deserialize)]
|
|
struct OpVector {
|
|
name: String,
|
|
op: OpJson,
|
|
expected_op_id: Option<String>,
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct OpJson {
|
|
alg: u8,
|
|
prior: String,
|
|
edits: Vec<EditJson>,
|
|
author: String,
|
|
intent: String,
|
|
tier: Tier,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct EditJson {
|
|
path: String,
|
|
anchor: AnchorJson,
|
|
replacement_hex: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
enum AnchorJson {
|
|
ByteRange { start: u64, end: u64 },
|
|
Hunk { hunk_id: String },
|
|
}
|
|
|
|
impl OpJson {
|
|
fn to_op(&self) -> Result<CodingOp, String> {
|
|
let mut author = [0u8; 32];
|
|
let bytes = hex::decode(&self.author).map_err(|e| e.to_string())?;
|
|
if bytes.len() != 32 {
|
|
return Err("author key must be 32 bytes".into());
|
|
}
|
|
author.copy_from_slice(&bytes);
|
|
let edits = self
|
|
.edits
|
|
.iter()
|
|
.map(|e| {
|
|
Ok(Edit {
|
|
path: e.path.clone(),
|
|
anchor: match &e.anchor {
|
|
AnchorJson::ByteRange { start, end } => Anchor::ByteRange {
|
|
start: *start,
|
|
end: *end,
|
|
},
|
|
AnchorJson::Hunk { hunk_id } => Anchor::Hunk {
|
|
hunk_id: ContentId(hunk_id.clone()),
|
|
},
|
|
},
|
|
replacement: hex::decode(&e.replacement_hex).map_err(|x| x.to_string())?,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
Ok(CodingOp {
|
|
alg: self.alg,
|
|
prior: ContentId(self.prior.clone()),
|
|
edits,
|
|
author,
|
|
intent: self.intent.clone(),
|
|
tier: self.tier,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct SigVector {
|
|
name: String,
|
|
verifying_key: Option<String>,
|
|
message_hex: Option<String>,
|
|
signature: Option<String>,
|
|
/// Present for op-binding cases: the signature preimage is `op_id ‖ prior`.
|
|
op_id: Option<String>,
|
|
prior: Option<String>,
|
|
expected_verify: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct ReceiptVector {
|
|
name: String,
|
|
receipt: ReceiptJson,
|
|
steward: Option<String>,
|
|
endpoint_verifying_key: Option<String>,
|
|
expected_envelope: Option<String>,
|
|
expected_verify: bool,
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct ReceiptJson {
|
|
op_id: String,
|
|
tier: Tier,
|
|
energy: EnergyJson,
|
|
sequence: u64,
|
|
nonce: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct EnergyJson {
|
|
measured_uj: Option<u64>,
|
|
estimated_uj: u64,
|
|
provenance: Provenance,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CascadeVector {
|
|
name: String,
|
|
stages: Vec<Tier>,
|
|
stage_closes: BTreeMap<String, bool>,
|
|
expected_closing_tier: Option<Tier>,
|
|
}
|
|
|
|
fn parse_tier(s: &str) -> Option<Tier> {
|
|
match s {
|
|
"L0" => Some(Tier::L0),
|
|
"L1" => Some(Tier::L1),
|
|
"L2" => Some(Tier::L2),
|
|
"L3" => Some(Tier::L3),
|
|
"L4" => Some(Tier::L4),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// Map a CargoError to the stable error token used in the vectors.
|
|
fn cargo_error_token(e: &CargoError) -> &'static str {
|
|
match e {
|
|
CargoError::EditsOverlap => "edits_overlap",
|
|
CargoError::EditsMisordered => "edits_misordered",
|
|
CargoError::UnknownAlgorithm(_) => "unknown_algorithm",
|
|
CargoError::NoEdits => "no_edits",
|
|
_ => "other",
|
|
}
|
|
}
|
|
|
|
fn receipt_error_token(e: &ReceiptError) -> &'static str {
|
|
match e {
|
|
ReceiptError::HwShuntRequiresMeasured => "hwshunt_requires_measured",
|
|
ReceiptError::EstimatorMustNotCarryMeasured => "estimator_must_not_carry_measured",
|
|
_ => "other",
|
|
}
|
|
}
|
|
|
|
// ---------- the four suites ----------
|
|
|
|
/// C1 — content addressing. Recompute `op_id`; honor expected errors.
|
|
pub fn verify_coding_ops(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<OpVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let op = match v.op.to_op() {
|
|
Ok(op) => op,
|
|
Err(e) => {
|
|
r.fail(&v.name, &format!("malformed vector: {e}"));
|
|
continue;
|
|
}
|
|
};
|
|
match (op.op_id(), v.error.as_deref()) {
|
|
// Expected an error and got the right one.
|
|
(Err(e), Some(expected)) => {
|
|
if cargo_error_token(&e) == expected {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(&v.name, &format!("wrong error: got {}, want {expected}", cargo_error_token(&e)));
|
|
}
|
|
}
|
|
// Expected an error, got an id.
|
|
(Ok(_), Some(expected)) => r.fail(&v.name, &format!("expected error {expected}, got an op_id")),
|
|
// Expected success.
|
|
(Ok(id), None) => match v.expected_op_id {
|
|
Some(expected) if is_placeholder(&expected) => {
|
|
r.skip(&v.name, &format!("op_id pending regen (computed {id})"));
|
|
}
|
|
Some(expected) => {
|
|
if id.as_str() == expected {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(&v.name, &format!("op_id mismatch: got {id}, want {expected}"));
|
|
}
|
|
}
|
|
None => r.fail(&v.name, "no expected_op_id and no error"),
|
|
},
|
|
(Err(e), None) => r.fail(&v.name, &format!("unexpected error: {e}")),
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// C2 — signing. Real Ed25519 known-answer cases run; op-binding cases whose
|
|
/// signature is a placeholder are skipped pending regeneration.
|
|
pub fn verify_signatures(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<SigVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let (Some(vk_hex), Some(sig_hex)) = (&v.verifying_key, &v.signature) else {
|
|
r.skip(&v.name, "no key/signature");
|
|
continue;
|
|
};
|
|
if is_placeholder(sig_hex) {
|
|
r.skip(&v.name, "signature pending regen");
|
|
continue;
|
|
}
|
|
let ok = (|| -> Result<bool, String> {
|
|
let vk_bytes: [u8; 32] = hex::decode(vk_hex)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "vk len".to_string())?;
|
|
let sig_bytes: [u8; 64] = hex::decode(sig_hex)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "sig len".to_string())?;
|
|
// KAT cases verify a raw message; op-binding cases verify over the
|
|
// preimage `op_id ‖ prior` (joule-code-cargo's signing preimage).
|
|
let msg: Vec<u8> = if let Some(msg_hex) = &v.message_hex {
|
|
hex::decode(msg_hex).map_err(|e| e.to_string())?
|
|
} else if let (Some(op_id), Some(prior)) = (&v.op_id, &v.prior) {
|
|
let mut pre = op_id.as_bytes().to_vec();
|
|
pre.extend_from_slice(prior.as_bytes());
|
|
pre
|
|
} else {
|
|
return Err("vector has neither message_hex nor op_id+prior".into());
|
|
};
|
|
let vk = VerifyingKey::from_bytes(&vk_bytes).map_err(|e| e.to_string())?;
|
|
Ok(vk.verify(&msg, &Signature::from_bytes(&sig_bytes)).is_ok())
|
|
})();
|
|
match ok {
|
|
Ok(got) if got == v.expected_verify => r.pass(&v.name),
|
|
Ok(got) => r.fail(&v.name, &format!("verify={got}, expected {}", v.expected_verify)),
|
|
Err(e) => r.fail(&v.name, &e),
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// C3 — receipt. The provenance↔measured coupling runs for real; positive
|
|
/// signature reproduction is skipped pending regeneration.
|
|
pub fn verify_receipts(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<ReceiptVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let energy = Energy {
|
|
measured_uj: v.receipt.energy.measured_uj,
|
|
estimated_uj: v.receipt.energy.estimated_uj,
|
|
provenance: v.receipt.energy.provenance,
|
|
};
|
|
match (energy.check_provenance(), v.error.as_deref()) {
|
|
(Err(e), Some(expected)) => {
|
|
if receipt_error_token(&e) == expected {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(&v.name, &format!("wrong error: got {}, want {expected}", receipt_error_token(&e)));
|
|
}
|
|
}
|
|
(Ok(()), Some(expected)) => r.fail(&v.name, &format!("expected error {expected}, got valid")),
|
|
(Err(e), None) => r.fail(&v.name, &format!("unexpected provenance error: {e}")),
|
|
(Ok(()), None) => {
|
|
// Coupling holds; verify the JCR-1 COSE_Sign1 envelope for real.
|
|
let Some(env_hex) = v.expected_envelope.as_deref() else {
|
|
r.fail(&v.name, "positive vector missing expected_envelope");
|
|
continue;
|
|
};
|
|
if is_placeholder(env_hex) {
|
|
r.skip(&v.name, "coupling OK; envelope pending regen");
|
|
continue;
|
|
}
|
|
match verify_receipt_envelope(&v, energy, env_hex) {
|
|
Ok(ok) if ok == v.expected_verify => r.pass(&v.name),
|
|
Ok(ok) => r.fail(&v.name, &format!("verify={ok}, expected {}", v.expected_verify)),
|
|
Err(e) => r.fail(&v.name, &e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// Reconstruct the [`Receipt`] from a vector and verify its JCR-1 COSE_Sign1
|
|
/// envelope against the endpoint key + steward (SPEC §6).
|
|
fn verify_receipt_envelope(v: &ReceiptVector, energy: Energy, env_hex: &str) -> Result<bool, String> {
|
|
let steward = v.steward.as_deref().ok_or("missing steward")?;
|
|
let vk_hex = v
|
|
.endpoint_verifying_key
|
|
.as_deref()
|
|
.ok_or("missing endpoint_verifying_key")?;
|
|
let vk_bytes: [u8; 32] = hex::decode(vk_hex)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "endpoint key len".to_string())?;
|
|
let envelope = hex::decode(env_hex).map_err(|e| e.to_string())?;
|
|
let nonce: [u8; 8] = hex::decode(&v.receipt.nonce)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "nonce len".to_string())?;
|
|
let receipt = Receipt {
|
|
op_id: ContentId(v.receipt.op_id.clone()),
|
|
tier: v.receipt.tier,
|
|
energy,
|
|
sequence: v.receipt.sequence,
|
|
nonce,
|
|
};
|
|
Ok(receipt.verify_envelope(&envelope, steward, &vk_bytes).is_ok())
|
|
}
|
|
|
|
/// C4 — cascade. Fully checkable: select the closing tier under
|
|
/// stop-at-first-close and compare to the vector.
|
|
pub fn verify_cascade(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<CascadeVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let closes = |t: Tier| -> bool {
|
|
v.stage_closes
|
|
.iter()
|
|
.any(|(k, &c)| parse_tier(k) == Some(t) && c)
|
|
};
|
|
let got = Cascade::select_closing_tier(&v.stages, closes);
|
|
if got == v.expected_closing_tier {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(
|
|
&v.name,
|
|
&format!("closing tier got {got:?}, expected {:?}", v.expected_closing_tier),
|
|
);
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
// ---------- C5: the JCP-gated cascade ----------
|
|
|
|
#[derive(Deserialize)]
|
|
struct GatedStageVector {
|
|
tier: String,
|
|
closes: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GatedExpect {
|
|
closing_tiers: Vec<Option<String>>,
|
|
spent_uj: u64,
|
|
verdicts: Vec<String>,
|
|
deny_reasons: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GatedVector {
|
|
name: String,
|
|
stages: Vec<GatedStageVector>,
|
|
granted_actions: Vec<String>,
|
|
budget_uj: u64,
|
|
runs: u32,
|
|
expected: GatedExpect,
|
|
}
|
|
|
|
/// Scripted stage for C5: closes (or not) at its tier.
|
|
struct Scripted {
|
|
tier: Tier,
|
|
closes: bool,
|
|
}
|
|
|
|
impl joule_code_cascade::Stage for Scripted {
|
|
fn tier(&self) -> Tier {
|
|
self.tier
|
|
}
|
|
fn try_resolve(
|
|
&self,
|
|
_req: &joule_code_cascade::Request,
|
|
) -> Option<joule_code_cascade::Resolution> {
|
|
if !self.closes {
|
|
return None;
|
|
}
|
|
Some(joule_code_cascade::Resolution {
|
|
op: None,
|
|
answer: Some(format!("closed at {:?}", self.tier)),
|
|
receipt: Receipt {
|
|
op_id: ContentId::of_bytes(b"c5-op"),
|
|
tier: self.tier,
|
|
energy: Energy {
|
|
measured_uj: None,
|
|
estimated_uj: joule_code_jcp::tier_cost_estimate(self.tier),
|
|
provenance: Provenance::Estimator,
|
|
},
|
|
sequence: 0,
|
|
nonce: [0; 8],
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
/// C5 — the JCP-gated cascade (SPEC §5.1). Runs the REAL jcp runtime: per
|
|
/// vector, issue a grant naming `granted_actions` with `budget_uj` over the
|
|
/// fixed fixture identities, wrap the scripted stages with the gate, run the
|
|
/// cascade `runs` times, and compare closing tiers, total debit, and the
|
|
/// sealed receipt verdicts (deny reasons included — a denial is evidence, not
|
|
/// a silent skip).
|
|
///
|
|
/// Fixture constants (third-party implementations must use the same):
|
|
/// issuer key = [1;32], agent key = [2;32], steward key = [7;32],
|
|
/// steward did = "did:example:joule-code",
|
|
/// resource = ContentId::of_bytes(b"repo:joule-code"),
|
|
/// request = { prior: "tree-hash", intent: "rename the function" },
|
|
/// per-tier costs = `tier_cost_estimate` (SPEC §1.1 class markers).
|
|
pub fn verify_gated_cascade(json: &str) -> Result<Report, String> {
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_schema::{Capability, Did, Grant};
|
|
|
|
let vectors: Vec<GatedVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
let issuer = SigningKey::from_bytes(&[1; 32]);
|
|
let agent = SigningKey::from_bytes(&[2; 32]);
|
|
let steward_sk = SigningKey::from_bytes(&[7; 32]);
|
|
let steward = "did:example:joule-code";
|
|
let steward_vk = steward_sk.verifying_key().to_bytes();
|
|
let tree = jcp_schema::ContentId::of_bytes(b"repo:joule-code");
|
|
|
|
'vectors: for v in vectors {
|
|
let grant = Grant {
|
|
issuer: Did::from_verifying_key(&issuer.verifying_key()),
|
|
subject: Did::from_verifying_key(&agent.verifying_key()),
|
|
capability: Capability::new(tree, v.granted_actions.iter().map(String::as_str)),
|
|
budget_uj: v.budget_uj,
|
|
budget_funds: None,
|
|
caveats: Vec::new(),
|
|
expiry: 0,
|
|
parent: None,
|
|
delegate_depth: None,
|
|
nonce: [0; 16],
|
|
};
|
|
let chain = match jcp_grant::issue(&issuer, grant) {
|
|
Ok(sg) => vec![sg],
|
|
Err(e) => {
|
|
r.fail(&v.name, &format!("grant issue: {e:?}"));
|
|
continue;
|
|
}
|
|
};
|
|
let gate = joule_code_jcp::JcpGate::new(
|
|
chain,
|
|
Did::from_verifying_key(&agent.verifying_key()),
|
|
steward_sk.clone(),
|
|
steward,
|
|
);
|
|
let mut stages: Vec<Box<dyn joule_code_cascade::Stage>> = Vec::new();
|
|
for s in &v.stages {
|
|
let Some(tier) = parse_tier(&s.tier.to_uppercase()) else {
|
|
r.fail(&v.name, &format!("bad tier {:?}", s.tier));
|
|
continue 'vectors;
|
|
};
|
|
stages.push(Box::new(gate.wrap(Scripted { tier, closes: s.closes })));
|
|
}
|
|
let cascade = Cascade::new(stages);
|
|
let req = joule_code_cascade::Request {
|
|
prior: "tree-hash".into(),
|
|
intent: "rename the function".into(),
|
|
};
|
|
|
|
let mut closings: Vec<Option<String>> = Vec::new();
|
|
for _ in 0..v.runs {
|
|
closings.push(
|
|
cascade
|
|
.resolve(&req)
|
|
.map(|res| format!("{:?}", res.receipt.tier).to_lowercase()),
|
|
);
|
|
}
|
|
let expected_closings: Vec<Option<String>> = v
|
|
.expected
|
|
.closing_tiers
|
|
.iter()
|
|
.map(|t| t.as_ref().map(|s| s.to_lowercase()))
|
|
.collect();
|
|
if closings != expected_closings {
|
|
r.fail(&v.name, &format!("closings got {closings:?}, want {expected_closings:?}"));
|
|
continue;
|
|
}
|
|
if gate.spent() != v.expected.spent_uj {
|
|
r.fail(&v.name, &format!("spent got {}, want {}", gate.spent(), v.expected.spent_uj));
|
|
continue;
|
|
}
|
|
let mut verdicts = Vec::new();
|
|
let mut deny_reasons = Vec::new();
|
|
for sealed in gate.receipts() {
|
|
match jcp_receipt::extract(&sealed, steward, &steward_vk) {
|
|
Ok(rec) => {
|
|
verdicts.push(rec.verdict.clone());
|
|
if let Some(reason) = rec.deny_reason {
|
|
deny_reasons.push(reason);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
r.fail(&v.name, &format!("receipt extract: {e:?}"));
|
|
continue 'vectors;
|
|
}
|
|
}
|
|
}
|
|
if verdicts != v.expected.verdicts {
|
|
r.fail(&v.name, &format!("verdicts got {verdicts:?}, want {:?}", v.expected.verdicts));
|
|
continue;
|
|
}
|
|
if deny_reasons != v.expected.deny_reasons {
|
|
r.fail(&v.name, &format!("deny reasons got {deny_reasons:?}, want {:?}", v.expected.deny_reasons));
|
|
continue;
|
|
}
|
|
r.pass(&v.name);
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// Verify a whole pack directory (the five suite files).
|
|
pub fn verify_pack(dir: &Path) -> Result<Report, String> {
|
|
let mut report = Report::default();
|
|
type Suite = (&'static str, fn(&str) -> Result<Report, String>);
|
|
let suites: &[Suite] = &[
|
|
("coding_op_vectors.json", verify_coding_ops),
|
|
("signature_vectors.json", verify_signatures),
|
|
("receipt_vectors.json", verify_receipts),
|
|
("cascade_vectors.json", verify_cascade),
|
|
("gated_cascade_vectors.json", verify_gated_cascade),
|
|
];
|
|
for (file, f) in suites {
|
|
let path = dir.join(file);
|
|
report.lines.push(format!("[{file}]"));
|
|
let json = std::fs::read_to_string(&path)
|
|
.map_err(|e| format!("{}: {e}", path.display()))?;
|
|
report.absorb(f(&json)?);
|
|
}
|
|
Ok(report)
|
|
}
|