Skip to content
Merged
73 changes: 48 additions & 25 deletions src/exit_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,53 @@
//!
//! These are also described in README.md.

// TODO: Maybe merge this with outcome::Status, and maybe merge with sysexit.

/// Everything worked and all the mutants were caught.
pub const SUCCESS: i32 = 0;

/// The wrong arguments, etc.
///
/// (1 is also the value returned by Clap.)
pub const USAGE: i32 = 1;

/// Found one or mutants that were not caught by tests.
pub const FOUND_PROBLEMS: i32 = 2;
use std::process::ExitCode as StdExitCode;

/// One or more tests timed out: probably the mutant caused an infinite loop, or the timeout is too low.
pub const TIMEOUT: i32 = 3;

/// The tests are already failing in an unmutated tree.
pub const BASELINE_FAILED: i32 = 4;

/// The filter diff new text does not match the source tree content.
pub const FILTER_DIFF_MISMATCH: i32 = 5;

/// The filter diff could not be parsed.
pub const FILTER_DIFF_INVALID: i32 = 6;
// TODO: Maybe merge this with outcome::Status, and maybe merge with sysexit.

/// An internal software error, from sysexit.
pub const SOFTWARE: i32 = 70;
/// Exit codes for cargo-mutants.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitCode {
/// Everything worked and all the mutants were caught.
Success,
/// The wrong arguments, etc.
///
/// (1 is also the value returned by Clap.)
Usage,
/// Found one or mutants that were not caught by tests.
FoundProblems,
/// One or more tests timed out: probably the mutant caused an infinite loop, or the timeout is too low.
Timeout,
/// The tests are already failing in an unmutated tree.
BaselineFailed,
/// The filter diff new text does not match the source tree content.
FilterDiffMismatch,
/// The filter diff could not be parsed.
FilterDiffInvalid,
/// An internal software error, from sysexit.
Software,
}

impl ExitCode {
/// Returns the numeric exit code value.
pub const fn code(self) -> i32 {
match self {
Self::Success => 0,
Comment thread
sourcefrog marked this conversation as resolved.
Outdated
Self::Usage => 1,
Self::FoundProblems => 2,
Self::Timeout => 3,
Self::BaselineFailed => 4,
Self::FilterDiffMismatch => 5,
Self::FilterDiffInvalid => 6,
Self::Software => 70,
}
}
}

impl From<ExitCode> for StdExitCode {
Comment thread
sourcefrog marked this conversation as resolved.
fn from(code: ExitCode) -> Self {
// All exit codes are known to be valid u8 values
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
StdExitCode::from(code.code() as u8)
}
}
12 changes: 6 additions & 6 deletions src/in_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ impl DiffFilterError {
///
/// Some errors such as an empty diff or one that changes no Rust source files
/// still mean we can't process any mutants, but it's not necessarily a problem.
pub fn exit_code(&self) -> i32 {
pub fn exit_code(&self) -> exit_code::ExitCode {
match self {
DiffFilterError::EmptyDiff
| DiffFilterError::NoSourceFiles
| DiffFilterError::NoMutants => exit_code::SUCCESS,
DiffFilterError::MismatchedDiff(_) => exit_code::FILTER_DIFF_MISMATCH,
| DiffFilterError::NoMutants => exit_code::ExitCode::Success,
DiffFilterError::MismatchedDiff(_) => exit_code::ExitCode::FilterDiffMismatch,
DiffFilterError::File(_) | DiffFilterError::InvalidDiff(_) => {
exit_code::FILTER_DIFF_INVALID
exit_code::ExitCode::FilterDiffInvalid
}
}
}
Expand Down Expand Up @@ -349,7 +349,7 @@ index eb42779..a0091b7 100644
";
let err = diff_filter(Vec::new(), diff);
assert_eq!(err, Err(DiffFilterError::NoMutants));
assert_eq!(err.unwrap_err().exit_code(), 0);
assert_eq!(err.unwrap_err().exit_code(), exit_code::ExitCode::Success);
}

/// <https://github.com/sourcefrog/cargo-mutants/issues/580>
Expand Down Expand Up @@ -415,7 +415,7 @@ index cc3ce8c..8fe9aa0 100644
";
let err = diff_filter(Vec::new(), diff);
assert_eq!(err, Err(DiffFilterError::NoSourceFiles));
assert_eq!(err.unwrap_err().exit_code(), 0);
assert_eq!(err.unwrap_err().exit_code(), exit_code::ExitCode::Success);
}

