rtest 0.2.2

integration test building framework
Documentation
use backtrace::{BytesOrWideString, PrintFmt};
use std::{
    error::Error,
    fmt::{self, Display},
};

use crate::TestError;

#[derive(Debug)]
pub(crate) struct PanicDetails {
    pub backtrace: backtrace::Backtrace,
    pub payload: Option<Box<dyn core::any::Any + Send>>,
    pub location: Option<(String, u32, u32)>,
    pub message: Option<String>,
    pub backtrace_function_name: String,
    pub test_id: u64,
}

#[derive(Debug)]
pub(crate) struct PanicError {
    pub details: Option<PanicDetails>,
}

impl PanicError {
    pub fn new(details: Option<PanicDetails>) -> Self { Self { details } }
}

impl Display for PanicError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.details {
            Some(pd) => {
                if let Some(msg) = &pd.message {
                    writeln!(f, "Panic: '{}'", msg)?;
                }
            },
            None => {
                write!(f, "no panic info")?;
            },
        }
        Ok(())
    }
}

impl Error for PanicError {}

impl TestError for PanicError {
    fn fails(&self) -> bool { true }

    fn exitcode(&self) -> (u8, u64) { (1, 1_000_000_000) }

    fn additional_details(&self) -> Option<String> {
        use std::fmt::Write;
        self.details.as_ref().map(|pd| {
            let mut buf = String::new();
            if let Some(loc) = &pd.location {
                writeln!(buf, "at {}:{}", loc.0, loc.1).unwrap();
            }
            let bt = FormatBacktrace(&pd.backtrace, Some(&pd.backtrace_function_name));
            writeln!(buf, "Stacktrace:").unwrap();
            write!(buf, "{:?}", &bt).unwrap();
            buf
        })
    }

    fn print_cli(&self) -> String {
        use colored::Colorize;
        use std::fmt::Write;
        let mut buf = String::new();

        match &self.details {
            Some(pd) => {
                if let Some(msg) = &pd.message {
                    writeln!(buf, "{}: '{}'", "Panic".bold(), msg.red()).unwrap();
                }
                if let Some(loc) = &pd.location {
                    writeln!(buf, "at {}:{}", loc.0.yellow(), loc.1.to_string().yellow()).unwrap();
                }
                let bt = FormatBacktrace(&pd.backtrace, Some(&pd.backtrace_function_name));
                writeln!(buf, "{}:", "Stacktrace".italic()).unwrap();
                write!(buf, "{:?}", &bt).unwrap();
            },
            None => {
                write!(buf, "no panic info").unwrap();
            },
        }
        buf
    }
}

pub(crate) struct FormatBacktrace<'a>(pub(crate) &'a backtrace::Backtrace, pub(crate) Option<&'a str>);

impl<'a> fmt::Debug for FormatBacktrace<'a> {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        let frames = &self.0.frames();
        let contains_string = self.1;
        let begin = frames
            .iter()
            .position(|f| {
                let begin = f
                    .symbols()
                    .first()
                    .map(|s| s.name().map(|n| n.as_str() == Some("rust_begin_unwind")));
                begin.flatten().unwrap_or(false)
            })
            .unwrap_or(0);
        let end = frames[begin..].iter().position(|f| {
            let end = f.symbols().first().map(|s| {
                s.name().map(|n| {
                    n.as_str().map(|s| match contains_string {
                        Some(cs) => s.contains(cs),
                        None => s == "main",
                    })
                })
            });
            end.flatten().flatten().unwrap_or(false)
        });
        let frames = match end {
            Some(end) => &frames[begin..end + begin + 1],
            None => &frames[begin..],
        };

        // When printing paths we try to strip the cwd if it exists, otherwise
        // we just print the path as-is. Note that we also only do this for the
        // short format, because if it's full we presumably want to print
        // everything.
        let cwd = std::env::current_dir();
        let mut print_path = move |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
            let path = path.into_path_buf();
            if let Ok(cwd) = &cwd {
                if let Ok(suffix) = path.strip_prefix(cwd) {
                    return fmt::Display::fmt(&suffix.display(), fmt);
                }
            }
            fmt::Display::fmt(&path.display(), fmt)
        };

        let mut f = backtrace::BacktraceFmt::new(fmt, PrintFmt::Short, &mut print_path);
        f.add_context()?;
        for frame in frames {
            f.frame().backtrace_frame(frame)?;
        }
        f.finish()?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use backtrace::Backtrace;

    use super::*;

    #[test]
    // DO NOT change test name
    fn stacktrace_test() {
        // DO NOT change the 3 following lines
        let file = std::file!();
        let column = std::column!();
        let line = std::line!();
        let trace = Backtrace::new();

        let bt = FormatBacktrace(&trace, Some("stacktrace_test"));

        let calculated_pos = format!("{}:{}:{}", file.trim_start_matches("rtest/"), line + 1, column - 1);

        let printed = format!("{:?}", bt);
        assert_eq!(
            printed,
            format!("   0: rtest::runner::panic::tests::stacktrace_test\n             at {calculated_pos}\n")
        )
    }
}