Skip to content

Commit b141d2f

Browse files
committed
feat: Turn experimental options into -Z unstable flags and options
This is modeled after rustc, and primarily Cargo.
1 parent 32950a2 commit b141d2f

35 files changed

Lines changed: 274 additions & 103 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cargo-mutest/src/main.rs

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#![feature(if_let_guard)]
2+
#![feature(trim_prefix_suffix)]
23

34
use std::collections::HashSet;
45
use std::env;
56
use std::fs;
67
use std::path::PathBuf;
78
use std::process::{self, Command};
89

10+
use mutest_driver_cli::{UnstableFlag, UnstableOption};
11+
912
pub mod build {
1013
pub const RUST_TOOLCHAIN_VERSION: &str = env!("RUST_TOOLCHAIN_VERSION");
1114
}
@@ -21,13 +24,43 @@ fn strip_arg(args: &mut Vec<String>, has_value: bool, short_arg: Option<&str>, l
2124
.or_else(|| long_arg.as_deref().and_then(|v| arg.strip_prefix(v)));
2225

2326
match arg_without_prefix.map(|v| has_value && !v.trim_start().starts_with("=") && i + 1 < args.len()) {
24-
Some(true) => { args.splice(i..=(i + 1), None); }
27+
Some(true) => { args.splice(i..=(i + 1), []); }
2528
Some(false) => { args.remove(i); }
2629
None => i += 1,
2730
}
2831
}
2932
}
3033

