Skip to content

Commit 4345379

Browse files
committed
cargo miri: add support for '--many-seeds' to run the program / tests many times with different seeds
1 parent c5e9424 commit 4345379

File tree

3 files changed

+155
-88
lines changed

3 files changed

+155
-88
lines changed

src/tools/miri/cargo-miri/src/phases.rs

+121-79
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
//! Implements the various phases of `cargo miri run/test`.
22
3-
use std::env;
43
use std::fs::{self, File};
5-
use std::io::BufReader;
4+
use std::io::{BufReader, Write};
65
use std::path::{Path, PathBuf};
76
use std::process::Command;
7+
use std::{env, thread};
88

99
use rustc_version::VersionMeta;
1010

@@ -119,7 +119,7 @@ pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
119119
// <https://github.com/rust-lang/miri/pull/1540#issuecomment-693553191> describes an alternative
120120
// approach that uses `cargo check`, making that part easier but target and binary handling
121121
// harder.
122-
let cargo_miri_path = std::env::current_exe()
122+
let cargo_miri_path = env::current_exe()
123123
.expect("current executable path invalid")
124124
.into_os_string()
125125
.into_string()
@@ -163,14 +163,22 @@ pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
163163
let target_dir = get_target_dir(&metadata);
164164
cmd.arg("--target-dir").arg(target_dir);
165165

166+
// Store many-seeds argument.
167+
let mut many_seeds = None;
166168
// *After* we set all the flags that need setting, forward everything else. Make sure to skip
167-
// `--target-dir` (which would otherwise be set twice).
169+
// `--target-dir` (which would otherwise be set twice) and `--many-seeds` (which is our flag, not cargo's).
168170
for arg in
169171
ArgSplitFlagValue::from_string_iter(&mut args, "--target-dir").filter_map(Result::err)
170172
{
171-
cmd.arg(arg);
173+
if arg == "--many-seeds" {
174+
many_seeds = Some(format!("0..256"));
175+
} else if let Some(val) = arg.strip_prefix("--many-seeds=") {
176+
many_seeds = Some(val.to_owned());
177+
} else {
178+
cmd.arg(arg);
179+
}
172180
}
173-
// Forward all further arguments (not consumed by `ArgSplitFlagValue`) to cargo.
181+
// Forward all further arguments after `--` (not consumed by `ArgSplitFlagValue`) to cargo.
174182
cmd.args(args);
175183

176184
// Set `RUSTC_WRAPPER` to ourselves. Cargo will prepend that binary to its usual invocation,
@@ -222,6 +230,9 @@ pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
222230
// Forward some crucial information to our own re-invocations.
223231
cmd.env("MIRI_SYSROOT", miri_sysroot);
224232
cmd.env("MIRI_LOCAL_CRATES", local_crates(&metadata));
233+
if let Some(many_seeds) = many_seeds {
234+
cmd.env("MIRI_MANY_SEEDS", many_seeds);
235+
}
225236
if verbose > 0 {
226237
cmd.env("MIRI_VERBOSE", verbose.to_string()); // This makes the other phases verbose.
227238
}
@@ -309,7 +320,7 @@ pub fn phase_rustc(mut args: impl Iterator<Item = String>, phase: RustcPhase) {
309320
}
310321
}
311322

312-
let verbose = std::env::var("MIRI_VERBOSE")
323+
let verbose = env::var("MIRI_VERBOSE")
313324
.map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
314325
let target_crate = is_target_crate();
315326

