use fast_observe::deploy::{DeploymentConfig, InitError, InitGuard, observe};
use std::assert_matches;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, Once};
static GUARD: Mutex<Option<InitGuard>> = Mutex::new(None);
static INIT: Once = Once::new();
#[allow(clippy::expect_used, reason = "test")]
fn ensure_init() -> bool {
let mut ours = false;
INIT.call_once(|| {
let guard = observe().init().expect("first init must succeed");
*GUARD.lock().expect("guard mutex not poisoned") = Some(guard);
ours = true;
});
ours
}
#[test]
#[allow(clippy::expect_used, reason = "test")]
fn init_twice_second_errors() {
ensure_init();
let second = observe().init();
let err = second.err().expect("second init must fail");
assert_eq!(err, InitError::AlreadyInitialized);
}
static PREVIOUS_HOOK_RAN: AtomicBool = AtomicBool::new(false);
#[test]
#[allow(
clippy::expect_used,
clippy::panic,
reason = "deliberate panic to verify panic-hook chaining"
)]
fn panic_hook_chains() {
std::panic::set_hook(Box::new(|_| {
PREVIOUS_HOOK_RAN.store(true, Ordering::SeqCst);
}));
let ours = ensure_init();
let result = std::panic::catch_unwind(|| std::panic::panic_any("deliberate test panic"));
assert_matches!(result, Err(_), "catch_unwind must observe the panic");
if ours {
assert!(
PREVIOUS_HOOK_RAN.load(Ordering::SeqCst),
"deployment panic hook must chain to the previously installed hook"
);
}
}
fn full_config() -> DeploymentConfig {
DeploymentConfig {
level: Some("debug".to_owned()),
stdout: Some(false),
layout: Some("json".to_owned()),
file_from_env: Some(true),
backends: Some("fastrace,tracy".to_owned()),
error_hook_throttle: Some(7),
traces: Some("off".to_owned()),
panic_hook: Some(false),
flush_on_exit: Some(false),
syslog: Some(true),
journald: Some(true),
async_append: Some(true),
task_local_diagnostic: Some(true),
rust_log_filter: Some(true),
}
}
#[test]
#[allow(clippy::expect_used, reason = "test")]
fn config_apply_sets_fields() {
let _deployment = full_config()
.apply(observe())
.expect("all-Some valid config must apply");
let cfg = DeploymentConfig {
level: Some("warn".to_owned()),
..DeploymentConfig::default()
};
let _deployment = fast_observe::Deployment::from_config(cfg)
.expect("valid config must apply via from_config");
}
#[test]
#[allow(clippy::expect_used, reason = "test")]
fn config_apply_rejects_bad_values() {
let bad_level = DeploymentConfig {
level: Some("bogus".to_owned()),
..DeploymentConfig::default()
};
let errors = bad_level
.apply(observe())
.err()
.expect("bogus level must fail");
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].field, "level");
assert_eq!(errors[0].value, "bogus");
let bad_backends = DeploymentConfig {
backends: Some("nope".to_owned()),
..DeploymentConfig::default()
};
let errors = bad_backends
.apply(observe())
.err()
.expect("bogus backends must fail");
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].field, "backends");
assert_eq!(errors[0].value, "nope");
let both_bad = DeploymentConfig {
level: Some("bogus".to_owned()),
backends: Some("nope".to_owned()),
..DeploymentConfig::default()
};
let errors = both_bad
.apply(observe())
.err()
.expect("two bogus fields must fail");
assert_eq!(errors.len(), 2, "errors collected, not first-wins");
assert!(errors.iter().any(|e| e.field == "level"));
assert!(errors.iter().any(|e| e.field == "backends"));
}
#[test]
#[allow(clippy::expect_used, reason = "test")]
fn config_apply_none_fields_are_noops() {
let _deployment = DeploymentConfig::default()
.apply(observe())
.expect("all-None config must apply");
}
#[cfg(feature = "serde")]
#[test]
#[allow(clippy::expect_used, reason = "test")]
fn config_roundtrip_json() {
let cfg = full_config();
let json = serde_json::to_string(&cfg).expect("serialize");
let back: DeploymentConfig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(cfg, back);
let bogus: Result<DeploymentConfig, _> = serde_json::from_str(r#"{"levl": "info"}"#);
assert_matches!(bogus, Err(_), "unknown field `levl` must be rejected");
}