34+
fn strip_arg_value_occurrences(args: &mut Vec<String>, short_arg: Option<&str>, long_arg: Option<&str>, value: &str) {
35+
let short_arg = short_arg.map(|v| format!("-{v}"));
36+
let long_arg = long_arg.map(|v| format!("--{v}"));
37+
38+
let mut i = 0;
39+
while i < args.len() {
40+
let arg = &args[i];
41+
42+
match () {
43+
_ if let Some(short_arg_without_prefix) = short_arg.as_deref().and_then(|v| arg.strip_prefix(v)) => {
44+
let short_arg_inline_value = short_arg_without_prefix.trim_prefix('=');
45+
match (short_arg_inline_value.is_empty(), args.get(i + 1)) {
46+
(false, _) if short_arg_inline_value == value => { args.remove(i); }
47+
(true, Some(v)) if v == value => { args.splice(i..=(i + 1), []); }
48+
_ => { i += 1; }
49+
}
50+
}
51+
_ if let Some(long_arg_without_prefix) = long_arg.as_deref().and_then(|v| arg.strip_prefix(v)) => {
52+
let long_arg_inline_value = long_arg_without_prefix.strip_prefix('=');
53+
match (long_arg_inline_value, args.get(i + 1)) {
54+
(Some(v), _) if v == value => { args.remove(i); }
55+
(None, Some(v)) if v == value => { args.splice(i..=(i + 1), []); }
56+
_ => { i += 1; }
57+
}
58+
}
59+
_ => { i += 1; }
60+
}
61+
}
62+
}
63+
3164
#[test]
3265
fn test_strip_arg() {
3366
let mut args = vec!["--lib".to_owned()];
@@ -51,6 +84,38 @@ fn test_strip_arg() {
5184
assert_eq!(&["--metadata-out-root-dir=target/mutest/json".to_owned(), "--print=code".to_owned()] as &[String], &args[..]);
5285
}
5386

87+
#[test]
88+
fn test_strip_arg_value_occurences() {
89+
let mut args = vec!["-Z".to_owned(), "write-json-eval-stream".to_owned()];
90+
strip_arg_value_occurrences(&mut args, Some("Z"), None, "write-json-eval-stream");
91+
assert_eq!(&[] as &[String], &args[..]);
92+
93+
let mut args = vec!["-Zwrite-json-eval-stream".to_owned()];
94+
strip_arg_value_occurrences(&mut args, Some("Z"), None, "write-json-eval-stream");
95+
assert_eq!(&[] as &[String], &args[..]);
96+
97+
let mut args = vec!["-Z=write-json-eval-stream".to_owned()];
98+
strip_arg_value_occurrences(&mut args, Some("Z"), None, "write-json-eval-stream");
99+
assert_eq!(&[] as &[String], &args[..]);
100+
101+
let mut args = vec!["-Z".to_owned(), "feature-a".to_owned(), "-Z".to_owned(), "feature-b".to_owned(), "-Z".to_owned(), "feature-c".to_owned()];
102+
strip_arg_value_occurrences(&mut args, Some("Z"), None, "feature-b");
103+
assert_eq!(&["-Z".to_owned(), "feature-a".to_owned(), "-Z".to_owned(), "feature-c".to_owned()] as &[String], &args[..]);
104+
105+
let mut args = vec!["-Z".to_owned(), "feature-b".to_owned(), "-Z".to_owned(), "feature-a".to_owned(), "-Z".to_owned(), "feature-b".to_owned()];
106+
strip_arg_value_occurrences(&mut args, Some("Z"), None, "feature-b");
107+
assert_eq!(&["-Z".to_owned(), "feature-a".to_owned()] as &[String], &args[..]);
108+
}
109+
110+
const RUN_UNSTABLE_FLAGS: &[UnstableFlag] = mutest_driver_cli::extend_const_slice!(mutest_driver_cli::UNSTABLE_FLAGS: &[UnstableFlag], &[
111+
// NOTE: Whenever these change, the `strip_arg_value_occurrences` calls in `run_cargo_with_mutest_driver` have to be updated as well.
112+
UnstableFlag::new("write-json-eval-stream", Some("During evaluation, write JSONL stream file into JSON output directory specified by `--metadata-out-root-dir`.")),
113+
]);
114+
115+
const RUN_UNSTABLE_OPTIONS: &[UnstableOption] = mutest_driver_cli::extend_const_slice!(mutest_driver_cli::UNSTABLE_OPTIONS: &[UnstableOption], &[
116+
// No Cargo or evaluation-specific unstable options at the moment.
117+
]);
118+
54119
mod run_isolate {
55120
mutest_driver_cli::exclusive_opts! { pub(crate) possible_values where
56121
UNSAFE = "unsafe"; ["Only isolate tests for unsafe mutations."]
@@ -113,6 +178,7 @@ fn main() {
113178
.arg(clap::arg!(--"no-build" "Do not compile the generated mutation test harness."))
114179
.arg(clap::arg!(--"no-run" "Do not run the generated mutation test harness."))
115180
.arg(clap::arg!(--color [WHEN] "Output coloring."))
181+
.arg(clap::arg!(Z: -Z [FLAG] "Experimental, unstable flags. See `-Z help` for details.").action(clap::ArgAction::Append))
116182
// NOTE: Whenever these change, the `strip_args` calls in `run_cargo_with_mutest_driver` have to be updated as well.
117183
.next_help_heading("Evaluation Options")
118184
.arg(clap::arg!(-i --inspect [INSPECT_OPTS] "Inspect mutations after evaluation. Options may be specified in the form `--inspect=[open][:<PORT>]`.").num_args(0..=1).require_equals(true).conflicts_with_all(["no-run", "no-build", "no-emit-metadata"]))
@@ -124,8 +190,6 @@ fn main() {
124190
.arg(clap::arg!(--"use-thread-pool" "Evaluate tests in a fixed-size thread pool.").conflicts_with_all(["no-run", "no-build"]))
125191
// Printing-related Arguments
126192
.arg(clap::arg!(--"eval-print" [PRINT] "Print additional information during mutation evaluation. Multiple may be specified, separated by commas.").value_delimiter(',').value_parser(run_print::possible_values()).conflicts_with_all(["no-run", "no-build"]))
127-
// Experimental Flags
128-
.arg(clap::arg!(--"Zwrite-json-eval-stream" "Write JSONL stream file into JSON output directory specified by `--metadata-out-root-dir`.").conflicts_with_all(["no-run", "no-build", "no-emit-metadata"]))
129193
// Passed arguments
130194
.arg(clap::arg!([PASSED_OPTIONS] ...).last(true).conflicts_with_all(["no-run", "no-build"]))
131195
// Cargo options
@@ -168,6 +232,16 @@ fn main() {
168232

169233
match matches.subcommand() {
170234
Some(("run", matches)) => {
235+
let unstable_flags = matches.get_many::<String>("Z").into_iter().flatten().map(String::as_str).collect::<Vec<_>>();
236+
if unstable_flags.contains(&"help") {
237+
mutest_driver_cli::print_unstable_flags_help(RUN_UNSTABLE_FLAGS);
238+
process::exit(0);
239+
}
240+
mutest_driver_cli::check_unstable_flags(&unstable_flags, RUN_UNSTABLE_FLAGS);
241+
if !unstable_flags.contains(&"unstable-options") {
242+
mutest_driver_cli::check_unstable_options(matches, RUN_UNSTABLE_OPTIONS);
243+
}
244+
171245
let inspect_opts = match matches.value_source("inspect") {
172246
Some(clap::parser::ValueSource::CommandLine) if let Some(inspect_opts_str) = matches.get_one::<String>("inspect") => {
173247
let mut unparsed_str: &str = inspect_opts_str;
@@ -211,7 +285,7 @@ fn main() {
211285
// whether specified explicitly through `--target-dir`, or implicitly chosen by Cargo.
212286
cargo_invocation.target_dir.push("mutest");
213287

214-
run_cargo_with_mutest_driver(&cargo_invocation, matches);
288+
run_cargo_with_mutest_driver(&cargo_invocation, matches, &unstable_flags);
215289

216290
if let Some((open, port)) = inspect_opts {
217291
run_mutest_inspector_from_cargo_invocation(open, port, &cargo_invocation, matches);
@@ -360,7 +434,7 @@ fn process_cargo_args<'a>(args: &'a [String], matches: &'a clap::ArgMatches) ->
360434
CargoInvocation { cargo_args, non_cargo_args, target_dir, explicit_targetings_count }
361435
}
362436

363-
fn run_cargo_with_mutest_driver(cargo_invocation: &CargoInvocation, matches: &clap::ArgMatches) {
437+
fn run_cargo_with_mutest_driver(cargo_invocation: &CargoInvocation, matches: &clap::ArgMatches, unstable_flags: &[&str]) {
364438
let mut mutest_args = cargo_invocation.non_cargo_args.clone();
365439

366440
let no_run = matches.get_flag("no-run");
@@ -380,7 +454,7 @@ fn run_cargo_with_mutest_driver(cargo_invocation: &CargoInvocation, matches: &cl
380454

381455
let mut cmd = cargo_command_base();
382456

383-
let embedded = matches.get_flag("Zembedded");
457+
let embedded = unstable_flags.contains(&"embedded");
384458
if embedded {
385459
let target = match matches.get_one::<String>("target") {
386460
Some(target) => target.clone(),
@@ -487,7 +561,7 @@ fn run_cargo_with_mutest_driver(cargo_invocation: &CargoInvocation, matches: &cl
487561
strip_arg(&mut mutest_args, true, None, Some("isolate"));
488562
strip_arg(&mut mutest_args, false, None, Some("use-thread-pool"));
489563
strip_arg(&mut mutest_args, true, None, Some("eval-print"));
490-
strip_arg(&mut mutest_args, false, None, Some("Zwrite-json-eval-stream"));
564+
strip_arg_value_occurrences(&mut mutest_args, Some("Z"), None, "write-json-eval-stream");
491565

492566
cmd.env("MUTEST_ENCODED_ARGS", mutest_args.join("\x1F"));
493567

@@ -511,8 +585,6 @@ fn run_cargo_with_mutest_driver(cargo_invocation: &CargoInvocation, matches: &cl
511585
if print_names.contains("all") { print_names = HashSet::from_iter(run_print::ALL.into_iter().map(|s| *s)); }
512586
for print_name in print_names { cmd.arg(format!("--print={print_name}")); }
513587

514-
if matches.get_flag("Zwrite-json-eval-stream") { cmd.arg("--Zwrite-json-eval-stream"); }
515-
516588
cmd.args((0..matches.get_count("verbose")).map(|_| "-v"));
517589
if matches.get_flag("timings") { cmd.arg("--timings"); }
518590
if !matches.get_flag("no-emit-metadata") {
@@ -525,6 +597,8 @@ fn run_cargo_with_mutest_driver(cargo_invocation: &CargoInvocation, matches: &cl
525597
cmd.arg(format!("--metadata-out-root-dir={out_dir}"));
526598
}
527599

600+
if unstable_flags.contains(&"write-json-eval-stream") { cmd.arg("--Zwrite-json-eval-stream"); }
601+
528602
cmd.args(matches.get_many::<String>("PASSED_OPTIONS").unwrap_or_default());
529603

530604
// NOTE: Disable insta snapshot creation for mutated program tests.

mutest-driver-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ edition = "2024"
1414

1515
[dependencies]
1616
clap = { version = "4", features = ["cargo"] }
17+
color-print = "0.3"

mutest-driver-cli/src/lib.rs

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,100 @@
11
#![feature(decl_macro)]
2+
#![feature(if_let_guard)]
23

34
use std::path::PathBuf;
5+
use std::process;
6+
7+
pub macro extend_const_slice($base:path: &[$ty:ty], $ext:expr) {
8+
&const {
9+
const EXT: &[$ty] = $ext;
10+
const LEN: usize = $base.len() + EXT.len();
11+
let mut dst: [$ty; LEN] = match ($base.len(), EXT.len()) {
12+
// HACK: We must generate a value of type `[$ty; LEN]` here
13+
// regardless of whether it will be used during const evaluation
14+
// (i.e., even if `LEN != 0`).
15+
// SAFETY: For the only case this will be used (`LEN == 0`),
16+
// the value will always be a `[_; 0]`, i.e., an empty `[]`.
17+
(0, 0) => unsafe { std::mem::transmute([0u8; std::mem::size_of::<$ty>() * LEN]) }
18+
(0, _) => [EXT[0]; LEN],
19+
_ => [$base[0]; LEN],
20+
};
21+
let mut i = 0;
22+
while i < $base.len() {
23+
dst[i] = $base[i];
24+
i += 1;
25+
}
26+
i = 0;
27+
while i < EXT.len() {
28+
dst[i + $base.len()] = EXT[i];
29+
i += 1;
30+
}
31+
dst
32+
}
33+
}
34+
35+
#[derive(Copy, Clone, Debug)]
36+
pub struct UnstableFlag {
37+
pub name: &'static str,
38+
pub help: Option<&'static str>,
39+
}
40+
41+
impl UnstableFlag {
42+
pub const fn new(name: &'static str, help: Option<&'static str>) -> Self {
43+
Self { name, help }
44+
}
45+
}
46+
47+
pub fn print_unstable_flags_help(flags: &[UnstableFlag]) {
48+
color_print::cprintln!("<bright-green,bold>Unstable Flags:</>");
49+
let w_name = flags.iter().map(|flag| flag.name.len()).max().unwrap_or_default();
50+
color_print::cprintln!(" <bright-blue,bold>-Z {:<w_name$}</> Print help information.", "help");
51+
for flag in flags {
52+
color_print::cprint!(" <bright-blue,bold>-Z {:<w_name$}</>", flag.name);
53+
if let Some(help) = &flag.help {
54+
color_print::cprint!(" {}", help);
55+
}
56+
color_print::cprintln!("");
57+
}
58+
}
59+
60+
pub fn check_unstable_flags(provided_flags: &[&str], known_flags: &[UnstableFlag]) {
61+
for flag in provided_flags {
62+
if !known_flags.iter().any(|f| f.name == *flag) {
63+
color_print::ceprintln!("<red,bold>error</>: unknown unstable flag `{}`", flag);
64+
process::exit(1);
65+
}
66+
}
67+
}
68+
69+
#[derive(Copy, Clone, Debug)]
70+
pub struct UnstableOption {
71+
pub name: &'static str,
72+
pub value: Option<&'static str>,
73+
}
74+
75+
impl UnstableOption {
76+
pub const fn new(name: &'static str, value: Option<&'static str>) -> Self {
77+
Self { name, value }
78+
}
79+
}
80+
81+
pub fn check_unstable_options(matches: &clap::ArgMatches, options: &[UnstableOption]) {
82+
for option in options {
83+
if let None | Some(clap::parser::ValueSource::DefaultValue) = matches.value_source(option.name) { continue; }
84+
85+
match option.value {
86+
None => {
87+
color_print::ceprintln!("<red,bold>error</>: the `--{}` flag is unstable, pass `-Z unstable-options` to enable it", option.name);
88+
process::exit(1);
89+
}
90+
Some(value) if let Some(mut values) = matches.get_many::<String>(option.name) && values.any(|v| v == value) => {
91+
color_print::ceprintln!("<red,bold>error</>: the `--{}={}` option is unstable, pass `-Z unstable-options` to enable it", option.name, value);
92+
process::exit(1);
93+
}
94+
_ => {}
95+
}
96+
}
97+
}
498

599
pub macro opts(
6100
$all:ident, $possible_values_vis:vis $possible_values:ident where
@@ -28,6 +122,20 @@ pub macro exclusive_opts(
28122
}
29123
}
30124

125+
pub const UNSTABLE_FLAGS: &[UnstableFlag] = &[
126+
UnstableFlag::new("unstable-options", Some("Enable the use of unstable options.")),
127+
// Permanently unstable options.
128+
UnstableFlag::new("verify-ast-lowering", Some("Verify whether all AST nodes are mapped to their HIR counterparts.")),
129+
// Experimental options.
130+
UnstableFlag::new("embedded", Some("Enable experimental support for embedded-test tests and embedded firmware generation with no_std support using a tethered embedded mutation runtime.")),
131+
// Legacy options.
132+
UnstableFlag::new("no-sanitize-macro-expns", Some("Skip sanitizing the identifiers and paths in macro expansions. This is legacy behavior and is not recommended.")),
133+
];
134+
135+
pub const UNSTABLE_OPTIONS: &[UnstableOption] = &[
136+
// No unstable options at the moment.
137+
];
138+
31139
pub mod mutation_operators {
32140
crate::opts! { ALL, pub(crate) possible_values where
33141
ARG_DEFAULT_SHADOW = "arg_default_shadow";
@@ -95,12 +203,6 @@ pub mod call_graph_non_local_call_view {
95203
}
96204
}
97205

98-
pub mod verify {
99-
crate::opts! { ALL, pub(crate) possible_values where
100-
AST_LOWERING = "ast_lowering";
101-
}
102-
}
103-
104206
pub const fn rustc_version_str() -> &'static str {
105207
env!("RUSTC_VERSION_STR")
106208
}
@@ -159,11 +261,7 @@ pub fn command(name: &'static str) -> clap::Command {
159261
.arg(clap::arg!(--"mutant-batch-size" [MUTANT_BATCH_SIZE] "Maximum number of mutations to batch into a single batch.").default_value("1").value_parser(clap::value_parser!(usize)))
160262
.arg(clap::arg!(--"mutant-batch-seed" [MUTANT_BATCH_SEED] "Random seed to use for randomness during mutation batching."))
161263
.arg(clap::arg!(--"mutant-batch-greedy-ordering-heuristic" [MUTANT_BATCH_GREEDY_ORDERING_HEURISTIC] "Ordering heuristic to use for `greedy` mutation batching algorithm.").value_parser(mutant_batch_greedy_ordering_heuristic::possible_values()).default_value(mutant_batch_greedy_ordering_heuristic::REVERSE_CONFLICTS))
162-
.arg(clap::arg!(--"mutant-batch-greedy-epsilon" [MUTANT_BATCH_GREEDY_EPSILON] "Optional epsilon parameter for `greedy` mutation batching algorithm, used to control the probability of random mutation assignment.").default_value("0").value_parser(clap::value_parser!(f64)))
163-
.next_help_heading("Experimental Options")
164-
.arg(clap::arg!(--Zverify [VERIFY] "Perform additional checks to verify correctness and completeness. Multiple may be specified, separated by commas.").value_delimiter(',').value_parser(verify::possible_values()))
165-
.arg(clap::arg!(--Zembedded "Enable experimental support for embedded-test tests and embedded firmware generation with no_std support using a tethered embedded mutation runtime."))
166-
.arg(clap::arg!(--"Zno-sanitize-macro-expns" "Skip sanitizing the identifiers and paths in the expanded output of macro invocations. This was the previous behavior and is not recommended."));
264+
.arg(clap::arg!(--"mutant-batch-greedy-epsilon" [MUTANT_BATCH_GREEDY_EPSILON] "Optional epsilon parameter for `greedy` mutation batching algorithm, used to control the probability of random mutation assignment.").default_value("0").value_parser(clap::value_parser!(f64)));
167265

168266
cmd
169267
}

mutest-driver/src/config.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,6 @@ pub enum MutationParallelism {
155155
DynamicScheduling,
156156
}
157157

158-
#[derive(Clone, Debug)]
159-
pub struct VerifyOptions {
160-
pub ast_lowering: bool,
161-
}
162-
163158
#[derive(Clone, Debug)]
164159
pub enum MutationFilter {
165160
/// `file:path/to/src.rs` or `file:path/to/src.rs:15` or `file:path/to/src.rs:15:17`.
@@ -168,6 +163,13 @@ pub enum MutationFilter {
168163
Def(String),
169164
}
170165

166+
#[derive(Clone, Debug, Default)]
167+
pub struct UnstableFlags {
168+
pub verify_ast_lowering: bool,
169+
pub embedded: bool,
170+
pub no_sanitize_macro_expns: bool,
171+
}
172+
171173
pub struct Options<'op, 'm> {
172174
pub crate_kind: CrateKind,
173175
pub cargo_target_kind: Option<CargoTargetKind>,
@@ -185,9 +187,7 @@ pub struct Options<'op, 'm> {
185187
pub mutation_filters: Vec<MutationFilter>,
186188
pub mutation_parallelism: Option<MutationParallelism>,
187189

188-
pub verify_opts: VerifyOptions,
189-
pub embedded: bool,
190-
pub sanitize_macro_expns: bool,
190+
pub unstable_flags: UnstableFlags,
191191
}
192192

193193
pub struct Config<'op, 'm> {

0 commit comments

Comments
 (0)