@@ -489,7 +500,7 @@ pub fn phase_rustc(mut args: impl Iterator<Item = String>, phase: RustcPhase) {
489500
// This is a host crate.
490501
// When we're running `cargo-miri` from `x.py` we need to pass the sysroot explicitly
491502
// due to bootstrap complications.
492-
if let Some(sysroot) = std::env::var_os("MIRI_HOST_SYSROOT") {
503+
if let Some(sysroot) = env::var_os("MIRI_HOST_SYSROOT") {
493504
cmd.arg("--sysroot").arg(sysroot);
494505
}
495506

@@ -532,7 +543,7 @@ pub enum RunnerPhase {
532543
}
533544

534545
pub fn phase_runner(mut binary_args: impl Iterator<Item = String>, phase: RunnerPhase) {
535-
let verbose = std::env::var("MIRI_VERBOSE")
546+
let verbose = env::var("MIRI_VERBOSE")
536547
.map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
537548

538549
let binary = binary_args.next().unwrap();
@@ -541,6 +552,7 @@ pub fn phase_runner(mut binary_args: impl Iterator<Item = String>, phase: Runner
541552
"file {:?} not found or `cargo-miri` invoked incorrectly; please only invoke this binary through `cargo miri`", binary
542553
));
543554
let file = BufReader::new(file);
555+
let binary_args = binary_args.collect::<Vec<_>>();
544556

545557
let info = serde_json::from_reader(file).unwrap_or_else(|_| {
546558
show_error!("file {:?} contains outdated or invalid JSON; try `cargo clean`", binary)
@@ -555,84 +567,114 @@ pub fn phase_runner(mut binary_args: impl Iterator<Item = String>, phase: Runner
555567
}
556568
};
557569

558-
let mut cmd = miri();
559-
560-
// Set missing env vars. We prefer build-time env vars over run-time ones; see
561-
// <https://github.com/rust-lang/miri/issues/1661> for the kind of issue that fixes.
562-
for (name, val) in info.env {
563-
// `CARGO_MAKEFLAGS` contains information about how to reach the jobserver, but by the time
564-
// the program is being run, that jobserver no longer exists (cargo only runs the jobserver
565-
// for the build portion of `cargo run`/`cargo test`). Hence we shouldn't forward this.
566-
// Also see <https://github.com/rust-lang/rust/pull/113730>.
567-
if name == "CARGO_MAKEFLAGS" {
568-
continue;
569-
}
570-
if let Some(old_val) = env::var_os(&name) {
571-
if old_val == val {
572-
// This one did not actually change, no need to re-set it.
573-
// (This keeps the `debug_cmd` below more manageable.)
570+
let many_seeds = env::var("MIRI_MANY_SEEDS");
571+
run_many_seeds(many_seeds.ok(), |seed| {
572+
let mut cmd = miri();
573+
574+
// Set missing env vars. We prefer build-time env vars over run-time ones; see
575+
// <https://github.com/rust-lang/miri/issues/1661> for the kind of issue that fixes.
576+
for (name, val) in &info.env {
577+
// `CARGO_MAKEFLAGS` contains information about how to reach the jobserver, but by the time
578+
// the program is being run, that jobserver no longer exists (cargo only runs the jobserver
579+
// for the build portion of `cargo run`/`cargo test`). Hence we shouldn't forward this.
580+
// Also see <https://github.com/rust-lang/rust/pull/113730>.
581+
if name == "CARGO_MAKEFLAGS" {
574582
continue;
575-
} else if verbose > 0 {
576-
eprintln!(
577-
"[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}"
578-
);
579583
}
584+
if let Some(old_val) = env::var_os(name) {
585+
if *old_val == *val {
586+
// This one did not actually change, no need to re-set it.
587+
// (This keeps the `debug_cmd` below more manageable.)
588+
continue;
589+
} else if verbose > 0 {
590+
eprintln!(
591+
"[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}"
592+
);
593+
}
594+
}
595+
cmd.env(name, val);
580596
}
581-
cmd.env(name, val);
582-
}
583597

584-
if phase != RunnerPhase::Rustdoc {
585-
// Set the sysroot. Not necessary in rustdoc, where we already set the sysroot in
586-
// `phase_rustdoc`. rustdoc will forward that flag when invoking rustc (i.e., us), so the
587-
// flag is present in `info.args`.
588-
cmd.arg("--sysroot").arg(env::var_os("MIRI_SYSROOT").unwrap());
589-
}
590-
// Forward rustc arguments.
591-
// We need to patch "--extern" filenames because we forced a check-only
592-
// build without cargo knowing about that: replace `.rlib` suffix by
593-
// `.rmeta`.
594-
// We also need to remove `--error-format` as cargo specifies that to be JSON,
595-
// but when we run here, cargo does not interpret the JSON any more. `--json`
596-
// then also needs to be dropped.
597-
let mut args = info.args.into_iter();
598-
while let Some(arg) = args.next() {
599-
if arg == "--extern" {
600-
forward_patched_extern_arg(&mut args, &mut cmd);
601-
} else if let Some(suffix) = arg.strip_prefix("--error-format") {
602-
assert!(suffix.starts_with('='));
603-
// Drop this argument.
604-
} else if let Some(suffix) = arg.strip_prefix("--json") {
605-
assert!(suffix.starts_with('='));
606-
// Drop this argument.
607-
} else {
608-
cmd.arg(arg);
598+
if phase != RunnerPhase::Rustdoc {
599+
// Set the sysroot. Not necessary in rustdoc, where we already set the sysroot in
600+
// `phase_rustdoc`. rustdoc will forward that flag when invoking rustc (i.e., us), so the
601+
// flag is present in `info.args`.
602+
cmd.arg("--sysroot").arg(env::var_os("MIRI_SYSROOT").unwrap());
603+
}
604+
// Forward rustc arguments.
605+
// We need to patch "--extern" filenames because we forced a check-only
606+
// build without cargo knowing about that: replace `.rlib` suffix by
607+
// `.rmeta`.
608+
// We also need to remove `--error-format` as cargo specifies that to be JSON,
609+
// but when we run here, cargo does not interpret the JSON any more. `--json`
610+
// then also needs to be dropped.
611+
let mut args = info.args.iter();
612+
while let Some(arg) = args.next() {
613+
if arg == "--extern" {
614+
forward_patched_extern_arg(&mut (&mut args).cloned(), &mut cmd);
615+
} else if let Some(suffix) = arg.strip_prefix("--error-format") {
616+
assert!(suffix.starts_with('='));
617+
// Drop this argument.
618+
} else if let Some(suffix) = arg.strip_prefix("--json") {
619+
assert!(suffix.starts_with('='));
620+
// Drop this argument.
621+
} else {
622+
cmd.arg(arg);
623+
}
624+
}
625+
// Respect `MIRIFLAGS`.
626+
if let Ok(a) = env::var("MIRIFLAGS") {
627+
let args = flagsplit(&a);
628+
cmd.args(args);
629+
}
630+
// Set the current seed.
631+
if let Some(seed) = seed {
632+
eprintln!("Trying seed: {seed}");
633+
cmd.arg(format!("-Zmiri-seed={seed}"));
609634
}
610-
}
611-
// Respect `MIRIFLAGS`.
612-
if let Ok(a) = env::var("MIRIFLAGS") {
613-
let args = flagsplit(&a);
614-
cmd.args(args);
615-
}
616-
617-
// Then pass binary arguments.
618-
cmd.arg("--");
619-
cmd.args(binary_args);
620-
621-
// Make sure we use the build-time working directory for interpreting Miri/rustc arguments.
622-
// But then we need to switch to the run-time one, which we instruct Miri to do by setting `MIRI_CWD`.
623-
cmd.current_dir(info.current_dir);
624-
cmd.env("MIRI_CWD", env::current_dir().unwrap());
625635

626-
// Run it.
627-
debug_cmd("[cargo-miri runner]", verbose, &cmd);
628-
match phase {
629-
RunnerPhase::Rustdoc => exec_with_pipe(cmd, &info.stdin, format!("{binary}.stdin")),
630-
RunnerPhase::Cargo => exec(cmd),
631-
}
636+
// Then pass binary arguments.
637+
cmd.arg("--");
638+
cmd.args(&binary_args);
639+
640+
// Make sure we use the build-time working directory for interpreting Miri/rustc arguments.
641+
// But then we need to switch to the run-time one, which we instruct Miri to do by setting `MIRI_CWD`.
642+
cmd.current_dir(&info.current_dir);
643+
cmd.env("MIRI_CWD", env::current_dir().unwrap());
644+
645+
// Run it.
646+
debug_cmd("[cargo-miri runner]", verbose, &cmd);
647+
648+
match phase {
649+
RunnerPhase::Rustdoc => {
650+
cmd.stdin(std::process::Stdio::piped());
651+
let mut child = cmd.spawn().expect("failed to spawn process");
652+
let child_stdin = child.stdin.take().unwrap();
653+
// Write stdin in a background thread, as it may block.
654+
let exit_status = thread::scope(|s| {
655+
s.spawn(|| {
656+
let mut child_stdin = child_stdin;
657+
// Ignore failure, it is most likely due to the process having terminated.
658+
let _ = child_stdin.write_all(&info.stdin);
659+
});
660+
child.wait().expect("failed to run command")
661+
});
662+
if !exit_status.success() {
663+
std::process::exit(exit_status.code().unwrap_or(-1));
664+
}
665+
}
666+
RunnerPhase::Cargo => {
667+
let exit_status = cmd.status().expect("failed to run command");
668+
if !exit_status.success() {
669+
std::process::exit(exit_status.code().unwrap_or(-1));
670+
}
671+
}
672+
}
673+
});
632674
}
633675

634676
pub fn phase_rustdoc(mut args: impl Iterator<Item = String>) {
635-
let verbose = std::env::var("MIRI_VERBOSE")
677+
let verbose = env::var("MIRI_VERBOSE")
636678
.map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
637679

638680
// phase_cargo_miri sets the RUSTDOC env var to ourselves, and puts a backup
@@ -681,7 +723,7 @@ pub fn phase_rustdoc(mut args: impl Iterator<Item = String>) {
681723
cmd.arg("--cfg").arg("miri");
682724

683725
// Make rustdoc call us back.
684-
let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
726+
let cargo_miri_path = env::current_exe().expect("current executable path invalid");
685727
cmd.arg("--test-builder").arg(&cargo_miri_path); // invoked by forwarding most arguments
686728
cmd.arg("--runtool").arg(&cargo_miri_path); // invoked with just a single path argument
687729

src/tools/miri/cargo-miri/src/util.rs

+31-5
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,16 @@ where
171171
drop(path); // We don't need the path, we can pipe the bytes directly
172172
cmd.stdin(std::process::Stdio::piped());
173173
let mut child = cmd.spawn().expect("failed to spawn process");
174-
{
175-
let stdin = child.stdin.as_mut().expect("failed to open stdin");
176-
stdin.write_all(input).expect("failed to write out test source");
177-
}
178-
let exit_status = child.wait().expect("failed to run command");
174+
let child_stdin = child.stdin.take().unwrap();
175+
// Write stdin in a background thread, as it may block.
176+
let exit_status = std::thread::scope(|s| {
177+
s.spawn(|| {
178+
let mut child_stdin = child_stdin;
179+
// Ignore failure, it is most likely due to the process having terminated.
180+
let _ = child_stdin.write_all(input);
181+
});
182+
child.wait().expect("failed to run command")
183+
});
179184
std::process::exit(exit_status.code().unwrap_or(-1))
180185
}
181186
}
@@ -317,3 +322,24 @@ pub fn clean_target_dir(meta: &Metadata) {
317322

318323
remove_dir_all_idem(&target_dir).unwrap_or_else(|err| show_error!("{}", err))
319324
}
325+
326+
/// Run `f` according to the many-seeds argument. In single-seed mode, `f` will only
327+
/// be called once, with `None`.
328+
pub fn run_many_seeds(many_seeds: Option<String>, f: impl Fn(Option<u32>)) {
329+
let Some(many_seeds) = many_seeds else {
330+
return f(None);
331+
};
332+
let (from, to) = many_seeds
333+
.split_once("..")
334+
.unwrap_or_else(|| show_error!("invalid format for `--many-seeds`: expected `from..to`"));
335+
let from: u32 = if from.is_empty() {
336+
0
337+
} else {
338+
from.parse().unwrap_or_else(|_| show_error!("invalid `from` in `--many-seeds=from..to"))
339+
};
340+
let to: u32 =
341+
to.parse().unwrap_or_else(|_| show_error!("invalid `to` in `--many-seeds=from..to"));
342+
for seed in from..to {
343+
f(Some(seed));
344+
}
345+
}

src/tools/miri/miri-script/src/main.rs

+3-4
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,14 @@ fn main() -> Result<()> {
182182
verbose = true;
183183
} else if let Some(val) = args.get_long_opt_with_default("many-seeds", "0..256")? {
184184
let (from, to) = val.split_once("..").ok_or_else(|| {
185-
anyhow!("invalid format for `--many-seeds-range`: expected `from..to`")
185+
anyhow!("invalid format for `--many-seeds`: expected `from..to`")
186186
})?;
187187
let from: u32 = if from.is_empty() {
188188
0
189189
} else {
190-
from.parse().context("invalid `from` in `--many-seeds-range=from..to")?
190+
from.parse().context("invalid `from` in `--many-seeds=from..to")?
191191
};
192-
let to: u32 =
193-
to.parse().context("invalid `to` in `--many-seeds-range=from..to")?;
192+
let to: u32 = to.parse().context("invalid `to` in `--many-seeds=from..to")?;
194193
many_seeds = Some(from..to);
195194
} else if let Some(val) = args.get_long_opt("target")? {
196195
target = Some(val);

0 commit comments

Comments
 (0)