diff --git a/src/build_dir.rs b/src/build_dir.rs index 1a84fcb0..d2132bf5 100644 --- a/src/build_dir.rs +++ b/src/build_dir.rs @@ -2,6 +2,8 @@ //! A directory containing mutated source to run cargo builds and tests. +use std::fmt::{self, Debug}; + use tempfile::TempDir; use tracing::info; @@ -13,7 +15,6 @@ use crate::*; /// /// Depending on how its constructed, this might be a copy in a tempdir /// or the original source directory. -#[derive(Debug)] pub struct BuildDir { /// The path of the root of the build directory. path: Utf8PathBuf, @@ -23,6 +24,14 @@ pub struct BuildDir { temp_dir: Option, } +impl Debug for BuildDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BuildDir") + .field("path", &self.path) + .finish() + } +} + impl BuildDir { /// Make a new build dir, copying from a source directory, subject to exclusions. pub fn copy_from( diff --git a/src/lab.rs b/src/lab.rs index 129d69fa..9a37e726 100644 --- a/src/lab.rs +++ b/src/lab.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2023 Martin Pool +// Copyright 2021-2024 Martin Pool //! Successively apply mutations to the source code and run cargo to check, build, and test them. @@ -9,9 +9,7 @@ use std::thread; use std::time::{Duration, Instant}; use itertools::Itertools; -use tracing::warn; -#[allow(unused)] -use tracing::{debug, debug_span, error, info, trace}; +use tracing::{debug, debug_span, error, info, trace, warn}; use crate::cargo::run_cargo; use crate::outcome::LabOutcome; @@ -29,16 +27,11 @@ use crate::*; pub fn test_mutants( mut mutants: Vec, workspace_dir: &Utf8Path, + output_dir: OutputDir, options: Options, console: &Console, ) -> Result { let start_time = Instant::now(); - let output_in_dir: &Utf8Path = options - .output_in_dir - .as_ref() - .map_or(workspace_dir, |p| p.as_path()); - let output_dir = OutputDir::new(output_in_dir)?; - console.set_debug_log(output_dir.open_debug_log()?); if options.shuffle { fastrand::shuffle(&mut mutants); @@ -148,7 +141,7 @@ pub fn test_mutants( )?; } Ok(None) => { - trace!("no more work"); + trace!("no more work for this thread"); return Ok(()); } } diff --git a/src/main.rs b/src/main.rs index af1aa4b7..2548b7fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,7 @@ use clap::builder::Styles; use clap::{ArgAction, CommandFactory, Parser, ValueEnum}; use clap_complete::{generate, Shell}; use color_print::cstr; +use output::{load_previously_caught, OutputDir}; use tracing::debug; use crate::build_dir::BuildDir; @@ -193,7 +194,7 @@ pub struct Args { )] in_place: bool, - /// Skip mutants that were caught in the previous run. + /// Skip mutants that were caught in previous runs. #[arg(long, help_heading = "Filters")] iterate: bool, @@ -407,7 +408,22 @@ fn main() -> Result<()> { } else { PackageFilter::Auto(start_dir.to_owned()) }; - let discovered = workspace.discover(&package_filter, &options, &console)?; + + let output_parent_dir = options + .output_in_dir + .clone() + .unwrap_or_else(|| workspace.dir.clone()); + + let mut discovered = workspace.discover(&package_filter, &options, &console)?; + + let previously_caught = if args.iterate { + let previously_caught = load_previously_caught(&output_parent_dir)?; + discovered.remove_previously_caught(&previously_caught); + Some(previously_caught) + } else { + None + }; + console.clear(); if args.list_files { list_files(FmtToIoWrite::new(io::stdout()), &discovered.files, &options)?; @@ -426,7 +442,12 @@ fn main() -> Result<()> { if args.list { list_mutants(FmtToIoWrite::new(io::stdout()), &mutants, &options)?; } else { - let lab_outcome = test_mutants(mutants, &workspace.dir, options, &console)?; + let output_dir = OutputDir::new(&output_parent_dir)?; + if let Some(previously_caught) = previously_caught { + output_dir.write_previously_caught(&previously_caught)?; + } + console.set_debug_log(output_dir.open_debug_log()?); + let lab_outcome = test_mutants(mutants, &workspace.dir, output_dir, options, &console)?; exit(lab_outcome.exit_code()); } Ok(()) diff --git a/src/output.rs b/src/output.rs index 90ef1777..a78020bb 100644 --- a/src/output.rs +++ b/src/output.rs @@ -13,7 +13,7 @@ use path_slash::PathExt; use serde::Serialize; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; -use tracing::info; +use tracing::{info, trace}; use crate::outcome::{LabOutcome, SummaryOutcome}; use crate::*; @@ -22,6 +22,9 @@ const OUTDIR_NAME: &str = "mutants.out"; const ROTATED_NAME: &str = "mutants.out.old"; const LOCK_JSON: &str = "lock.json"; const LOCK_POLL: Duration = Duration::from_millis(100); +static CAUGHT_TXT: &str = "caught.txt"; +static PREVIOUSLY_CAUGHT_TXT: &str = "previously_caught.txt"; +static UNVIABLE_TXT: &str = "unviable.txt"; /// The contents of a `lock.json` written into the output directory and used as /// a lock file to ensure that two cargo-mutants invocations don't try to write @@ -139,10 +142,10 @@ impl OutputDir { .open(output_dir.join("missed.txt")) .context("create missed.txt")?; let caught_list = list_file_options - .open(output_dir.join("caught.txt")) + .open(output_dir.join(CAUGHT_TXT)) .context("create caught.txt")?; let unviable_list = list_file_options - .open(output_dir.join("unviable.txt")) + .open(output_dir.join(UNVIABLE_TXT)) .context("create unviable.txt")?; let timeout_list = list_file_options .open(output_dir.join("timeout.txt")) @@ -222,10 +225,49 @@ impl OutputDir { pub fn take_lab_outcome(self) -> LabOutcome { self.lab_outcome } + + pub fn write_previously_caught(&self, caught: &[String]) -> Result<()> { + let p = self.path.join(PREVIOUSLY_CAUGHT_TXT); + // TODO: with_capacity when mutants knows to skip that; https://github.com/sourcefrog/cargo-mutants/issues/315 + // let mut b = String::with_capacity(caught.iter().map(|l| l.len() + 1).sum()); + let mut b = String::new(); + for l in caught { + b.push_str(l); + b.push('\n'); + } + File::options() + .create_new(true) + .write(true) + .open(&p) + .and_then(|mut f| f.write_all(b.as_bytes())) + .with_context(|| format!("Write {p:?}")) + } +} + +/// Return the string names of mutants previously caught in this output directory, including +/// unviable mutants. +/// +/// Returns an empty vec if there are none. +pub fn load_previously_caught(output_parent_dir: &Utf8Path) -> Result> { + let mut r = Vec::new(); + for filename in [CAUGHT_TXT, UNVIABLE_TXT, PREVIOUSLY_CAUGHT_TXT] { + let p = output_parent_dir.join(OUTDIR_NAME).join(filename); + trace!(?p, "read previously caught"); + if p.is_file() { + r.extend( + read_to_string(&p) + .with_context(|| format!("Read previously caught mutants from {p:?}"))? + .lines() + .map(|s| s.to_owned()), + ); + } + } + Ok(r) } #[cfg(test)] mod test { + use fs::write; use indoc::indoc; use itertools::Itertools; use pretty_assertions::assert_eq; @@ -297,7 +339,7 @@ mod test { #[test] fn rotate() { - let temp_dir = tempfile::TempDir::new().unwrap(); + let temp_dir = TempDir::new().unwrap(); let temp_dir_path = Utf8Path::from_path(temp_dir.path()).unwrap(); // Create an initial output dir with one log. @@ -338,4 +380,45 @@ mod test { .join("mutants.out.old/log/baseline.log") .is_file()); } + + #[test] + fn track_previously_caught() { + let temp_dir = TempDir::new().unwrap(); + let parent = Utf8Path::from_path(temp_dir.path()).unwrap(); + + let example = "src/process.rs:213:9: replace ProcessStatus::is_success -> bool with true +src/process.rs:248:5: replace get_command_output -> Result with Ok(String::new()) +"; + + // Read from an empty dir: succeeds. + assert!(load_previously_caught(parent) + .expect("load succeeds") + .is_empty()); + + let output_dir = OutputDir::new(parent).unwrap(); + assert!(load_previously_caught(parent) + .expect("load succeeds") + .is_empty()); + + write(parent.join("mutants.out/caught.txt"), example.as_bytes()).unwrap(); + let previously_caught = load_previously_caught(parent).expect("load succeeds"); + assert_eq!( + previously_caught.iter().collect_vec(), + example.lines().collect_vec() + ); + + // make a new output dir, moving away the old one, and write this + drop(output_dir); + let output_dir = OutputDir::new(parent).unwrap(); + output_dir + .write_previously_caught(&previously_caught) + .unwrap(); + assert_eq!( + read_to_string(parent.join("mutants.out/caught.txt")).expect("read caught.txt"), + "" + ); + assert!(parent.join("mutants.out/previously_caught.txt").is_file()); + let now = load_previously_caught(parent).expect("load succeeds"); + assert_eq!(now.iter().collect_vec(), example.lines().collect_vec()); + } } diff --git a/src/visit.rs b/src/visit.rs index 1d8ff6f5..9c32596e 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -35,6 +35,19 @@ pub struct Discovered { pub files: Vec, } +impl Discovered { + pub(crate) fn remove_previously_caught(&mut self, previously_caught: &[String]) { + self.mutants.retain(|m| { + let name = m.name(true, false); + let c = previously_caught.contains(&name); + if c { + trace!(?name, "skip previously caught mutant"); + } + !c + }) + } +} + /// Discover all mutants and all source files. /// /// The list of source files includes even those with no mutants.