fn make_diff(old: &str, new: &str) -> String {
Expand Down
43 changes: 22 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ use std::env;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use std::process::ExitCode as StdExitCode;

use anyhow::{Context, Result, anyhow, ensure};
use anyhow::{Context, Result, anyhow, bail};
use camino::{Utf8Path, Utf8PathBuf};
use clap::{
ArgAction, CommandFactory, Parser, ValueEnum,
Expand Down Expand Up @@ -492,32 +492,30 @@ pub struct Args {
common: Common,
}

fn main() -> Result<()> {
// TODO: Perhaps return an ExitCode and avoid having calls to exit(). And as
// part of that, perhaps we should have an error type that implements Termination,
// to report its exit code.
fn main() -> Result<StdExitCode> {
let args = match Cargo::try_parse() {
Ok(Cargo::Mutants(args)) => args,
Err(e) => {
e.print().expect("Failed to show clap error message");
// Clap by default exits with code 2.
let code = match e.exit_code() {
2 => exit_code::USAGE,
0 => 0,
_ => exit_code::SOFTWARE,
2 => exit_code::ExitCode::Usage,
0 => exit_code::ExitCode::Success,
_ => exit_code::ExitCode::Software,
};
exit(code);
return Ok(code.into());
}
};

if args.version {
println!("{NAME} {VERSION}");
return Ok(());
return Ok(exit_code::ExitCode::Success.into());
} else if let Some(shell) = args.completions {
generate(shell, &mut Cargo::command(), "cargo", &mut io::stdout());
return Ok(());
return Ok(exit_code::ExitCode::Success.into());
} else if let Some(schema_type) = args.emit_schema {
return emit_schema(schema_type);
emit_schema(schema_type)?;
return Ok(exit_code::ExitCode::Success.into());
}

let console = Console::new();
Expand All @@ -533,14 +531,17 @@ fn main() -> Result<()> {
config::Config::default()
};
let options = Options::new(&args, &config)?;
return mutate_file(path, &options);
mutate_file(path, &options)?;
return Ok(exit_code::ExitCode::Success.into());
}

let start_dir: &Utf8Path = if let Some(manifest_path) = &args.manifest_path {
ensure!(manifest_path.is_file(), "Manifest path is not a file");
if !manifest_path.is_file() {
bail!("Manifest path is not a file");
Comment thread
sourcefrog marked this conversation as resolved.
Outdated
}
manifest_path
.parent()
.ok_or(anyhow!("Manifest path has no parent"))?
.context("Manifest path has no parent")?
} else if let Some(dir) = &args.dir {
dir
} else {
Expand Down Expand Up @@ -588,19 +589,19 @@ fn main() -> Result<()> {
console.clear();
if args.list_files {
print!("{}", list_files(&discovered.files, &options));
return Ok(());
return Ok(exit_code::ExitCode::Success.into());
}
let mut mutants = discovered.mutants;
if let Some(diff_path) = &args.in_diff {
mutants = match diff_filter_file(mutants, diff_path) {
Ok(mutants) => mutants,
Err(err) => {
if err.exit_code() == 0 {
if err.exit_code() == exit_code::ExitCode::Success {
info!("{err}");
} else {
error!("{err}");
}
exit(err.exit_code());
return Ok(err.exit_code().into());
}
};
}
Expand All @@ -609,16 +610,16 @@ fn main() -> Result<()> {
}
if args.list {
print!("{}", list_mutants(&mutants, &options));
Ok(exit_code::ExitCode::Success.into())
} else {
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, output_dir, &options, &console)?;
exit(lab_outcome.exit_code());
Ok(lab_outcome.exit_code().into())
}
Ok(())
}

fn emit_schema(schema_type: SchemaType) -> Result<()> {
Expand Down
10 changes: 5 additions & 5 deletions src/outcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,20 @@ impl LabOutcome {
}

/// Return the overall program exit code reflecting this outcome.
pub fn exit_code(&self) -> i32 {
pub fn exit_code(&self) -> exit_code::ExitCode {
Comment thread
sourcefrog marked this conversation as resolved.
Outdated
// TODO: Maybe move this into an error returned from experiment()?
if self
.outcomes
.iter()
.any(|o| !o.scenario.is_mutant() && !o.success())
{
exit_code::BASELINE_FAILED
exit_code::ExitCode::BaselineFailed
} else if self.timeout > 0 {
exit_code::TIMEOUT
exit_code::ExitCode::Timeout
} else if self.missed > 0 {
exit_code::FOUND_PROBLEMS
exit_code::ExitCode::FoundProblems
} else {
exit_code::SUCCESS
exit_code::ExitCode::Success
}
}

Expand Down
46 changes: 46 additions & 0 deletions tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3778,3 +3778,49 @@ fn mutate_single_file() {
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
assert_eq!(String::from_utf8_lossy(&out.stdout), "");
}

#[test]
fn in_diff_with_mismatched_content_returns_exit_code_5() {
// Test that when the diff doesn't match the source tree, we get exit code 5
let tmp = copy_of_testdata("diff1");

// Create a diff that shows the new file content as something different
// from what's actually in the tree. The diff parser will try to match
// line 1 to be 'WRONG_CONTENT' but the actual file has 'pub fn one() -> String {'
let diff_text = "\
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,3 @@
-pub fn old() -> String {
+WRONG_CONTENT
\"one\".to_owned()
}
";

let mut diff_file = NamedTempFile::new().unwrap();
diff_file.write_all(diff_text.as_bytes()).unwrap();

run()
.args(["mutants", "-d"])
.arg(tmp.path())
.arg("--in-diff")
.arg(diff_file.path())
.assert()
.code(5)
.stderr(contains("Diff content doesn't match source file"));
}

#[test]
fn in_diff_with_nonexistent_file_returns_exit_code_6() {
// Test that a nonexistent diff file returns exit code 6
let tmp = copy_of_testdata("diff1");

run()
.args(["mutants", "-d"])
.arg(tmp.path())
.arg("--in-diff")
.arg("/nonexistent/path/to/diff.patch")
.assert()
.code(6)
.stderr(contains("Failed to read diff file").or(contains("Failed to open diff file")));
}