`cargo fmt --all -- --check` has failed on every run in this repository's history, identically on `main` and on every branch. This is #12. Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other extension touched. `cargo check --workspace` exits 0 afterwards, so nothing changed semantically. ON THE ORDERING, WHICH WAS THE REAL QUESTION. HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree reformat before #7 and #8 return "would put a conflict in every file of 861 commits and make the reviews those items exist to enable unreadable". That is measurably too pessimistic, and it had been reasoned rather than tested. Measured here by three-way merging a rustfmt'd `main` against both unmerged branches, file by file: file/branch pairs tested 32 merges CLEAN 28 merges CONFLICTING 4 (8 conflict hunks total) sylpheed-cli/src/main.rs 1 hunk sylpheed-export/src/check.rs 1 sylpheed-export/src/screen.rs 4 sylpheed-export/src/video.rs 2 All four are against `auto/frame-blend-draw-path` only; `auto/port-p6-audio` does not conflict anywhere. The earlier framing -- 154 dirty files, 133 that cannot collide, 21 that can, the collision set carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say is that most of the 21 still merge cleanly, because rustfmt's edits and the branches' edits rarely land on the same lines. So the cost of sweeping now is 4 files and 8 hunks for one branch, against a check that is otherwise red forever. Deliberately NOT folded into the WASM PR: 154 reformatted files would make that one unreviewable. Closes #12
161 lines
5.7 KiB
Rust
161 lines
5.7 KiB
Rust
//! The save parser against the three real saves committed under `docs/re/captures`.
|
|
//!
|
|
//! These need no disc and no emulator — the samples are in the repo — so this
|
|
//! runs in plain `cargo test`. The load-bearing assertion is the **byte-identical
|
|
//! round-trip**: the title's serializer writes its struct field by field with no
|
|
//! packing, so a correct parse must reproduce the payload exactly. Anything less
|
|
//! means a field has the wrong width or the chunk stream has an unread gap.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use sylpheed_formats::savegame::{self, Confidence, DevelopState, GHAD_SIZE, RECORD_COUNT};
|
|
|
|
fn captures() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/re/captures")
|
|
}
|
|
|
|
fn sample(name: &str) -> Vec<u8> {
|
|
std::fs::read(captures().join(name)).unwrap_or_else(|e| panic!("read {name}: {e}"))
|
|
}
|
|
|
|
const SAMPLES: [&str; 3] = [
|
|
"savedata-game02-samestate.bin",
|
|
"savedata-game03-developed-mg1.bin",
|
|
"savedata-stage02-5pct.bin",
|
|
];
|
|
|
|
#[test]
|
|
fn every_sample_parses_and_round_trips_byte_identically() {
|
|
for name in SAMPLES {
|
|
let raw = sample(name);
|
|
let save = savegame::parse(&raw).unwrap_or_else(|e| panic!("{name}: {e}"));
|
|
assert_eq!(
|
|
save.payload.len(),
|
|
545,
|
|
"{name}: the whole save is 545 bytes"
|
|
);
|
|
assert_eq!(save.ghad.len(), GHAD_SIZE, "{name}");
|
|
assert_eq!(save.records.len(), RECORD_COUNT, "{name}");
|
|
assert!(
|
|
save.round_trips(),
|
|
"{name}: re-serializing did not reproduce the payload"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn phase_is_one_of_the_titles_screen_ids() {
|
|
for name in SAMPLES {
|
|
let save = savegame::parse(&sample(name)).unwrap();
|
|
assert!(
|
|
save.phase.starts_with("GP_"),
|
|
"{name}: phase {:?} is not a GP_* screen id",
|
|
save.phase
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn trailer_closes_the_stream() {
|
|
for name in SAMPLES {
|
|
let save = savegame::parse(&sample(name)).unwrap();
|
|
let (tname, tag, _) = &save.trailer;
|
|
assert_eq!(tname, "BUNK", "{name}");
|
|
assert_eq!(tag, "NETA", "{name}");
|
|
}
|
|
}
|
|
|
|
/// The develop differential: the `game03` save was taken after developing exactly
|
|
/// one Arsenal weapon (Light Machine Gun MG I, 4000 P) from the `game02` state.
|
|
/// Exactly three things moved, and this is what gave the blob its alphabet.
|
|
#[test]
|
|
fn developing_one_weapon_moves_points_ratio_and_two_blob_entries() {
|
|
let before = savegame::parse(&sample("savedata-game02-samestate.bin")).unwrap();
|
|
let after = savegame::parse(&sample("savedata-game03-developed-mg1.bin")).unwrap();
|
|
|
|
// Points fell by the item's 4000 P cost — and +28 did NOT move, which is what
|
|
// separates the spendable balance from its twin.
|
|
let (p0, p1) = (before.points().unwrap(), after.points().unwrap());
|
|
assert_eq!(p0 - p1, 4000, "Points should fall by the 4000 P cost");
|
|
let twin = |s: &savegame::SaveGame| {
|
|
savegame::GHAD_LAYOUT
|
|
.iter()
|
|
.find(|f| f.offset == 28)
|
|
.and_then(|f| s.ghad_value(f))
|
|
.unwrap()
|
|
};
|
|
assert_eq!(twin(&before), twin(&after), "+28 must not move on a spend");
|
|
|
|
// The clear ratio counts collection, not only stages.
|
|
assert_eq!(
|
|
after.clear_ratio_pct().unwrap(),
|
|
before.clear_ratio_pct().unwrap() + 1
|
|
);
|
|
|
|
// Blob: the bought item became developed, its successor became developable.
|
|
let (b, a) = (before.develop_state(), after.develop_state());
|
|
let moved: Vec<usize> = (0..b.len()).filter(|&i| b[i] != a[i]).collect();
|
|
assert_eq!(
|
|
moved.len(),
|
|
2,
|
|
"exactly two blob entries move, got {moved:?}"
|
|
);
|
|
assert_eq!(a[moved[0]], DevelopState::Developed);
|
|
assert_eq!(a[moved[1]], DevelopState::Developable);
|
|
}
|
|
|
|
/// The two saves of the same state differ only in the header (its FILETIME and
|
|
/// the uninitialised pointer padding) — the payload is a pure function of game
|
|
/// state.
|
|
#[test]
|
|
fn payload_is_a_pure_function_of_game_state() {
|
|
let a = savegame::parse(&sample("savedata-game02-samestate.bin")).unwrap();
|
|
let b = savegame::parse(&sample("savedata-stage02-5pct.bin")).unwrap();
|
|
assert_eq!(
|
|
a.payload, b.payload,
|
|
"the same state saved twice must deflate to the same payload"
|
|
);
|
|
}
|
|
|
|
/// The summary copy the Details panel reads must agree with the payload it
|
|
/// mirrors — the two are written together, and a disagreement is what a
|
|
/// payload-only edit produces.
|
|
#[test]
|
|
fn header_summary_agrees_with_the_payload_it_mirrors() {
|
|
for name in SAMPLES {
|
|
let save = savegame::parse(&sample(name)).unwrap();
|
|
for m in save.header.summary() {
|
|
let Some(goff) = m.ghad_offset else { continue };
|
|
let spec = savegame::GHAD_LAYOUT
|
|
.iter()
|
|
.find(|f| f.offset == goff)
|
|
.expect("mirror names a real GHAD field");
|
|
let payload_value = save.ghad_value(spec).unwrap();
|
|
assert_eq!(
|
|
u64::from(m.value),
|
|
payload_value,
|
|
"{name}: header {:#x} ({}) disagrees with GHAD +{}",
|
|
m.header_offset,
|
|
m.name,
|
|
goff
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The eleven still-unknown GHAD fields are a documented fact, not an oversight.
|
|
/// If a future session names one, this count moves — deliberately.
|
|
#[test]
|
|
fn unknown_fields_are_still_declared_unknown() {
|
|
let unknown = savegame::GHAD_LAYOUT
|
|
.iter()
|
|
.filter(|f| f.confidence == Confidence::Unknown)
|
|
.count();
|
|
let refuted = savegame::GHAD_LAYOUT
|
|
.iter()
|
|
.filter(|f| f.confidence == Confidence::Refuted)
|
|
.count();
|
|
assert_eq!(unknown, 7, "unknown GHAD fields");
|
|
assert_eq!(refuted, 2, "fields tested and refuted (+36, +56)");
|
|
}
|