use crate::runner::{
panic::FormatBacktrace,
result_map::{ExecutionResult, ExecutionResultMap, TextCaseStatus},
test_repo::{TestCaseInfo, TestRepoInfo},
};
use colored::{ColoredString, Colorize};
use std::{
collections::HashMap,
io::{BufWriter, Write},
time::Duration,
};
struct CaseResult<'a> {
case: &'a TestCaseInfo,
results: Vec<&'a ExecutionResult>,
status: Option<TextCaseStatus>,
}
#[derive(Default)]
struct Group<'a> {
name: String,
members: Vec<CaseResult<'a>>,
status: Vec<Option<TextCaseStatus>>,
}
struct OffsetWriter<'a, W> {
last_ended_with_newline: bool,
offset: usize,
writer: &'a mut W,
}
pub struct Printer {}
impl Printer {
pub fn print_to_stdout(result_map: ExecutionResultMap, repo_info: TestRepoInfo) {
let lock = std::io::stdout().lock();
Self::print(lock, result_map, repo_info).expect("could not write to stdout");
}
pub fn print<W: Write>(w: W, result_map: ExecutionResultMap, repo_info: TestRepoInfo) -> std::io::Result<()> {
let mut w = BufWriter::new(w);
let mut groups = Printer::gen_groups(&result_map, &repo_info);
groups.sort_by_key(|g| g.name.clone());
groups.sort_by_key(|g| TextCaseStatus::strongest_statuses(&g.status).clone());
for group in &mut groups {
group.members.sort_by_key(|c| c.case.display_name.clone());
group.members.sort_by_key(|c| c.status.clone());
}
writeln!(w, "rtest Results:")?;
for group in &groups {
write!(w, "[")?;
for group_status in &group.status {
write!(w, "{}", iconize_status(group_status))?;
}
writeln!(w, "] {}", group.name.bold())?;
let mut w = OffsetWriter::new(2, &mut w);
for cr in &group.members {
Printer::print_case(&mut w, cr)?;
}
}
let total_tests = repo_info.cases.len();
let successes = result_map.successes();
let fails = result_map.fails();
let total = if fails > 0 {
"Failed".bold().red()
} else {
"Sucessful".bold().green()
};
writeln!(
w,
"Total Tests: {total_tests}. Total Runs: {} Fails: {fails}",
successes + fails
)?;
writeln!(w, "{}", total)?;
Ok(())
}
fn print_case<W: Write>(mut w: &mut W, case: &CaseResult) -> std::io::Result<()> {
let ec = iconize_status(&case.status);
if let Some(t) = ExecutionResult::avg_runtime(&case.results) {
if t >= Duration::from_millis(1) {
writeln!(w, "[{}] {} - {}ms", ec, case.case.display_name, t.as_millis())?;
} else {
writeln!(w, "[{}] {}", ec, case.case.display_name)?;
}
} else {
writeln!(w, "[{}] {}", ec, case.case.display_name)?;
}
if case.status != Some(TextCaseStatus::Success) {
let mut w = OffsetWriter::new(4, &mut w);
for (i, res) in case.results.iter().enumerate() {
writeln!(w, "--- Run: {}/{} ---", i + 1, case.results.len())?;
if !res.success {
if let Some(e) = &res.error {
writeln!(w, "{}: {}", "Error".bold(), format!("{}", &e).red())?;
}
if let Some(pd) = &res.panic_details {
if let Some(msg) = &pd.message {
writeln!(w, "{}: '{}'", "Panic".bold(), msg.red())?;
}
if let Some(loc) = &pd.location {
writeln!(w, "at {}:{}", loc.0.yellow(), loc.1.to_string().yellow())?;
}
let bt = FormatBacktrace(&pd.backtrace, Some(&case.case.executable_function_name));
writeln!(w, "{}:", "Stacktrace".italic())?;
write!(w, "{:?}", &bt)?;
};
#[cfg(feature = "capture_tracing")]
{
writeln!(w, "{}:", "Logs".italic())?;
let mut w = OffsetWriter::new(2, &mut w);
write!(w, "{}", &res.tracing_logs)?;
}
}
}
}
Ok(())
}
fn gen_groups<'a>(result_map: &'a ExecutionResultMap, repo_info: &'a TestRepoInfo) -> Vec<Group<'a>> {
let mut groups: HashMap<String, Group<'a>> = HashMap::new();
for case in &repo_info.cases {
let group_name = case.test_arguments.group.clone().unwrap_or_default();
let case_name = case.executable_function_name.clone();
let results = result_map.results_of(&case_name);
let status = TextCaseStatus::evaluate(&results);
let cr = CaseResult { case, results, status };
let group = groups.entry(group_name.clone()).or_default();
group.name = group_name;
group.members.push(cr);
}
for group in groups.values_mut() {
group.status = TextCaseStatus::dedup_statuses(group.members.iter().map(|m| &m.status))
}
groups.into_values().collect()
}
}
fn iconize_status(status: &Option<TextCaseStatus>) -> ColoredString {
match status {
None => "⚡".yellow(),
Some(TextCaseStatus::Success) => "✓".green(),
Some(TextCaseStatus::Fail) => "x".red(),
Some(TextCaseStatus::CouldNotVerify) => "⚡".bright_yellow(),
}
}
impl<'a, W> Write for OffsetWriter<'a, W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.last_ended_with_newline && self.offset > 0 {
const SPACE: u8 = 32;
let buf: Vec<_> = (0..self.offset).map(|_| SPACE).collect();
self.writer.write_all(&buf)?;
self.last_ended_with_newline = false;
}
const NEWLINE: u8 = 10;
if matches!(buf.last(), Some(&NEWLINE)) {
self.last_ended_with_newline = true;
}
self.writer.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> { self.writer.flush() }
}
impl<'a, W> OffsetWriter<'a, W> {
fn new(offset: usize, writer: &'a mut W) -> Self {
Self {
last_ended_with_newline: true,
offset,
writer,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offset_writer() -> Result<(), Box<dyn std::error::Error>> {
let mut buf = BufWriter::new(Vec::new());
let mut off = OffsetWriter::new(2, &mut buf);
writeln!(off, "Hello World")?;
write!(off, "Hello")?;
write!(off, " ")?;
write!(off, "World")?;
let buf = buf.into_inner()?;
let string = String::from_utf8(buf)?;
assert_eq!(string, " Hello World\n Hello World");
Ok(())
}
}