use core::cell::RefCell;
use core::marker::PhantomData;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
const LIMIT: usize = 64;
#[derive(Clone, Debug)]
pub(crate) struct Run {
pending: Arc<Mutex<Pending>>,
}
impl Run {
pub(crate) fn new() -> Self {
Self {
pending: Arc::new(Mutex::new(Pending::new())),
}
}
}
thread_local! {
static ACTIVE: RefCell<Option<Run>> = const { RefCell::new(None) };
}
pub(crate) fn enter(run: Option<&Run>) -> Scope {
let previous = ACTIVE.with(|active| active.replace(run.cloned()));
Scope {
previous,
not_send: PhantomData,
}
}
pub(crate) struct Scope {
previous: Option<Run>,
not_send: PhantomData<Rc<()>>,
}
impl Drop for Scope {
fn drop(&mut self) {
ACTIVE.with(|active| {
let _previous = active.replace(self.previous.take());
});
}
}
pub(crate) fn current() -> Option<Run> {
ACTIVE.with(|active| active.borrow().clone())
}
#[derive(Debug)]
struct Pending {
lines: Vec<String>,
dropped: usize,
}
impl Pending {
const fn new() -> Self {
Self {
lines: Vec::new(),
dropped: 0,
}
}
}
pub(crate) fn note(message: impl Into<String>) {
let Some(run) = current() else {
return;
};
let Ok(mut pending) = run.pending.lock() else {
return;
};
if pending.lines.len() < LIMIT {
pending.lines.push(message.into());
} else {
pending.dropped += 1;
}
}
#[must_use]
pub(crate) fn drain() -> Vec<String> {
let Some(run) = current() else {
return Vec::new();
};
let Ok(mut pending) = run.pending.lock() else {
return Vec::new();
};
let mut lines = core::mem::take(&mut pending.lines);
let dropped = core::mem::take(&mut pending.dropped);
if dropped > 0 {
lines.push(format!("and {} more like these", crate::report::quantity(dropped, "diagnostic")));
}
lines
}
#[cfg(test)]
pub(crate) fn alone<T>(body: impl FnOnce() -> T) -> T {
static EXCLUSIVE: Mutex<()> = Mutex::new(());
let _guard = EXCLUSIVE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let run = Run::new();
let _scope = enter(Some(&run));
let _discarded = drain();
let outcome = body();
let _discarded = drain();
outcome
}
#[cfg(test)]
mod tests {
use std::sync::Barrier;
use super::*;
#[test]
fn notes_come_back_in_the_order_they_were_raised() {
alone(|| {
note("first");
note("second");
assert_eq!(drain(), vec!["first".to_owned(), "second".to_owned()]);
});
}
#[test]
fn draining_empties_the_buffer() {
alone(|| {
note("said once");
assert_eq!(drain().len(), 1);
assert!(drain().is_empty());
});
}
#[test]
fn notes_past_the_cap_are_counted_rather_than_kept() {
alone(|| {
for index in 0..LIMIT + 5 {
note(format!("note {index}"));
}
let drained = drain();
assert_eq!(drained.len(), LIMIT + 1);
assert_eq!(drained[0], "note 0");
assert_eq!(drained[LIMIT - 1], format!("note {}", LIMIT - 1));
assert_eq!(drained[LIMIT], "and 5 diagnostics more like these");
});
}
#[test]
fn a_process_that_raised_nothing_drains_nothing() {
alone(|| assert!(drain().is_empty()));
}
#[test]
fn concurrent_runs_drain_only_their_own_notes() {
let first = Run::new();
let second = Run::new();
let ready = Barrier::new(2);
std::thread::scope(|scope| {
let first_ready = &ready;
let first = first.clone();
let left = scope.spawn(move || {
let _scope = enter(Some(&first));
note("first");
let _ready = first_ready.wait();
drain()
});
let second_ready = &ready;
let second = second.clone();
let right = scope.spawn(move || {
let _scope = enter(Some(&second));
note("second");
let _ready = second_ready.wait();
drain()
});
assert_eq!(left.join().expect("first run"), vec!["first"]);
assert_eq!(right.join().expect("second run"), vec!["second"]);
});
}
}