use std::cell::Cell;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Level {
Quiet,
Normal,
Verbose,
}
impl Level {
const fn as_u8(self) -> u8 {
match self {
Level::Quiet => 0,
Level::Normal => 1,
Level::Verbose => 2,
}
}
const fn from_u8(v: u8) -> Level {
match v {
0 => Level::Quiet,
2 => Level::Verbose,
_ => Level::Normal,
}
}
}
#[must_use]
pub fn resolve(quiet: bool, verbose: bool) -> Level {
if quiet {
Level::Quiet
} else if verbose {
Level::Verbose
} else {
Level::Normal
}
}
static PROCESS_LEVEL: AtomicU8 = AtomicU8::new(Level::Normal.as_u8());
static PROCESS_JSON: AtomicBool = AtomicBool::new(false);
thread_local! {
static THREAD_LEVEL: Cell<Option<Level>> = const { Cell::new(None) };
static THREAD_JSON: Cell<Option<bool>> = const { Cell::new(None) };
}
pub fn latch(quiet: bool, verbose: bool, json: bool) {
match resolve(quiet, verbose) {
Level::Normal => {}
level => PROCESS_LEVEL.store(level.as_u8(), Ordering::SeqCst),
}
if json {
PROCESS_JSON.store(true, Ordering::SeqCst);
}
}
pub struct VerbosityScope(Option<Level>, Option<bool>);
impl Drop for VerbosityScope {
fn drop(&mut self) {
THREAD_LEVEL.with(|c| c.set(self.0));
THREAD_JSON.with(|c| c.set(self.1));
}
}
#[must_use]
pub fn scope(level: Level, json: bool) -> VerbosityScope {
let prev_level = THREAD_LEVEL.with(|c| c.replace(Some(level)));
let prev_json = THREAD_JSON.with(|c| c.replace(Some(json)));
VerbosityScope(prev_level, prev_json)
}
#[must_use]
pub fn level() -> Level {
if let Some(l) = THREAD_LEVEL.with(Cell::get) {
return l;
}
Level::from_u8(PROCESS_LEVEL.load(Ordering::SeqCst))
}
#[must_use]
pub fn json_enabled() -> bool {
if let Some(j) = THREAD_JSON.with(Cell::get) {
return j;
}
PROCESS_JSON.load(Ordering::SeqCst)
}
#[must_use]
pub fn is_quiet() -> bool {
level() == Level::Quiet
}
#[must_use]
pub fn is_verbose() -> bool {
level() == Level::Verbose
}
#[must_use]
pub fn stdout_suppressed() -> bool {
!json_enabled() && is_quiet()
}
#[must_use]
pub fn preamble_lines(
version: &str,
offline: bool,
skip_contract: bool,
paths: &[std::path::PathBuf],
) -> Vec<String> {
let mut out = vec![format!("verbose: apr {version}")];
out.push(format!(
"verbose: offline = {}",
if offline { "on" } else { "off" }
));
if skip_contract {
out.push("verbose: contract gate = skipped (--skip-contract)".to_string());
} else if paths.is_empty() {
out.push(
"verbose: contract gate = not applicable (no gated model path for this command)"
.to_string(),
);
} else {
out.push(format!(
"verbose: contract gate = enforced over {} path(s)",
paths.len()
));
}
for p in paths {
let size = std::fs::metadata(p).map_or_else(
|_| "unreadable".to_string(),
|m| format!("{} bytes", m.len()),
);
out.push(format!("verbose: model = {} ({size})", p.display()));
}
out
}
macro_rules! println {
() => {
if !$crate::verbosity::stdout_suppressed() { ::std::println!() }
};
($($arg:tt)*) => {
if !$crate::verbosity::stdout_suppressed() { ::std::println!($($arg)*) }
};
}
macro_rules! print {
($($arg:tt)*) => {
if !$crate::verbosity::stdout_suppressed() { ::std::print!($($arg)*) }
};
}
macro_rules! vprintln {
($($arg:tt)*) => {
if $crate::verbosity::is_verbose() { ::std::println!($($arg)*) }
};
}
macro_rules! emitln {
() => { ::std::println!() };
($($arg:tt)*) => { ::std::println!($($arg)*) };
}
macro_rules! emit {
($($arg:tt)*) => { ::std::print!($($arg)*) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quiet_wins_over_verbose() {
assert_eq!(resolve(true, true), Level::Quiet);
assert_eq!(resolve(true, false), Level::Quiet);
assert_eq!(resolve(false, true), Level::Verbose);
assert_eq!(resolve(false, false), Level::Normal);
}
#[test]
fn default_level_prints() {
let _s = scope(Level::Normal, false);
assert!(!stdout_suppressed());
assert!(!is_quiet());
assert!(!is_verbose());
}
#[test]
fn quiet_suppresses_stdout() {
let _s = scope(Level::Quiet, false);
assert!(stdout_suppressed(), "--quiet must suppress ordinary stdout");
}
#[test]
fn json_survives_quiet() {
let _s = scope(Level::Quiet, true);
assert!(
!stdout_suppressed(),
"--json --quiet must still emit the JSON document"
);
}
#[test]
fn verbose_does_not_suppress() {
let _s = scope(Level::Verbose, false);
assert!(!stdout_suppressed());
assert!(is_verbose());
}
#[test]
fn scope_restores_previous_level() {
let baseline = level();
{
let _s = scope(Level::Quiet, false);
assert_eq!(level(), Level::Quiet);
}
assert_eq!(level(), baseline, "scope must restore on drop");
}
#[test]
fn preamble_reports_the_gate_decision_not_a_fixed_string() {
let none: Vec<std::path::PathBuf> = vec![];
let skipped = preamble_lines("9.9.9", false, true, &none);
let inapplicable = preamble_lines("9.9.9", false, false, &none);
let enforced = preamble_lines("9.9.9", true, false, &[std::path::PathBuf::from("/x.apr")]);
assert!(
skipped.iter().any(|l| l.contains("--skip-contract")),
"--skip-contract must be visible under --verbose, got {skipped:?}"
);
assert!(
inapplicable.iter().any(|l| l.contains("not applicable")),
"a command the gate exempts must say so rather than stay mute, got {inapplicable:?}"
);
assert!(
enforced.iter().any(|l| l.contains("enforced over 1 path")),
"an enforced gate must report its paths, got {enforced:?}"
);
assert!(
enforced.iter().any(|l| l.contains("/x.apr")),
"the resolved model path must be reported, got {enforced:?}"
);
assert!(
enforced.iter().any(|l| l.contains("offline = on")),
"--offline must be visible under --verbose, got {enforced:?}"
);
assert_ne!(
skipped, inapplicable,
"the three gate outcomes must be distinguishable"
);
}
const CHILD_ENV: &str = "APR_VERBOSITY_LATCH_CHILD";
const BEGIN: &str = "<<<APR-2401-BEGIN>>>";
const END: &str = "<<<APR-2401-END>>>";
const TEST_PATH: &str =
"verbosity::tests::quiet_and_verbose_reach_a_command_that_never_receives_them";
const OBS_ENV: &str = "APR_VERBOSITY_OBS_FILE";
fn parent_observation_file() -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!("apr-2401-obs-{}.json", std::process::id()));
std::fs::write(&p, r#"{"output":"{\"a\":1}","finish_reason":"stop"}"#)
.expect("write observation file");
p
}
fn child_runs_gbnf_lint(mode: &str) {
let mode = mode.to_string();
std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || {
use clap::Parser;
let obs = std::env::var(OBS_ENV).expect("parent must hand down the same obs file");
let mut argv = vec!["apr", "gbnf-lint", "--observation-file", obs.as_str()];
match mode.as_str() {
"quiet" => argv.push("--quiet"),
"verbose" => argv.push("--verbose"),
_ => {}
}
let cli = crate::Cli::parse_from(argv);
::std::println!("{BEGIN}");
crate::execute_command(&cli)
.expect("gbnf-lint on a well-formed observation must succeed");
::std::println!("{END}");
})
.expect("spawn")
.join()
.expect("gbnf-lint verbosity falsifier panicked");
}
fn run_child(mode: &str, obs: &std::path::Path) -> String {
let exe = std::env::current_exe().expect("current test binary");
let out = std::process::Command::new(exe)
.args(["--exact", TEST_PATH, "--nocapture", "--test-threads=1"])
.env(CHILD_ENV, mode)
.env(OBS_ENV, obs)
.output()
.expect("re-run this test binary as a child");
assert!(
out.status.success(),
"child ({mode}) failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let all = String::from_utf8_lossy(&out.stdout).into_owned();
let start = all
.find(BEGIN)
.map(|i| i + BEGIN.len())
.unwrap_or_else(|| panic!("child ({mode}) never reached the command; got:\n{all}"));
let end = all[start..]
.find(END)
.unwrap_or_else(|| panic!("child ({mode}) never finished the command; got:\n{all}"));
all[start..start + end].trim().to_string()
}
#[test]
fn quiet_and_verbose_reach_a_command_that_never_receives_them() {
if let Ok(mode) = std::env::var(CHILD_ENV) {
child_runs_gbnf_lint(&mode);
return;
}
let obs = parent_observation_file();
let normal = run_child("normal", &obs);
let quiet = run_child("quiet", &obs);
let verbose = run_child("verbose", &obs);
let _ = std::fs::remove_file(&obs);
assert!(
normal.contains("gbnf-lint report"),
"control run must print the report, got:\n{normal}"
);
assert!(
quiet.is_empty(),
"--quiet must suppress the PASS report as its own help text promises \
(`Quiet mode (errors only)`); `apr gbnf-lint -q` still printed:\n{quiet}"
);
assert_ne!(
normal, verbose,
"--verbose must not be a byte-for-byte no-op; it was on 13 of 16 \
sampled commands in v0.63.0"
);
assert!(
verbose.contains("verbose: contract gate ="),
"--verbose must report the dispatcher's gate decision, got:\n{verbose}"
);
}
}