Files
Sylpheed/crates/sylpheed-formats/tests/ui_prm_primitives_disc.rs
Fabian Hamm 15d51b30ac 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

234 lines
9.1 KiB
Rust

//! `.prm` elements are untextured quads, and the declaration table says so.
//!
//! Every composite the port draws is missing them: they carry no sprite, so
//! `compose` skips them. What they are was settled statically on 2026-08-19 —
//! see `docs/re/structures/ui-prm-primitives.md`. This pins the three facts the
//! decoding rests on, and is disc-gated like the rest of the corpus.
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);
}
}
}
}
/// **Bit `0x10` of `kind` means "untextured primitive", and it is exact.**
///
/// This is the point of the whole item: the primitive-ness is a *decoded field*,
/// not a guess from the `.prm` file extension. Over every screen build on the
/// disc, `kind & 0x10` and a `.prm` name agree with **zero** exceptions in
/// either direction — so a port can classify without parsing names.
///
/// Also pinned here because it is the same sweep: no `.prm` element has a RATC
/// child of its own name. All 369 of them are payload-less, which is what
/// "untextured" means concretely — there is nothing in the bundle to draw.
#[test]
fn kind_bit_0x10_means_prm_exactly_and_prm_carries_no_payload() {
let Some(root) = disc_root() else {
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
return;
};
let (mut prm, mut prm_without_bit, mut bit_without_prm, mut with_child) = (0, 0, 0, 0);
let (mut full_screen, mut sized) = (0, 0);
for_each_build(&root, |pak, bytes| {
let Some(build) = ui_layout::parse_build(bytes) else {
return;
};
if build.from_fallback {
return;
}
let kids = ratc::parse(bytes).unwrap_or_default();
for el in &build.elements {
let is_prm = el.name.to_ascii_lowercase().ends_with(".prm");
let has_bit = el.kind & 0x10 != 0;
if is_prm && !has_bit {
prm_without_bit += 1;
eprintln!("{pak}: {} is .prm without bit 0x10", el.name);
}
if has_bit && !is_prm {
bit_without_prm += 1;
eprintln!("{pak}: {} has bit 0x10 but is not .prm", el.name);
}
if !is_prm {
continue;
}
prm += 1;
if kids.iter().any(|c| c.name == el.name) {
with_child += 1;
eprintln!("{pak}: {} has a payload child", el.name);
}
sized += 1;
if (el.pivot_x * 2, el.pivot_y * 2) == (1280, 720) {
full_screen += 1;
}
}
});
assert!(
prm > 300,
"only {prm} `.prm` elements — the sweep did not run"
);
assert_eq!(prm_without_bit, 0, "a `.prm` element without kind bit 0x10");
assert_eq!(
bit_without_prm, 0,
"kind bit 0x10 on something that is not `.prm`"
);
assert_eq!(with_child, 0, "a `.prm` element with a payload child");
// 361 of 369 are exactly the design space; the handful that are not are
// small coloured quads (844x600, and two degenerate 0x720).
assert!(
full_screen * 10 > sized * 9,
"only {full_screen}/{sized} `.prm` elements are full-screen"
);
eprintln!(
"{prm} `.prm` elements: all kind&0x10, none with a payload, {full_screen} full-screen"
);
}
/// **A `.prm` fade quad rests at its transparent plateau**, which is what makes
/// it drawable at all.
///
/// The title's `pteff00.prm` is a screen transition: opaque black at t=12,
/// transparent by t=64, held transparent to t=74, opaque black again on the way
/// out. What the title screen sits at is the transparent plateau — and this
/// element is painted **last** in the measured order, so getting it wrong means
/// a full-screen opaque black quad over everything.
///
/// It *was* wrong: the old longest-dwell resting rule picked the opaque
/// endpoint. This test asserted that defect deliberately until the plateau rule
/// replaced it (`docs/re/structures/ui-resting-pose.md`); now it guards the fix.
#[test]
fn the_title_fade_quad_rests_transparent() {
let Some(root) = disc_root() else {
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
return;
};
let arc = PakArchive::open(root.join("dat").join("GP_TITLE.pak")).expect("GP_TITLE.pak");
let mut checked = 0usize;
for e in arc.entries() {
let Ok(bytes) = arc.read(e) else { continue };
let Some(build) = ui_layout::parse_build(&bytes) else {
continue;
};
for el in &build.elements {
if el.name != "pteff00.prm" || el.keyframes.len() != 4 {
continue;
}
checked += 1;
let a: Vec<u32> = el.keyframes.iter().map(|k| k.fade >> 24).collect();
assert_eq!(
a,
vec![0xff, 0x00, 0x00, 0xff],
"the fade group is opaque → transparent → transparent → opaque"
);
assert_eq!(
el.rest().map(|k| k.fade),
Some(0x0000_0000),
"the fade quad must rest at its transparent plateau — an opaque \
answer here paints a full-screen black quad over the title"
);
}
}
assert!(
checked >= 3,
"found {checked} title fade quads, expected several"
);
}
/// **Drawing the primitives with the derived order swallows screens** — which
/// is why `ComposeOptions::include_primitives` is off by default.
///
/// A primitive has no `T8aD` header, so it has no layer key, and
/// `derived_paint_order` sorts the keyless to the very end. That is not a
/// harmless default: `GP_DIALOG`'s `pzeff00.prm` is a **single** keyframe of
/// opaque black at full screen, and painted last it wipes the build.
///
/// The ground truth contradicts *both* simple defaults. On the developer-logo
/// splash the game paints `palogo_eff0.prm` **first** — it is the black
/// backdrop. On the title it paints `pteff02.prm` at slot 4, beneath the
/// wordmark, and `pteff00.prm` **last**, as the fade-out. So the order is real,
/// per-element, and not derivable from anything decoded so far.
///
/// This test measures the damage rather than asserting the feature works, so the
/// number stays honest and moves when the ordering is solved.
#[test]
fn the_derived_order_puts_primitives_last_and_that_wipes_screens() {
let Some(root) = disc_root() else {
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
return;
};
let opts = |primitives| ui_layout::ComposeOptions {
backdrop: [0, 0, 0, 255],
include_primitives: primitives,
..Default::default()
};
let flatness = |c: &ui_layout::ComposedScreen| {
let mut hist = std::collections::HashMap::<[u8; 3], usize>::new();
for p in c.rgba.chunks_exact(4) {
*hist.entry([p[0], p[1], p[2]]).or_default() += 1;
}
*hist.values().max().unwrap_or(&0) as f64 / (c.width * c.height) as f64
};
let (mut with_prm, mut wiped_on, mut wiped_off) = (0usize, 0usize, 0usize);
for_each_build(&root, |_pak, bytes| {
if !ui_layout::is_build(bytes) {
return;
}
let Some(b) = ui_layout::parse_build(bytes) else {
return;
};
if b.from_fallback
|| !b.elements.iter().any(|e| {
e.kind & 0x10 != 0
&& e.sprite.is_none()
&& e.rest().map(|k| (k.fade >> 24) & 0xff).unwrap_or(0) != 0
})
{
return;
}
with_prm += 1;
if flatness(&ui_layout::compose(&b, bytes, opts(true), None)) > 0.99 {
wiped_on += 1;
}
if flatness(&ui_layout::compose(&b, bytes, opts(false), None)) > 0.99 {
wiped_off += 1;
}
});
assert!(with_prm > 50, "only {with_prm} builds draw a primitive");
assert_eq!(
wiped_off, 0,
"the DEFAULT composite wipes {wiped_off} builds — primitives are supposed \
to be off unless asked for"
);
assert!(
wiped_on > 0,
"no build is wiped with primitives on — the ordering problem this flag \
exists for may be solved, in which case turn it on by default"
);
eprintln!(
"{with_prm} builds draw a visible primitive; with the derived order \
{wiped_on} of them come out >99% one colour, {wiped_off} by default"
);
}