use keyhog::exit_codes::{
help, DEFINITIONS, EXIT_REQUIRE_GPU_UNMET, EXIT_SOURCE_FAILED, EXIT_USER_ERROR,
};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
#[test]
fn scan_reachable_exit_codes_are_unique() {
let mut seen = BTreeMap::new();
for definition in DEFINITIONS.iter().filter(|d| d.scan_reachable) {
if let Some(previous) = seen.insert(definition.code, definition.label) {
panic!(
"scan-reachable exit code {} is reused by both {:?} and {:?}",
definition.code, previous, definition.label
);
}
}
}
#[test]
fn user_gpu_and_source_failures_have_distinct_codes() {
assert_ne!(EXIT_USER_ERROR, EXIT_REQUIRE_GPU_UNMET);
assert_ne!(EXIT_USER_ERROR, EXIT_SOURCE_FAILED);
assert_ne!(EXIT_REQUIRE_GPU_UNMET, EXIT_SOURCE_FAILED);
}
#[test]
fn help_text_names_every_owned_exit_code() {
for definition in DEFINITIONS {
assert!(
help().contains(&definition.code.to_string()),
"exit help omits owned code {} ({})",
definition.code,
definition.label
);
}
}
#[test]
fn production_code_does_not_construct_numeric_exit_codes_inline() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let roots = [manifest.join("src"), manifest.join("../scanner/src")];
let mut violations = Vec::new();
for src_root in roots {
for path in rust_sources(&src_root) {
let rel = path.strip_prefix(manifest).unwrap_or(&path);
let text = fs::read_to_string(&path).unwrap_or_else(|err| {
panic!("read {} for exit-code owner check: {err}", path.display())
});
for (line_idx, line) in text.lines().enumerate() {
if contains_inline_numeric_exit_code(line) {
violations.push(format!("{}:{}", rel.display(), line_idx + 1));
}
}
}
}
assert!(
violations.is_empty(),
"production code constructs numeric exit codes outside exit_codes.rs: {}",
violations.join(", ")
);
}
#[test]
fn scanner_library_contains_no_process_exit() {
let scanner_src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scanner/src");
let mut offenders = Vec::new();
for path in rust_sources(&scanner_src) {
let rel = path.strip_prefix(&scanner_src).unwrap_or(&path);
let text = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read scanner source {}: {err}", path.display()));
for (line_idx, line) in text.lines().enumerate() {
if line.contains("std::process::exit(") || line.contains("process::exit(") {
offenders.push(format!("{}:{}", rel.display(), line_idx + 1));
}
}
}
assert!(
offenders.is_empty(),
"scanner library must return structured errors instead of exiting: {}",
offenders.join(", ")
);
}
fn rust_sources(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(path) = stack.pop() {
let entries = fs::read_dir(&path)
.unwrap_or_else(|err| panic!("read source directory {}: {err}", path.display()));
for entry in entries {
let entry = entry.unwrap_or_else(|err| panic!("read source entry: {err}"));
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|ext| ext == "rs") {
out.push(path);
}
}
}
out.sort();
out
}
fn contains_inline_numeric_exit_code(line: &str) -> bool {
numeric_call_arg_starts_with_digit(line, "ExitCode::from(")
|| numeric_call_arg_starts_with_digit(line, "std::process::exit(")
|| numeric_call_arg_starts_with_digit(line, "process::exit(")
}
fn numeric_call_arg_starts_with_digit(line: &str, call: &str) -> bool {
let mut tail = line;
while let Some(idx) = tail.find(call) {
let after_call = &tail[idx + call.len()..];
let first = after_call.trim_start().chars().next();
if first.is_some_and(|ch| ch.is_ascii_digit()) {
return true;
}
tail = after_call;
}
false
}