Iter 4b: ail diff für semantischen Modul-Vergleich
ail diff <a> <b> [--json] vergleicht zwei Module strukturell per Def-Hash. Vier Kategorien (added/removed/changed/unchanged), alphabetisch sortiert, deterministisches JSON-Schema. Exit 1 bei Unterschieden, Exit 0 bei Identität — skript-tauglich. Kein Typcheck-Zwang, damit man auch kaputte Module diffen kann. Helper def_name/def_kind in ailang-core für stabile Def-Identität.
This commit is contained in:
@@ -64,6 +64,129 @@ fn list_sum_via_match() {
|
||||
assert_eq!(stdout.trim(), "42");
|
||||
}
|
||||
|
||||
/// Schützt `ail diff`: ein modifizierter Body ändert den Hash von `sum`,
|
||||
/// während `main` unverändert bleibt. Erwartet Exit-Code 1, `changed`
|
||||
/// enthält genau `sum`, `unchanged` enthält `main`, `added`/`removed` leer.
|
||||
#[test]
|
||||
fn diff_detects_changed_def() {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
||||
let src_a = workspace.join("examples").join("sum.ail.json");
|
||||
|
||||
// Variante: lade sum.ail.json, mutiere den `then`-Zweig (statt 0 nun 1)
|
||||
// in der `sum`-Definition. `main` bleibt bitidentisch.
|
||||
let raw = std::fs::read(&src_a).expect("read sum.ail.json");
|
||||
let mut module: serde_json::Value = serde_json::from_slice(&raw).expect("parse sum.ail.json");
|
||||
{
|
||||
let defs = module
|
||||
.get_mut("defs")
|
||||
.and_then(|d| d.as_array_mut())
|
||||
.expect("defs array");
|
||||
for def in defs.iter_mut() {
|
||||
if def.get("name").and_then(|n| n.as_str()) == Some("sum") {
|
||||
// Ersetze den then-Zweig literal 0 → literal 1.
|
||||
let new_then = serde_json::json!({
|
||||
"t": "lit",
|
||||
"lit": { "kind": "int", "value": 1 }
|
||||
});
|
||||
def["body"]["then"] = new_then;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tmp = std::env::temp_dir().join(format!(
|
||||
"ailang_diff_changed_{}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let path_b = tmp.join("sum_v2.ail.json");
|
||||
std::fs::write(&path_b, serde_json::to_vec_pretty(&module).unwrap())
|
||||
.expect("write sum_v2.ail.json");
|
||||
|
||||
let output = Command::new(ail_bin())
|
||||
.args([
|
||||
"diff",
|
||||
src_a.to_str().unwrap(),
|
||||
path_b.to_str().unwrap(),
|
||||
"--json",
|
||||
])
|
||||
.output()
|
||||
.expect("ail diff failed to run");
|
||||
|
||||
let code = output.status.code().expect("process terminated by signal");
|
||||
assert_eq!(
|
||||
code,
|
||||
1,
|
||||
"expected exit code 1 for differing modules; stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout utf8");
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON");
|
||||
|
||||
let added = v["added"].as_array().expect("added array");
|
||||
let removed = v["removed"].as_array().expect("removed array");
|
||||
let changed = v["changed"].as_array().expect("changed array");
|
||||
let unchanged = v["unchanged"].as_array().expect("unchanged array");
|
||||
|
||||
assert!(added.is_empty(), "expected added empty: {added:?}");
|
||||
assert!(removed.is_empty(), "expected removed empty: {removed:?}");
|
||||
assert_eq!(changed.len(), 1, "expected one changed entry: {changed:?}");
|
||||
assert_eq!(
|
||||
changed[0].get("name").and_then(|n| n.as_str()),
|
||||
Some("sum")
|
||||
);
|
||||
assert_ne!(
|
||||
changed[0].get("hash_a").and_then(|n| n.as_str()),
|
||||
changed[0].get("hash_b").and_then(|n| n.as_str()),
|
||||
"hash_a and hash_b must differ for a changed def"
|
||||
);
|
||||
|
||||
assert!(
|
||||
unchanged.iter().any(|e| e.get("name").and_then(|n| n.as_str()) == Some("main")),
|
||||
"expected `main` in unchanged: {unchanged:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Diff eines Moduls mit sich selbst: Exit 0, alle Listen außer `unchanged` leer.
|
||||
#[test]
|
||||
fn diff_no_changes_exit_zero() {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
||||
let src = workspace.join("examples").join("sum.ail.json");
|
||||
|
||||
let output = Command::new(ail_bin())
|
||||
.args([
|
||||
"diff",
|
||||
src.to_str().unwrap(),
|
||||
src.to_str().unwrap(),
|
||||
"--json",
|
||||
])
|
||||
.output()
|
||||
.expect("ail diff failed to run");
|
||||
|
||||
let code = output.status.code().expect("process terminated by signal");
|
||||
assert_eq!(
|
||||
code,
|
||||
0,
|
||||
"expected exit code 0 for identical modules; stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout utf8");
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON");
|
||||
|
||||
assert!(v["added"].as_array().unwrap().is_empty());
|
||||
assert!(v["removed"].as_array().unwrap().is_empty());
|
||||
assert!(v["changed"].as_array().unwrap().is_empty());
|
||||
assert!(
|
||||
!v["unchanged"].as_array().unwrap().is_empty(),
|
||||
"self-diff should report unchanged defs"
|
||||
);
|
||||
}
|
||||
|
||||
/// Schützt das `--json`-Diagnostic-Format für Tooling-Konsumenten.
|
||||
/// `broken_unbound.ail.json` referenziert eine nicht-existente Variable;
|
||||
/// erwartet wird Exit-Code 1 und mindestens ein Diagnostic mit
|
||||
|
||||
Reference in New Issue
Block a user