#![expect(clippy::expect_used)]
use super::*;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock poisoned")
}
fn with_bzr_output<T>(value: Option<&str>, f: impl FnOnce() -> T) -> T {
let _guard = env_lock();
let old = std::env::var("BZR_OUTPUT").ok();
match value {
Some(value) => unsafe { std::env::set_var("BZR_OUTPUT", value) },
None => unsafe { std::env::remove_var("BZR_OUTPUT") },
}
let result = f();
match old {
Some(old) => unsafe { std::env::set_var("BZR_OUTPUT", old) },
None => unsafe { std::env::remove_var("BZR_OUTPUT") },
}
result
}
fn base_cli(command: Commands) -> Cli {
Cli {
server: None,
output: None,
json: false,
no_color: false,
quiet: false,
api: None,
verbose: 0,
command,
}
}
fn dummy_command() -> Commands {
Commands::Whoami {
action: Some(bzr::cli::WhoamiAction::Show),
}
}
#[test]
fn resolve_format_json_flag() {
let mut cli = base_cli(dummy_command());
cli.json = true;
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Json);
}
#[test]
fn resolve_format_output_json() {
let mut cli = base_cli(dummy_command());
cli.output = Some(OutputFormat::Json);
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Json);
}
#[test]
fn resolve_format_output_table() {
let mut cli = base_cli(dummy_command());
cli.output = Some(OutputFormat::Table);
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Table);
}
#[test]
fn resolve_format_json_overrides_output() {
let mut cli = base_cli(dummy_command());
cli.json = true;
cli.output = Some(OutputFormat::Table);
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Json);
}
#[test]
fn resolve_format_output_overrides_env() {
with_bzr_output(Some("json"), || {
let mut cli = base_cli(dummy_command());
cli.output = Some(OutputFormat::Table);
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Table);
});
}
#[test]
fn resolve_format_json_overrides_env() {
with_bzr_output(Some("table"), || {
let mut cli = base_cli(dummy_command());
cli.json = true;
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Json);
});
}
#[test]
fn resolve_format_env_json() {
with_bzr_output(Some("json"), || {
let cli = base_cli(dummy_command());
let fmt = resolve_format(&cli).expect("should resolve");
assert_eq!(fmt, OutputFormat::Json);
});
}
#[test]
fn resolve_format_invalid_env_returns_input_validation_error() {
with_bzr_output(Some("xml"), || {
let cli = base_cli(dummy_command());
let err = resolve_format(&cli).expect_err("invalid format should fail");
assert!(matches!(err, BzrError::InputValidation(_)));
});
}
#[test]
fn tracing_filter_quiet_returns_off() {
assert_eq!(tracing_filter_directive(true, 0, false), Some("off"));
}
#[test]
fn tracing_filter_quiet_overrides_verbose() {
assert_eq!(tracing_filter_directive(true, 3, false), Some("off"));
}
#[test]
fn tracing_filter_quiet_overrides_rust_log() {
assert_eq!(tracing_filter_directive(true, 0, true), Some("off"));
}
#[test]
fn tracing_filter_rust_log_defers() {
assert_eq!(tracing_filter_directive(false, 0, true), None);
}
#[test]
fn tracing_filter_default_warn() {
assert_eq!(tracing_filter_directive(false, 0, false), Some("bzr=warn"));
}
#[test]
fn tracing_filter_verbose_levels() {
assert_eq!(tracing_filter_directive(false, 1, false), Some("bzr=info"));
assert_eq!(tracing_filter_directive(false, 2, false), Some("bzr=debug"));
assert_eq!(tracing_filter_directive(false, 3, false), Some("bzr=trace"));
assert_eq!(
tracing_filter_directive(false, 10, false),
Some("bzr=trace")
);
}
#[test]
fn resolve_format_falls_back_to_tty_detection() {
with_bzr_output(None, || {
let cli = base_cli(dummy_command());
let fmt = resolve_format(&cli).expect("should resolve");
assert!(matches!(fmt, OutputFormat::Json | OutputFormat::Table));
});
}
#[test]
fn exit_code_maps_known_error_to_exit_code() {
let err = BzrError::InputValidation("bad input".into());
let code = exit_code(&err);
let rendered = format!("{code:?}");
assert!(
rendered.contains(&err.exit_code().to_string()),
"expected ExitCode debug to include {}, got {rendered}",
err.exit_code()
);
}
#[test]
fn exit_code_maps_other_variant() {
let err = BzrError::Other("boom".into());
let code = exit_code(&err);
let rendered = format!("{code:?}");
assert!(rendered.contains(&err.exit_code().to_string()));
}
#[test]
fn exit_code_maps_highest_tls_code() {
let err = BzrError::PinMismatch {
server: "example.com".into(),
expected: "sha256//old".into(),
actual: "sha256//new".into(),
};
assert_eq!(err.exit_code(), 13);
let code = exit_code(&err);
let rendered = format!("{code:?}");
assert!(
rendered.contains("13"),
"expected ExitCode debug to include 13, got {rendered}"
);
}
#[test]
fn format_dispatch_error_renders_json() {
let err = BzrError::Config("bad config".into());
let out = format_dispatch_error(&err, OutputFormat::Json);
let parsed: serde_json::Value = serde_json::from_str(&out).expect("output must be valid JSON");
let inner = parsed.get("error").expect("has error key");
assert_eq!(inner["type"], err.error_type());
assert_eq!(inner["exit_code"], err.exit_code());
assert!(
inner["message"]
.as_str()
.expect("message is string")
.contains("bad config"),
"message should contain underlying error: {out}"
);
}
#[test]
fn format_dispatch_error_renders_table() {
let err = BzrError::Config("bad config".into());
let out = format_dispatch_error(&err, OutputFormat::Table);
assert!(out.starts_with("error:"), "got {out:?}");
assert!(out.contains("bad config"), "got {out:?}");
}
#[test]
fn format_dispatch_error_table_for_other_error() {
let err = BzrError::Other("kaboom".into());
let out = format_dispatch_error(&err, OutputFormat::Table);
assert!(out.contains("kaboom"));
}
#[cfg(unix)]
#[test]
fn suppress_stdout_redirects_fd1_to_devnull() {
use std::io::{Read, Seek};
use std::os::unix::io::AsRawFd;
extern "C" {
fn dup(fd: std::ffi::c_int) -> std::ffi::c_int;
fn dup2(oldfd: std::ffi::c_int, newfd: std::ffi::c_int) -> std::ffi::c_int;
fn close(fd: std::ffi::c_int) -> std::ffi::c_int;
fn write(fd: std::ffi::c_int, buf: *const u8, count: usize) -> isize;
}
let _guard = env_lock();
let tmp = tempfile::NamedTempFile::new().expect("tmpfile");
let tmp_fd = tmp.as_file().as_raw_fd();
let saved = unsafe { dup(1) };
assert!(saved >= 0, "dup(1) failed");
unsafe {
dup2(tmp_fd, 1);
}
suppress_stdout();
let marker = b"MARKER_AFTER_SUPPRESS";
unsafe {
write(1, marker.as_ptr(), marker.len());
}
unsafe {
dup2(saved, 1);
close(saved);
}
let mut captured = Vec::new();
let mut f = tmp.reopen().expect("reopen");
f.seek(std::io::SeekFrom::Start(0)).expect("seek");
f.read_to_end(&mut captured).expect("read");
let captured_str = String::from_utf8_lossy(&captured);
assert!(
!captured_str.contains("MARKER_AFTER_SUPPRESS"),
"suppress_stdout did not redirect fd 1; marker reached temp file: {captured_str:?}"
);
}