Files
Sylpheed/crates/sylpheed-formats/tests/ui_header_time_disc.rs
Fabian Hamm 64f3df7d72
All checks were successful
CI / Native — linux (pull_request) Successful in 33m40s
CI / WASM — Web (pull_request) Successful in 29m29s
CI / Formatting (pull_request) Successful in 1m1s
test(formats): make $SYLPHEED_DISC an actual control (#16 remedy 3)
Before this commit, `unset SYLPHEED_DISC` did not disable the disc-backed
suites on the machine that has the disc: every `disc_root()` fell back to a
hardcoded absolute path that exists on this box. The env var looked like a
control and was not one. Same for $SYLPHEED_RES3D and $SYLPHEED_ISO.

Replace the duplicated resolvers with one `tests/common/mod.rs`:

  - 17 local `disc_root()` definitions -> 1
  - 7 copies of the skip macro -> 1 (`skip_without_disc!` and siblings)
  - 16 hardcoded absolute paths -> 0 executable ones
    (3 of those were inline in `mesh_disc.rs`, in no resolver at all,
     and 2 were in `examples/`)
  - `corpus_report.rs` now reports on the SAME resolver the suites use,
    instead of a second copy of the logic its own comments flagged as a
    drift risk.

The 17 copies had already drifted into FIVE variants, and they were not all
the same function. `movie_manifest_disc`, `movie_subtitle_disc` and `slb_disc`
honoured $SYLPHEED_DISC and nothing else, while the other 14 fell back. So one
name already meant two things -- a third instance of the shape #16 is about.
The shared helper adopts the env-only behaviour those three already had, rather
than inventing a sixth variant.

Two module docs still described the fallback after it was deleted, which is the
same defect in prose: `texture_disc` claimed "or the default dev path exists"
and `pak_idxd_disc` said "or drop it at the default dev path below". Both now
say what the code does.

Verified both ways on the machine that HAS the corpus, which is the only place
this refactor can be falsified:

  A  env unset  -> "ABSENT -- $SYLPHEED_DISC unset; its suites self-skip"
                   suites=31 passed=209 failed=0 ignored=14, slowest 0.12s
  B  env set    -> "PRESENT via $SYLPHEED_DISC" (all three corpora)
                   suites=31 passed=209 failed=0 ignored=14,
                   slowest 1235.53s (mesh_consistency_disc)

Identical tallies, opposite corpus states, ~10000x apart in wall clock. (A) is
new behaviour -- it was previously unreachable here. (B) proves nothing broke.

`just test-disc` sources `.env` (already gitignored) for the set case. Note the
quoting trap documented there: the corpus paths contain spaces, and an unquoted
`VAR=a b c` parses as "run command `b`", failing silently into ABSENT -- which
looks exactly like a working skip.

Remedy (1) (`#[ignore]` + `--ignored`) is deliberately NOT done here: (3) already
moves the mode from the filesystem into the environment, and `#[ignore]` already
carries three meanings in this directory (corpus-absent, known-failing, bare).
Overloading it a fourth time would re-create the defect.

`cargo fmt --all --check` clean; no new compiler warnings.

Refs #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 20:47:23 +02:00

156 lines
5.8 KiB
Rust

//! Is the bundle header's `+0x08` really an animation **duration in frames**?
//!
//! The header sweep found `+0x04` takes only `0x3C0000` and `0x1E0000` — exactly
//! `60.0` and `30.0` in 16.16 — and `+0x08` takes 30 / 1200 / 120 / 60, which
//! *looks* like a frame rate and a length. That reading came from the values
//! alone, and this checks it against something the file states independently:
//! the **keyframe times** in the placement region.
//!
//! If `+0x08` is the length of the bundle's animation, the largest keyframe time
//! in the bundle should never exceed it, and should reach it on bundles that
//! animate all the way through. If instead the times run past it, the reading is
//! wrong and the amber has to come off the other way.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
mod common;
use common::disc_root;
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 ratc::is_ratc(&bytes) {
f(&name, &bytes);
}
}
}
}
fn be32(b: &[u8], at: usize) -> u32 {
u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}
#[test]
fn header_0x08_against_the_keyframe_times() {
let Some(root) = disc_root() else {
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
return;
};
let (mut animated, mut within, mut exact, mut over) = (0usize, 0, 0, 0);
let mut worst: Vec<String> = Vec::new();
// How the ratio max_time / header_0x08 is distributed, in tenths.
let mut ratio: HashMap<u32, usize> = HashMap::new();
// Does the rate word co-vary with anything?
let mut by_rate: HashMap<u32, (usize, u32)> = HashMap::new(); // rate -> (bundles, max seen 0x08)
// The 16.16 frame-rate reading rests on twelve bundles at 30.0. If those are
// VARIANTS of 60.0 bundles - same elements, different rate - the reading
// gains a real discriminator; if they are unrelated one-offs it does not.
let mut odd_rate: Vec<String> = Vec::new();
for_each_build(&root, |pak, bytes| {
if bytes.len() < 0x20 {
return;
}
let Some(build) = ui_layout::parse_build(bytes) else {
return;
};
if build.from_fallback {
return;
}
let dur = be32(bytes, 0x08);
let rate = be32(bytes, 0x04);
let e = by_rate.entry(rate).or_default();
e.0 += 1;
e.1 = e.1.max(dur);
if rate != 0x3C_0000 && odd_rate.len() < 20 {
let names: Vec<&str> = build
.elements
.iter()
.map(|e| e.name.as_str())
.take(6)
.collect();
odd_rate.push(format!(
"{pak}: rate {rate:#x} dur {dur} elements {} {:?}",
build.elements.len(),
names
));
}
let max_time = build
.elements
.iter()
.flat_map(|el| el.keyframes.iter())
.filter_map(|k| k.time)
.max();
let Some(max_time) = max_time else { return };
if dur == 0 {
return;
}
animated += 1;
if max_time <= dur {
within += 1;
if max_time == dur {
exact += 1;
}
} else {
over += 1;
if worst.len() < 10 {
worst.push(format!("{pak}: max keyframe {max_time} > header {dur}"));
}
}
let r = ((max_time as f64 / dur as f64) * 10.0).round() as u32;
*ratio.entry(r.min(30)).or_default() += 1;
});
eprintln!("bundles with keyframe times and a non-zero +0x08: {animated}");
eprintln!(" max keyframe time <= +0x08: {within} of which EXACTLY equal: {exact}");
eprintln!(" max keyframe time > +0x08: {over}");
let mut r: Vec<_> = ratio.iter().collect();
r.sort();
eprintln!(" ratio max_time/+0x08 (tenths -> bundles): {r:?}");
let mut br: Vec<_> = by_rate.iter().collect();
br.sort();
eprintln!(" +0x04 rate word -> (bundles, largest +0x08 seen): {br:?}");
for w in &worst {
eprintln!(" over: {w}");
}
for o in &odd_rate {
eprintln!(" non-60 rate: {o}");
}
assert!(animated > 0, "no animated bundles — the sweep is broken");
// MEASURED 2026-08-24. +0x08 bounds the keyframe times in EVERY one of the
// 2313 bundles that have both, and 444 of them reach it exactly. The
// spread-out ratio histogram is what rules out the boring explanation: a
// large unrelated constant would bound everything too, but then the ratios
// would pile up near zero instead of peaking at 1.0.
assert_eq!(over, 0, "a keyframe time runs past the header's +0x08");
assert!(
exact > 400,
"the bound is never attained — it may be unrelated"
);
let near_one = ratio.get(&10).copied().unwrap_or(0);
let near_zero = ratio.get(&0).copied().unwrap_or(0);
assert!(
near_one > near_zero * 4,
"the max_time/+0x08 ratio does not peak at 1.0 — the bound may be vacuous"
);
}