Files

318 lines
12 KiB
Rust

//! Saved-source Details comparator. No desktop renderer, controller or history inspection.
//! name BASELINE_SOURCE ACTUAL_SOURCE OUT ELEMENT NEW_NAME
//! typed EXPECTED_PROJECT_JSON ACTUAL_SOURCE OUT CHECKPOINT
//! self-check INCIDENT_SOURCE SERVICE_SOURCE OUT
use oa_core::{
DiagramLayout, Id, Project, execute_batch, source_files,
source_project::{ProjectLimits, compile_bundle},
validation::Severity,
};
use serde_json::{Value, json};
use std::{fs, io::Write, path::Path};
const INCIDENT: &str = include_str!("fixtures/incident-workspace.arcadia");
const SERVICE: &str = include_str!("fixtures/service-details.arcadia");
const EXPECTED: [(&str, &str); 6] = [
("baseline", include_str!("expected/baseline.project.json")),
("1", include_str!("expected/after-1.expected.project.json")),
(
"3a",
include_str!("expected/after-3a.expected.project.json"),
),
(
"3b",
include_str!("expected/after-3b.expected.project.json"),
),
(
"3c",
include_str!("expected/after-3c.expected.project.json"),
),
(
"3d",
include_str!("expected/after-3d.expected.project.json"),
),
];
fn incident(last: u128) -> Id {
Id::from_u128(0x70200000000040008000000000000000 | last)
}
fn service(last: u128) -> Id {
Id::from_u128(0x80300000000040008000000000000000 | last)
}
// Object order is irrelevant; array order and every serialized scalar remain exact.
// Scalar serialization distinguishes signed zero, unlike float PartialEq.
fn canonical(value: &Value) -> String {
match value {
Value::Object(map) => {
let mut keys: Vec<_> = map.keys().collect();
keys.sort();
format!(
"{{{}}}",
keys.into_iter()
.map(|k| format!(
"{}:{}",
serde_json::to_string(k).unwrap(),
canonical(&map[k])
))
.collect::<Vec<_>>()
.join(",")
)
}
Value::Array(a) => format!(
"[{}]",
a.iter().map(canonical).collect::<Vec<_>>().join(",")
),
_ => serde_json::to_string(value).unwrap(),
}
}
fn full(project: &Project) -> String {
canonical(&serde_json::to_value(project).unwrap())
}
fn put(path: &Path, bytes: &[u8]) {
let mut f = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.unwrap();
f.write_all(bytes).unwrap();
f.sync_all().unwrap();
}
fn load_source(path: &Path, project_id: Id) -> Project {
assert!(fs::symlink_metadata(path).unwrap().file_type().is_file());
let loaded = source_files::load_project(path, ProjectLimits::default()).unwrap();
assert!(
!loaded
.diagnostics
.iter()
.any(|d| d.diagnostic.severity == Severity::Error),
"source loader errors: {:?}",
loaded.diagnostics
);
let raw = loaded.reachable_bundle();
assert_eq!(
raw.files.len(),
1,
"Bound native fixtures require exactly one source file"
);
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
assert_eq!(
compiled.bundle, raw,
"No migration, identity insertion or passive source rewrite is admitted"
);
let p = compiled.decoded.project;
execute_batch(&p, &[]).unwrap();
assert_eq!(p.id, project_id, "Wrong fixture Project identity");
p
}
fn renamed(base: &Project, selected: Id, name: &str) -> Result<Project, String> {
if base.id != incident(1) {
return Err("Name mode requires the incident fixture Project".into());
}
let mut p = base.clone();
let e = p
.elements
.get_mut(&selected)
.ok_or("Selected identity must be an existing element")?;
if name.is_empty() || e.name == name {
return Err("Name must be a predetermined nonempty change".into());
}
e.name = name.into();
execute_batch(&p, &[]).map_err(|e| format!("Invalid name candidate: {e:?}"))?;
Ok(p)
}
fn approved_expected(raw: &str, checkpoint: &str) -> Result<Project, String> {
let approved = EXPECTED
.iter()
.find(|(key, _)| *key == checkpoint)
.ok_or("Unknown approved checkpoint")?
.1;
let value: Value = serde_json::from_str(raw).map_err(|e| e.to_string())?;
let p: Project = serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
// Do not silently discard unknown/defaulted fields while decoding expected JSON.
if full(&p) != canonical(&value) {
return Err("Expected JSON does not round-trip as the complete Project".into());
}
if p.id != service(0)
|| !p.elements.contains_key(&service(0x50))
|| p.diagrams.len() != 1
|| p.diagrams
.get(&service(0x70))
.and_then(|d| d.projection.scenario())
!= Some(service(0x50))
{
return Err("Wrong expected service fixture identity".into());
}
let allowed: Value = serde_json::from_str(approved).map_err(|e| e.to_string())?;
if raw.as_bytes() != approved.as_bytes() || canonical(&value) != canonical(&allowed) {
return Err("Expected Project differs from independently approved checkpoint".into());
}
execute_batch(&p, &[]).map_err(|e| format!("Expected checkpoint is not admitted: {e:?}"))?;
Ok(p)
}
fn self_check(base: &Project, typed: &Project, out: &Path) {
assert_eq!(
base.elements[&incident(0x20)].name,
"Initial incident report"
);
let name = "Initial incident report [Details trial]";
let expected = renamed(base, incident(0x20), name).unwrap();
let expected_full = full(&expected);
assert_ne!(full(base), expected_full);
let wrong = renamed(base, incident(0x21), name).unwrap();
assert_ne!(
full(&wrong),
expected_full,
"Wrong occurrence must fail full comparison"
);
let mut alias = expected.clone();
alias
.elements
.get_mut(&incident(0x20))
.unwrap()
.alias
.push_str("_extra");
assert_ne!(
full(&alias),
expected_full,
"Extra alias change must not be ignored"
);
let mut reordered = serde_json::to_value(&expected).unwrap();
reordered
.pointer_mut(&format!(
"/elements/{}/body/Oa/Shared/Scenario/steps",
incident(0x10)
))
.unwrap()
.as_array_mut()
.unwrap()
.swap(0, 1);
let reordered: Project = serde_json::from_value(reordered).unwrap();
assert_ne!(
full(&reordered),
expected_full,
"Semantic order is not normalized away"
);
let mut geometry = expected.clone();
let Some(DiagramLayout::Scenario(layout)) =
&mut geometry.diagrams.get_mut(&incident(0x40)).unwrap().layout
else {
panic!("Scenario layout required")
};
layout.lanes[0].x += 1.;
assert_ne!(
full(&geometry),
expected_full,
"Extra layout mutation rejected"
);
let mut zeros = base.clone();
let Some(DiagramLayout::Scenario(layout)) =
&mut zeros.diagrams.get_mut(&incident(0x40)).unwrap().layout
else {
panic!("Scenario layout required")
};
layout.lanes[0].x = -0.;
let negative = full(&zeros);
let Some(DiagramLayout::Scenario(layout)) =
&mut zeros.diagrams.get_mut(&incident(0x40)).unwrap().layout
else {
unreachable!()
};
layout.lanes[0].x = 0.;
assert_ne!(negative, full(&zeros));
assert!(
renamed(base, incident(0x40), name).is_err(),
"Diagram identity is outside element-name mode"
);
assert!(renamed(base, incident(0x20), "Initial incident report").is_err());
let baseline = approved_expected(EXPECTED[0].1, "baseline").unwrap();
assert_eq!(
full(typed),
full(&baseline),
"Actual admitted fixture must equal approved baseline"
);
for (key, text) in EXPECTED {
assert!(approved_expected(text, key).is_ok());
}
assert!(
approved_expected(EXPECTED[1].1, "3a").is_err(),
"Wrong checkpoint rejected"
);
let mut wrong_id: Value = serde_json::from_str(EXPECTED[0].1).unwrap();
wrong_id["id"] = json!(incident(1));
assert!(approved_expected(&canonical(&wrong_id), "baseline").is_err());
let mut unknown: Value = serde_json::from_str(EXPECTED[0].1).unwrap();
unknown["unapproved_extra"] = json!(true);
assert!(approved_expected(&canonical(&unknown), "baseline").is_err());
put(
&out.join("incident.baseline.project.json"),
full(base).as_bytes(),
);
put(
&out.join("service.baseline.project.json"),
full(typed).as_bytes(),
);
put(&out.join("self-check.json"),serde_json::to_string_pretty(&json!({"status":"passed",
"checks":["name_only_candidate","wrong_occurrence","extra_alias","semantic_array_order","extra_layout",
"signed_zero","non_element_target","unchanged_name","admitted_service_baseline", "all_six_approved_checkpoints",
"wrong_checkpoint","wrong_expected_project_identity","unknown_expected_field"],
"scope":"Comparator self-check only; no saved native output or runtime/history acceptance"})).unwrap().as_bytes());
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let mode = args.get(1).expect("Expected name, typed or self-check");
assert_eq!(
args.len(),
match mode.as_str() {
"name" => 7,
"typed" => 6,
"self-check" => 5,
_ => panic!("Unknown mode"),
}
);
let out = Path::new(&args[4]);
fs::create_dir(out).expect("Output must be fresh; preserve earlier evidence");
if mode == "self-check" {
assert_eq!(fs::read_to_string(&args[2]).unwrap(), INCIDENT);
assert_eq!(fs::read_to_string(&args[3]).unwrap(), SERVICE);
self_check(
&load_source(Path::new(&args[2]), incident(1)),
&load_source(Path::new(&args[3]), service(0)),
out,
);
println!("Details comparator self-check passed; no native result");
return;
}
let (expected, actual, change) = if mode == "name" {
let base = load_source(Path::new(&args[2]), incident(1));
put(&out.join("baseline.project.json"), full(&base).as_bytes());
let selected = args[5].parse::<Id>().unwrap();
let expected = renamed(&base, selected, &args[6]).unwrap();
let old = &base.elements[&selected].name;
(
expected,
load_source(Path::new(&args[3]), incident(1)),
json!({"element":selected,"before":old,"after":args[6]}),
)
} else {
let expected = approved_expected(&fs::read_to_string(&args[2]).unwrap(), &args[5]).unwrap();
(
expected,
load_source(Path::new(&args[3]), service(0)),
json!({"approved_checkpoint":args[5]}),
)
};
let expected = full(&expected);
let actual = full(&actual);
let equal = expected == actual;
put(&out.join("expected.project.json"), expected.as_bytes());
put(&out.join("actual.project.json"), actual.as_bytes());
put(&out.join("comparison.json"),serde_json::to_string_pretty(&json!({"status":if equal {"passed"}else{"failed"},
"mode":mode,"expected_change":change,"full_project_exact":equal,
"scope":"Complete admitted saved-source Project only; no runtime, editor-buffer, camera, Undo stack or native action claim"})).unwrap().as_bytes());
assert!(
equal,
"Complete Project differs; inspect preserved expected/actual outputs"
);
println!("Details saved-source comparison passed: {mode}");
}