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:
@@ -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>> {
|
||||
|
||||
Reference in New Issue
Block a user