use std::io::Write;
use std::sync::{Mutex, OnceLock};
const MAX_RETAINED: usize = 256;
fn sink() -> &'static Mutex<Vec<String>> {
static SINK: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
SINK.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn warn(message: impl Into<String>) {
let message = message.into();
append_to_file(&message);
if let Ok(mut warnings) = sink().lock() {
if warnings.len() >= MAX_RETAINED {
warnings.remove(0);
}
warnings.push(message);
}
}
#[allow(dead_code)]
pub fn drain() -> Vec<String> {
sink()
.lock()
.map(|mut warnings| std::mem::take(&mut *warnings))
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn count() -> usize {
sink().lock().map(|w| w.len()).unwrap_or(0)
}
fn append_to_file(message: &str) {
let Ok(path) = std::env::var("PROCYON_LOG") else {
return;
};
if path.is_empty() {
return;
}
let line = format!("{} {}\n", chrono::Utc::now().to_rfc3339(), message);
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
let _ = file.write_all(line.as_bytes());
}
}
#[cfg(test)]
pub fn test_lock() -> std::sync::MutexGuard<'static, ()> {
static EXCLUSIVE: Mutex<()> = Mutex::new(());
EXCLUSIVE.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_warning_is_retained_and_drained_once() {
let _guard = test_lock();
drain();
warn("first");
warn("second");
let warnings = drain();
assert_eq!(warnings, vec!["first", "second"]);
assert!(
drain().is_empty(),
"draining twice must not repeat warnings"
);
}
#[test]
fn the_sink_is_bounded() {
let _guard = test_lock();
drain();
for i in 0..MAX_RETAINED + 10 {
warn(format!("w{}", i));
}
assert_eq!(count(), MAX_RETAINED);
drain();
}
}