use std::env;
#[cfg(test)]
use std::ffi::OsString;
use std::time::Duration;
use anyhow::Context;
use camino::{Utf8Path, Utf8PathBuf};
use clap::ArgAction;
use globset::GlobSet;
use regex::RegexSet;
use schemars::JsonSchema;
use serde::Deserialize;
use strum::{Display, EnumString};
use syn::Expr;
use tracing::warn;
use crate::annotation::ResolvedAnnotation;
use crate::config::Config;
use crate::glob::build_glob_set;
use crate::mutant::Mutant;
use crate::shard::Sharding;
use crate::{Args, BaselineStrategy, Phase, Result, ValueEnum};
#[derive(Default, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct Options {
pub annotations: ResolvedAnnotation,
pub baseline: BaselineStrategy,
pub cap_lints: bool,
pub check_only: bool,
pub copy_vcs: bool,
pub gitignore: bool,
pub copy_target: bool,
pub in_place: bool,
pub jobserver: bool,
pub jobserver_tasks: Option<usize>,
pub leak_dirs: bool,
pub test_timeout: Option<Duration>,
pub test_timeout_multiplier: Option<f64>,
pub test_package: TestPackages,
pub build_timeout: Option<Duration>,
pub build_timeout_multiplier: Option<f64>,
pub minimum_test_timeout: Duration,
pub print_caught: bool,
pub print_unviable: bool,
pub show_times: bool,
pub show_all_logs: bool,
pub show_line_col: bool,
pub shuffle: bool,
pub skip_calls: Vec<String>,
pub profile: Option<String>,
pub additional_cargo_args: Vec<String>,
pub additional_cargo_test_args: Vec<String>,
pub features: Vec<String>,
pub no_default_features: bool,
pub all_features: bool,
pub examine_globset: Option<GlobSet>,
pub exclude_globset: Option<GlobSet>,
pub examine_name_re: RegexSet,
pub exclude_name_re: RegexSet,
pub output_in_dir: Option<Utf8PathBuf>,
pub jobs: Option<usize>,
pub error_values: Vec<String>,
pub colors: Colors,
pub emit_json: bool,
pub common: Common,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::Args, Deserialize, JsonSchema)]
pub struct Common {
#[arg(long, long = "diff", help_heading = "Output", action = ArgAction::SetTrue)]
#[serde(skip)] pub emit_diffs: Option<bool>,
#[arg(long, help_heading = "Execution")]
pub test_tool: Option<TestTool>,
#[arg(long, help_heading = "Execution", requires = "shard")]
pub sharding: Option<Sharding>,
}
impl Common {
#[allow(clippy::trivially_copy_pass_by_ref)] pub fn merge(&self, other: &Common) -> Common {
Common {
emit_diffs: self.emit_diffs.or(other.emit_diffs),
test_tool: self.test_tool.or(other.test_tool),
sharding: self.sharding.or(other.sharding),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, EnumString, Display, Deserialize)]
pub enum TestPackages {
#[default]
Mutated,
Workspace,
Named(Vec<String>),
}
#[derive(
Debug, Default, Clone, Copy, PartialEq, Eq, EnumString, Display, Deserialize, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum TestTool {
#[default]
Cargo,
Nextest,
}
fn join_slices(a: &[String], b: &[String]) -> Vec<String> {
a.iter().chain(b).cloned().collect()
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, Deserialize, ValueEnum)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum Colors {
#[default]
Auto,
Always,
Never,
}
impl Colors {
pub fn forced_value(self) -> Option<bool> {
if env::var("NO_COLOR").is_ok_and(|x| x != "0") {
Some(false)
} else if env::var("CLICOLOR_FORCE").is_ok_and(|x| x != "0") {
Some(true)
} else {
match self {
Colors::Always => Some(true),
Colors::Never => Some(false),
Colors::Auto => None, }
}
}
#[mutants::skip] pub fn active_stdout(self) -> bool {
self.forced_value()
.unwrap_or_else(::console::colors_enabled)
}
}
impl Options {
#[allow(clippy::too_many_lines)] pub(crate) fn new(args: &Args, config: &Config) -> Result<Options> {
if args.no_copy_target {
warn!("--no-copy-target is deprecated; use --copy-target=false instead");
}
let minimum_test_timeout = Duration::from_secs_f64(
args.minimum_test_timeout
.or(config.minimum_test_timeout)
.unwrap_or(20f64),
);
let test_package = if args.test_workspace == Some(true) {
TestPackages::Workspace
} else if !args.test_package.is_empty() {
TestPackages::Named(
args.test_package
.iter()
.flat_map(|s| s.split(','))
.map(ToString::to_string)
.collect(),
)
} else if args.test_workspace.is_none() && config.test_workspace == Some(true) {
TestPackages::Workspace
} else if !config.test_package.is_empty() {
TestPackages::Named(config.test_package.clone())
} else {
TestPackages::Mutated
};
let mut skip_calls: Vec<String> = args
.skip_calls
.iter()
.flat_map(|s| s.split(','))
.map(ToString::to_string)
.chain(config.skip_calls.iter().cloned())
.collect();
if args
.skip_calls_defaults
.or(config.skip_calls_defaults)
.unwrap_or(true)
{
skip_calls.push("with_capacity".to_owned());
}
let options = Options {
additional_cargo_args: join_slices(&args.cargo_arg, &config.additional_cargo_args),
additional_cargo_test_args: args
.cargo_test_arg
.iter()
.chain(&args.cargo_test_args)
.chain(&config.additional_cargo_test_args)
.cloned()
.collect(),
all_features: args.all_features || config.all_features.unwrap_or(false),
annotations: args.annotations.resolve(),
baseline: args.baseline,
build_timeout: args.build_timeout.map(Duration::from_secs_f64),
build_timeout_multiplier: args
.build_timeout_multiplier
.or(config.build_timeout_multiplier),
cap_lints: args.cap_lints.unwrap_or(config.cap_lints),
check_only: args.check,
colors: args.colors,
copy_vcs: args.copy_vcs.or(config.copy_vcs).unwrap_or(false),
emit_json: args.json,
common: args.common.merge(&config.common),
error_values: join_slices(&args.error, &config.error_values),
examine_name_re: RegexSet::new(args.examine_re.iter().chain(config.examine_re.iter()))
.context("Failed to compile examine_re regex")?,
exclude_name_re: RegexSet::new(args.exclude_re.iter().chain(&config.exclude_re))
.context("Failed to compile exclude_re regex")?,
examine_globset: build_glob_set(args.file.iter().chain(config.examine_globs.iter()))?,
exclude_globset: build_glob_set(
args.exclude.iter().chain(config.exclude_globs.iter()),
)?,
features: join_slices(&args.features, &config.features),
gitignore: args
.gitignore
.unwrap_or(config.gitignore.unwrap_or_default()),
copy_target: (!args.no_copy_target)
&& args.copy_target.or(config.copy_target).unwrap_or(false),
in_place: args.in_place,
jobs: args.jobs,
jobserver: args.jobserver,
jobserver_tasks: args.jobserver_tasks,
leak_dirs: args.leak_dirs,
minimum_test_timeout,
no_default_features: args.no_default_features
|| config.no_default_features.unwrap_or(false),
output_in_dir: args
.output
.clone()
.or(config.output.as_ref().map(Utf8PathBuf::from)),
print_caught: args.caught,
print_unviable: args.unviable,
profile: args.profile.as_ref().or(config.profile.as_ref()).cloned(),
shuffle: args.shuffle,
show_line_col: args.line_col,
show_times: !args.no_times,
show_all_logs: args.all_logs,
skip_calls,
test_package,
test_timeout: args.timeout.map(Duration::from_secs_f64),
test_timeout_multiplier: args.timeout_multiplier.or(config.timeout_multiplier),
};
if let Some(jobs) = options.jobs
&& jobs > 8
{
warn!(
"--jobs={jobs} is probably too high and may overload your machine: each job runs a separate `cargo` process, and cargo may internally start many threads and subprocesses; values <= 8 are usually safe"
);
}
options.error_values.iter().for_each(|e| {
if e.starts_with("Err(") {
warn!(
"error_value option gives the value of the error, and probably should not start with Err(: got {}",
e
);
}
});
Ok(options)
}
#[cfg(test)]
pub fn from_args(args: &Args) -> Result<Options> {
Options::new(args, &Config::default())
}
#[cfg(test)]
pub fn from_arg_strs<I: IntoIterator<Item = S>, S: Into<OsString> + Clone>(args: I) -> Options {
use crate::Args;
use clap::Parser;
let args = Args::try_parse_from(args).expect("Failed to parse args");
Options::from_args(&args).expect("Build options from args")
}
#[cfg(test)]
pub fn from_arg_strs_and_config<I: IntoIterator<Item = S>, S: Into<OsString> + Clone>(
args: I,
config: &str,
) -> Options {
use crate::Args;
use clap::Parser;
use std::str::FromStr;
let args = Args::try_parse_from(args).expect("Failed to parse args");
let config = Config::from_str(config).expect("Failed to parse config");
Options::new(&args, &config).expect("Build options from args")
}
pub fn phases(&self) -> &[Phase] {
if self.check_only {
&[Phase::Check]
} else {
&[Phase::Build, Phase::Test]
}
}
pub(crate) fn parsed_error_exprs(&self) -> Result<Vec<Expr>> {
self.error_values
.iter()
.map(|e| {
syn::parse_str(e).with_context(|| format!("Failed to parse error value {e:?}"))
})
.collect()
}
pub fn allows_source_file_path(&self, path: &Utf8Path) -> bool {
self.examine_globset
.as_ref()
.is_none_or(|g| g.is_match(path))
&& !self
.exclude_globset
.as_ref()
.is_some_and(|g| g.is_match(path))
}
pub fn allows_mutant(&self, mutant: &Mutant) -> bool {
let name = mutant.name(true);
(self.examine_name_re.is_empty() || self.examine_name_re.is_match(&name))
&& (self.exclude_name_re.is_empty() || !self.exclude_name_re.is_match(&name))
}
pub fn emit_diffs(&self) -> bool {
self.common.emit_diffs.unwrap_or(false)
}
pub fn test_tool(&self) -> TestTool {
self.common.test_tool.unwrap_or_default()
}
pub fn sharding(&self) -> Sharding {
self.common.sharding.unwrap_or_default()
}
}
#[cfg(test)]
mod test {
use std::io::Write;
use std::str::FromStr;
use clap::Parser;
use indoc::indoc;
use tempfile::NamedTempFile;
use super::*;
use crate::Args;
#[test]
fn default_options() {
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.check_only);
assert!(options.common.test_tool.is_none());
assert_eq!(options.test_tool(), TestTool::Cargo);
assert!(!options.cap_lints);
assert!(options.common.sharding.is_none());
assert_eq!(options.sharding(), Sharding::Slice);
}
#[test]
fn options_from_test_tool_arg() {
let args = Args::parse_from(["mutants", "--test-tool", "nextest"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_tool(), TestTool::Nextest);
}
#[test]
fn options_from_baseline_arg() {
let args = Args::parse_from(["mutants", "--baseline", "skip"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.baseline, BaselineStrategy::Skip);
let args = Args::parse_from(["mutants", "--baseline", "run"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.baseline, BaselineStrategy::Run);
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.baseline, BaselineStrategy::Run);
}
#[test]
fn options_from_timeout_args() {
let args = Args::parse_from(["mutants", "--timeout=2.0"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_timeout, Some(Duration::from_secs(2)));
let args = Args::parse_from(["mutants", "--timeout-multiplier=2.5"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_timeout_multiplier, Some(2.5));
let args = Args::parse_from(["mutants", "--minimum-test-timeout=60.0"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.minimum_test_timeout, Duration::from_secs(60));
let args = Args::parse_from(["mutants", "--build-timeout=3.0"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.build_timeout, Some(Duration::from_secs(3)));
let args = Args::parse_from(["mutants", "--build-timeout-multiplier=3.5"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.build_timeout_multiplier, Some(3.5));
}
#[test]
fn cli_timeout_multiplier_overrides_config() {
let config = indoc! { r"
timeout_multiplier = 1.0
build_timeout_multiplier = 2.0
"};
let mut config_file = NamedTempFile::new().unwrap();
config_file.write_all(config.as_bytes()).unwrap();
let args = Args::parse_from([
"mutants",
"--timeout-multiplier=2.0",
"--build-timeout-multiplier=1.0",
]);
let config = Config::read_file(config_file.path()).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_timeout_multiplier, Some(2.0));
assert_eq!(options.build_timeout_multiplier, Some(1.0));
}
#[test]
fn conflicting_timeout_options() {
let args = Args::try_parse_from(["mutants", "--timeout=1", "--timeout-multiplier=1"])
.expect_err("--timeout and --timeout-multiplier should conflict");
let rendered = format!("{}", args.render());
assert!(rendered.contains("error: the argument '--timeout <TIMEOUT>' cannot be used with '--timeout-multiplier <TIMEOUT_MULTIPLIER>'"));
}
#[test]
fn conflicting_build_timeout_options() {
let args = Args::try_parse_from([
"mutants",
"--build-timeout=1",
"--build-timeout-multiplier=1",
])
.expect_err("--build-timeout and --build-timeout-multiplier should conflict");
let rendered = format!("{}", args.render());
assert!(rendered.contains("error: the argument '--build-timeout <BUILD_TIMEOUT>' cannot be used with '--build-timeout-multiplier <BUILD_TIMEOUT_MULTIPLIER>'"));
}
#[test]
fn from_config() {
let config = indoc! { r#"
test_tool = "nextest"
cap_lints = true
"#};
let mut config_file = NamedTempFile::new().unwrap();
config_file.write_all(config.as_bytes()).unwrap();
let args = Args::try_parse_from(["mutants"]).unwrap();
let config = Config::read_file(config_file.path()).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_tool(), TestTool::Nextest);
assert!(options.cap_lints);
}
#[test]
fn features_arg() {
let args = Args::try_parse_from(["mutants", "--features", "nice,shiny features"]).unwrap();
assert_eq!(args.features.iter().as_ref(), ["nice,shiny features"]);
assert!(!args.no_default_features);
assert!(!args.all_features);
let options = Options::new(&args, &Config::default()).unwrap();
assert_eq!(options.features.iter().as_ref(), ["nice,shiny features"]);
assert!(!options.no_default_features);
assert!(!options.all_features);
}
#[test]
fn no_default_features_arg() {
let args = Args::try_parse_from([
"mutants",
"--no-default-features",
"--features",
"nice,shiny features",
])
.unwrap();
let options = Options::new(&args, &Config::default()).unwrap();
assert_eq!(options.features.iter().as_ref(), ["nice,shiny features"]);
assert!(options.no_default_features);
assert!(!options.all_features);
}
#[test]
fn default_jobserver_settings() {
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(options.jobserver);
assert_eq!(options.jobserver_tasks, None);
}
#[test]
fn disable_jobserver() {
let args = Args::parse_from(["mutants", "--jobserver=false"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.jobserver);
assert_eq!(options.jobserver_tasks, None);
}
#[test]
fn jobserver_tasks() {
let args = Args::parse_from(["mutants", "--jobserver-tasks=13"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(options.jobserver);
assert_eq!(options.jobserver_tasks, Some(13));
}
#[test]
fn all_features_arg() {
let args = Args::try_parse_from([
"mutants",
"--all-features",
"--features",
"nice,shiny features",
])
.unwrap();
let options = Options::new(&args, &Config::default()).unwrap();
assert_eq!(options.features.iter().as_ref(), ["nice,shiny features"]);
assert!(!options.no_default_features);
assert!(options.all_features);
}
#[test]
fn copy_target_default_false() {
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.copy_target);
}
#[test]
fn copy_target_from_command_line() {
let args = Args::parse_from(["mutants", "--copy-target=true"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(options.copy_target);
}
#[test]
fn copy_target_from_config() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("copy_target = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.copy_target);
}
#[test]
fn copy_target_config_only() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("copy_target = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.copy_target);
}
#[test]
fn gitignore_off_by_default() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.gitignore, "Default gitignore should be off");
}
#[test]
fn gitignore_true_on_command_line() {
let args = Args::parse_from(["mutants", "--gitignore=true"]);
let config = Config::from_str("").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.gitignore, "gitignore should be on");
}
#[test]
fn gitignore_false_on_command_line() {
let args = Args::parse_from(["mutants", "--gitignore=false"]);
let config = Config::from_str("").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.gitignore, "gitignore should be off");
}
#[test]
fn gitignore_true_in_config() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("gitignore = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.gitignore, "gitignore should be on");
}
#[test]
fn gitignore_false_in_config() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("gitignore = false ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.gitignore, "gitignore should be off");
}
#[test]
fn gitignore_command_line_overrides_config() {
let args = Args::parse_from(["mutants", "--gitignore=false"]);
let config = Config::from_str("gitignore = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.gitignore, "gitignore should be off");
}
#[test]
fn copy_target_command_line_overrides_config() {
let args = Args::parse_from(["mutants", "--copy-target=false"]);
let config = Config::from_str("copy_target = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.copy_target);
}
#[test]
fn features_from_config() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str(indoc! { r#"
features = ["config-feature1", "config-feature2"]
"#})
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.features, vec!["config-feature1", "config-feature2"]);
assert!(!options.no_default_features);
assert!(!options.all_features);
}
#[test]
fn features_command_line_and_config_combined() {
let args = Args::parse_from(["mutants", "--features", "cli-feature"]);
let config = Config::from_str(indoc! { r#"
features = ["config-feature"]
"#})
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.features, vec!["cli-feature", "config-feature"]);
}
#[test]
fn all_features_from_config() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("all_features = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.all_features);
}
#[test]
fn all_features_command_line_overrides_config() {
let args = Args::parse_from(["mutants", "--all-features"]);
let config = Config::from_str("all_features = false ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.all_features);
}
#[test]
fn no_default_features_from_config() {
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("no_default_features = true ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.no_default_features);
}
#[test]
fn no_default_features_command_line_overrides_config() {
let args = Args::parse_from(["mutants", "--no-default-features"]);
let config = Config::from_str("no_default_features = false").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.no_default_features);
}
#[test]
fn no_copy_target_deprecated_option_still_works() {
let args = Args::parse_from(["mutants", "--no-copy-target"]);
let config = Config::from_str("copy_target = true").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.copy_target);
}
#[test]
fn no_copy_target_sets_copy_target_false() {
let args = Args::parse_from(["mutants", "--no-copy-target"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.copy_target);
}
#[test]
fn copy_target_command_line_true_overrides_config() {
let args = Args::parse_from(["mutants", "--copy-target=true"]);
let config = Config::from_str("copy_target = false ").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.copy_target);
}
#[test]
fn profile_option_from_args() {
let args = Args::parse_from(["mutants", "--profile=mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.profile.unwrap(), "mutants");
}
#[test]
fn profile_from_config() {
let args = Args::try_parse_from(["mutants", "-j3"]).unwrap();
let config = indoc! { r#"
profile = "mutants"
timeout_multiplier = 1.0
build_timeout_multiplier = 2.0
"#};
let mut config_file = NamedTempFile::new().unwrap();
config_file.write_all(config.as_bytes()).unwrap();
let config = Config::read_file(config_file.path()).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.profile.unwrap(), "mutants");
}
#[test]
fn test_workspace_arg_true() {
let args = Args::parse_from(["mutants", "--test-workspace=true"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_package, TestPackages::Workspace);
}
#[test]
fn test_workspace_arg_false() {
let args = Args::parse_from(["mutants", "--test-workspace=false"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_package, TestPackages::Mutated);
}
#[test]
fn test_workspace_config_true() {
let args = Args::try_parse_from(["mutants"]).unwrap();
let config = indoc! { r"
test_workspace = true
"};
let config = Config::from_str(config).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_package, TestPackages::Workspace);
}
#[test]
fn test_workspace_config_false() {
let args = Args::try_parse_from(["mutants"]).unwrap();
let config = indoc! { r"
test_workspace = false
"};
let config = Config::from_str(config).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_package, TestPackages::Mutated);
}
#[test]
fn test_workspace_args_override_config_true() {
let args = Args::try_parse_from(["mutants", "--test-workspace=true"]).unwrap();
let config = indoc! { r"
test_workspace = false
"};
let config = Config::from_str(config).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_package, TestPackages::Workspace);
}
#[test]
fn test_workspace_args_override_config_false() {
let args = Args::try_parse_from(["mutants", "--test-workspace=false"]).unwrap();
let config = indoc! { r"
test_workspace = true
"};
let config = Config::from_str(config).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.test_package, TestPackages::Mutated);
}
#[test]
fn test_workspace_arg_false_allows_packages_from_config() {
let args = Args::try_parse_from(["mutants", "--test-workspace=false"]).unwrap();
let config = indoc! { r#"
# Normally the packages would be ignored, but --test-workspace=false.
test_workspace = true
test_package = ["foo", "bar"]
"#};
let config = Config::from_str(config).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.test_package,
TestPackages::Named(vec!["foo".to_string(), "bar".to_string()])
);
}
#[test]
fn test_package_arg_with_commas() {
let args = Args::parse_from(["mutants", "--test-package=foo,bar"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.test_package,
TestPackages::Named(vec!["foo".to_string(), "bar".to_string()])
);
}
#[test]
fn default_skip_calls_includes_with_capacity() {
let args = Args::try_parse_from(["mutants"]).unwrap();
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.skip_calls, ["with_capacity"]);
}
#[test]
fn arg_configure_skip_calls_default_off() {
let args = Args::try_parse_from(["mutants", "--skip-calls-defaults=false"]).unwrap();
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.skip_calls, [""; 0]);
}
#[test]
fn arg_redundantly_configure_skip_calls_default_on() {
let args = Args::try_parse_from(["mutants", "--skip-calls-defaults=true"]).unwrap();
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.skip_calls, ["with_capacity"]);
}
#[test]
fn skip_calls_from_args() {
let args = Args::try_parse_from(["mutants", "--skip-calls=a", "--skip-calls=b,c"]).unwrap();
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.skip_calls, ["a", "b", "c", "with_capacity"]);
}
#[test]
fn skip_calls_from_args_and_options() {
let args = Args::try_parse_from(["mutants", "--skip-calls=a", "--skip-calls=b,c"]).unwrap();
let config = Config::from_str(
r#"
skip_calls = ["d", "e"]
"#,
)
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.skip_calls,
["a", "b", "c", "d", "e", "with_capacity"]
);
}
#[test]
fn config_skip_calls_default_off() {
let args = Args::try_parse_from(["mutants"]).unwrap();
let config = Config::from_str(
r"
skip_calls_defaults = false
skip_calls = []
",
)
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.skip_calls, [""; 0]);
}
#[test]
fn arg_overrides_config_skip_calls_defaults() {
let args =
Args::try_parse_from(["mutants", "--skip-calls-defaults=true", "--skip-calls=x"])
.unwrap();
let config = Config::from_str(
r#"
skip_calls_defaults = false
skip_calls = ["y"]
"#,
)
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.skip_calls, ["x", "y", "with_capacity"]);
}
#[test]
fn copy_vcs() {
let args = Args::parse_from(["mutants", "--copy-vcs=true"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(options.copy_vcs);
let args = Args::parse_from(["mutants", "--copy-vcs=false"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.copy_vcs);
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("copy_vcs = true").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(options.copy_vcs);
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("").unwrap();
let options = Options::new(&args, &config).unwrap();
assert!(!options.copy_vcs);
}
#[test]
fn cargo_test_arg_from_command_line() {
let args = Args::parse_from(["mutants", "--cargo-test-arg=--lib"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.additional_cargo_test_args, vec!["--lib"]);
}
#[test]
fn cargo_test_arg_multiple_from_command_line() {
let args = Args::parse_from([
"mutants",
"--cargo-test-arg=--lib",
"--cargo-test-arg=--no-fail-fast",
]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.additional_cargo_test_args,
vec!["--lib", "--no-fail-fast"]
);
}
#[test]
fn cargo_test_args_after_double_dash() {
let args = Args::parse_from(["mutants", "--", "--lib", "--no-fail-fast"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.additional_cargo_test_args,
vec!["--lib", "--no-fail-fast"]
);
}
#[test]
fn cargo_test_arg_and_cargo_test_args_combined() {
let args = Args::parse_from(["mutants", "--cargo-test-arg=--lib", "--", "--no-fail-fast"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.additional_cargo_test_args,
vec!["--lib", "--no-fail-fast"]
);
}
#[test]
fn cargo_test_arg_and_config_combined() {
let args = Args::parse_from(["mutants", "--cargo-test-arg=--lib"]);
let config = Config::from_str(indoc! { r#"
additional_cargo_test_args = ["--no-fail-fast"]
"#})
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.additional_cargo_test_args,
vec!["--lib", "--no-fail-fast"]
);
}
#[test]
fn cargo_test_arg_cargo_test_args_and_config_combined() {
let args = Args::parse_from(["mutants", "--cargo-test-arg=--all-targets", "--", "--lib"]);
let config = Config::from_str(indoc! { r#"
additional_cargo_test_args = ["--no-fail-fast"]
"#})
.unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.additional_cargo_test_args,
vec!["--all-targets", "--lib", "--no-fail-fast"]
);
}
#[test]
fn sharding() {
let args = Args::try_parse_from(["mutants", "--sharding=slice", "--shard=0/10"]).unwrap();
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.sharding(), Sharding::Slice);
let args = Args::parse_from(["mutants", "--sharding=round-robin", "--shard=0/10"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.sharding(), Sharding::RoundRobin);
let args = Args::parse_from(["mutants"]);
let config = Config::from_str(r#"sharding = "slice""#).unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.sharding(), Sharding::Slice);
let args = Args::parse_from(["mutants"]);
let config = Config::from_str(r#"sharding = "round-robin""#).unwrap();
dbg!(&config);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.sharding(), Sharding::RoundRobin);
let args = Args::parse_from(["mutants"]);
let config = Config::from_str("").unwrap();
let options = Options::new(&args, &config).unwrap();
assert_eq!(
options.sharding(),
Sharding::Slice,
"Default sharding should be slice"
);
}
#[test]
fn no_shuffle_by_default() {
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.shuffle);
}
#[test]
fn no_shuffle_option_is_accepted() {
let args = Args::parse_from(["mutants", "--no-shuffle"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(!options.shuffle);
}
#[test]
fn shuffle_option_is_accepted() {
let args = Args::parse_from(["mutants", "--shuffle"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert!(options.shuffle);
}
#[test]
fn merge_exclude_from_config_and_command_line() {
let args = Args::parse_from([
"mutants",
"--exclude-re=foo",
"--exclude-re=bar",
"--exclude=foo.rs",
"--file=ex1.rs",
"--examine-re=exr1",
]);
let config = Config::from_str(
r#"
exclude_re = ["baz"]
exclude_globs = ["baz.rs"]
examine_globs = ["ex2.rs"]
examine_re = ["exr2"]
"#,
)
.unwrap();
let options = Options::new(&args, &config).unwrap();
for name in ["foo", "bar", "baz"] {
assert!(
options.exclude_name_re.is_match(name),
"Expected {name} to be excluded"
);
}
assert!(
!options.exclude_name_re.is_match("qux"),
"Expected qux not to be excluded"
);
assert!(
options.exclude_globset.as_ref().unwrap().is_match("baz.rs"),
"Expected baz.rs to be excluded"
);
assert!(
options.exclude_globset.as_ref().unwrap().is_match("foo.rs"),
"Expected foo.rs to be excluded"
);
assert!(
!options.exclude_globset.as_ref().unwrap().is_match("qux.rs"),
"Expected qux.rs NOT to be excluded"
);
for path in ["ex1.rs", "ex2.rs"] {
assert!(
options.examine_globset.as_ref().unwrap().is_match(path),
"Expected {path} to be examined"
);
}
for name in ["exr1", "exr2"] {
assert!(
options.examine_name_re.is_match(name),
"Expected {name} to be examined"
);
}
}
}
#[cfg(test)]
mod single_threaded_tests {
use clap::Parser;
use rusty_fork::rusty_fork_test;
use super::*;
use crate::Args;
use crate::test_util::{single_threaded_remove_env_var, single_threaded_set_env_var};
rusty_fork_test! {
#[test]
fn color_control_from_cargo_env() {
single_threaded_set_env_var("CARGO_TERM_COLOR", "always");
single_threaded_remove_env_var("CLICOLOR_FORCE");
single_threaded_remove_env_var("NO_COLOR");
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.colors.forced_value(), Some(true));
single_threaded_set_env_var("CARGO_TERM_COLOR", "never");
let args = Args::parse_from(["mutants"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.colors.forced_value(), Some(false));
single_threaded_set_env_var("CARGO_TERM_COLOR", "auto");
let args = Args::parse_from(["mutants"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.colors.forced_value(), None);
single_threaded_remove_env_var("CARGO_TERM_COLOR");
let args = Args::parse_from(["mutants"]);
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.colors.forced_value(), None);
}
#[test]
fn color_control_from_env() {
use crate::test_util::single_threaded_remove_env_var;
single_threaded_remove_env_var("CARGO_TERM_COLOR");
single_threaded_remove_env_var("CLICOLOR_FORCE");
single_threaded_remove_env_var("NO_COLOR");
let args = Args::parse_from(["mutants"]);
let config = Config::default();
let options = Options::new(&args, &config).unwrap();
assert_eq!(options.colors.forced_value(), None);
single_threaded_remove_env_var("CLICOLOR_FORCE");
single_threaded_set_env_var("NO_COLOR", "1");
let options = Options::new(&args, &Config::default()).unwrap();
assert_eq!(options.colors.forced_value(), Some(false));
single_threaded_remove_env_var("NO_COLOR");
single_threaded_set_env_var("CLICOLOR_FORCE", "1");
let options = Options::new(&args, &Config::default()).unwrap();
assert_eq!(options.colors.forced_value(), Some(true));
single_threaded_remove_env_var("CLICOLOR_FORCE");
single_threaded_remove_env_var("NO_COLOR");
let options = Options::new(&args, &Config::default()).unwrap();
assert_eq!(options.colors.forced_value(), None);
}
}
}