Files
Sylpheed/crates/sylpheed-formats/tests/savegame_samples.rs
Fabian Hamm ed54f95d54 style: rustfmt sweep -- 774 hunks across 154 files -> 0
`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
2026-09-08 20:07:01 +02:00

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)");
}