mirror of
https://github.com/typst/typst
synced 2025-05-17 02:25:27 +08:00
154 lines
4.4 KiB
Rust
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(())
|
|
}
|
|
}
|