162 lines
5.2 KiB
Rust
162 lines
5.2 KiB
Rust
//! 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
|
||
}
|