use std::{
backtrace::{Backtrace, BacktraceStatus},
collections::VecDeque,
error::Error,
fmt::Display,
thread::{self, ThreadId},
};
use records::{Global, Local, Records, Thread};
use yansi::Condition;
pub mod records;
#[cfg(test)]
mod tests;
#[macro_export]
macro_rules! call {
($($id:tt)*) => {
$crate::records::Records::push(::std::format!($($id)*), ::std::file!(), ::std::line!());
};
}
pub struct CallRecorder<T: Thread = Global> {
thread: T,
}
impl CallRecorder {
pub fn new() -> Self {
Self::new_raw()
}
}
impl CallRecorder<Local> {
pub fn new_local() -> Self {
Self::new_raw()
}
}
impl<T: Thread> CallRecorder<T> {
fn new_raw() -> Self {
Self { thread: T::init() }
}
#[track_caller]
pub fn verify(&mut self, expect: impl ToCall) {
self.verify_with_msg(expect, "mismatch call");
}
#[track_caller]
pub fn verify_with_msg(&mut self, expect: impl ToCall, msg: &str) {
match self.result_with_msg(expect, msg) {
Ok(_) => {}
Err(e) => {
panic!("{:#}", e.display(true, Condition::tty_and_color()));
}
}
}
fn result_with_msg(&mut self, expect: impl ToCall, msg: &str) -> Result<(), CallMismatchError> {
let expect: Call = expect.to_call();
let actual = self.thread.take_actual();
expect.verify(actual, msg)
}
}
impl<T: Thread> Default for CallRecorder<T> {
fn default() -> Self {
Self::new_raw()
}
}
impl<T: Thread> Drop for CallRecorder<T> {
fn drop(&mut self) {}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Call {
Id(String),
Seq(VecDeque<Call>),
Par(Vec<Call>),
Any(Vec<Call>),
}
impl Call {
pub fn id(id: impl Display) -> Self {
Self::Id(id.to_string())
}
pub fn empty() -> Self {
Self::Seq(VecDeque::new())
}
pub fn seq(p: impl IntoIterator<Item = impl ToCall>) -> Self {
Self::Seq(p.into_iter().map(|x| x.to_call()).collect())
}
pub fn par(p: impl IntoIterator<Item = impl ToCall>) -> Self {
Self::Par(p.into_iter().map(|x| x.to_call()).collect())
}
pub fn any(p: impl IntoIterator<Item = impl ToCall>) -> Self {
Self::Any(p.into_iter().map(|x| x.to_call()).collect())
}
fn verify(mut self, actual: Records, msg: &str) -> Result<(), CallMismatchError> {
match self.verify_nexts(&actual.0) {
Ok(_) => Ok(()),
Err(mut e) => {
e.actual = actual;
e.expect.sort();
e.expect.dedup();
e.msg = msg.to_string();
Err(e)
}
}
}
fn verify_nexts(&mut self, actual: &[Record]) -> Result<(), CallMismatchError> {
for index in 0..=actual.len() {
self.verify_next(index, actual.get(index))?;
}
Ok(())
}
fn verify_next(&mut self, index: usize, a: Option<&Record>) -> Result<(), CallMismatchError> {
if let Err(e) = self.next(a) {
if a.is_none() && e.is_empty() {
return Ok(());
}
Err(CallMismatchError::new(e, index))
} else {
Ok(())
}
}
fn next(&mut self, p: Option<&Record>) -> Result<(), Vec<String>> {
match self {
Call::Id(id) => {
if Some(id.as_str()) == p.as_ref().map(|x| x.id.as_str()) {
*self = Call::Seq(VecDeque::new());
Ok(())
} else {
Err(vec![id.to_string()])
}
}
Call::Seq(list) => {
while !list.is_empty() {
match list[0].next(p) {
Err(e) if e.is_empty() => list.pop_front(),
ret => return ret,
};
}
Err(Vec::new())
}
Call::Par(s) => {
let mut es = Vec::new();
for i in s.iter_mut() {
match i.next(p) {
Ok(_) => return Ok(()),
Err(mut e) => es.append(&mut e),
}
}
Err(es)
}
Call::Any(s) => {
let mut is_end = false;
let mut is_ok = false;
let mut es = Vec::new();
s.retain_mut(|s| match s.next(p) {
Ok(_) => {
is_ok = true;
true
}
Err(e) => {
is_end |= e.is_empty();
es.extend(e);
false
}
});
if is_ok {
Ok(())
} else if is_end {
Err(Vec::new())
} else {
Err(es)
}
}
}
}
}
pub trait ToCall {
fn to_call(&self) -> Call;
}
impl<T: ?Sized + ToCall> ToCall for &T {
fn to_call(&self) -> Call {
T::to_call(self)
}
}
impl ToCall for Call {
fn to_call(&self) -> Call {
self.clone()
}
}
impl ToCall for str {
fn to_call(&self) -> Call {
Call::id(self)
}
}
impl ToCall for String {
fn to_call(&self) -> Call {
Call::id(self)
}
}
impl ToCall for usize {
fn to_call(&self) -> Call {
Call::id(self)
}
}
impl<T: ToCall> ToCall for [T] {
fn to_call(&self) -> Call {
Call::seq(self)
}
}
impl<T: ToCall, const N: usize> ToCall for [T; N] {
fn to_call(&self) -> Call {
Call::seq(self)
}
}
impl<T: ToCall> ToCall for Vec<T> {
fn to_call(&self) -> Call {
Call::seq(self)
}
}
impl ToCall for () {
fn to_call(&self) -> Call {
Call::empty()
}
}
#[derive(Debug)]
struct CallMismatchError {
msg: String,
actual: Records,
expect: Vec<String>,
mismatch_index: usize,
thread_id: ThreadId,
}
impl CallMismatchError {
fn new(expect: Vec<String>, mismatch_index: usize) -> Self {
Self {
msg: String::new(),
actual: Records::empty(),
expect,
mismatch_index,
thread_id: thread::current().id(),
}
}
fn actual_id(&self, index: usize) -> &str {
if let Some(a) = self.actual.0.get(index) {
&a.id
} else {
"(end)"
}
}
#[cfg(test)]
fn set_dummy_file_line(&mut self) {
for a in &mut self.actual.0 {
a.set_dummy_file_line();
}
}
pub fn display(&self, backtrace: bool, color: bool) -> impl Display + '_ {
struct CallMismatchErrorDisplay<'a> {
this: &'a CallMismatchError,
backtrace: bool,
color: bool,
}
impl std::fmt::Display for CallMismatchErrorDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.this.fmt_with(f, self.backtrace, self.color)
}
}
CallMismatchErrorDisplay {
this: self,
backtrace,
color,
}
}
fn fmt_with(
&self,
f: &mut std::fmt::Formatter<'_>,
backtrace: bool,
color: bool,
) -> std::fmt::Result {
let around = 5;
if backtrace && self.actual.has_bakctrace() {
writeln!(f, "actual calls with backtrace :")?;
self.actual.fmt_backtrace(f, self.mismatch_index, around)?;
writeln!(f)?;
}
writeln!(f, "actual calls :")?;
self.actual
.fmt_summary(f, self.mismatch_index, around, color)?;
writeln!(f)?;
writeln!(f, "{}", self.msg)?;
if let Some(a) = self.actual.0.get(self.mismatch_index) {
writeln!(f, "{}:{}", a.file, a.line)?;
}
if backtrace {
writeln!(f, "thread : {:?}", self.thread_id)?;
}
writeln!(f, "actual : {}", self.actual_id(self.mismatch_index))?;
writeln!(f, "expect : {}", self.expect.join(", "))?;
Ok(())
}
}
impl Display for CallMismatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_with(f, false, false)
}
}
impl Error for CallMismatchError {}
#[derive(Debug)]
struct Record {
id: String,
file: &'static str,
line: u32,
backtrace: Backtrace,
thread_id: ThreadId,
}
impl Record {
#[cfg(test)]
fn set_dummy_file_line(&mut self) {
self.file = r"tests\test.rs";
self.line = 10;
}
}
impl Display for Record {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "# {}", self.id)?;
writeln!(f, "{}:{}", self.file, self.line)?;
if self.backtrace.status() == BacktraceStatus::Captured {
writeln!(f)?;
writeln!(f, "{}", self.backtrace)?;
}
Ok(())
}
}