test(pf-capture): the suite the splits enable — 38 Linux + 19 Windows tests (Phase 6)

The plan's priority order was "every producer- or OS-controlled parser, blend, geometry guard or
negotiation decision", none of which had a test before this sweep (X3). Phase 5's splits are what
made most of them reachable without a compositor or a driver.

**6.1 `bitmap_extent`** — extracted from `update_cursor_meta`, which is the guard whose own SAFETY
proof says a missing bound "SIGSEGVs inside the PipeWire `.process` callback (a segfault
`catch_unwind` cannot catch)". Every input except the region size is producer-written. 5 tests:
a bitmap that fits (with and without header/pixel offsets); the last-row rule (`stride·(bh−1) + row`,
so a bitmap ending flush against the region is accepted rather than rejected by padding that is
never read — and one byte short is rejected); anything past the region; degenerate geometry; and four
distinct overflow vectors including the near-`i32::MAX` stride the SAFETY comment calls out. One
assertion I first wrote was wrong, not the code — with a single row `stride·0 == 0`, so even a
`usize::MAX`-wide row is arithmetically in range; the test now exercises the ≥2-row case that really
overflows and says why the other is fine (the caller has already capped `bw` at 1024).

**6.2 the composite blits** — 8 tests over `composite_cursor` / `composite_cursor_rgb10`: every
packed `PixelFormat` arm lands the colour in its OWN channels (so a byte-order slip cannot pass);
clipping off all four edges plus six fully-outside positions; zero-alpha, `visible: false` and
no-bitmap-yet all drawing nothing; the integer alpha blend; NV12/Yuv444 declined rather than
mis-blitted; and for the 10-bit path a bit-exact round trip of an untouched pixel (including the two
alpha bits the repack must preserve), the R-at-bit-20 vs R-at-bit-0 distinction between `X2Rgb10` and
`X2Bgr10`, and the same clipping. Plus `decode_bitmap_pixel`'s four byte orders.

**6.4 the pod builders** — 4 tests. The `dataType` bitmask is pinned per Buffers pod, because each
bit is load-bearing in a different direction: the mappable pod MUST include DmaBuf (or gamescope's
modifier-bearing format pod wins and the buffer intersection is empty — a link silently stuck in
"negotiating"), the SHM-only pod MUST exclude it (or Mutter hands dmabufs and the race-free download
path is not), and the dmabuf pod MUST exclude the mappable types (or an HDR session can be handed a
MemFd buffer, which Mutter paints 8-bit ARGB32 regardless of the negotiated 10-bit format). Also:
every pod parses back through `Pod::from_bytes`; the HDR pods carry MANDATORY PQ + BT.2020 +
LINEAR-modifier; and only the NV12 offer pins the colour matrix/range. The `dataType` reader parses
the SPA property layout literally (`key, flags, size, type, value`) — a "find the first
plausible-looking int" heuristic read the `size` word, which is also 4, and reported the wrong mask.

**6.5 `FrameToken` generation masking (W14)** — 2 tests, with `IDD_GENERATION` parked two below the
24-bit boundary so the WRAP is what gets exercised: every minted generation is non-zero, fits the
token's field, and survives the pack/unpack round trip `try_consume` performs; and the cleared-
`latest` 0 sentinel never matches a live generation.

**6.6 the cursor conversion (Windows)** — `convert`'s pixel logic extracted from its GDI plumbing
into `mono_planes_to_rgba` / `apply_and_mask_alpha` / `alpha_is_empty`, which is what makes it
testable at all (the caller needs a live `HCURSOR` and a screen DC). 6 tests: the four-state AND/XOR
truth table in one row; transparency surviving without an invert neighbour; the invert outline
covering all eight neighbours, overwriting only TRANSPARENT ones, and clipping at the edges; the
alpha-less colour cursor taking alpha from the AND mask; and a short mask not panicking.

**6.3 / 6.7** landed with the fixes they guard (`negotiation_plan` in 2.3, `f32_to_f16` in 3.5).

38 Linux tests, up from 6 at the start of the sweep — ci.yml already runs them. 19 `#[test]`s on the
Windows side; Phase 0.1's `--all-targets` lint type-checks them all, but note it does not RUN them
(the workflow lints only), so the Windows ones execute on a Windows box. clippy --all-targets clean
on both targets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 11:31:34 +02:00
co-authored by Claude Opus 5
parent 306f4a514d
commit 14914bc72f
4 changed files with 754 additions and 60 deletions
+55
View File
@@ -1829,6 +1829,61 @@ mod tests {
use super::stall::Stall;
use super::*;
/// W14: the mint must stay inside the publish token's 24-bit generation field, and must skip 0.
///
/// `IDD_GENERATION` is a full `u32` while `FrameToken` carries 24 bits and `unpack` MASKS what it
/// reads, so an unmasked `self.generation` stops matching any token past 2²⁴ recreates and
/// `try_consume`'s `tok.generation != self.generation` becomes permanently true — every frame
/// rejected, forever. The counter is parked just below the boundary here so the wrap is what gets
/// exercised, not the happy path. (Same-module access to the private static; no capturer is
/// running, and no other test touches it.)
#[test]
fn the_ring_generation_survives_the_publish_token() {
IDD_GENERATION.store(frame::FrameToken::GENERATION_MASK - 2, Ordering::Relaxed);
let mut seen = Vec::new();
for _ in 0..8 {
let g = next_generation();
assert_ne!(g, 0, "0 also means the cleared-`latest` sentinel");
assert_eq!(
g & frame::FrameToken::GENERATION_MASK,
g,
"generation {g} does not fit the token's field"
);
// The round trip `try_consume` actually performs.
let tok = frame::FrameToken {
generation: g,
seq: 12345,
slot: 2,
};
let back = frame::FrameToken::unpack(tok.pack());
assert_eq!(back.generation, g, "generation lost in the token");
assert_eq!(back.seq, 12345, "seq lost in the token");
assert_eq!(back.slot, 2, "slot lost in the token");
seen.push(g);
}
// The wrap really happened (we started 2 below the mask), and produced no duplicate 0.
assert!(
seen.contains(&frame::FrameToken::GENERATION_MASK),
"{seen:?}"
);
assert!(
seen.iter().any(|&g| g < 8),
"the counter should have wrapped: {seen:?}"
);
}
/// The 0 sentinel `recreate_ring` stores must be REJECTED by the generation compare, whatever
/// the live generation is — that is what stops a consume from the unwritten new ring.
#[test]
fn the_cleared_latest_sentinel_never_matches_a_live_generation() {
let cleared = frame::FrameToken::unpack(0);
assert_eq!(cleared.generation, 0);
assert_eq!(cleared.seq, 0);
for g in [1u32, 2, 0x7F_FFFF, frame::FrameToken::GENERATION_MASK] {
assert_ne!(cleared.generation, g, "sentinel matched generation {g}");
}
}
/// Feed a [`StallWatch`] fresh frames at the given offsets (ms from a common origin) and
/// return what each `note_fresh` produced.
fn watch_run(offsets_ms: &[u64]) -> Vec<Option<Stall>> {