assert_call/
records.rs

1use std::{
2    backtrace::{Backtrace, BacktraceStatus},
3    cell::RefCell,
4    cmp::min,
5    fmt::{self, Formatter},
6    marker::PhantomData,
7    mem::take,
8    sync::{Condvar, Mutex},
9    thread,
10};
11
12use yansi::{Condition, Paint};
13
14use crate::Record;
15
16thread_local! {
17    static ACTUAL_LOCAL: RefCell<Option<Vec<Record>>> = const { RefCell::new(None) };
18}
19
20static ACTUAL_GLOBAL: Mutex<Option<Vec<Record>>> = Mutex::new(None);
21static ACTUAL_GLOBAL_CONDVAR: Condvar = Condvar::new();
22
23pub trait Thread {
24    fn init() -> Self;
25    fn take_actual(&self) -> Records;
26}
27
28pub struct Local(PhantomData<*mut ()>);
29
30impl Thread for Local {
31    fn init() -> Self {
32        ACTUAL_LOCAL.with(|actual| {
33            let mut actual = actual.borrow_mut();
34            if actual.is_some() {
35                panic!("CallRecorder::new_local() is already called in this thread");
36            }
37            *actual = Some(Vec::new());
38        });
39        Self(PhantomData)
40    }
41    fn take_actual(&self) -> Records {
42        Records(ACTUAL_LOCAL.with(|actual| take(actual.borrow_mut().as_mut().unwrap())))
43    }
44}
45impl Drop for Local {
46    fn drop(&mut self) {
47        ACTUAL_LOCAL.with(|actual| actual.borrow_mut().take());
48    }
49}
50
51#[non_exhaustive]
52pub struct Global {}
53
54impl Thread for Global {
55    fn init() -> Self {
56        let mut actual = ACTUAL_GLOBAL.lock().unwrap();
57        while actual.is_some() {
58            actual = ACTUAL_GLOBAL_CONDVAR.wait(actual).unwrap();
59        }
60        *actual = Some(Vec::new());
61        Self {}
62    }
63    fn take_actual(&self) -> Records {
64        Records(take(ACTUAL_GLOBAL.lock().unwrap().as_mut().unwrap()))
65    }
66}
67impl Drop for Global {
68    fn drop(&mut self) {
69        ACTUAL_GLOBAL.lock().unwrap().take();
70        ACTUAL_GLOBAL_CONDVAR.notify_all();
71    }
72}
73
74#[derive(Debug)]
75pub struct Records(pub(crate) Vec<Record>);
76
77impl Records {
78    pub(crate) fn empty() -> Self {
79        Self(Vec::new())
80    }
81
82    #[track_caller]
83    pub fn push(id: String, file: &'static str, line: u32) {
84        let record = Record {
85            id,
86            file,
87            line,
88            backtrace: Backtrace::capture(),
89            thread_id: thread::current().id(),
90        };
91        if let Err(e) = ACTUAL_LOCAL.with(|actual| {
92            if let Some(actual) = &mut *actual.borrow_mut() {
93                actual.push(record);
94                Ok(())
95            } else if let Some(seq) = ACTUAL_GLOBAL.lock().unwrap().as_mut() {
96                seq.push(record);
97                Ok(())
98            } else {
99                let id = record.id;
100                Err(format!(
101                    "`CallRecorder` is not initialized. (\"{id}\")\n{file}:{line}"
102                ))
103            }
104        }) {
105            panic!("{e}");
106        }
107    }
108
109    fn id(&self, index: usize) -> &str {
110        if let Some(a) = self.0.get(index) {
111            &a.id
112        } else {
113            "(end)"
114        }
115    }
116
117    pub(crate) fn fmt_summary(
118        &self,
119        f: &mut Formatter,
120        mismatch_index: usize,
121        around: usize,
122        color: bool,
123    ) -> fmt::Result {
124        let mut start = 0;
125        let end = self.0.len();
126        if mismatch_index > around {
127            start = mismatch_index - around;
128        }
129        let end = min(mismatch_index + around + 1, end);
130        if start > 0 {
131            writeln!(f, "  ...(previous {start} calls omitted)")?;
132        }
133        for index in start..end {
134            self.fmt_item_summary(f, mismatch_index == index, self.id(index), color)?;
135        }
136        if end == self.0.len() {
137            self.fmt_item_summary(f, mismatch_index == self.0.len(), "(end)", color)?;
138        } else {
139            writeln!(f, "  ...(following {} calls omitted)", self.0.len() - end)?;
140        }
141        Ok(())
142    }
143    fn fmt_item_summary(
144        &self,
145        f: &mut Formatter,
146        is_mismatch: bool,
147        id: &str,
148        color: bool,
149    ) -> fmt::Result {
150        let head = if is_mismatch { "*" } else { " " };
151        let cond = if is_mismatch && color {
152            Condition::ALWAYS
153        } else {
154            Condition::NEVER
155        };
156        writeln!(f, "{}", format_args!("{head} {id}").red().whenever(cond))
157    }
158    pub(crate) fn fmt_backtrace(
159        &self,
160        f: &mut Formatter,
161        mismatch_index: usize,
162        around: usize,
163    ) -> fmt::Result {
164        let mut start = 0;
165        let end = self.0.len();
166        if mismatch_index > around {
167            start = mismatch_index - around;
168        }
169        let end = min(mismatch_index + 1, end);
170        if start > 0 {
171            writeln!(f, "# ...(previous {start} calls omitted)")?;
172        }
173        for index in start..end {
174            let r = &self.0[index];
175            writeln!(f, "# {}", r.id)?;
176            writeln!(f, "{}:{}", r.file, r.line)?;
177            writeln!(f, "thread: {:?}", r.thread_id)?;
178            writeln!(f, "{}", r.backtrace)?;
179        }
180
181        if end == self.0.len() {
182            writeln!(f, "# (end)")?;
183        } else {
184            writeln!(f, "  ...(following {} calls omitted)", self.0.len() - end)?;
185        }
186        Ok(())
187    }
188
189    pub(crate) fn has_bakctrace(&self) -> bool {
190        self.0
191            .iter()
192            .any(|r| r.backtrace.status() == BacktraceStatus::Captured)
193    }
194}