const TUI_AREAS: &[&str] = &["src/app", "src/ui", "src/aws"];
const PRINT_MACROS: &[&str] = &["println!", "eprintln!", "print!(", "eprint!("];
fn print_sites_under(areas: &[&str]) -> Vec<String> {
let mut hits = Vec::new();
for m in PRINT_MACROS {
for site in super::scan::find_in_production(m) {
if areas.iter().any(|a| site.starts_with(a)) {
hits.push(format!("{site} — {m}"));
}
}
}
hits.sort();
hits
}
#[test]
fn the_tui_never_prints_to_the_terminal() {
let sites = print_sites_under(TUI_AREAS);
assert!(
sites.is_empty(),
"ARCHITECTURE rule 5: the alternate screen swallows these and they \
corrupt the display. Use `tracing::*` instead — output goes to \
~/.cache/ebman/ebman.log.\n{}",
sites.join("\n")
);
}
#[test]
fn the_print_detector_actually_detects_prints() {
let cli = print_sites_under(&["src/cli"]);
assert!(
cli.len() > 20,
"the CLI prints by design, so a detector that finds {} sites there is \
not working — which would make the rule-5 guard above vacuous",
cli.len()
);
}
#[test]
fn the_tui_areas_exist_and_hold_source() {
let files = super::scan::source_files();
for area in TUI_AREAS {
let n = files.iter().filter(|(p, _)| p.starts_with(area)).count();
assert!(
n > 0,
"{area} matched no source files — the rule-5 guard is looking at \
nothing. Did a module move?"
);
}
}
const TERMINAL_HANDLES: &[&str] = &["stdout()", "stderr()"];
const HANDLE_EXCEPTIONS: &[(&str, usize, &str)] = &[(
"src/app/spawn_refresh.rs",
1,
"BEL (0x07), written to ring the terminal bell when a new env goes red. \
A control character rather than display text, so it cannot corrupt the \
alternate screen — which is the whole of what rule 5 protects.",
)];
fn handle_sites() -> std::collections::BTreeMap<String, usize> {
let mut per_file: std::collections::BTreeMap<String, usize> = Default::default();
for h in TERMINAL_HANDLES {
for site in super::scan::find_in_production(h) {
let path = site
.rsplit_once(':')
.map_or(site.clone(), |(p, _)| p.to_string());
if TUI_AREAS.iter().any(|a| path.starts_with(a)) {
*per_file.entry(path).or_default() += 1;
}
}
}
per_file
}
#[test]
fn every_direct_terminal_write_in_the_tui_is_justified() {
let sites = handle_sites();
let mut problems = Vec::new();
for (path, found) in &sites {
match HANDLE_EXCEPTIONS.iter().find(|(p, _, _)| p == path) {
None => problems.push(format!(
"{path}: {found} direct terminal write(s) with no justification. \
Rule 5 — use `tracing::*`, or add an entry here saying why this \
one cannot corrupt the alternate screen."
)),
Some((_, expected, why)) if expected != found => problems.push(format!(
"{path}: {found} direct terminal write(s), but the allowlist \
justifies {expected}. The justification on record is: {why}"
)),
Some(_) => {}
}
}
for (path, _, _) in HANDLE_EXCEPTIONS {
if !sites.contains_key(*path) {
problems.push(format!(
"{path} is allowlisted for a direct terminal write but has none \
— drop the entry."
));
}
}
assert!(problems.is_empty(), "{}", problems.join("\n"));
}