#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT"
)]
pub mod assets;
pub mod bench;
pub mod catalogue;
pub mod conformance;
pub mod evidence;
pub mod judgement;
pub mod measured;
pub mod replay;
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::load::{LoadError, compile_schema};
#[derive(Debug, Clone)]
pub struct RenderedFile {
pub name: String,
pub body: String,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("runner defect: {0}")]
Catalogue(#[source] LoadError),
#[error("{}", join_lines(.0))]
Artifacts(Vec<LoadError>),
#[error("{0}")]
Missing(String),
#[error("cannot read {path}: {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("cannot write {path}: {source}")]
Write {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("cannot create {path}: {source}")]
CreateDir {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{context}: {message}")]
Parse {
context: String,
message: String,
},
#[error("{0}")]
Party(String),
#[error("{context}: {source}")]
Serialize {
context: String,
#[source]
source: serde_json::Error,
},
#[error("{}", join_prefixed(.0, "results invariant: "))]
ResultsInvariants(Vec<crate::party::PartyError>),
#[error("{}", join_prefixed(.0, "results invariant: "))]
RecordedInvariants(Vec<crate::party::PartyError>),
#[error("{0}")]
Selector(String),
#[error("{0}")]
Instrument(String),
#[error("{0}")]
Evidence(#[from] crate::evidence::EvidenceError),
}
fn join_lines<T: std::fmt::Display>(items: &[T]) -> String {
items
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
}
fn join_prefixed<T: std::fmt::Display>(items: &[T], prefix: &str) -> String {
items
.iter()
.map(|item| format!("{prefix}{item}"))
.collect::<Vec<_>>()
.join("\n")
}
pub fn load_clean_root(root: &Path) -> Result<crate::artifacts::Loaded, Error> {
let loaded = crate::artifacts::load_root(root).map_err(Error::Catalogue)?;
if loaded.errors.is_empty() {
Ok(loaded)
} else {
Err(Error::Artifacts(loaded.errors))
}
}
pub fn load_party_json<T: serde::de::DeserializeOwned>(
path: &Path,
schema: &serde_json::Value,
schema_name: &str,
) -> Result<T, Error> {
let text = std::fs::read_to_string(path)
.map_err(|e| Error::Party(format!("{}: {e}", path.display())))?;
let value: serde_json::Value = serde_json::from_str(&text)
.map_err(|e| Error::Party(format!("{}: JSON: {e}", path.display())))?;
let validator = compile_schema(schema, schema_name).map_err(|e| Error::Party(e.to_string()))?;
let violations: Vec<String> = validator
.iter_errors(&value)
.map(|e| format!("{}: {e}", e.instance_path()))
.collect();
if !violations.is_empty() {
return Err(Error::Party(format!(
"{}: schema: {}",
path.display(),
violations.join("; ")
)));
}
serde_json::from_value(value)
.map_err(|e| Error::Party(format!("{}: model: {e}", path.display())))
}
pub fn load_ixit(path: &Path) -> Result<(crate::ixit::Ixit, String), Error> {
let text = std::fs::read_to_string(path).map_err(|source| Error::Read {
path: path.to_owned(),
source,
})?;
let mut ixit: crate::ixit::Ixit = serde_json::from_str(&text).map_err(|e| Error::Parse {
context: "ixit".to_owned(),
message: e.to_string(),
})?;
ixit.rebase_paths(path.parent().unwrap_or_else(|| Path::new(".")));
Ok((ixit, text))
}
pub fn load_statement(path: &Path) -> Result<(crate::party::Statement, String), Error> {
let text = std::fs::read_to_string(path).map_err(|source| Error::Read {
path: path.to_owned(),
source,
})?;
let statement = serde_json::from_str(&text).map_err(|e| Error::Parse {
context: "statement".to_owned(),
message: e.to_string(),
})?;
Ok((statement, text))
}
pub fn read_json<T: serde::de::DeserializeOwned>(path: &Path, context: &str) -> Result<T, Error> {
let text = std::fs::read_to_string(path).map_err(|source| Error::Read {
path: path.to_owned(),
source,
})?;
serde_json::from_str(&text).map_err(|e| Error::Parse {
context: context.to_owned(),
message: e.to_string(),
})
}
pub fn to_json_document<T: serde::Serialize>(value: &T, context: &str) -> Result<String, Error> {
let mut text = serde_json::to_string_pretty(value).map_err(|source| Error::Serialize {
context: context.to_owned(),
source,
})?;
text.push('\n');
Ok(text)
}
pub fn ensure_parent_dir(path: &Path) -> Result<(), Error> {
let Some(parent) = path.parent() else {
return Ok(());
};
std::fs::create_dir_all(parent).map_err(|source| Error::CreateDir {
path: parent.to_owned(),
source,
})
}
pub fn write_file(path: &Path, body: &str) -> Result<(), Error> {
std::fs::write(path, body).map_err(|source| Error::Write {
path: path.to_owned(),
source,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_filesystem_failure_names_the_path_it_was_working_on() {
let dir = assert_fs::TempDir::new().expect("temp dir");
let missing = dir.path().join("absent.json");
let error = read_json::<serde_json::Value>(&missing, "results")
.expect_err("nothing was written there");
assert!(
matches!(&error, Error::Read { path, .. } if path == &missing),
"{error}"
);
assert!(error.to_string().contains("absent.json"), "{error}");
let unwritable = dir.path().join("no-such-directory/results.json");
let error = write_file(&unwritable, "{}").expect_err("the parent does not exist");
assert!(
matches!(&error, Error::Write { path, .. } if path == &unwritable),
"{error}"
);
ensure_parent_dir(&unwritable).expect("the parent is created");
write_file(&unwritable, "{}").expect("the file lands once its parent exists");
ensure_parent_dir(Path::new("")).expect("a parentless path creates nothing");
}
#[test]
fn a_document_that_does_not_parse_names_the_reader_that_wanted_it() {
let dir = assert_fs::TempDir::new().expect("temp dir");
let path = dir.path().join("ixit.json");
std::fs::write(&path, "{ not json").expect("staging the malformed document");
let error =
read_json::<serde_json::Value>(&path, "results").expect_err("the document is not JSON");
assert!(
matches!(&error, Error::Parse { context, .. } if context == "results"),
"{error}"
);
let error = load_ixit(&path).expect_err("the document is not a topology");
assert!(
matches!(&error, Error::Parse { context, .. } if context == "ixit"),
"{error}"
);
let error =
load_ixit(&dir.path().join("absent.json")).expect_err("nothing was written there");
assert!(matches!(error, Error::Read { .. }), "{error}");
}
#[test]
fn a_defective_tree_renders_one_diagnostic_per_file() {
let dir = assert_fs::TempDir::new().expect("temp dir");
let root = dir.path().join("artifacts");
std::fs::create_dir_all(root.join("schedule/performance"))
.expect("the performance directory");
for name in ["PERF-one.yaml", "PERF-two.yaml"] {
std::fs::write(
root.join("schedule/performance").join(name),
"id: [broken\n",
)
.expect("staging a defective case");
}
let error = load_clean_root(&root).expect_err("neither case loads");
let Error::Artifacts(diagnostics) = &error else {
panic!("a per-file failure is Error::Artifacts, got {error}");
};
assert_eq!(diagnostics.len(), 2, "{diagnostics:?}");
let rendered = error.to_string();
for name in ["PERF-one.yaml", "PERF-two.yaml"] {
assert!(
rendered.contains(name),
"the rendered error hides {name}: {rendered}"
);
}
}
#[test]
fn an_emitted_document_is_pretty_json_with_a_trailing_newline() {
let document = to_json_document(&serde_json::json!({ "b": 1, "a": 2 }), "example")
.expect("the value serializes");
assert_eq!(document, "{\n \"b\": 1,\n \"a\": 2\n}\n");
}
#[test]
fn results_invariants_render_one_prefixed_line_per_violation() {
let missing = |case: &str| crate::party::PartyError::MissingCitation {
case: case.to_owned(),
status: "skipped",
};
let violations = || vec![missing("CASE-one"), missing("CASE-two")];
let recorded = Error::RecordedInvariants(violations()).to_string();
assert_eq!(
recorded,
Error::ResultsInvariants(violations()).to_string(),
"both seams report one violation the same way"
);
let lines: Vec<&str> = recorded.lines().collect();
assert_eq!(lines.len(), 2, "{recorded}");
for line in lines {
assert!(line.starts_with("results invariant: "), "{line}");
}
}
}