use crate::host_capture::{terminal_stderr_is_terminal, TerminalStderr};
use crate::internal::{RegisteredTest, TestResult};
use anstyle::{AnsiColor, Style};
use std::io::Write;
use std::sync::Mutex;
pub(crate) struct StderrProgress {
state: Mutex<State>,
}
struct State {
index_field_length: usize,
}
impl StderrProgress {
pub(crate) fn new() -> Self {
Self {
state: Mutex::new(State {
index_field_length: 0,
}),
}
}
fn style_progress() -> Style {
Style::new()
.bold()
.fg_color(Some(AnsiColor::BrightWhite.into()))
}
fn style_ok() -> Style {
Style::new().fg_color(Some(AnsiColor::Green.into()))
}
fn style_failed() -> Style {
Style::new().bold().fg_color(Some(AnsiColor::Red.into()))
}
fn style_ignored() -> Style {
Style::new()
.dimmed()
.fg_color(Some(AnsiColor::Yellow.into()))
}
fn style_bench() -> Style {
Style::new().fg_color(Some(AnsiColor::Cyan.into()))
}
fn write_line(line: &str) {
let mut stderr = TerminalStderr;
if terminal_stderr_is_terminal() {
let _ = writeln!(stderr, "{line}");
} else {
let _ = writeln!(stderr, "{}", strip_ansi(line));
}
let _ = stderr.flush();
}
pub(crate) fn start_suite(&self, count: usize) {
{
let mut state = self.state.lock().unwrap();
state.index_field_length = format!("{}/{}", count, count).len();
}
let style = Self::style_progress();
Self::write_line(&format!(
"{}Running {} tests{}",
style.render(),
count,
style.render_reset(),
));
}
pub(crate) fn start_running_test(&self, test: &RegisteredTest, idx: usize, count: usize) {
let padding = self.index_padding(idx, count);
let style = Self::style_progress();
Self::write_line(&format!(
"{}[{}{}/{}]{} Running test: {}",
style.render(),
padding,
idx + 1,
count,
style.render_reset(),
test.fully_qualified_name(),
));
}
pub(crate) fn finished_running_test(
&self,
test: &RegisteredTest,
idx: usize,
count: usize,
result: &TestResult,
) {
let padding = self.index_padding(idx, count);
let progress = Self::style_progress();
let status = match result {
TestResult::Passed { .. } => {
let s = Self::style_ok();
format!("[{}PASSED{}]", s.render(), s.render_reset())
}
TestResult::Benchmarked { .. } => {
let s = Self::style_bench();
format!("[{}BENCH{}]", s.render(), s.render_reset())
}
TestResult::Failed { .. } => {
let s = Self::style_failed();
format!("[{}FAILED{}]", s.render(), s.render_reset())
}
TestResult::Ignored { .. } => {
let s = Self::style_ignored();
format!("[{}IGNORED{}]", s.render(), s.render_reset())
}
};
Self::write_line(&format!(
"{}[{}{}/{}]{} Finished test: {} {status}",
progress.render(),
padding,
idx + 1,
count,
progress.render_reset(),
test.fully_qualified_name(),
));
}
fn index_padding(&self, idx: usize, count: usize) -> String {
let state = self.state.lock().unwrap();
let index_field = format!("{}/{}", idx + 1, count);
" ".repeat(state.index_field_length.saturating_sub(index_field.len()))
}
}
fn strip_ansi(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
if matches!(chars.next(), Some('[')) {
for cc in chars.by_ref() {
if ('@'..='~').contains(&cc) {
break;
}
}
}
} else {
out.push(c);
}
}
out
}