use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("workspace root")
}
fn golden() -> PathBuf {
repo_root().join("conformance/fixtures/estate-small.golden")
}
fn manifest() -> BTreeMap<String, String> {
std::fs::read_to_string(golden().join("MANIFEST"))
.expect("golden MANIFEST")
.lines()
.filter_map(|l| l.split_once('='))
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect()
}
fn fixture_copy() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("scratch dir");
copy_tree(
&repo_root().join("conformance/fixtures/estate-small"),
dir.path(),
);
dir
}
fn copy_tree(from: &Path, to: &Path) {
std::fs::create_dir_all(to).expect("mkdir");
for entry in std::fs::read_dir(from).expect("read_dir") {
let entry = entry.expect("entry");
let target = to.join(entry.file_name());
if entry.file_type().expect("file type").is_dir() {
copy_tree(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), &target).expect("copy");
}
}
}
fn files(root: &Path) -> BTreeMap<String, Vec<u8>> {
fn walk(base: &Path, dir: &Path, out: &mut BTreeMap<String, Vec<u8>>) {
for entry in std::fs::read_dir(dir).expect("read_dir") {
let entry = entry.expect("entry");
let path = entry.path();
if entry.file_type().expect("file type").is_dir() {
walk(base, &path, out);
} else {
let rel = path
.strip_prefix(base)
.expect("under base")
.to_string_lossy()
.into_owned();
out.insert(rel, std::fs::read(&path).expect("read"));
}
}
}
let mut out = BTreeMap::new();
if root.exists() {
walk(root, root, &mut out);
}
out
}
fn ontomark() -> Command {
Command::new(env!("CARGO_BIN_EXE_ontomark"))
}
const LAYOUT_TOLERANCE: f64 = 0.01;
fn is_layout_sensitive(name: &str) -> bool {
name.ends_with("graph/overview.json")
}
fn json_diff_within(a: &[u8], b: &[u8], tol: f64) -> Option<String> {
fn walk(path: &str, a: &serde_json::Value, b: &serde_json::Value, tol: f64) -> Option<String> {
use serde_json::Value::{Array, Number, Object};
match (a, b) {
(Number(x), Number(y)) if x.is_f64() || y.is_f64() => {
let (x, y) = (x.as_f64()?, y.as_f64()?);
((x - y).abs() > tol)
.then(|| format!("{path}: {x} differs from {y} by more than {tol}"))
}
(Array(x), Array(y)) => {
if x.len() != y.len() {
return Some(format!(
"{path}: array length {} against {}",
x.len(),
y.len()
));
}
x.iter()
.zip(y)
.enumerate()
.find_map(|(i, (p, q))| walk(&format!("{path}[{i}]"), p, q, tol))
}
(Object(x), Object(y)) => {
if x.keys().ne(y.keys()) {
return Some(format!("{path}: object keys differ"));
}
x.iter()
.find_map(|(k, p)| walk(&format!("{path}.{k}"), p, &y[k], tol))
}
_ => (a != b).then(|| format!("{path}: {a} differs from {b}")),
}
}
let parse = |v: &[u8]| serde_json::from_slice::<serde_json::Value>(v).ok();
match (parse(a), parse(b)) {
(Some(x), Some(y)) => walk("$", &x, &y, tol),
_ => Some("not valid JSON".to_string()),
}
}
fn tree_diff(label: &str, want: &Path, got: &Path) -> Vec<String> {
let (want, got) = (files(want), files(got));
let mut diffs = Vec::new();
for (path, bytes) in &want {
match got.get(path) {
None => diffs.push(format!("{label}/{path}: missing")),
Some(b) if path.ends_with(".generation.json") => {
diffs.extend(marker_diff(&format!("{label}/{path}"), bytes, b));
}
Some(b) if b != bytes && is_layout_sensitive(path) => {
if let Some(why) = json_diff_within(bytes, b, LAYOUT_TOLERANCE) {
diffs.push(format!("{label}/{path}: {why}"));
}
}
Some(b) if b != bytes => diffs.push(format!("{label}/{path}: bytes differ")),
Some(_) => {}
}
}
diffs.extend(
got.keys()
.filter(|p| !want.contains_key(*p))
.map(|p| format!("{label}/{p}: unexpected")),
);
diffs
}
fn marker_diff(label: &str, want: &[u8], got: &[u8]) -> Vec<String> {
let parse = |b: &[u8]| -> serde_json::Value {
serde_json::from_slice(b).expect("a generation marker is JSON")
};
let (mut want, mut got) = (parse(want), parse(got));
let mut diffs = Vec::new();
let take = |v: &mut serde_json::Value, key: &str| v.as_object_mut().and_then(|o| o.remove(key));
let want_digest = take(&mut want, "content_digest");
let got_digest = take(&mut got, "content_digest");
if want_digest.is_none() || got_digest.is_none() {
diffs.push(format!("{label}: a marker has no content_digest"));
}
let want_artifacts = take(&mut want, "artifacts").unwrap_or_default();
let got_artifacts = take(&mut got, "artifacts").unwrap_or_default();
if want != got {
diffs.push(format!(
"{label}: identity fields differ\n legacy: {want}\n current: {got}"
));
}
let got_set: std::collections::BTreeSet<String> = got_artifacts
.as_array()
.map(|a| a.iter().map(ToString::to_string).collect())
.unwrap_or_default();
for artifact in want_artifacts.as_array().into_iter().flatten() {
let name = artifact["name"].as_str().unwrap_or_default();
if is_layout_sensitive(name) || name.ends_with(".generation.json") {
continue;
}
if !got_set.contains(&artifact.to_string()) {
diffs.push(format!(
"{label}: legacy artefact missing or changed in the current marker: {artifact}"
));
}
}
diffs
}
#[test]
fn build_is_byte_identical_to_the_legacy_golden() {
let m = manifest();
let vault = fixture_copy();
let out = tempfile::tempdir().expect("out dir");
let status = ontomark()
.arg("--repo")
.arg(vault.path())
.args(["--json", "build", "--vault", &m["vault_kind"], "--stats"])
.arg("--out")
.arg(out.path().join("out"))
.arg("--publish-out")
.arg(out.path().join("publish"))
.arg("--with-markdown-mirror")
.arg("--no-redact-private-links")
.args(["--generated-at", &m["generated_at"]])
.args(["--source-commit", &m["snapshot_commit"]])
.output()
.expect("run ontomark build");
assert!(
status.status.success(),
"build failed: {}",
String::from_utf8_lossy(&status.stderr)
);
let expected = golden().join("expected");
let mut diffs = tree_diff("out", &expected.join("out"), &out.path().join("out"));
diffs.extend(tree_diff(
"publish",
&expected.join("publish"),
&out.path().join("publish"),
));
assert!(
diffs.is_empty(),
"{} difference(s):\n{}",
diffs.len(),
diffs.join("\n")
);
}
#[test]
fn verbs_print_the_same_bytes_and_exit_codes() {
let vault = fixture_copy();
let table = std::fs::read_to_string(golden().join("verbs.tsv")).expect("verbs.tsv");
let mut diffs = Vec::new();
let mut checked = 0;
for line in table.lines() {
let cols: Vec<&str> = line.split('\t').collect();
let (name, code, command) = (cols[0], cols[1], cols[3]);
let args: Vec<&str> = command.split_whitespace().collect();
let output = ontomark()
.current_dir(vault.path())
.args(&args)
.output()
.expect("run verb");
let want = std::fs::read(golden().join("expected/verbs").join(name)).expect("golden verb");
let got_code = output.status.code().unwrap_or(-1).to_string();
if got_code != code {
diffs.push(format!("{name}: exit {got_code}, golden {code}"));
}
if output.stdout != want {
diffs.push(format!("{name}: stdout differs"));
}
checked += 1;
}
assert_eq!(checked, 12, "the golden records twelve verb outputs");
assert!(diffs.is_empty(), "{}", diffs.join("\n"));
}
#[test]
fn layout_tolerance_admits_last_digit_drift_and_nothing_else() {
let a = br#"{"nodes": [{"id": 1, "x": -1987.011, "y": -291.395}], "n": 1}"#;
let drift = br#"{"nodes": [{"id": 1, "x": -1987.011, "y": -291.394}], "n": 1}"#;
let moved = br#"{"nodes": [{"id": 1, "x": -1987.011, "y": -250.0}], "n": 1}"#;
let renumbered = br#"{"nodes": [{"id": 2, "x": -1987.011, "y": -291.395}], "n": 1}"#;
assert_eq!(json_diff_within(a, drift, LAYOUT_TOLERANCE), None);
assert!(json_diff_within(a, moved, LAYOUT_TOLERANCE).is_some());
assert!(json_diff_within(a, renumbered, LAYOUT_TOLERANCE).is_some());
}
#[test]
fn ingest_and_compile_agree_on_human_actors() {
use ontomark_compile::input::okf::Actor;
for s in [
"human:npub1abc",
" human:npub1abc ",
"human:",
"agent:ontology-mesh/1",
"process:vault/1.0",
"did:nostr:jjohare",
"npub1abc",
"",
] {
let compile = s.parse::<Actor>().is_ok_and(|a| a.is_human());
assert_eq!(ontomark_ingest::is_human_actor(s), compile, "{s:?}");
}
}