Skip to content

Commit

Permalink
Basic implementation of --iterate
Browse files Browse the repository at this point in the history
  • Loading branch information
sourcefrog committed Jun 12, 2024
1 parent c3974e7 commit d30c7ec
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 19 deletions.
11 changes: 10 additions & 1 deletion src/build_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -23,6 +24,14 @@ pub struct BuildDir {
temp_dir: Option<TempDir>,
}

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(
Expand Down
15 changes: 4 additions & 11 deletions src/lab.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -29,16 +27,11 @@ use crate::*;
pub fn test_mutants(
mut mutants: Vec<Mutant>,
workspace_dir: &Utf8Path,
output_dir: OutputDir,
options: Options,
console: &Console,
) -> Result<LabOutcome> {
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);
Expand Down Expand Up @@ -148,7 +141,7 @@ pub fn test_mutants(
)?;
}
Ok(None) => {
trace!("no more work");
trace!("no more work for this thread");
return Ok(());
}
}
Expand Down
27 changes: 24 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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_by_name(&previously_caught);
Some(previously_caught)
} else {
None
};

console.clear();
if args.list_files {
list_files(FmtToIoWrite::new(io::stdout()), &discovered.files, &options)?;
Expand All @@ -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(())
Expand Down
91 changes: 87 additions & 4 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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<Vec<String>> {
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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<String> 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());
}
}
13 changes: 13 additions & 0 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ pub struct Discovered {
pub files: Vec<SourceFile>,
}

impl Discovered {
pub(crate) fn remove_by_name(&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.
Expand Down

0 comments on commit d30c7ec

Please sign in to comment.