Skip to content

Commit

Permalink
Subjectively better progress bars (#190)
Browse files Browse the repository at this point in the history
* Show outcomes on the left, where they're easier to scan
* Show overall progress below everything else, where it won't get lost in the noise of all the completed scenarios
* Show missed mutants as `MISSED`
* Format times using humantime
  • Loading branch information
sourcefrog authored Dec 16, 2023
2 parents 1d1459f + 95d7381 commit d533e93
Show file tree
Hide file tree
Showing 24 changed files with 245 additions and 260 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ctrlc = { version = "3.2.1", features = ["termination"] }
fastrand = "2"
fs2 = "0.4"
globset = "0.4.8"
humantime = "2.1.0"
ignore = "0.4.20"
indoc = "2.0.0"
itertools = "0.11"
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Improved progress bars and console output, including putting the outcome of each mutant on the left, and the overall progress bar at the bottom. Improved display of estimated remaining time, and other times.

- Fixed: Correctly traverse `mod` statements within package top source files that are not named `lib.rs` or `main.rs`, by following the `path` setting of each target within the manifest.

- Improved: Don't generate function mutants that have the same AST as the code they're replacing.
Expand Down
117 changes: 45 additions & 72 deletions src/console.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021, 2022 Martin Pool
// Copyright 2021-2023 Martin Pool

//! Print messages and progress bars on the terminal.
Expand All @@ -12,7 +12,7 @@ use std::time::{Duration, Instant};
use anyhow::Context;
use camino::Utf8Path;
use console::{style, StyledObject};

use humantime::format_duration;
use nutmeg::Destination;
use tracing::Level;
use tracing_subscriber::fmt::MakeWriter;
Expand All @@ -23,8 +23,6 @@ use crate::scenario::Scenario;
use crate::tail_file::TailFile;
use crate::{Mutant, Options, Phase, Result, ScenarioOutcome};

static COPY_MESSAGE: &str = "Copy source to scratch directory";

/// An interface to the console for the rest of cargo-mutants.
///
/// This wraps the Nutmeg view and model.
Expand Down Expand Up @@ -103,9 +101,9 @@ impl Console {
let mut s = String::with_capacity(100);
write!(
s,
"{} ... {}",
"{:8} {}",
style_outcome(outcome),
style_scenario(scenario, true),
style_outcome(outcome)
)
.unwrap();
if options.show_times {
Expand Down Expand Up @@ -139,7 +137,7 @@ impl Console {
pub fn autoset_timeout(&self, timeout: Duration) {
self.message(&format!(
"Auto-set test timeout to {}\n",
style_secs(timeout)
style_duration(timeout)
));
}

Expand Down Expand Up @@ -373,27 +371,25 @@ impl nutmeg::Model for LabModel {
if !s.is_empty() {
s.push('\n')
}
for sm in self.scenario_models.iter_mut() {
s.push_str(&sm.render(width));
s.push('\n');
}
if let Some(lab_start_time) = self.lab_start_time {
let elapsed = lab_start_time.elapsed();
let percent = if self.n_mutants > 0 {
((self.mutants_done as f64) / (self.n_mutants as f64) * 100.0).round()
} else {
0.0
};
write!(
s,
"{}/{} mutants tested, {}% done",
"{}/{} mutants tested",
style(self.mutants_done).cyan(),
style(self.n_mutants).cyan(),
style(percent).cyan(),
)
.unwrap();
if self.mutants_missed > 0 {
write!(
s,
", {} {}",
style(self.mutants_missed).cyan(),
style("missed").red()
style("MISSED").red()
)
.unwrap();
}
Expand All @@ -419,26 +415,23 @@ impl nutmeg::Model for LabModel {
// if self.failures > 0 {
// write!(s, ", {} failures", self.failures).unwrap();
// }
write!(s, ", {} elapsed", style_minutes_seconds(elapsed)).unwrap();
write!(s, ", {} elapsed", style_duration(elapsed)).unwrap();
if self.mutants_done > 2 {
let done = self.mutants_done as u64;
let remain = self.n_mutants as u64 - done;
let mut remaining_secs = lab_start_time.elapsed().as_secs() * remain / done;
if remaining_secs > 300 {
remaining_secs = (remaining_secs + 30) / 60 * 60;
}
write!(
s,
", about {} remaining",
style(nutmeg::estimate_remaining(
&self.mutants_start_time.unwrap(),
self.mutants_done,
self.n_mutants
))
.cyan()
style_duration(Duration::from_secs(remaining_secs))
)
.unwrap();
}
writeln!(s).unwrap();
}
for sm in self.scenario_models.iter_mut() {
s.push_str(&sm.render(width));
s.push('\n');
}
while s.ends_with('\n') {
s.pop();
}
Expand Down Expand Up @@ -520,23 +513,25 @@ impl ScenarioModel {

impl nutmeg::Model for ScenarioModel {
fn render(&mut self, _width: usize) -> String {
let mut s = String::with_capacity(100);
write!(s, "{} ... ", self.name).unwrap();
let mut prs = self
.previous_phase_durations
.iter()
.map(|(phase, duration)| format!("{} {}", style_secs(*duration), style(phase).dim()))
.collect::<Vec<_>>();
let mut parts = Vec::new();
if let Some(phase) = self.phase {
prs.push(format!(
"{} {}",
style_secs(self.phase_start.elapsed()),
style(phase).dim()
));
parts.push(style(format!("{phase:8}")).bold().cyan().to_string());
}
write!(s, "{}", prs.join(" + ")).unwrap();
parts.push(self.name.to_string());
parts.push("...".to_string());
parts.push(style_secs(self.phase_start.elapsed()).to_string());
// let mut prs = self
// .previous_phase_durations
// .iter()
// .map(|(phase, duration)| format!("{} {}", style_secs(*duration), style(phase).dim()))
// .collect::<Vec<_>>();
// if prs.len() > 1 {
// prs.insert(0, String::new())
// }
// parts.push(prs.join(" + "));
let mut s = parts.join(" ");
if let Ok(last_line) = self.log_tail.last_line() {
write!(s, "\n {}", style(last_line).dim()).unwrap();
write!(s, "\n{:8} {}", style("└").cyan(), style(last_line).dim()).unwrap();
}
s
}
Expand Down Expand Up @@ -569,10 +564,10 @@ impl CopyModel {
impl nutmeg::Model for CopyModel {
fn render(&mut self, _width: usize) -> String {
format!(
"{} ... {} in {}",
COPY_MESSAGE,
"{:8} {} in {}",
style("copy").cyan(),
style_mb(self.bytes_copied),
style_elapsed_secs(self.start),
style_secs(self.start.elapsed()),
)
}
}
Expand All @@ -587,31 +582,26 @@ fn nutmeg_options() -> nutmeg::Options {
pub fn style_outcome(outcome: &ScenarioOutcome) -> StyledObject<&'static str> {
match outcome.summary() {
SummaryOutcome::CaughtMutant => style("caught").green(),
SummaryOutcome::MissedMutant => style("NOT CAUGHT").red().bold(),
SummaryOutcome::MissedMutant => style("MISSED").red().bold(),
SummaryOutcome::Failure => style("FAILED").red().bold(),
SummaryOutcome::Success => style("ok").green(),
SummaryOutcome::Unviable => style("unviable").blue(),
SummaryOutcome::Timeout => style("TIMEOUT").red().bold(),
}
}

fn style_elapsed_secs(since: Instant) -> String {
style_secs(since.elapsed())
}

fn style_secs(duration: Duration) -> String {
style(format!("{:.1}s", duration.as_secs_f32()))
.cyan()
.to_string()
}

fn style_minutes_seconds(duration: Duration) -> String {
style(duration_minutes_seconds(duration)).cyan().to_string()
}

pub fn duration_minutes_seconds(duration: Duration) -> String {
let secs = duration.as_secs();
format!("{}:{:02}", secs / 60, secs % 60)
fn style_duration(duration: Duration) -> String {
// We don't want silly precision.
let duration = Duration::from_secs(duration.as_secs());
style(format_duration(duration).to_string())
.cyan()
.to_string()
}

fn format_mb(bytes: u64) -> String {
Expand All @@ -636,20 +626,3 @@ pub fn plural(n: usize, noun: &str) -> String {
format!("{n} {noun}s")
}
}

#[cfg(test)]
mod test {
use super::*;
use std::time::Duration;

#[test]
fn test_duration_minutes_seconds() {
assert_eq!(duration_minutes_seconds(Duration::ZERO), "0:00");
assert_eq!(duration_minutes_seconds(Duration::from_secs(3)), "0:03");
assert_eq!(duration_minutes_seconds(Duration::from_secs(73)), "1:13");
assert_eq!(
duration_minutes_seconds(Duration::from_secs(6003)),
"100:03"
);
}
}
4 changes: 2 additions & 2 deletions src/log_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ impl LogFile {
}

fn clean_filename(s: &str) -> String {
let s = s.replace('/', "__");
s.chars()
s.replace('/', "__")
.chars()
.map(|c| match c {
'\\' | ' ' | ':' | '<' | '>' | '?' | '*' | '|' | '"' => '_',
c => c,
Expand Down
34 changes: 19 additions & 15 deletions src/outcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ use std::time::Duration;
use std::time::Instant;

use anyhow::Context;
use humantime::format_duration;
use serde::ser::SerializeStruct;
use serde::Serialize;
use serde::Serializer;

use crate::console::{duration_minutes_seconds, plural};
use crate::console::plural;
use crate::exit_code;
use crate::log_file::LogFile;
use crate::process::ProcessStatus;
Expand Down Expand Up @@ -48,7 +49,7 @@ impl Phase {

impl fmt::Display for Phase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
f.pad(self.name())
}
}

Expand Down Expand Up @@ -107,33 +108,36 @@ impl LabOutcome {

/// Return an overall summary, to show at the end of the program.
pub fn summary_string(&self, start_time: Instant, options: &Options) -> String {
let mut s = format!("{} tested", plural(self.total_mutants, "mutant"),);
let mut s = Vec::new();
s.push(format!("{} tested", plural(self.total_mutants, "mutant")));
if options.show_times {
s.push_str(" in ");
s.push_str(&duration_minutes_seconds(start_time.elapsed()));
s.push(format!(
" in {}",
format_duration(Duration::from_secs(start_time.elapsed().as_secs()))
));
}
s.push_str(": ");
let mut parts: Vec<String> = Vec::new();
s.push(": ".into());
let mut by_outcome: Vec<String> = Vec::new();
if self.missed > 0 {
parts.push(format!("{} missed", self.missed));
by_outcome.push(format!("{} missed", self.missed));
}
if self.caught > 0 {
parts.push(format!("{} caught", self.caught));
by_outcome.push(format!("{} caught", self.caught));
}
if self.unviable > 0 {
parts.push(format!("{} unviable", self.unviable));
by_outcome.push(format!("{} unviable", self.unviable));
}
if self.timeout > 0 {
parts.push(format!("{} timeouts", self.timeout));
by_outcome.push(format!("{} timeouts", self.timeout));
}
if self.success > 0 {
parts.push(format!("{} succeeded", self.success));
by_outcome.push(format!("{} succeeded", self.success));
}
if self.failure > 0 {
parts.push(format!("{} failed", self.failure));
by_outcome.push(format!("{} failed", self.failure));
}
s.push_str(&parts.join(", "));
s
s.push(by_outcome.join(", "));
s.join("")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,10 @@ src/console.rs: replace style_outcome -> StyledObject<&'static str> with StyledO
src/console.rs: replace style_outcome -> StyledObject<&'static str> with StyledObject::from_iter(["xyzzy"])
src/console.rs: replace style_outcome -> StyledObject<&'static str> with StyledObject::new("xyzzy")
src/console.rs: replace style_outcome -> StyledObject<&'static str> with StyledObject::from("xyzzy")
src/console.rs: replace style_elapsed_secs -> String with String::new()
src/console.rs: replace style_elapsed_secs -> String with "xyzzy".into()
src/console.rs: replace style_secs -> String with String::new()
src/console.rs: replace style_secs -> String with "xyzzy".into()
src/console.rs: replace style_minutes_seconds -> String with String::new()
src/console.rs: replace style_minutes_seconds -> String with "xyzzy".into()
src/console.rs: replace duration_minutes_seconds -> String with String::new()
src/console.rs: replace duration_minutes_seconds -> String with "xyzzy".into()
src/console.rs: replace style_duration -> String with String::new()
src/console.rs: replace style_duration -> String with "xyzzy".into()
src/console.rs: replace format_mb -> String with String::new()
src/console.rs: replace format_mb -> String with "xyzzy".into()
src/console.rs: replace style_mb -> StyledObject<String> with StyledObject::new()
Expand Down
Loading

0 comments on commit d533e93

Please sign in to comment.