formats: check the derived paint order against the screens already verified
The layer-key order was adopted from two measured screens and then applied to every build on the disc, so it owed a regression check against the screens the corpus had already validated against the running game. Rendered the tutorial PAUSE menu and the title main menu both ways and diffed: 3.8 % and 1.1 % of pixels differ, max delta 45/255 and 34/255, and the two renders are indistinguishable in layout — the change is confined to blends where translucent sprites overlap. No regression, but which order is more faithful on those two screens is unsettled and recorded as such. Adds a corpus-wide test asserting every composite's draw list is strictly increasing in (layer key, declaration index), streaming one pak at a time so it does not OOM alongside the other whole-disc tests. It reports the rule's reach: 341 of 965 builds are reordered, and it fails if that share collapses.
This commit is contained in:
@@ -71,6 +71,31 @@ fn builds(root: &Path) -> Vec<(String, Vec<u8>)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Every parseable screen build, handed over **one pak at a time**.
|
||||
///
|
||||
/// `builds` holds the whole disc's bundles in memory at once, and four tests in
|
||||
/// this file want the whole corpus; run together under the default test harness
|
||||
/// that is enough to get the process OOM-killed. This streams instead.
|
||||
fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) {
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
for p in &paks {
|
||||
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
for e in arc.entries() {
|
||||
let Ok(bytes) = arc.read(e) else { continue };
|
||||
if ui_layout::is_build(&bytes) {
|
||||
f(&name, &bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The builds of one pak, in the order `sylpheed-cli screen list` numbers them.
|
||||
fn pak_builds(root: &Path, pak: &str) -> Vec<Vec<u8>> {
|
||||
let arc = PakArchive::open(root.join("dat").join(pak)).expect("open pak");
|
||||
@@ -427,3 +452,66 @@ fn the_derived_order_matches_the_measured_ones_up_to_ties() {
|
||||
"expected both measured builds; seen = {seen:?} over {checked} matches"
|
||||
);
|
||||
}
|
||||
|
||||
/// The layer-key order is what every composite on the disc actually paints in,
|
||||
/// and it is not a no-op dressed up as a discovery.
|
||||
///
|
||||
/// Two things are checked corpus-wide, because the derived order was adopted on
|
||||
/// the strength of **two** measured screens and then applied to all of them:
|
||||
///
|
||||
/// * every composite's draw list is non-decreasing in the layer key, so the
|
||||
/// rule really reaches the whole corpus and a future accidental revert to
|
||||
/// declaration order fails here rather than silently;
|
||||
/// * the derived order reorders a substantial share of the disc's builds. If it
|
||||
/// were a near-no-op the two measured screens would be the only evidence
|
||||
/// there is, and the rule would deserve much less credit than it has.
|
||||
#[test]
|
||||
fn every_composite_paints_in_layer_key_order() {
|
||||
skip_without_disc!(root);
|
||||
let (mut total, mut checked, mut reordered) = (0usize, 0usize, 0usize);
|
||||
for_each_build(&root, |pak, bytes| {
|
||||
total += 1;
|
||||
let Some(b) = ui_layout::parse_build(bytes) else {
|
||||
return;
|
||||
};
|
||||
if b.from_fallback {
|
||||
return;
|
||||
}
|
||||
let key = |i: usize| {
|
||||
(
|
||||
ui_layout::sprite_layer_key(&b, bytes, &b.elements[i]).unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
};
|
||||
let mut want: Vec<usize> = (0..b.elements.len()).collect();
|
||||
want.sort_by_key(|&i| key(i));
|
||||
if want != (0..b.elements.len()).collect::<Vec<_>>() {
|
||||
reordered += 1;
|
||||
}
|
||||
let c = ui_layout::compose(&b, bytes, ComposeOptions::default(), None);
|
||||
// The two screens read off the running game keep their measured order,
|
||||
// which agrees with the derived one only up to ties — skip those.
|
||||
if c.drawn.len() < 2 || b.elements.len() == 24 || b.elements.len() == 7 {
|
||||
return;
|
||||
}
|
||||
checked += 1;
|
||||
for w in c.drawn.windows(2) {
|
||||
assert!(
|
||||
key(w[0]) < key(w[1]),
|
||||
"{pak}: painted element {} (key {:#x}) before {} (key {:#x}) — the \
|
||||
composite is no longer in layer-key order",
|
||||
w[0],
|
||||
key(w[0]).0,
|
||||
w[1],
|
||||
key(w[1]).0
|
||||
);
|
||||
}
|
||||
});
|
||||
assert!(checked > 100, "only {checked} builds composed 2+ elements");
|
||||
eprintln!("layer-key order: {reordered}/{total} builds reordered");
|
||||
assert!(
|
||||
reordered * 4 > total,
|
||||
"the derived order reorders only {reordered} of {total} builds — too few \
|
||||
to carry the weight the write-up puts on it"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user