use super::*;
use crate::common::diagnostics::{NamErrorCode, NamLogger, SystemSnapshot};
static TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_error_code_display() {
let code = NamErrorCode::NambCrc32Mismatch;
assert_eq!(code.code(), "E1201");
assert_eq!(code.mnemonic(), "NAMB_CRC32_MISMATCH");
assert_eq!(format!("{}", code), "E1201 | NAMB_CRC32_MISMATCH");
}
#[test]
fn test_all_codes_have_unique_numeric() {
use NamErrorCode::*;
let all = [
FileNotFound,
FileReadError,
UnknownExtension,
NamJsonParseError,
NamJsonWeightsExceedLimit,
NamJsonTrainingTooLarge,
NamJsonTrainingTooDeep,
NambCrc32Mismatch,
NambCrc32Missing,
NambInvalidMagic,
NambUnsupportedVersion,
NambTruncated,
UnsupportedArchitecture,
TopologyDetectionFailed,
WeightCountMismatch,
ModelBuildFailed,
PipewireInitFailed,
StreamConnectFailed,
ResamplerBuildFailed,
ResamplerChannelFull,
SchedFifoDenied,
CpuAffinityFailed,
ParamChannelFull,
InvalidGainValue,
UnknownCommand,
CtrlCHandlerFailed,
DeadlineExceeded,
];
let codes: Vec<&str> = all.iter().map(|c| c.code()).collect();
let mut sorted = codes.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
codes.len(),
sorted.len(),
"Duplicate numeric codes detected"
);
}
#[test]
fn test_system_snapshot_capture() {
let snap = SystemSnapshot::capture();
assert!(!snap.version.is_empty());
assert!(!snap.arch.is_empty());
assert!(!snap.os.is_empty());
}
#[test]
fn test_diagnostic_support_block_contains_code() {
let _guard = TEST_MUTEX.lock().unwrap();
let snap = SystemSnapshot::capture();
let diag = NamDiagnostic::new(NamErrorCode::NambCrc32Mismatch, &snap)
.message("Corrupted file")
.hint("Download it again")
.param("expected", "0xDEADBEEF")
.param("computed", "0x12345678");
let block = diag.support_block();
assert!(block.contains("E1201"), "Block must contain the code");
assert!(
block.contains("NAMB_CRC32_MISMATCH"),
"Block must contain the mnemonic"
);
assert!(
block.contains("expected=0xDEADBEEF"),
"Block must contain parameters"
);
assert!(
block.contains("computed=0x12345678"),
"Block must contain parameters"
);
assert!(
block.contains("Copy the block above"),
"Block must contain copy instructions"
);
}
#[test]
fn test_diagnostic_display() {
let snap = SystemSnapshot::capture();
let diag = NamDiagnostic::new(NamErrorCode::FileNotFound, &snap).message("Model not found");
assert_eq!(format!("{}", diag), "[E1100] Model not found");
}
#[test]
fn test_days_to_date_epoch() {
let (y, m, d) = days_to_date(0);
assert_eq!((y, m, d), (1970, 1, 1));
}
#[test]
fn test_days_to_date_known() {
let (y, m, d) = days_to_date(20553);
assert_eq!(y, 2026);
assert_eq!(m, 4);
assert_eq!(d, 10);
}
#[test]
fn test_timestamp_format() {
let ts = NamDiagnostic::timestamp();
assert_eq!(ts.len(), 20, "Timestamp must be 20 characters long");
assert!(ts.ends_with('Z'), "Timestamp must end with Z");
assert_eq!(&ts[4..5], "-", "Date separator");
assert_eq!(&ts[10..11], "T", "T separator");
}
#[test]
fn test_emit_irq_advisory_safety() {
let snap = SystemSnapshot::capture();
snap.emit_irq_advisory(0);
snap.emit_irq_advisory(31);
snap.emit_irq_advisory(32);
snap.emit_irq_advisory(63);
snap.emit_irq_advisory(64); snap.emit_irq_advisory(128); }
#[test]
fn test_diagnostic_bundle_nominal() {
let _guard = TEST_MUTEX.lock().unwrap();
let bundle = DiagnosticBundle::capture();
let rendered = bundle.render();
assert!(
rendered.contains("NAM-rs Diagnostic"),
"Must contain standard header"
);
assert!(
rendered.contains(&format!("nam-rs v{}", bundle.system.version)),
"Must contain version"
);
assert!(!rendered.contains("E1"), "Must not contain error codes");
assert!(
!rendered.contains("CRC32"),
"Must not contain error mnemonics"
);
assert!(rendered.contains("arch="), "Must contain system info");
assert!(rendered.contains("os="), "Must contain system info");
}
#[test]
fn test_diagnostic_bundle_with_error_matches() {
let _guard = TEST_MUTEX.lock().unwrap();
let snap = SystemSnapshot::capture();
let code = NamErrorCode::NambCrc32Mismatch;
let params = vec![
("expected", "0xDEADBEEF".to_string()),
("computed", "0x12345678".to_string()),
];
let diag = NamDiagnostic::new(code, &snap)
.param("expected", "0xDEADBEEF")
.param("computed", "0x12345678");
let bundle = DiagnosticBundle::capture_with_error(code, params);
let block_diag = diag.support_block();
let block_bundle = bundle.render();
let lines_diag: Vec<&str> = block_diag
.lines()
.filter(|l| !l.starts_with("timestamp="))
.collect();
let lines_bundle: Vec<&str> = block_bundle
.lines()
.filter(|l| !l.starts_with("timestamp="))
.collect();
assert_eq!(lines_diag, lines_bundle);
}
#[test]
fn test_diagnostic_bundle_redaction() {
let _guard = TEST_MUTEX.lock().unwrap();
if let Ok(mut name) = ACTIVE_MODEL_NAME.write() {
*name = "/home/user/my_secret_path/model.nam".to_string();
}
ACTIVE_SAMPLE_RATE.store(44100, std::sync::atomic::Ordering::Relaxed);
let bundle_redacted = DiagnosticBundle::capture();
let rendered_redacted = bundle_redacted.render();
assert!(
rendered_redacted.contains("model=model.nam"),
"Should render only basename in redacted mode"
);
assert!(
!rendered_redacted.contains("my_secret_path"),
"Should not expose path details in redacted mode"
);
assert!(
rendered_redacted.contains("sample_rate=44100"),
"Should render active sample rate"
);
let bundle_full = DiagnosticBundle::capture().with_full(true);
let rendered_full = bundle_full.render();
assert!(
rendered_full.contains("model=/home/user/my_secret_path/model.nam"),
"Should render full path in full mode"
);
assert!(
rendered_full.contains("sample_rate=44100"),
"Should render active sample rate"
);
if let Ok(mut name) = ACTIVE_MODEL_NAME.write() {
*name = String::new();
}
ACTIVE_SAMPLE_RATE.store(0, std::sync::atomic::Ordering::Relaxed);
}
#[test]
fn test_diagnostic_bundle_includes_log_trace_when_logger_initialized() {
let _guard = TEST_MUTEX.lock().unwrap();
let _ = NamLogger::init_standalone(log::LevelFilter::Info);
log::info!("Test log message 1");
log::warn!("Test warning with /path/to/file");
log::error!("Test error message");
let bundle = DiagnosticBundle::capture();
let rendered = bundle.render();
assert!(
rendered.contains("──── Recent Log Trace ─────────────────────────"),
"Must contain log trace header when logger is initialized"
);
assert!(
rendered.contains("Test log message 1"),
"Must contain emitted log messages in trace"
);
assert!(
rendered.contains("Test warning"),
"Must contain warning in trace"
);
assert!(
rendered.contains("Test error message"),
"Must contain error in trace"
);
}
#[test]
fn test_diagnostic_bundle_log_trace_redaction() {
let _guard = TEST_MUTEX.lock().unwrap();
let _ = NamLogger::init_standalone(log::LevelFilter::Info);
if let Ok(home) = std::env::var("HOME") {
let test_path = format!("{}/test_model.nam", home);
log::info!("Loading model from {}", test_path);
let bundle_redacted = DiagnosticBundle::capture();
let rendered_redacted = bundle_redacted.render();
assert!(
!rendered_redacted.contains(&test_path),
"Full HOME path should not appear in redacted trace"
);
assert!(
rendered_redacted.contains("~/test_model.nam"),
"HOME path should be redacted to ~/... in trace"
);
let bundle_full = DiagnosticBundle::capture().with_full(true);
let rendered_full = bundle_full.render();
assert!(
rendered_full.contains(&test_path),
"Full HOME path should appear in full trace"
);
}
}