use runemark::{
Console, ProgressMode, ProgressSink, SilentProgress, TerminalProgress, Tone, Verdict,
};
use auditmysite::cli::ProgressPolicy;
use auditmysite::util::truncate_url;
pub struct BatchLifecyclePresenter {
sink: Box<dyn ProgressSink>,
}
impl BatchLifecyclePresenter {
pub fn new(progress: ProgressPolicy, quiet: bool, console: Console, is_terminal: bool) -> Self {
let sink: Box<dyn ProgressSink> = if quiet {
Box::new(SilentProgress)
} else {
match progress {
ProgressPolicy::Never => Box::new(SilentProgress),
ProgressPolicy::Auto => Box::new(TerminalProgress::stderr(
ProgressMode::Auto,
console,
is_terminal,
)),
ProgressPolicy::Always => Box::new(TerminalProgress::stderr(
ProgressMode::Always,
console,
is_terminal,
)),
}
};
Self { sink }
}
#[cfg(test)]
fn from_sink(sink: Box<dyn ProgressSink>) -> Self {
Self { sink }
}
pub fn start(&self, total: usize, stage: &str) {
self.sink.start(total as u64, stage);
}
pub fn advance(&self, completed: usize, url: &str) {
self.sink.advance(completed as u64, &truncate_url(url, 50));
}
pub fn notice_error(&self, url: &str, err: &str) {
self.sink.notice(Tone::Error, &format!("{url}: {err}"));
}
pub fn notice(&self, message: &str) {
self.sink.notice(Tone::Info, message);
}
pub fn finish(&self, verdict: Verdict, message: &str) {
self.sink.finish(verdict, message);
}
pub fn finish_verdict(&self, verdict: auditmysite::Verdict, message: &str) {
let mapped = match verdict {
auditmysite::Verdict::Pass => Verdict::Passed,
auditmysite::Verdict::Warn => Verdict::Warning,
auditmysite::Verdict::Fail => Verdict::Failed,
};
self.finish(mapped, message);
}
pub fn finish_then_render<T>(
&self,
verdict: auditmysite::Verdict,
message: &str,
render: impl FnOnce() -> T,
) -> T {
self.finish_verdict(verdict, message);
render()
}
}
#[cfg(test)]
mod tests {
use super::*;
use runemark::{ColorMode, PlainProgress};
use std::sync::{Arc, Mutex};
struct SharedBuf(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for SharedBuf {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn plain_presenter() -> (BatchLifecyclePresenter, Arc<Mutex<Vec<u8>>>) {
let buf = Arc::new(Mutex::new(Vec::new()));
let console = Console::stdout(ColorMode::Never);
let sink = PlainProgress::new(console, SharedBuf(buf.clone()));
(BatchLifecyclePresenter::from_sink(Box::new(sink)), buf)
}
fn buf_to_string(buf: &Arc<Mutex<Vec<u8>>>) -> String {
String::from_utf8(buf.lock().unwrap().clone()).unwrap()
}
#[test]
fn start_notice_finish_appear_in_order_and_bounded() {
let (presenter, buf) = plain_presenter();
presenter.start(3, "Auditing URLs");
presenter.advance(1, "https://example.com/a");
presenter.notice_error("https://example.com/b", "timeout");
presenter.finish(Verdict::Warning, "2/3 passed");
let output = buf_to_string(&buf);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3, "unexpected output: {output:?}");
assert!(lines[0].contains("Auditing URLs") && lines[0].contains("(0/3)"));
assert!(lines[1].contains("timeout"));
assert!(lines[2].contains("2/3 passed"));
}
#[test]
fn advance_does_not_produce_one_line_per_url() {
let (presenter, buf) = plain_presenter();
presenter.start(5, "Auditing URLs");
for i in 1..=5 {
presenter.advance(i, &format!("https://example.com/{i}"));
}
presenter.finish(Verdict::Passed, "done");
let output = buf_to_string(&buf);
assert_eq!(
output.lines().count(),
2,
"advance() must stay silent: {output:?}"
);
}
#[test]
fn lifecycle_is_finished_before_report_renders() {
let (presenter, buf) = plain_presenter();
presenter.start(2, "Auditing URLs");
let seen_at_render =
presenter.finish_then_render(auditmysite::Verdict::Warn, "1/2 passed", || {
buf_to_string(&buf)
});
assert!(
seen_at_render
.lines()
.last()
.is_some_and(|l| l.contains("1/2 passed")),
"verdict must be written before the report renders: {seen_at_render:?}"
);
assert_eq!(buf_to_string(&buf), seen_at_render);
}
#[test]
fn quiet_forces_silence_regardless_of_progress_policy() {
let presenter = BatchLifecyclePresenter::new(
ProgressPolicy::Always,
true,
Console::stdout(ColorMode::Never),
false,
);
presenter.start(1, "x");
presenter.advance(1, "https://example.com");
presenter.notice_error("https://example.com", "boom");
presenter.finish(Verdict::Failed, "done");
}
#[test]
fn progress_never_forces_silence() {
let presenter = BatchLifecyclePresenter::new(
ProgressPolicy::Never,
false,
Console::stdout(ColorMode::Never),
false,
);
presenter.start(1, "x");
presenter.finish(Verdict::Passed, "done");
}
}