Files
Senbei/senbei-cli/tests/strict_integrity.rs
T

194 lines
6.1 KiB
Rust

//! CLI behavior of `--strict-integrity`.
//!
//! The flag is off by default and only changes the exit status (see the unit
//! tests in `src/main.rs` for the errors-versus-suspect matrix). These tests
//! drive the binary: help text, flag parsing, the error exit, and the quiet
//! contract. The suspect-only exit policy is covered separately by unit tests
//! so these process tests do not depend on the optional protected corpus.
use std::path::Path;
use std::process::Command;
fn senbei() -> Command {
Command::new(env!("CARGO_BIN_EXE_senbei"))
}
fn run(args: &[&str]) -> std::process::Output {
senbei().args(args).output().expect("spawn senbei")
}
fn assert_silent(output: &std::process::Output) {
assert!(
output.stdout.is_empty() && output.stderr.is_empty(),
"quiet>=2 must stay silent, stdout={:?} stderr={:?}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn write_plain(dir: &Path) -> std::path::PathBuf {
let path = dir.join("plain.bin");
std::fs::write(&path, b"not a protected image").unwrap();
path
}
fn list_logs(dir: &Path) -> Vec<std::path::PathBuf> {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("senbei-") && name.ends_with(".log"))
})
.collect()
}
#[test]
fn help_documents_strict_integrity_behavior() {
let output = run(&["--help"]);
assert!(output.status.success());
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("--strict-integrity"));
assert!(text.contains("--quiet"));
assert!(text.contains("exit non-zero"));
assert!(text.contains("suspect"));
assert!(text.contains("Off by default"));
assert!(text.contains("still written"));
assert!(text.contains("exits 0 unless"));
assert!(text.contains("a file errors"));
assert!(text.contains("Quiet levels are unchanged"));
}
#[test]
fn short_help_and_version_ignore_the_flag_position() {
let help = run(&["-h", "--strict-integrity"]);
assert!(help.status.success());
assert!(String::from_utf8_lossy(&help.stdout).contains("--strict-integrity"));
let version = run(&["--version"]);
assert!(version.status.success());
assert!(String::from_utf8_lossy(&version.stdout).starts_with("Senbei "));
}
#[test]
fn unknown_flag_still_exits_2() {
let output = run(&["--strict"]);
assert_eq!(output.status.code(), Some(2));
let err = String::from_utf8_lossy(&output.stderr);
assert!(err.contains("unknown option '--strict'"));
assert!(String::from_utf8_lossy(&output.stdout).contains("--strict-integrity"));
}
#[test]
fn missing_input_exits_2_with_the_flag() {
let output = run(&["--strict-integrity", "--no-pause"]);
assert_eq!(output.status.code(), Some(2));
assert!(String::from_utf8_lossy(&output.stdout).contains("--strict-integrity"));
}
#[test]
fn errors_exit_1_by_default_and_when_strict_and_write_nothing() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
for extra in [None, Some("--strict-integrity")] {
let out = td
.path()
.join(if extra.is_some() { "strict" } else { "default" });
let mut args = vec![
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--no-log",
"--no-pause",
];
if let Some(flag) = extra {
args.push(flag);
}
let output = run(&args);
assert_eq!(output.status.code(), Some(1), "{output:?}");
assert!(
!out.join("plain.unpack.bin").exists(),
"an error must not leave an artifact"
);
let text = String::from_utf8_lossy(&output.stdout);
assert!(text.contains("0 unpacked"));
assert!(text.contains("1 errors"));
assert!(text.contains("0 suspect"));
}
}
#[test]
fn double_quiet_stays_silent_for_errors_in_both_modes() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
let out = td.path().join("out");
for extra in [None, Some("--strict-integrity")] {
let mut args = vec![
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--no-log",
"--no-pause",
"-q",
"-q",
];
if let Some(flag) = extra {
args.push(flag);
}
let output = run(&args);
assert_eq!(output.status.code(), Some(1));
assert_silent(&output);
}
}
#[test]
fn single_quiet_keeps_the_summary_and_the_error() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
let out = td.path().join("out");
let output = run(&[
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--no-log",
"--no-pause",
"-q",
"--strict-integrity",
]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
// One -q only suppresses per-file lines. The banner and the summary still
// print; that is the existing quiet contract and strict mode must not
// change it.
assert!(stdout.contains("Senbei "), "{stdout}");
assert!(stdout.contains("1 errors"), "{stdout}");
assert!(stdout.contains("0 suspect"), "{stdout}");
}
#[test]
fn strict_does_not_change_what_the_log_records() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
let out = td.path().join("out");
let output = run(&[
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--strict-integrity",
"--no-pause",
"-q",
"-q",
]);
assert_eq!(output.status.code(), Some(1));
assert_silent(&output);
let logs = list_logs(&out);
assert_eq!(logs.len(), 1);
let text = std::fs::read_to_string(&logs[0]).unwrap();
assert!(text.contains("summary:"));
assert!(text.contains("1 errors"));
assert!(text.contains("0 suspect"));
}