DazedTL/util/wolfdawn-master/crates/wolf-decompiler/tests/full_file_recompile.rs
2026-07-03 13:54:07 -05:00

160 lines
5.5 KiB
Rust

//! Whole-file recompile gate: `compile(decompile_edit(file), base=file).write() == file`.
//!
//! This is the real edit-to-play round trip. It exercises the document envelope, the patch
//! substitution, and the format writer together, asserting byte-identity across every
//! CommonEvent.dat and .mps reachable (main corpus, every editor version, GamePro and Pro).
//! cargo test -p wolf-decompiler --test full_file_recompile -- --nocapture
use std::path::{Path, PathBuf};
use wolf_decompiler::{
compile_common_events_edit, compile_map_edit, decompile_common_events_edit, decompile_map_edit,
};
use wolf_formats::common_event::CommonEventsFile;
use wolf_formats::map::Map;
fn wolf_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("..")
}
/// Recurse `dir`, collecting every CommonEvent.dat and *.mps.
fn collect(dir: &Path, ces: &mut Vec<PathBuf>, maps: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
collect(&p, ces, maps);
} else if p.file_name().and_then(|s| s.to_str()) == Some("CommonEvent.dat") {
ces.push(p);
} else if p.extension().and_then(|s| s.to_str()) == Some("mps") {
maps.push(p);
}
}
}
fn ce_roundtrip(bytes: &[u8]) -> Result<bool, String> {
let ce = CommonEventsFile::read(bytes).map_err(|e| e.to_string())?;
let doc = decompile_common_events_edit(&ce);
let mut base = CommonEventsFile::read(bytes).map_err(|e| e.to_string())?;
compile_common_events_edit(&doc, &mut base).map_err(|e| e.to_string())?;
Ok(base.write() == bytes)
}
fn map_roundtrip(bytes: &[u8]) -> Result<bool, String> {
let map = Map::read(bytes).map_err(|e| e.to_string())?;
let doc = decompile_map_edit(&map);
let mut base = Map::read(bytes).map_err(|e| e.to_string())?;
compile_map_edit(&doc, &mut base).map_err(|e| e.to_string())?;
Ok(base.write() == bytes)
}
#[test]
fn whole_file_round_trip_byte_exact() {
let root = wolf_root();
let mut ces = Vec::new();
let mut maps = Vec::new();
for sub in ["Data", "ALLWOLFVERSIONS", "GamePro_Data", "GameProData"] {
collect(&root.join(sub), &mut ces, &mut maps);
}
ces.sort();
ces.dedup();
maps.sort();
maps.dedup();
if ces.is_empty() && maps.is_empty() {
eprintln!("no corpus found, skipping");
return;
}
let mut ok = 0usize;
let mut fail = 0usize;
let mut skip = 0usize;
let rel = |p: &Path| {
p.strip_prefix(&root)
.unwrap_or(p)
.display()
.to_string()
.replace('\\', "/")
};
// Encrypted (packaged) files need the archive layer to decrypt first. They are not plaintext
// inputs to the format/recompile path, so skip rather than fail.
let mut tally = |p: &Path, r: Result<bool, String>| match r {
Ok(true) => ok += 1,
Ok(false) => {
fail += 1;
eprintln!(" DIFF {}", rel(p));
}
Err(e) if e.contains("encrypted") => skip += 1,
Err(e) => {
fail += 1;
eprintln!(" ERR {} ({e})", rel(p));
}
};
for p in &ces {
let Ok(bytes) = std::fs::read(p) else {
continue;
};
tally(p, ce_roundtrip(&bytes));
}
for p in &maps {
let Ok(bytes) = std::fs::read(p) else {
continue;
};
tally(p, map_roundtrip(&bytes));
}
eprintln!(
"\nwhole-file recompile: {ok} byte-exact, {fail} fail, {skip} skipped (encrypted) \
({} CE, {} maps)",
ces.len(),
maps.len()
);
assert_eq!(fail, 0, "some files did not recompile byte-exact");
assert!(ok > 0, "no files were exercised");
}
#[test]
fn edit_then_compile_takes_effect() {
// Prove the whole-file path is driven by the document text, not a memory of the base. Edit a
// Message in the decompiled doc, recompile, and confirm the bytes changed and the new text is
// present, and that the re-read file still parses.
let path = wolf_root().join("Data/BasicData/CommonEvent.dat");
let Ok(bytes) = std::fs::read(&path) else {
eprintln!("corpus not found, skipping");
return;
};
let ce = CommonEventsFile::read(&bytes).unwrap();
let doc = decompile_common_events_edit(&ce);
// Find any Message line to mutate.
let Some(line) = doc.lines().find(|l| l.trim_start().starts_with("Message ")) else {
eprintln!("no Message command in corpus doc, skipping");
return;
};
let edited = doc.replacen(line, "Message \"WOLFDAWN_EDIT_MARKER\"", 1);
assert_ne!(edited, doc, "edit did not change the document");
let mut base = CommonEventsFile::read(&bytes).unwrap();
compile_common_events_edit(&edited, &mut base).expect("edited doc compiles");
let out = base.write();
assert_ne!(out, bytes, "edited file should differ from the original");
// Re-read: the marker is present and the file is structurally valid.
let reread = CommonEventsFile::read(&out).expect("edited file re-parses");
let found = reread.events.iter().any(|ev| {
ev.commands.iter().any(|c| {
c.cid == 101
&& c.str_args
.first()
.map_or(false, |s| s.as_bytes() == b"WOLFDAWN_EDIT_MARKER\0")
})
});
assert!(found, "edited Message text not found after recompile");
}