pmacs/tests/common/sigint_conformance.rs

162 lines
5.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Shared SIGINT-guard conformance vectors
//! (`docs/gpu-probe-sigint-framing.md` §7c).
//!
//! # Why these live here rather than in each suite
//!
//! The contract is that the **shell** consumer (`scripts/gate`) and the
//! **Rust** consumer (R-d's `sigint_diagnosis`) agree on every case.
//! Two independently written copies of the list can drift while both
//! still report "45 cases" — the same-length-different-content
//! divergence this matrix exists to rule out. One generator, two
//! consumers.
/// What a consumer must do with a given `(status, stdout)` pair.
///
/// `ValidatedError` and `Boundary` **both exit 2**, so a test comparing
/// only exit codes cannot separate them — and a validator that accepted
/// *every* status 2 would pass. They are told apart by whether the
/// child's stderr is surfaced: a validated verdict speaks with the
/// helper's voice; a boundary failure must not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Safe,
ValidatedIgnored,
ValidatedError,
Boundary,
}
pub const TOKEN_SAFE: &[u8] = b"pmacs-sigint-v1:safe";
pub const TOKEN_IGNORED: &[u8] = b"pmacs-sigint-v1:ignored";
pub const TOKEN_ERROR: &[u8] = b"pmacs-sigint-v1:error";
/// Emitted on stderr by **every** stub, so a consumer's output can be
/// searched for it: present ⇒ the child's stderr was surfaced.
pub const SENTINEL: &str = "PMACS-CONFORMANCE-SENTINEL";
pub struct Case {
pub name: String,
pub status: i32,
pub stdout: Vec<u8>,
pub expect: Outcome,
}
/// A `/bin/sh` stub reproducing one case, sentinel included.
#[must_use]
pub fn stub_script(case: &Case) -> String {
let octal = case.stdout.iter().fold(String::new(), |mut acc, b| {
use std::fmt::Write as _;
let _ = write!(acc, "\\{b:03o}");
acc
});
format!(
"#!/bin/sh\nprintf '{octal}'\necho '{SENTINEL}' >&2\nexit {}\n",
case.status
)
}
/// The shared set: ten classes × encodings × three statuses, plus
/// X1/X3/X4. Only the diagonal validates.
///
/// X2 — a spawn error with no status — is deliberately absent: the
/// shell boundary cannot represent it, because an `exec` failure there
/// becomes a status. Rust exercises it separately.
#[must_use]
pub fn shared_cases() -> Vec<Case> {
let toks: [(&str, &[u8]); 3] = [
("safe", TOKEN_SAFE),
("ignored", TOKEN_IGNORED),
("error", TOKEN_ERROR),
];
let mut out = Vec::new();
for (idx, (name, correct)) in toks.iter().enumerate() {
let status = i32::try_from(idx).expect("0..=2");
let diagonal = match status {
0 => Outcome::Safe,
1 => Outcome::ValidatedIgnored,
_ => Outcome::ValidatedError,
};
let mut lf = correct.to_vec();
lf.push(b'\n');
out.push(Case {
name: format!("{status}/V/{name}/bare"),
status,
stdout: correct.to_vec(),
expect: diagonal,
});
out.push(Case {
name: format!("{status}/V/{name}/lf"),
status,
stdout: lf,
expect: diagonal,
});
for (other, bytes) in &toks {
if other == name {
continue;
}
let mut olf = bytes.to_vec();
olf.push(b'\n');
out.push(Case {
name: format!("{status}/M/{other}/bare"),
status,
stdout: bytes.to_vec(),
expect: Outcome::Boundary,
});
out.push(Case {
name: format!("{status}/M/{other}/lf"),
status,
stdout: olf,
expect: Outcome::Boundary,
});
}
let mut leading = vec![b'\n'];
leading.extend_from_slice(correct);
let mut extra = correct.to_vec();
extra.extend_from_slice(b"\n\n");
let mut spaces = b" ".to_vec();
spaces.extend_from_slice(correct);
spaces.push(b' ');
let mut crlf = correct.to_vec();
crlf.extend_from_slice(b"\r\n");
let mut doubled = correct.to_vec();
doubled.extend_from_slice(correct);
let mut nul = correct.to_vec();
nul.push(0);
for (cls, bytes) in [
("E/empty", Vec::new()),
("U/unknown", b"pmacs-sigint-v2:safe".to_vec()),
("L/leading-lf", leading),
("X/extra-lf", extra),
("S/spaces", spaces),
("C/crlf", crlf),
("D/doubled", doubled),
("N/nul", nul),
] {
out.push(Case {
name: format!("{status}/{cls}"),
status,
stdout: bytes,
expect: Outcome::Boundary,
});
}
}
out.push(Case {
name: "X1/status-126".to_owned(),
status: 126,
stdout: TOKEN_SAFE.to_vec(),
expect: Outcome::Boundary,
});
out.push(Case {
name: "X3/ignored-text-no-token".to_owned(),
status: 1,
stdout: Vec::new(),
expect: Outcome::Boundary,
});
out.push(Case {
name: "X4/stderr-noise".to_owned(),
status: 0,
stdout: TOKEN_SAFE.to_vec(),
expect: Outcome::Safe,
});
out
}