use colored::Colorize;
use std::env;
use std::io::IsTerminal;
pub struct StatusReporter {
silent: bool,
use_color: bool,
}
impl StatusReporter {
pub fn new(silent: bool) -> Self {
Self {
silent,
use_color: Self::should_use_color(),
}
}
fn should_use_color() -> bool {
if env::var("NO_COLOR").is_ok() {
return false;
}
if let Ok(term) = env::var("TERM")
&& term == "dumb"
{
return false;
}
std::io::stderr().is_terminal()
}
pub fn operation(&self, operation: &str, context: &str) {
if !self.silent {
println!("{operation} {context}...");
}
}
pub fn step(&self, message: &str) {
if !self.silent {
println!(" {message}");
}
}
pub fn success(&self, message: &str) {
if !self.silent {
let symbol = if self.use_color {
"✓".green().bold().to_string()
} else {
"[OK]".to_string()
};
println!("{symbol} {message}");
}
}
pub fn error(&self, message: &str) {
let symbol = if self.use_color {
"✗".red().bold().to_string()
} else {
"[ERROR]".to_string()
};
eprintln!("{symbol} {message}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
vars: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn new() -> Self {
Self { vars: Vec::new() }
}
fn set(&mut self, key: &str, value: &str) {
let old = env::var(key).ok();
self.vars.push((key.to_string(), old));
unsafe {
env::set_var(key, value);
}
}
fn remove(&mut self, key: &str) {
let old = env::var(key).ok();
self.vars.push((key.to_string(), old));
unsafe {
env::remove_var(key);
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, value) in self.vars.iter().rev() {
match value {
Some(v) => unsafe { env::set_var(key, v) },
None => unsafe { env::remove_var(key) },
}
}
}
}
static OUTPUT: Mutex<Vec<String>> = Mutex::new(Vec::new());
static ERROR_OUTPUT: Mutex<Vec<String>> = Mutex::new(Vec::new());
pub struct TestReporter {
inner: StatusReporter,
}
impl TestReporter {
pub fn new(silent: bool) -> Self {
Self {
inner: StatusReporter::new(silent),
}
}
pub fn operation(&self, operation: &str, context: &str) {
if !self.inner.silent {
OUTPUT
.lock()
.unwrap()
.push(format!("{operation} {context}..."));
}
}
pub fn step(&self, message: &str) {
if !self.inner.silent {
OUTPUT.lock().unwrap().push(format!(" {message}"));
}
}
pub fn success(&self, message: &str) {
if !self.inner.silent {
let symbol = if self.inner.use_color {
"✓".green().bold().to_string()
} else {
"[OK]".to_string()
};
OUTPUT.lock().unwrap().push(format!("{symbol} {message}"));
}
}
pub fn error(&self, message: &str) {
let symbol = if self.inner.use_color {
"✗".red().bold().to_string()
} else {
"[ERROR]".to_string()
};
ERROR_OUTPUT
.lock()
.unwrap()
.push(format!("{symbol} {message}"));
}
pub fn get_output() -> Vec<String> {
OUTPUT.lock().unwrap().clone()
}
pub fn get_error_output() -> Vec<String> {
ERROR_OUTPUT.lock().unwrap().clone()
}
pub fn clear_output() {
OUTPUT.lock().unwrap().clear();
ERROR_OUTPUT.lock().unwrap().clear();
}
}
#[test]
fn test_message_formatting() {
TestReporter::clear_output();
let reporter = TestReporter::new(false);
reporter.operation("Installing", "temurin@21");
reporter.step("Downloading JDK");
reporter.success("Installation complete");
let output = TestReporter::get_output();
assert_eq!(output.len(), 3);
assert_eq!(output[0], "Installing temurin@21...");
assert_eq!(output[1], " Downloading JDK");
assert!(output[2].contains("Installation complete"));
}
#[test]
fn test_silent_mode() {
TestReporter::clear_output();
let reporter = TestReporter::new(true);
reporter.operation("Installing", "JDK");
reporter.step("Step 1");
reporter.success("Done");
let output = TestReporter::get_output();
assert_eq!(output.len(), 0);
reporter.error("Something went wrong");
let error_output = TestReporter::get_error_output();
assert_eq!(error_output.len(), 1);
assert!(error_output[0].contains("Something went wrong"));
}
#[test]
fn test_error_always_shown() {
TestReporter::clear_output();
let silent_reporter = TestReporter::new(true);
silent_reporter.error("Error in silent mode");
let error_output = TestReporter::get_error_output();
assert!(
error_output
.iter()
.any(|s| s.contains("Error in silent mode")),
"Silent mode should still show errors"
);
TestReporter::clear_output();
let normal_reporter = TestReporter::new(false);
normal_reporter.error("Error in normal mode");
let error_output = TestReporter::get_error_output();
assert!(
error_output
.iter()
.any(|s| s.contains("Error in normal mode")),
"Normal mode should show errors"
);
}
#[test]
fn test_should_use_color_with_no_color_env() {
let _guard = ENV_LOCK.lock().unwrap();
let mut env_guard = EnvGuard::new();
env_guard.set("NO_COLOR", "1");
assert!(!StatusReporter::should_use_color());
}
#[test]
fn test_should_use_color_with_dumb_term() {
let _guard = ENV_LOCK.lock().unwrap();
let mut env_guard = EnvGuard::new();
env_guard.set("TERM", "dumb");
env_guard.remove("NO_COLOR");
assert!(!StatusReporter::should_use_color());
}
#[test]
fn test_color_symbols() {
let _guard = ENV_LOCK.lock().unwrap();
let mut env_guard = EnvGuard::new();
env_guard.remove("NO_COLOR");
env_guard.set("TERM", "xterm-256color");
TestReporter::clear_output();
let test_reporter = TestReporter {
inner: StatusReporter {
silent: false,
use_color: true, },
};
test_reporter.success("With color");
let output = TestReporter::get_output();
assert!(output[0].contains("✓"));
TestReporter::clear_output();
let test_reporter = TestReporter {
inner: StatusReporter {
silent: false,
use_color: false, },
};
test_reporter.success("Without color");
let output = TestReporter::get_output();
assert!(output[0].starts_with("[OK]"));
}
#[test]
fn test_error_symbols() {
TestReporter::clear_output();
let test_reporter = TestReporter {
inner: StatusReporter {
silent: false,
use_color: true,
},
};
test_reporter.error("Color error");
let output = TestReporter::get_error_output();
assert!(output[0].contains("✗"));
TestReporter::clear_output();
let test_reporter = TestReporter {
inner: StatusReporter {
silent: false,
use_color: false,
},
};
test_reporter.error("No color error");
let output = TestReporter::get_error_output();
assert!(output[0].starts_with("[ERROR]"));
}
#[test]
fn test_environment_detection() {
let _guard = ENV_LOCK.lock().unwrap();
let mut env_guard = EnvGuard::new();
env_guard.remove("NO_COLOR");
env_guard.remove("CI");
env_guard.set("TERM", "xterm-256color");
let _result = StatusReporter::should_use_color();
}
#[test]
fn test_message_consistency() {
TestReporter::clear_output();
let reporter = TestReporter::new(false);
reporter.operation("Processing", "batch job");
reporter.step("Step 1: Initialize");
reporter.step("Step 2: Process data");
reporter.step("Step 3: Cleanup");
reporter.success("Batch job completed successfully");
let output = TestReporter::get_output();
assert_eq!(output.len(), 5);
assert_eq!(output[0], "Processing batch job...");
assert_eq!(output[1], " Step 1: Initialize");
assert_eq!(output[2], " Step 2: Process data");
assert_eq!(output[3], " Step 3: Cleanup");
assert!(output[4].contains("Batch job completed successfully"));
}
}