use std::fs;
use std::io::ErrorKind;
use camino::{Utf8Path, Utf8PathBuf};
use serde::Deserialize;
use crate::commands::{RunArgs, SelectArgs};
use crate::error::{Error, error};
use crate::{Result, bounds};
const RELATIVE_PATH: &str = "gamma.toml";
const FOREIGN_PATH: &str = ".cargo/mutants.toml";
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub mutators: Option<Vec<String>>,
pub files: Vec<String>,
pub exclude_files: Vec<String>,
pub exclude_trait_impls: Vec<String>,
pub min_score: Option<f64>,
pub jobs: Option<usize>,
pub test_timeout_multiplier: Option<f64>,
pub incremental: Option<crate::exec::IncrementalMode>,
pub no_baseline: Option<bool>,
pub no_confirm: Option<bool>,
pub packages: Vec<String>,
pub test_packages: Vec<String>,
pub test_workspace: Option<bool>,
pub whole_test_binaries: Option<bool>,
pub include_tests: Vec<String>,
pub exclude_tests: Vec<String>,
pub features: Vec<String>,
pub all_features: Option<bool>,
pub no_default_features: Option<bool>,
pub profile: Option<String>,
pub cargo_args: Vec<String>,
pub cargo_test_args: Vec<String>,
pub errors: Vec<String>,
pub minimum_test_timeout: Option<f64>,
pub nextest: Option<bool>,
pub memory: Option<crate::exec::MemoryControl>,
pub memory_multiplier: Option<f64>,
pub memory_headroom: Option<String>,
pub memory_limit: Option<String>,
pub baseline_memory_limit: Option<String>,
pub build_timeout: Option<f64>,
pub build_timeout_multiplier: Option<f64>,
pub artifact_dir: Option<Utf8PathBuf>,
#[serde(default)]
pub shard: Shard,
}
fn size(text: Option<&str>) -> Option<u64> {
let text = text?;
bounds::size(text).ok()
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "kebab-case")]
pub struct Shard {
pub count: Option<u32>,
pub index: Option<u32>,
}
impl Config {
#[must_use]
pub(crate) fn cargo_options(&self) -> crate::exec::CargoOptions {
crate::exec::CargoOptions {
profile: self.profile.clone(),
extra: self.cargo_args.clone(),
..crate::exec::CargoOptions::default()
}
}
pub fn resolve(select: &SelectArgs) -> Result<Self> {
if select.config.no_config {
return Ok(Self::default());
}
let Some(path) = select.config.path.as_ref() else {
return Self::load(&select.dir);
};
let text = fs::read_to_string(path).map_err(|cause| error!("could not read `{path}`").caused_by(cause))?;
Self::parse(&text).map_err(|cause| error!("{path}: {cause}").usage())
}
pub fn load(dir: &Utf8Path) -> Result<Self> {
let path = dir.join(RELATIVE_PATH);
let text = match fs::read_to_string(&path) {
Ok(text) => text,
Err(cause) if cause.kind() == ErrorKind::NotFound => return Ok(Self::default()),
Err(cause) => return Err(error!("could not read `{path}`").caused_by(cause)),
};
Self::parse(&text).map_err(|cause| error!("{path}: {cause}").usage())
}
pub fn parse(text: &str) -> Result<Self, String> {
let config: Self = toml::from_str(text).map_err(|cause| {
cause.message().to_owned()
})?;
config.validate()?;
Ok(config)
}
fn validate(&self) -> Result<(), String> {
type Check = (&'static str, Option<f64>, fn(&str, f64) -> Result<f64, String>);
let checks: [Check; 6] = [
("test-timeout-multiplier", self.test_timeout_multiplier, bounds::factor),
("minimum-test-timeout", self.minimum_test_timeout, bounds::seconds),
("build-timeout", self.build_timeout, bounds::seconds),
("build-timeout-multiplier", self.build_timeout_multiplier, bounds::factor),
("min-score", self.min_score, bounds::percentage),
("memory-multiplier", self.memory_multiplier, bounds::factor),
];
for (key, value, check) in checks {
if let Some(value) = value {
let _checked = check(&value.to_string(), value).map_err(|cause| format!("{key}: {cause}"))?;
}
}
let sizes = [
("memory-headroom", self.memory_headroom.as_deref()),
("memory-limit", self.memory_limit.as_deref()),
("baseline-memory-limit", self.baseline_memory_limit.as_deref()),
];
for (key, value) in sizes {
if let Some(value) = value {
let _checked = bounds::size(value).map_err(|cause| format!("{key}: {cause}"))?;
}
}
if let Some(name) = self
.exclude_trait_impls
.iter()
.find(|name| syn::parse_str::<syn::Ident>(name).is_err())
{
return Err(format!(
"exclude-trait-impls entry `{name}` must be one unqualified Rust identifier"
));
}
Ok(())
}
#[must_use]
pub fn foreign_present(dir: &Utf8Path) -> bool {
dir.join(FOREIGN_PATH).is_file() && !dir.join(RELATIVE_PATH).is_file()
}
pub fn apply(&self, args: &mut RunArgs) -> Result<()> {
self.apply_selection(&mut args.select)?;
let implied_by_cli = crate::exec::implied_memory_control(args.measure.memory_limit, args.measure.baseline_memory_limit);
args.min_score = args.min_score.or(self.min_score);
args.measure.jobs = args.measure.jobs.or(self.jobs);
args.measure.test_timeout_multiplier = args.measure.test_timeout_multiplier.or(self.test_timeout_multiplier);
args.measure.minimum_test_timeout = args.measure.minimum_test_timeout.or(self.minimum_test_timeout);
args.measure.nextest = args.measure.nextest || self.nextest.unwrap_or(false);
args.measure.memory = args.measure.memory.or(implied_by_cli).or(self.memory);
args.measure.memory_multiplier = args.measure.memory_multiplier.or(self.memory_multiplier);
args.measure.memory_headroom = args.measure.memory_headroom.or_else(|| size(self.memory_headroom.as_deref()));
args.measure.memory_limit = args.measure.memory_limit.or_else(|| size(self.memory_limit.as_deref()));
args.measure.baseline_memory_limit = args
.measure
.baseline_memory_limit
.or_else(|| size(self.baseline_memory_limit.as_deref()));
args.limits.build_timeout = args.limits.build_timeout.or(self.build_timeout);
args.limits.build_timeout_multiplier = args.limits.build_timeout_multiplier.or(self.build_timeout_multiplier);
args.incremental = args.incremental.or(self.incremental);
args.measure.profile = args.measure.profile.take().or_else(|| self.profile.clone());
args.measure.cargo_args.extend(self.cargo_args.iter().cloned());
args.measure.cargo_test_args.extend(self.cargo_test_args.iter().cloned());
args.measure.test_packages.extend(self.test_packages.iter().cloned());
args.measure.test_workspace = args.measure.test_workspace || self.test_workspace.unwrap_or(false);
args.measure.whole_test_binaries = args.measure.whole_test_binaries || self.whole_test_binaries.unwrap_or(false);
args.measure.include_tests.extend(self.include_tests.iter().cloned());
args.measure.exclude_tests.extend(self.exclude_tests.iter().cloned());
args.no_baseline = args.no_baseline || self.no_baseline.unwrap_or(false);
args.no_confirm = args.no_confirm || self.no_confirm.unwrap_or(false);
args.artifact_dir = args.artifact_dir.take().or_else(|| self.artifact_dir.clone());
if !args.measure.test_packages.is_empty() && args.measure.test_workspace {
return Err(contradiction(
"test-packages",
!self.test_packages.is_empty(),
"test-workspace",
self.test_workspace == Some(true),
));
}
Ok(())
}
pub fn apply_selection(&self, select: &mut SelectArgs) -> Result<()> {
if select.mutators.is_none()
&& let Some(selectors) = self.mutators.as_ref()
{
select.mutators = Some(selectors.join(","));
}
select.files.extend(self.files.iter().cloned());
select.exclude_files.extend(self.exclude_files.iter().cloned());
select.exclude_trait_impls.extend(self.exclude_trait_impls.iter().cloned());
select.packages.extend(self.packages.iter().cloned());
select.errors.extend(self.errors.iter().cloned());
select.features.features.extend(self.features.iter().cloned());
select.features.all_features = select.features.all_features || self.all_features.unwrap_or(false);
select.features.no_default_features = select.features.no_default_features || self.no_default_features.unwrap_or(false);
select.shard_count = select.shard_count.or(self.shard.count);
select.shard_index = select.shard_index.or(self.shard.index);
self.validate_effective(select)
}
fn validate_effective(&self, select: &SelectArgs) -> Result<()> {
if !select.packages.is_empty() && select.workspace {
return Err(contradiction("packages", !self.packages.is_empty(), "workspace", false));
}
Ok(())
}
}
fn contradiction(first: &str, first_from_file: bool, second: &str, second_from_file: bool) -> Error {
let source = |from_file: bool| if from_file { RELATIVE_PATH } else { "the command line" };
error!(
"`{first}` from {} and `{second}` from {} cannot both apply.\n\
Drop one of them, or state the one you want on the command line and remove the other from {RELATIVE_PATH}.",
source(first_from_file),
source(second_from_file)
)
.usage()
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use tempfile::TempDir;
use super::*;
use crate::commands::{BuildLimitArgs, FeatureArgs, MeasureArgs};
fn select_args(dir: &Utf8Path) -> SelectArgs {
SelectArgs {
dir: dir.to_path_buf(),
..SelectArgs::default()
}
}
#[test]
fn a_configured_package_list_contradicts_workspace_on_the_command_line() {
let dir = TempDir::new().expect("temp dir");
let path = Utf8Path::from_path(dir.path()).expect("utf-8");
let config = Config::parse("packages = [\"a\"]\n").expect("a package list parses");
let mut select = SelectArgs {
workspace: true,
..select_args(path)
};
let failure = config.apply_selection(&mut select).expect_err("the pair cannot both apply");
let text = failure.to_string();
assert!(text.contains("packages"), "{text}");
assert!(text.contains("workspace"), "{text}");
assert!(text.contains(RELATIVE_PATH), "{text}");
assert!(text.contains("the command line"), "{text}");
}
#[test]
fn a_configured_test_package_list_contradicts_test_workspace_on_the_command_line() {
let dir = TempDir::new().expect("temp dir");
let path = Utf8Path::from_path(dir.path()).expect("utf-8");
let config = Config::parse("test-packages = [\"a\"]\n").expect("a test package list parses");
let mut args = RunArgs {
select: select_args(path),
..RunArgs::default()
};
args.measure.test_workspace = true;
let failure = config.apply(&mut args).expect_err("the pair cannot both apply");
let text = failure.to_string();
assert!(text.contains("test-packages"), "{text}");
assert!(text.contains("test-workspace"), "{text}");
}
#[test]
fn a_configured_list_on_its_own_is_not_a_contradiction() {
let dir = TempDir::new().expect("temp dir");
let path = Utf8Path::from_path(dir.path()).expect("utf-8");
let config = Config::parse("packages = [\"a\"]\ntest-packages = [\"b\"]\n").expect("both lists parse");
let mut args = RunArgs {
select: select_args(path),
..RunArgs::default()
};
config.apply(&mut args).expect("one half of each pair is no contradiction");
assert_eq!(args.select.packages, vec!["a".to_owned()]);
assert_eq!(args.measure.test_packages, vec!["b".to_owned()]);
}
#[test]
fn every_config_reachable_conflicting_pair_is_checked_after_the_merge() {
type Pair = (&'static str, fn(&mut RunArgs));
let dir = TempDir::new().expect("temp dir");
let path = Utf8Path::from_path(dir.path()).expect("utf-8");
let pairs: [Pair; 2] = [
("packages", |args| args.select.workspace = true),
("test-packages", |args| args.measure.test_workspace = true),
];
for (key, raise) in pairs {
let config = Config::parse(&format!("{key} = [\"a\"]\n")).expect("the list parses");
let mut args = RunArgs {
select: select_args(path),
..RunArgs::default()
};
raise(&mut args);
let failure = config.apply(&mut args).expect_err("the pair cannot both apply");
assert!(failure.to_string().contains(key), "{key}");
}
}
#[test]
fn no_config_wins_over_a_file_that_is_there() {
let dir = TempDir::new().expect("temp dir");
let root = Utf8Path::from_path(dir.path()).expect("utf-8 path");
fs::write(root.join(RELATIVE_PATH), "jobs = 7\n").expect("write");
let mut select = select_args(root);
select.config.no_config = true;
assert_eq!(Config::resolve(&select).expect("resolves").jobs, None);
select.config.no_config = false;
assert_eq!(Config::resolve(&select).expect("resolves").jobs, Some(7));
}
#[test]
fn an_explicit_config_path_is_read_instead_of_the_default_one() {
let dir = TempDir::new().expect("temp dir");
let root = Utf8Path::from_path(dir.path()).expect("utf-8 path");
let elsewhere = root.join("elsewhere.toml");
fs::write(root.join(RELATIVE_PATH), "jobs = 7\n").expect("write");
fs::write(&elsewhere, "jobs = 3\n").expect("write");
let mut select = select_args(root);
select.config.path = Some(elsewhere);
assert_eq!(Config::resolve(&select).expect("resolves").jobs, Some(3));
}
#[test]
fn an_explicit_config_path_that_is_missing_is_an_error() {
let dir = TempDir::new().expect("temp dir");
let root = Utf8Path::from_path(dir.path()).expect("utf-8 path");
let mut select = select_args(root);
select.config.path = Some(root.join("nope.toml"));
let _cause = Config::resolve(&select).unwrap_err();
}
#[test]
fn memory_sizes_in_the_file_are_parsed_and_merged_into_the_arguments() {
let config = Config::parse(
"memory = \"enforce\"\nmemory-headroom = \"256MiB\"\nmemory-limit = \"2GiB\"\nbaseline-memory-limit = \"4GiB\"\n",
)
.expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.measure.memory, Some(crate::exec::MemoryControl::Enforce));
assert_eq!(args.measure.memory_headroom, Some(256 * 1024 * 1024));
assert_eq!(args.measure.memory_limit, Some(2 * 1024 * 1024 * 1024));
assert_eq!(args.measure.baseline_memory_limit, Some(4 * 1024 * 1024 * 1024));
}
#[test]
fn command_line_memory_limits_override_a_configured_memory_mode() {
let config = Config::parse("memory = \"off\"\n").expect("parses");
let mut enforcing = RunArgs::default();
enforcing.measure.memory_limit = Some(1024);
config.apply(&mut enforcing).expect("merges");
assert_eq!(enforcing.measure.memory, Some(crate::exec::MemoryControl::Enforce));
let mut measuring = RunArgs::default();
measuring.measure.baseline_memory_limit = Some(1024);
config.apply(&mut measuring).expect("merges");
assert_eq!(measuring.measure.memory, Some(crate::exec::MemoryControl::Measure));
}
#[test]
fn an_explicit_command_line_memory_mode_overrides_a_size_flag_implication() {
let config = Config::parse("memory = \"measure\"\n").expect("parses");
let mut args = RunArgs::default();
args.measure.memory = Some(crate::exec::MemoryControl::Off);
args.measure.memory_limit = Some(1024);
config.apply(&mut args).expect("merges");
assert_eq!(args.measure.memory, Some(crate::exec::MemoryControl::Off));
}
#[test]
fn nextest_in_the_file_is_merged_in_and_adds_to_the_command_line() {
let config = Config::parse("nextest = true\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert!(args.measure.nextest);
let mut chosen = RunArgs::default();
chosen.measure.nextest = false;
config
.apply(&mut chosen)
.expect("the merged settings do not contradict one another");
assert!(chosen.measure.nextest);
}
#[test]
fn test_workspace_in_the_file_selects_the_same_oracle_as_the_flag() {
let config = Config::parse("test-workspace = true\n").expect("the documented equivalence must hold");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert!(args.measure.test_workspace);
}
#[test]
fn test_workspace_on_the_command_line_survives_a_file_that_does_not_set_it() {
let config = Config::parse("test-workspace = false\n").expect("parses");
let mut args = RunArgs::default();
args.measure.test_workspace = true;
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert!(args.measure.test_workspace);
}
#[test]
fn whole_test_binaries_in_the_file_selects_the_same_oracle_as_the_flag() {
let config = Config::parse("whole-test-binaries = true\n").expect("the documented equivalence must hold");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert!(args.measure.whole_test_binaries);
}
#[test]
fn whole_test_binaries_on_the_command_line_survives_a_false_file_setting() {
let config = Config::parse("whole-test-binaries = false\n").expect("parses");
let mut args = RunArgs::default();
args.measure.whole_test_binaries = true;
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert!(args.measure.whole_test_binaries);
}
#[test]
fn a_memory_size_that_is_not_a_size_is_reported_rather_than_ignored() {
let cause = Config::parse("memory-limit = \"lots\"\n").expect_err("must be rejected");
assert!(cause.contains("memory-limit"), "{cause}");
}
#[test]
fn an_empty_file_is_valid() {
let config = Config::parse("").expect("an empty file is a valid file");
assert!(config.mutators.is_none());
assert!(config.files.is_empty());
}
#[test]
fn a_misspelled_key_is_an_error_rather_than_a_silent_no_op() {
let cause = Config::parse("exclude-file = [\"src/main.rs\"]\n").expect_err("must be rejected");
assert!(cause.contains("unknown field"), "{cause}");
}
#[test]
fn a_misspelled_key_in_a_table_is_also_an_error() {
let cause = Config::parse("[shard]\ncount = 4\nidx = 0\n").expect_err("must be rejected");
assert!(cause.contains("unknown field"), "{cause}");
}
#[test]
fn keys_are_spelled_in_kebab_case() {
let config =
Config::parse("exclude-files = [\"tests/**\"]\ntest-timeout-multiplier = 3.0\n").expect("kebab-case is the file's spelling");
assert_eq!(config.exclude_files, vec!["tests/**".to_owned()]);
assert_eq!(config.test_timeout_multiplier, Some(3.0));
}
#[test]
fn trait_implementation_exclusions_reach_mutant_selection() {
let config = Config::parse("exclude-trait-impls = [\"Debug\", \"Display\"]\n").expect("the trait exclusions parse");
let mut select = SelectArgs::default();
config
.apply_selection(&mut select)
.expect("the exclusion does not contradict another setting");
assert_eq!(select.exclude_trait_impls, ["Debug", "Display"]);
}
#[test]
fn a_trait_exclusion_names_terminal_identifiers() {
let qualified = Config::parse("exclude-trait-impls = [\"Debug\", \"fmt::Display\"]\n").expect_err("must be rejected");
let empty_entry = Config::parse("exclude-trait-impls = [\"Debug\", \"\"]\n").expect_err("must be rejected");
assert!(qualified.contains("one unqualified Rust identifier"), "{qualified}");
assert!(empty_entry.contains("one unqualified Rust identifier"), "{empty_entry}");
}
#[test]
fn trait_exclusions_reject_the_old_table() {
let old_field = Config::parse("[[exclude-mutants]]\ntrait-impls = \"Debug\"\nreason = \"diagnostic\"\n")
.expect_err("the old table must be rejected");
assert!(old_field.contains("unknown field"), "{old_field}");
}
#[test]
fn ops_are_joined_into_the_selector_list_the_flag_parses() {
let config = Config::parse("mutators = [\"@arithmetic\", \"!bitwise\"]\n").expect("parses");
let mut select = SelectArgs::default();
config
.apply_selection(&mut select)
.expect("the merged settings do not contradict one another");
assert_eq!(select.mutators.as_deref(), Some("@arithmetic,!bitwise"));
}
#[test]
fn the_command_line_wins_for_scalars() {
let config = Config::parse("mutators = [\"stmt\"]\nmin-score = 10.0\njobs = 1\n").expect("parses");
let mut args = RunArgs {
select: SelectArgs {
mutators: Some("relational".to_owned()),
..SelectArgs::default()
},
min_score: Some(90.0),
..RunArgs::default()
};
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.select.mutators.as_deref(), Some("relational"));
assert_eq!(args.min_score, Some(90.0));
assert_eq!(args.measure.jobs, Some(1));
}
#[test]
fn lists_concatenate_rather_than_replace() {
let config = Config::parse("exclude-files = [\"generated/**\"]\n").expect("parses");
let mut args = RunArgs {
select: SelectArgs {
exclude_files: vec!["tests/**".to_owned()],
..SelectArgs::default()
},
..RunArgs::default()
};
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.select.exclude_files, vec!["tests/**".to_owned(), "generated/**".to_owned()]);
}
#[test]
fn a_configured_flag_turns_on_and_the_command_line_cannot_turn_it_off() {
let config = Config::parse("no-baseline = true\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert!(args.no_baseline);
}
#[test]
fn artifact_directory_comes_from_the_file_when_the_command_line_is_silent() {
let config = Config::parse("artifact-dir = \"out\"\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.artifact_dir.as_deref(), Some(Utf8Path::new("out")));
args.artifact_dir = Some("cli-out".into());
config.apply(&mut args).expect("command line wins");
assert_eq!(args.artifact_dir.as_deref(), Some(Utf8Path::new("cli-out")));
}
#[test]
fn sharding_can_be_set_entirely_from_the_file() {
let config = Config::parse("[shard]\ncount = 30\nindex = 7\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.select.shard().expect("valid sharding"), Some((30, 7)));
}
#[test]
fn a_count_from_the_file_and_an_index_from_the_command_line_make_one_shard() {
let config = Config::parse("[shard]\ncount = 8\n").expect("parses");
let mut args = RunArgs::default();
args.select.shard_index = Some(3);
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.select.shard_count, Some(8));
assert_eq!(args.select.shard().expect("the pair is whole after merging"), Some((8, 3)));
}
#[test]
fn an_index_from_the_file_and_a_count_from_the_command_line_make_one_shard() {
let config = Config::parse("[shard]\nindex = 0\n").expect("parses");
let mut args = RunArgs::default();
args.select.shard_count = Some(2);
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.select.shard().expect("the pair is whole after merging"), Some((2, 0)));
}
#[test]
fn a_shard_named_on_the_command_line_overrides_the_file() {
let config = Config::parse("[shard]\ncount = 8\nindex = 7\n").expect("parses");
let mut args = RunArgs::default();
args.select.shard_count = Some(3);
args.select.shard_index = Some(1);
config.apply(&mut args).expect("the merged settings do not contradict one another");
assert_eq!(args.select.shard().expect("valid sharding"), Some((3, 1)));
}
#[test]
fn half_a_shard_in_the_file_and_nothing_on_the_command_line_is_refused() {
let config = Config::parse("[shard]\ncount = 8\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
let error = args.select.shard().expect_err("a count with no index is not a shard");
assert!(error.is_usage(), "{error}");
assert!(error.to_string().contains("--shard-index"), "{error}");
}
#[test]
fn an_index_alone_in_the_file_is_refused() {
let config = Config::parse("[shard]\nindex = 2\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
let error = args.select.shard().expect_err("an index with no count is not a shard");
assert!(error.is_usage(), "{error}");
assert!(error.to_string().contains("--shard-count"), "{error}");
}
#[test]
fn a_file_count_that_the_command_line_index_falls_outside_is_refused() {
let config = Config::parse("[shard]\ncount = 4\n").expect("parses");
let mut args = RunArgs::default();
args.select.shard_index = Some(4);
config.apply(&mut args).expect("the merged settings do not contradict one another");
let error = args.select.shard().expect_err("index 4 of 4 shards does not exist");
assert!(error.to_string().contains("out of range"), "{error}");
}
#[test]
fn a_zero_count_in_the_file_is_refused() {
let config = Config::parse("[shard]\ncount = 0\nindex = 0\n").expect("parses");
let mut args = RunArgs::default();
config.apply(&mut args).expect("the merged settings do not contradict one another");
let error = args.select.shard().expect_err("zero shards is not a division");
assert!(error.to_string().contains("at least 1"), "{error}");
}
#[test]
fn a_missing_file_is_not_an_error() {
let dir = TempDir::new().expect("a temporary directory");
let path = Utf8Path::from_path(dir.path()).expect("path is not UTF-8");
let config = Config::load(path).expect("an absent file is the common case");
assert!(config.mutators.is_none());
}
#[test]
fn a_present_file_is_read() {
let dir = TempDir::new().expect("a temporary directory");
let path = Utf8Path::from_path(dir.path()).expect("path is not UTF-8");
fs::write(path.join(RELATIVE_PATH), "jobs = 3\n").expect("could not write the config");
let config = Config::load(path).expect("the file is valid");
assert_eq!(config.jobs, Some(3));
}
#[test]
fn a_malformed_file_is_a_usage_error_naming_the_path() {
let dir = TempDir::new().expect("a temporary directory");
let path = Utf8Path::from_path(dir.path()).expect("path is not UTF-8");
fs::write(path.join(RELATIVE_PATH), "jobs = \n").expect("could not write the config");
let cause = Config::load(path).expect_err("a malformed file must stop the run");
assert!(cause.is_usage(), "{cause}");
assert!(cause.to_string().contains("gamma.toml"), "{cause}");
}
#[test]
fn a_foreign_config_file_is_noticed_but_never_read() {
let dir = TempDir::new().expect("a temporary directory");
let path = Utf8Path::from_path(dir.path()).expect("path is not UTF-8");
fs::create_dir_all(path.join(".cargo")).expect("could not create .cargo");
fs::write(path.join(FOREIGN_PATH), "exclude_re = [\"impl Debug\"]\n").expect("could not write the foreign config");
assert!(Config::foreign_present(path));
let config = Config::load(path).expect("the foreign file must not be parsed as ours");
assert!(config.mutators.is_none());
}
#[test]
fn a_config_that_cannot_be_read_is_an_error_rather_than_the_defaults() {
let dir = TempDir::new().expect("a temporary directory");
let path = Utf8Path::from_path(dir.path()).expect("path is not UTF-8");
fs::create_dir_all(path.join(RELATIVE_PATH)).expect("could not create a directory in the config's place");
let error = Config::load(path).expect_err("an unreadable config must not be treated as absent");
assert!(error.to_string().contains(RELATIVE_PATH), "{error}");
}
fn every_key_set() -> Config {
Config {
mutators: Some(vec!["arith".to_owned(), "!arith.add_to_sub".to_owned()]),
files: vec!["file-from-the-file".to_owned()],
exclude_files: vec!["excluded-file-from-the-file".to_owned()],
exclude_trait_impls: vec!["TraitFromTheFile".to_owned()],
min_score: Some(61.5),
jobs: Some(62),
test_timeout_multiplier: Some(63.5),
incremental: Some(crate::exec::IncrementalMode::No),
no_baseline: Some(true),
no_confirm: Some(true),
packages: vec!["package-from-the-file".to_owned()],
test_packages: vec!["test-package-from-the-file".to_owned()],
test_workspace: Some(false),
whole_test_binaries: Some(true),
include_tests: vec!["included-test-from-the-file".to_owned()],
exclude_tests: vec!["excluded-test-from-the-file".to_owned()],
features: vec!["feature-from-the-file".to_owned()],
all_features: Some(true),
no_default_features: Some(true),
profile: Some("profile-from-the-file".to_owned()),
cargo_args: vec!["--cargo-argument-from-the-file".to_owned()],
cargo_test_args: vec!["--cargo-test-argument-from-the-file".to_owned()],
errors: vec!["ErrorFromTheFile".to_owned()],
minimum_test_timeout: Some(64.5),
nextest: Some(true),
memory: Some(crate::exec::MemoryControl::Measure),
memory_multiplier: Some(65.5),
memory_headroom: Some("128MiB".to_owned()),
memory_limit: Some("2GiB".to_owned()),
baseline_memory_limit: Some("4GiB".to_owned()),
build_timeout: Some(66.5),
build_timeout_multiplier: Some(67.5),
artifact_dir: Some(Utf8PathBuf::from("artifacts-from-the-file")),
shard: Shard {
count: Some(68),
index: Some(9),
},
}
}
#[test]
fn every_configured_key_reaches_its_setting() {
let config = every_key_set();
let mut args = RunArgs::default();
config
.apply(&mut args)
.expect("no two settings in this file contradict one another");
assert_eq!(args.select.mutators.as_deref(), Some("arith,!arith.add_to_sub"));
assert_eq!(args.select.files, ["file-from-the-file"]);
assert_eq!(args.select.exclude_files, ["excluded-file-from-the-file"]);
assert_eq!(args.select.exclude_trait_impls, ["TraitFromTheFile"]);
assert_eq!(args.select.packages, ["package-from-the-file"]);
assert_eq!(args.select.errors, ["ErrorFromTheFile"]);
assert_eq!(args.select.features.features, ["feature-from-the-file"]);
assert!(args.select.features.all_features);
assert!(args.select.features.no_default_features);
assert_eq!(args.select.shard_count, Some(68));
assert_eq!(args.select.shard_index, Some(9));
assert_eq!(args.min_score, Some(61.5));
assert_eq!(args.measure.jobs, Some(62));
assert_eq!(args.measure.test_timeout_multiplier, Some(63.5));
assert_eq!(args.measure.minimum_test_timeout, Some(64.5));
assert!(args.measure.nextest);
assert_eq!(args.measure.memory, Some(crate::exec::MemoryControl::Measure));
assert_eq!(args.measure.memory_multiplier, Some(65.5));
assert_eq!(args.measure.memory_headroom, Some(128 * 1024 * 1024));
assert_eq!(args.measure.memory_limit, Some(2 * 1024 * 1024 * 1024));
assert_eq!(args.measure.baseline_memory_limit, Some(4 * 1024 * 1024 * 1024));
assert_eq!(args.limits.build_timeout, Some(66.5));
assert_eq!(args.limits.build_timeout_multiplier, Some(67.5));
assert_eq!(args.incremental, Some(crate::exec::IncrementalMode::No));
assert_eq!(args.measure.profile.as_deref(), Some("profile-from-the-file"));
assert_eq!(args.measure.cargo_args, ["--cargo-argument-from-the-file"]);
assert_eq!(args.measure.cargo_test_args, ["--cargo-test-argument-from-the-file"]);
assert_eq!(args.measure.test_packages, ["test-package-from-the-file"]);
assert!(!args.measure.test_workspace, "the file said false, so nothing may turn it on");
assert!(args.measure.whole_test_binaries);
assert_eq!(args.measure.include_tests, ["included-test-from-the-file"]);
assert_eq!(args.measure.exclude_tests, ["excluded-test-from-the-file"]);
assert!(args.no_baseline);
assert!(args.no_confirm);
assert_eq!(args.artifact_dir.as_deref(), Some(Utf8Path::new("artifacts-from-the-file")));
}
fn every_setting_typed() -> RunArgs {
RunArgs {
select: SelectArgs {
mutators: Some("literal".to_owned()),
files: vec!["file-from-the-command-line".to_owned()],
exclude_files: vec!["excluded-file-from-the-command-line".to_owned()],
packages: vec!["package-from-the-command-line".to_owned()],
errors: vec!["ErrorFromTheCommandLine".to_owned()],
shard_count: Some(3),
shard_index: Some(1),
features: FeatureArgs {
features: vec!["feature-from-the-command-line".to_owned()],
..FeatureArgs::default()
},
..SelectArgs::default()
},
min_score: Some(11.5),
incremental: Some(crate::exec::IncrementalMode::Build),
artifact_dir: Some(Utf8PathBuf::from("artifacts-from-the-command-line")),
measure: MeasureArgs {
jobs: Some(12),
test_timeout_multiplier: Some(13.5),
minimum_test_timeout: Some(14.5),
memory: Some(crate::exec::MemoryControl::Off),
memory_multiplier: Some(15.5),
memory_headroom: Some(1),
memory_limit: Some(2),
baseline_memory_limit: Some(3),
profile: Some("profile-from-the-command-line".to_owned()),
cargo_args: vec!["--cargo-argument-from-the-command-line".to_owned()],
cargo_test_args: vec!["--cargo-test-argument-from-the-command-line".to_owned()],
test_packages: vec!["test-package-from-the-command-line".to_owned()],
include_tests: vec!["included-test-from-the-command-line".to_owned()],
exclude_tests: vec!["excluded-test-from-the-command-line".to_owned()],
..MeasureArgs::default()
},
limits: BuildLimitArgs {
build_timeout: Some(16.5),
build_timeout_multiplier: Some(17.5),
..BuildLimitArgs::default()
},
..RunArgs::default()
}
}
#[test]
fn the_command_line_outranks_the_file_for_every_scalar() {
let config = every_key_set();
let mut args = every_setting_typed();
config
.apply(&mut args)
.expect("no two settings in this merge contradict one another");
assert_eq!(args.select.mutators.as_deref(), Some("literal"));
assert_eq!(args.select.shard_count, Some(3));
assert_eq!(args.select.shard_index, Some(1));
assert_eq!(args.min_score, Some(11.5));
assert_eq!(args.measure.jobs, Some(12));
assert_eq!(args.measure.test_timeout_multiplier, Some(13.5));
assert_eq!(args.measure.minimum_test_timeout, Some(14.5));
assert_eq!(args.measure.memory_multiplier, Some(15.5));
assert_eq!(args.measure.memory_headroom, Some(1));
assert_eq!(args.measure.memory_limit, Some(2));
assert_eq!(args.measure.baseline_memory_limit, Some(3));
assert_eq!(args.limits.build_timeout, Some(16.5));
assert_eq!(args.limits.build_timeout_multiplier, Some(17.5));
assert_eq!(args.incremental, Some(crate::exec::IncrementalMode::Build));
assert_eq!(args.measure.profile.as_deref(), Some("profile-from-the-command-line"));
assert_eq!(args.artifact_dir.as_deref(), Some(Utf8Path::new("artifacts-from-the-command-line")));
assert_eq!(args.measure.memory, Some(crate::exec::MemoryControl::Off));
}
#[test]
fn every_list_concatenates_with_the_command_line_first() {
let config = every_key_set();
let mut args = every_setting_typed();
config
.apply(&mut args)
.expect("no two settings in this merge contradict one another");
assert_eq!(args.select.files, ["file-from-the-command-line", "file-from-the-file"]);
assert_eq!(
args.select.exclude_files,
["excluded-file-from-the-command-line", "excluded-file-from-the-file"]
);
assert_eq!(args.select.packages, ["package-from-the-command-line", "package-from-the-file"]);
assert_eq!(args.select.errors, ["ErrorFromTheCommandLine", "ErrorFromTheFile"]);
assert_eq!(
args.select.features.features,
["feature-from-the-command-line", "feature-from-the-file"]
);
assert_eq!(
args.measure.cargo_args,
["--cargo-argument-from-the-command-line", "--cargo-argument-from-the-file"]
);
assert_eq!(
args.measure.cargo_test_args,
["--cargo-test-argument-from-the-command-line", "--cargo-test-argument-from-the-file"]
);
assert_eq!(
args.measure.test_packages,
["test-package-from-the-command-line", "test-package-from-the-file"]
);
assert_eq!(
args.measure.include_tests,
["included-test-from-the-command-line", "included-test-from-the-file"]
);
assert_eq!(
args.measure.exclude_tests,
["excluded-test-from-the-command-line", "excluded-test-from-the-file"]
);
assert_eq!(args.select.exclude_trait_impls, ["TraitFromTheFile"]);
}
#[test]
fn a_typed_ceiling_implies_a_mode_that_outranks_the_file() {
let config = every_key_set();
let mut args = RunArgs {
measure: MeasureArgs {
memory_limit: Some(9),
..MeasureArgs::default()
},
..RunArgs::default()
};
config
.apply(&mut args)
.expect("no two settings in this merge contradict one another");
assert_eq!(args.measure.memory, Some(crate::exec::MemoryControl::Enforce));
}
#[test]
fn a_configured_test_workspace_is_merged_and_then_contradicts_configured_test_packages() {
let config = Config {
test_workspace: Some(true),
..every_key_set()
};
let mut args = RunArgs::default();
let error = config
.apply(&mut args)
.expect_err("`test-packages` and `test-workspace` cannot both apply");
assert!(error.is_usage(), "{error}");
assert!(error.to_string().contains("test-packages"), "{error}");
assert!(error.to_string().contains("test-workspace"), "{error}");
}
}