use owo_colors::{OwoColorize as _, Style};
use super::text::VERB_WIDTH;
use crate::model::Outcome;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Styler {
enabled: bool,
}
impl Styler {
#[must_use]
pub const fn new(enabled: bool) -> Self {
Self { enabled }
}
#[must_use]
pub const fn enabled(self) -> bool {
self.enabled
}
#[must_use]
pub fn apply(self, text: &str, style: Style) -> String {
if self.enabled {
text.style(style).to_string()
} else {
text.to_owned()
}
}
#[must_use]
fn column(self, text: &str, style: Style) -> String {
self.apply(&format!("{text:>VERB_WIDTH$}"), style)
}
#[must_use]
pub fn verb(self, text: &str) -> String {
self.column(text, Style::new().bold().green())
}
#[must_use]
pub fn note(self, text: &str) -> String {
self.column(text, Style::new().bold().cyan())
}
#[must_use]
pub fn error(self, text: &str) -> String {
self.apply(text, Style::new().bold().red())
}
#[must_use]
pub fn warning(self) -> String {
self.column("warning", Style::new().bold().yellow())
}
#[must_use]
pub fn outcome(self, outcome: Outcome) -> String {
let (text, style) = outcome_style(outcome);
self.column(text, style)
}
}
const fn outcome_style(outcome: Outcome) -> (&'static str, Style) {
match outcome {
Outcome::Killed => ("killed", Style::new().green()),
Outcome::Survived => ("SURVIVED", Style::new().red().bold()),
Outcome::Timeout => ("TIMEOUT", Style::new().yellow()),
Outcome::OutOfMemory => ("OUTOFMEM", Style::new().magenta().bold()),
Outcome::Flaky => ("FLAKY", Style::new().yellow().bold()),
Outcome::NoCoverage => ("uncovered", Style::new().yellow()),
Outcome::CompileError => ("unviable", Style::new()),
Outcome::Ignored => ("skipped", Style::new()),
Outcome::NotBuilt => ("notbuilt", Style::new()),
Outcome::Pending => ("pending", Style::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outcome_labels_are_distinct() {
let outcomes = [
Outcome::Killed,
Outcome::Survived,
Outcome::Timeout,
Outcome::OutOfMemory,
Outcome::CompileError,
Outcome::NoCoverage,
Outcome::Ignored,
Outcome::NotBuilt,
Outcome::Pending,
];
let styler = Styler::new(false);
let labels: crate::HashSet<String> = outcomes.into_iter().map(|o| styler.outcome(o)).collect();
assert_eq!(labels.len(), outcomes.len());
}
#[test]
fn a_disabled_styler_emits_no_escape_sequences() {
let styler = Styler::new(false);
assert_eq!(styler.verb("Found").trim(), "Found");
assert_eq!(styler.error("error:"), "error:");
assert!(!styler.outcome(Outcome::Survived).contains('\x1b'));
}
#[test]
fn an_enabled_styler_emits_escape_sequences() {
let styler = Styler::new(true);
assert!(styler.verb("Found").contains('\x1b'));
assert!(styler.outcome(Outcome::Survived).contains('\x1b'));
}
#[test]
fn a_label_occupies_the_status_column_even_when_it_is_colored() {
let plain = Styler::new(false).verb("Found");
let colorful = Styler::new(true).verb("Found");
assert_eq!(plain.len(), VERB_WIDTH);
assert!(colorful.contains(&plain), "the padding must be inside the styling");
}
#[test]
fn every_status_label_fits_the_column() {
let styler = Styler::new(false);
for label in [
"Found",
"Skipped",
"Summary",
"Analyzing",
"Scanning",
"Copying",
"Rewriting",
"Building",
"Baseline",
"Estimate",
"Testing",
"Timing",
"Hangs",
"Rollback",
"Also",
"Wrote",
"Note",
"Kept",
"Iterating",
"Advice",
"Merged",
"Freshness",
"Rotation",
"Warning",
"Finished",
"Suppressed",
"Scope",
"Withdrawn",
] {
assert_eq!(styler.verb(label).len(), VERB_WIDTH, "`{label}` does not fit the status column");
}
}
#[test]
fn stylers_do_not_share_state() {
let plain = Styler::new(false);
let colorful = Styler::new(true);
assert_eq!(plain.verb("Found").trim(), "Found");
assert!(colorful.verb("Found").contains('\x1b'));
assert_eq!(plain.verb("Found").trim(), "Found");
}
}