#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
use std::env;
use std::iter::once;
use std::time::{Duration, Instant};
use nextest_metadata::NextestExitCode;
use tracing::{debug, debug_span, warn};
use crate::Result;
use crate::build_dir::BuildDir;
use crate::console::Console;
use crate::interrupt::check_interrupted;
use crate::options::{Options, TestTool};
use crate::outcome::{Phase, PhaseResult};
use crate::output::ScenarioOutput;
use crate::package::PackageSelection;
use crate::process::{Exit, Process};
const NEXTEST_ALLOWED_CODES: &[i32] = &[
NextestExitCode::NO_TESTS_RUN,
NextestExitCode::TEST_RUN_FAILED,
NextestExitCode::BUILD_FAILED,
];
#[allow(clippy::too_many_arguments)] pub fn run_cargo(
build_dir: &BuildDir,
jobserver: Option<&jobserver::Client>,
packages: &PackageSelection,
phase: Phase,
timeout: Option<Duration>,
scenario_output: &mut ScenarioOutput,
options: &Options,
console: &Console,
) -> Result<PhaseResult> {
let _span = debug_span!("run", ?phase).entered();
let start = Instant::now();
let argv = cargo_argv(packages, phase, options);
let mut env = vec![
("INSTA_UPDATE".to_owned(), "no".to_owned()),
("INSTA_FORCE_PASS".to_owned(), "0".to_owned()),
];
if let Some(encoded_rustflags) = encoded_rustflags(options) {
debug!(?encoded_rustflags);
env.push(("CARGO_ENCODED_RUSTFLAGS".to_owned(), encoded_rustflags));
}
let process_status = Process::run(
&argv,
&env,
build_dir.path(),
timeout,
jobserver,
scenario_output,
console,
)?;
check_interrupted()?;
debug!(?process_status, elapsed = ?start.elapsed());
if let Exit::Failure(code) = process_status
&& argv[1] == "nextest"
&& !NEXTEST_ALLOWED_CODES.contains(&code)
{
warn!(%code, "nextest process exited with unexpected code (allowed: {NEXTEST_ALLOWED_CODES:?})");
}
Ok(PhaseResult {
phase,
duration: start.elapsed(),
process_status,
argv,
})
}
pub fn cargo_bin() -> String {
env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned())
}
fn cargo_argv(packages: &PackageSelection, phase: Phase, options: &Options) -> Vec<String> {
let mut cargo_args = vec![cargo_bin()];
match phase {
Phase::Test => match &options.test_tool() {
TestTool::Cargo => cargo_args.push("test".to_string()),
TestTool::Nextest => {
cargo_args.push("nextest".to_string());
cargo_args.push("run".to_string());
}
},
Phase::Build => {
match &options.test_tool() {
TestTool::Cargo => {
cargo_args.push("test".to_string());
cargo_args.push("--no-run".to_string());
}
TestTool::Nextest => {
cargo_args.push("nextest".to_string());
cargo_args.push("run".to_string());
cargo_args.push("--no-run".to_string());
}
}
}
Phase::Check => {
cargo_args.push("check".to_string());
cargo_args.push("--tests".to_string());
}
}
if let Some(profile) = &options.profile {
match options.test_tool() {
TestTool::Cargo => {
cargo_args.push(format!("--profile={profile}"));
}
TestTool::Nextest => {
cargo_args.push(format!("--cargo-profile={profile}"));
}
}
}
cargo_args.push("--verbose".to_string());
match packages {
PackageSelection::All => {
cargo_args.push("--workspace".to_string());
}
PackageSelection::Explicit(packages) => {
cargo_args.extend(
packages
.iter()
.map(|p| format!("--package={}", p.version_qualified_name())),
);
}
}
if options.no_default_features {
cargo_args.push("--no-default-features".to_owned());
}
if options.all_features {
cargo_args.push("--all-features".to_owned());
}
cargo_args.extend(options.features.iter().map(|f| format!("--features={f}")));
cargo_args.extend(options.additional_cargo_args.iter().cloned());
if phase == Phase::Test {
cargo_args.extend(options.additional_cargo_test_args.iter().cloned());
}
cargo_args
}
fn encoded_rustflags(options: &Options) -> Option<String> {
let cap_lints_arg = "--cap-lints=warn";
let separator = "\x1f";
if !options.cap_lints {
None
} else if let Ok(encoded) = env::var("CARGO_ENCODED_RUSTFLAGS") {
if encoded.is_empty() {
Some(cap_lints_arg.to_owned())
} else {
Some(encoded + separator + cap_lints_arg)
}
} else if let Ok(rustflags) = env::var("RUSTFLAGS") {
if rustflags.is_empty() {
Some(cap_lints_arg.to_owned())
} else {
Some(
rustflags
.split(' ')
.filter(|s| !s.is_empty())
.chain(once("--cap-lints=warn"))
.collect::<Vec<&str>>()
.join(separator),
)
}
} else {
Some(cap_lints_arg.to_owned())
}
}
#[cfg(test)]
mod test {
use clap::Parser;
use pretty_assertions::assert_eq;
use rusty_fork::rusty_fork_test;
use crate::{
Args,
test_util::{single_threaded_remove_env_var, single_threaded_set_env_var},
};
use super::*;
#[test]
fn generate_cargo_args_for_baseline_with_default_options() {
let options = Options::default();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
["check", "--tests", "--verbose", "--workspace"]
);
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Build, &options)[1..],
["test", "--no-run", "--verbose", "--workspace"]
);
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Test, &options)[1..],
["test", "--verbose", "--workspace"]
);
}
#[test]
fn generate_cargo_args_with_additional_cargo_test_args_and_package() {
let mut options = Options::default();
options
.additional_cargo_test_args
.extend(["--lib", "--no-fail-fast"].iter().map(ToString::to_string));
assert_eq!(
cargo_argv(
&PackageSelection::one(
"cargo-mutants-testdata-something",
"0.1.0",
"",
"src/lib.rs"
),
Phase::Check,
&options
)[1..],
[
"check",
"--tests",
"--verbose",
"--package=cargo-mutants-testdata-something@0.1.0",
]
);
}
#[test]
fn generate_cargo_args_with_additional_cargo_args_and_test_args() {
let mut options = Options::default();
options
.additional_cargo_test_args
.extend(["--lib", "--no-fail-fast"].iter().map(|&s| s.to_string()));
options
.additional_cargo_args
.extend(["--release".to_owned()]);
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
["check", "--tests", "--verbose", "--workspace", "--release"]
);
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Build, &options)[1..],
["test", "--no-run", "--verbose", "--workspace", "--release"]
);
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Test, &options)[1..],
[
"test",
"--verbose",
"--workspace",
"--release",
"--lib",
"--no-fail-fast"
]
);
}
#[test]
fn no_default_features_args_passed_to_cargo() {
let args = Args::try_parse_from(["mutants", "--no-default-features"].as_slice()).unwrap();
let options = Options::from_args(&args).unwrap();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
[
"check",
"--tests",
"--verbose",
"--workspace",
"--no-default-features"
]
);
}
#[test]
fn all_features_args_passed_to_cargo() {
let args = Args::try_parse_from(["mutants", "--all-features"].as_slice()).unwrap();
let options = Options::from_args(&args).unwrap();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
[
"check",
"--tests",
"--verbose",
"--workspace",
"--all-features"
]
);
}
#[test]
fn cap_lints_passed_to_cargo() {
let args = Args::try_parse_from(["mutants", "--cap-lints=true"].as_slice()).unwrap();
let options = Options::from_args(&args).unwrap();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
["check", "--tests", "--verbose", "--workspace",]
);
}
#[test]
fn feature_args_passed_to_cargo() {
let args = Args::try_parse_from(
["mutants", "--features", "foo", "--features", "bar,baz"].as_slice(),
)
.unwrap();
let options = Options::from_args(&args).unwrap();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
[
"check",
"--tests",
"--verbose",
"--workspace",
"--features=foo",
"--features=bar,baz"
]
);
}
#[test]
fn profile_arg_passed_to_cargo() {
let args = Args::try_parse_from(["mutants", "--profile", "mutants"].as_slice()).unwrap();
let options = Options::from_args(&args).unwrap();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..],
[
"check",
"--tests",
"--profile=mutants",
"--verbose",
"--workspace",
]
);
}
#[test]
fn nextest_gets_special_cargo_profile_option() {
let args = Args::try_parse_from(
["mutants", "--test-tool=nextest", "--profile", "mutants"].as_slice(),
)
.unwrap();
let options = Options::from_args(&args).unwrap();
assert_eq!(
cargo_argv(&PackageSelection::All, Phase::Build, &options)[1..],
[
"nextest",
"run",
"--no-run",
"--cargo-profile=mutants",
"--verbose",
"--workspace",
]
);
}
rusty_fork_test! {
#[test]
fn rustflags_without_cap_lints_and_no_environment_variables() {
single_threaded_remove_env_var("RUSTFLAGS");
single_threaded_remove_env_var("CARGO_ENCODED_RUSTFLAGS");
assert_eq!(
encoded_rustflags(&Options {
..Default::default()
}),
None
);
}
#[test]
fn rustflags_with_cap_lints_and_no_environment_variables() {
single_threaded_remove_env_var("RUSTFLAGS");
single_threaded_remove_env_var("CARGO_ENCODED_RUSTFLAGS");
assert_eq!(
encoded_rustflags(&Options {
cap_lints: true,
..Default::default()
}),
Some("--cap-lints=warn".into())
);
}
#[test]
fn rustflags_with_empty_encoded_rustflags() {
single_threaded_set_env_var("CARGO_ENCODED_RUSTFLAGS", "");
assert_eq!(
encoded_rustflags(&Options {
cap_lints: true,
..Default::default()
}).unwrap(),
"--cap-lints=warn"
);
}
#[test]
fn rustflags_added_to_existing_encoded_rustflags() {
single_threaded_set_env_var("RUSTFLAGS", "--something\x1f--else");
single_threaded_remove_env_var("CARGO_ENCODED_RUSTFLAGS");
let options = Options {
cap_lints: true,
..Default::default()
};
assert_eq!(encoded_rustflags(&options).unwrap(), "--something\x1f--else\x1f--cap-lints=warn");
}
#[test]
fn rustflags_added_to_existing_rustflags() {
single_threaded_set_env_var("RUSTFLAGS", "-Dwarnings");
single_threaded_remove_env_var("CARGO_ENCODED_RUSTFLAGS");
assert_eq!(encoded_rustflags(&Options {
cap_lints: true,
..Default::default()
}).unwrap(), "-Dwarnings\x1f--cap-lints=warn");
}
}
}