typst/tests/src/logger.rs
Michael Färber df4e6715cf
HTML tests (#5594)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2024-12-20 09:48:17 +00:00

154 lines
4.4 KiB
Rust

use std::io::{self, IsTerminal, StderrLock, Write};
use std::time::{Duration, Instant};
use crate::collect::Test;
/// The result of running a single test.
pub struct TestResult {
/// The error log for this test. If empty, the test passed.
pub errors: String,
/// The info log for this test.
pub infos: String,
/// Whether the output was mismatched.
pub mismatched_output: bool,
}
/// Receives status updates by individual test runs.
pub struct Logger<'a> {
selected: usize,
passed: usize,
failed: usize,
skipped: usize,
mismatched_output: bool,
active: Vec<&'a Test>,
last_change: Instant,
temp_lines: usize,
terminal: bool,
}
impl<'a> Logger<'a> {
/// Create a new logger.
pub fn new(selected: usize, skipped: usize) -> Self {
Self {
selected,
passed: 0,
failed: 0,
skipped,
mismatched_output: false,
active: vec![],
temp_lines: 0,
last_change: Instant::now(),
terminal: std::io::stderr().is_terminal(),
}
}
/// Register the start of a test.
pub fn start(&mut self, test: &'a Test) {
self.active.push(test);
self.last_change = Instant::now();
self.refresh();
}
/// Register a finished test.
pub fn end(&mut self, test: &'a Test, result: std::thread::Result<TestResult>) {
self.active.retain(|t| t.name != test.name);
let result = match result {
Ok(result) => result,
Err(_) => {
self.failed += 1;
self.temp_lines = 0;
self.print(move |out| {
writeln!(out, "❌ {test} panicked")?;
Ok(())
})
.unwrap();
return;
}
};
if result.errors.is_empty() {
self.passed += 1;
} else {
self.failed += 1;
}
self.mismatched_output |= result.mismatched_output;
self.last_change = Instant::now();
self.print(move |out| {
if !result.errors.is_empty() {
writeln!(out, "❌ {test}")?;
if !crate::ARGS.compact {
for line in result.errors.lines() {
writeln!(out, " {line}")?;
}
}
} else if crate::ARGS.verbose || !result.infos.is_empty() {
writeln!(out, "✅ {test}")?;
}
for line in result.infos.lines() {
writeln!(out, " {line}")?;
}
Ok(())
})
.unwrap();
}
/// Prints a summary and returns whether the test suite passed.
pub fn finish(&self) -> bool {
let Self { selected, passed, failed, skipped, .. } = *self;
eprintln!("{passed} passed, {failed} failed, {skipped} skipped");
assert_eq!(selected, passed + failed, "not all tests were executed successfully");
if self.mismatched_output {
eprintln!(" pass the --update flag to update the reference output");
}
self.failed == 0
}
/// Refresh the status. Returns whether we still seem to be making progress.
pub fn refresh(&mut self) -> bool {
self.print(|_| Ok(())).unwrap();
self.last_change.elapsed() < Duration::from_secs(10)
}
/// Refresh the status print.
fn print(
&mut self,
inner: impl FnOnce(&mut StderrLock<'_>) -> io::Result<()>,
) -> io::Result<()> {
let mut out = std::io::stderr().lock();
// Clear the status lines.
for _ in 0..self.temp_lines {
write!(out, "\x1B[1F\x1B[0J")?;
self.temp_lines = 0;
}
// Print the result of a finished test.
inner(&mut out)?;
// Print the status line.
let done = self.failed + self.passed;
if done < self.selected {
if self.last_change.elapsed() > Duration::from_secs(2) {
for test in &self.active {
writeln!(out, "⏰ {test} is taking a long time ...")?;
if self.terminal {
self.temp_lines += 1;
}
}
}
if self.terminal {
writeln!(out, "💨 {done} / {}", self.selected)?;
self.temp_lines += 1;
}
}
Ok(())
}
}