Skip to content

Commit 5958f5e

Browse files
committed
Auto merge of rust-lang#123317 - RalfJung:test-in-miri, r=m-ou-se,saethlin,onur-ozkan
Support running library tests in Miri This adds a new bootstrap subcommand `./x.py miri` which can test libraries in Miri. This is in preparation for eventually doing that as part of bors CI, but this PR only adds the infrastructure, and doesn't enable it yet. `@rust-lang/bootstrap` should this be `x.py test --miri library/core` or `x.py miri library/core`? The flag has the advantage that we don't have to copy all the arguments from `Subcommand::Test`. It has the disadvantage that most test steps just ignore `--miri` and still run tests the regular way. For clippy you went the route of making it a separate subcommand. ~~I went with a flag now as that seemed easier, but I can change this.~~ I made it a new subcommand. Note however that the regular cargo invocation would be `cargo miri test ...`, so `x.py` is still going to be different in that the `test` is omitted. That said, we could also make it `./x.py miri-test` to make that difference smaller -- that's in fact more consistent with the internal name of the command when bootstrap invokes cargo. `@rust-lang/libs` ~~unfortunately this PR does some unholy things to the `lib.rs` files of our library crates.~~ `@m-ou-se` found a way that entirely avoids library-level hacks, except for some new small `lib.miri.rs` files that hopefully you will never have to touch. There's a new hack in cargo-miri but there it is in good company...
2 parents 4563f70 + 3ad9c83 commit 5958f5e

File tree

17 files changed

+490
-68
lines changed

17 files changed

+490
-68
lines changed

library/alloc/benches/vec_deque_append.rs

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ const WARMUP_N: usize = 100;
55
const BENCH_N: usize = 1000;
66

77
fn main() {
8+
if cfg!(miri) {
9+
// Don't benchmark Miri...
10+
// (Due to bootstrap quirks, this gets picked up by `x.py miri library/alloc --no-doc`.)
11+
return;
12+
}
813
let a: VecDeque<i32> = (0..VECDEQUE_LEN).collect();
914
let b: VecDeque<i32> = (0..VECDEQUE_LEN).collect();
1015

library/alloc/src/lib.miri.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! Grep bootstrap for `MIRI_REPLACE_LIBRS_IF_NOT_TEST` to learn what this is about.
2+
#![no_std]
3+
extern crate alloc as realalloc;
4+
pub use realalloc::*;

library/core/src/lib.miri.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! Grep bootstrap for `MIRI_REPLACE_LIBRS_IF_NOT_TEST` to learn what this is about.
2+
#![no_std]
3+
extern crate core as realcore;
4+
pub use realcore::*;

library/std/src/lib.miri.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! Grep bootstrap for `MIRI_REPLACE_LIBRS_IF_NOT_TEST` to learn what this is about.
2+
#![no_std]
3+
extern crate std as realstd;
4+
pub use realstd::*;

src/bootstrap/src/core/build_steps/test.rs

+57-22
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ impl Step for Miri {
595595
// This is for the tests so everything is done with the target compiler.
596596
let miri_sysroot = Miri::build_miri_sysroot(builder, target_compiler, target);
597597
builder.ensure(compile::Std::new(target_compiler, host));
598-
let sysroot = builder.sysroot(target_compiler);
598+
let host_sysroot = builder.sysroot(target_compiler);
599599

600600
// # Run `cargo test`.
601601
// This is with the Miri crate, so it uses the host compiler.
@@ -618,7 +618,7 @@ impl Step for Miri {
618618

619619
// miri tests need to know about the stage sysroot
620620
cargo.env("MIRI_SYSROOT", &miri_sysroot);
621-
cargo.env("MIRI_HOST_SYSROOT", &sysroot);
621+
cargo.env("MIRI_HOST_SYSROOT", &host_sysroot);
622622
cargo.env("MIRI", &miri);
623623

624624
// Set the target.
@@ -681,10 +681,6 @@ impl Step for Miri {
681681
}
682682
}
683683

684-
// Tell `cargo miri` where to find the sysroots.
685-
cargo.env("MIRI_SYSROOT", &miri_sysroot);
686-
cargo.env("MIRI_HOST_SYSROOT", sysroot);
687-
688684
// Finally, pass test-args and run everything.
689685
cargo.arg("--").args(builder.config.test_args());
690686
let mut cargo = Command::from(cargo);
@@ -2540,9 +2536,14 @@ fn prepare_cargo_test(
25402536
//
25412537
// Note that to run the compiler we need to run with the *host* libraries,
25422538
// but our wrapper scripts arrange for that to be the case anyway.
2543-
let mut dylib_path = dylib_path();
2544-
dylib_path.insert(0, PathBuf::from(&*builder.sysroot_libdir(compiler, target)));
2545-
cargo.env(dylib_path_var(), env::join_paths(&dylib_path).unwrap());
2539+
//
2540+
// We skip everything on Miri as then this overwrites the libdir set up
2541+
// by `Cargo::new` and that actually makes things go wrong.
2542+
if builder.kind != Kind::Miri {
2543+
let mut dylib_path = dylib_path();
2544+
dylib_path.insert(0, PathBuf::from(&*builder.sysroot_libdir(compiler, target)));
2545+
cargo.env(dylib_path_var(), env::join_paths(&dylib_path).unwrap());
2546+
}
25462547

25472548
if builder.remote_tested(target) {
25482549
cargo.env(
@@ -2598,28 +2599,62 @@ impl Step for Crate {
25982599
let target = self.target;
25992600
let mode = self.mode;
26002601

2602+
// Prepare sysroot
26012603
// See [field@compile::Std::force_recompile].
26022604
builder.ensure(compile::Std::force_recompile(compiler, compiler.host));
26032605

2604-
if builder.config.build != target {
2605-
builder.ensure(compile::Std::force_recompile(compiler, target));
2606-
builder.ensure(RemoteCopyLibs { compiler, target });
2607-
}
2608-
26092606
// If we're not doing a full bootstrap but we're testing a stage2
26102607
// version of libstd, then what we're actually testing is the libstd
26112608
// produced in stage1. Reflect that here by updating the compiler that
26122609
// we're working with automatically.
26132610
let compiler = builder.compiler_for(compiler.stage, compiler.host, target);
26142611

2615-
let mut cargo = builder::Cargo::new(
2616-
builder,
2617-
compiler,
2618-
mode,
2619-
SourceType::InTree,
2620-
target,
2621-
builder.kind.as_str(),
2622-
);
2612+
let mut cargo = if builder.kind == Kind::Miri {
2613+
if builder.top_stage == 0 {
2614+
eprintln!("ERROR: `x.py miri` requires stage 1 or higher");
2615+
std::process::exit(1);
2616+
}
2617+
2618+
// Build `cargo miri test` command
2619+
// (Implicitly prepares target sysroot)
2620+
let mut cargo = builder::Cargo::new(
2621+
builder,
2622+
compiler,
2623+
mode,
2624+
SourceType::InTree,
2625+
target,
2626+
"miri-test",
2627+
);
2628+
// This hack helps bootstrap run standard library tests in Miri. The issue is as
2629+
// follows: when running `cargo miri test` on libcore, cargo builds a local copy of core
2630+
// and makes it a dependency of the integration test crate. This copy duplicates all the
2631+
// lang items, so the build fails. (Regular testing avoids this because the sysroot is a
2632+
// literal copy of what `cargo build` produces, but since Miri builds its own sysroot
2633+
// this does not work for us.) So we need to make it so that the locally built libcore
2634+
// contains all the items from `core`, but does not re-define them -- we want to replace
2635+
// the entire crate but a re-export of the sysroot crate. We do this by swapping out the
2636+
// source file: if `MIRI_REPLACE_LIBRS_IF_NOT_TEST` is set and we are building a
2637+
// `lib.rs` file, and a `lib.miri.rs` file exists in the same folder, we build that
2638+
// instead. But crucially we only do that for the library, not the test builds.
2639+
cargo.env("MIRI_REPLACE_LIBRS_IF_NOT_TEST", "1");
2640+
cargo
2641+
} else {
2642+
// Also prepare a sysroot for the target.
2643+
if builder.config.build != target {
2644+
builder.ensure(compile::Std::force_recompile(compiler, target));
2645+
builder.ensure(RemoteCopyLibs { compiler, target });
2646+
}
2647+
2648+
// Build `cargo test` command
2649+
builder::Cargo::new(
2650+
builder,
2651+
compiler,
2652+
mode,
2653+
SourceType::InTree,
2654+
target,
2655+
builder.kind.as_str(),
2656+
)
2657+
};
26232658

26242659
match mode {
26252660
Mode::Std => {

src/bootstrap/src/core/builder.rs

+20-2
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ pub enum Kind {
631631
Format,
632632
#[value(alias = "t")]
633633
Test,
634+
Miri,
634635
Bench,
635636
#[value(alias = "d")]
636637
Doc,
@@ -673,6 +674,7 @@ impl Kind {
673674
Kind::Fix => "fix",
674675
Kind::Format => "fmt",
675676
Kind::Test => "test",
677+
Kind::Miri => "miri",
676678
Kind::Bench => "bench",
677679
Kind::Doc => "doc",
678680
Kind::Clean => "clean",
@@ -822,6 +824,7 @@ impl<'a> Builder<'a> {
822824
// Run run-make last, since these won't pass without make on Windows
823825
test::RunMake,
824826
),
827+
Kind::Miri => describe!(test::Crate),
825828
Kind::Bench => describe!(test::Crate, test::CrateLibrustc),
826829
Kind::Doc => describe!(
827830
doc::UnstableBook,
@@ -970,6 +973,7 @@ impl<'a> Builder<'a> {
970973
Subcommand::Fix => (Kind::Fix, &paths[..]),
971974
Subcommand::Doc { .. } => (Kind::Doc, &paths[..]),
972975
Subcommand::Test { .. } => (Kind::Test, &paths[..]),
976+
Subcommand::Miri { .. } => (Kind::Miri, &paths[..]),
973977
Subcommand::Bench { .. } => (Kind::Bench, &paths[..]),
974978
Subcommand::Dist => (Kind::Dist, &paths[..]),
975979
Subcommand::Install => (Kind::Install, &paths[..]),
@@ -1226,6 +1230,7 @@ impl<'a> Builder<'a> {
12261230
assert!(run_compiler.stage > 0, "miri can not be invoked at stage 0");
12271231
let build_compiler = self.compiler(run_compiler.stage - 1, self.build.build);
12281232

1233+
// Prepare the tools
12291234
let miri = self.ensure(tool::Miri {
12301235
compiler: build_compiler,
12311236
target: self.build.build,
@@ -1236,7 +1241,7 @@ impl<'a> Builder<'a> {
12361241
target: self.build.build,
12371242
extra_features: Vec::new(),
12381243
});
1239-
// Invoke cargo-miri, make sure we can find miri and cargo.
1244+
// Invoke cargo-miri, make sure it can find miri and cargo.
12401245
let mut cmd = Command::new(cargo_miri);
12411246
cmd.env("MIRI", &miri);
12421247
cmd.env("CARGO", &self.initial_cargo);
@@ -1299,7 +1304,11 @@ impl<'a> Builder<'a> {
12991304
if cmd == "clippy" {
13001305
cargo = self.cargo_clippy_cmd(compiler);
13011306
cargo.arg(cmd);
1302-
} else if let Some(subcmd) = cmd.strip_prefix("miri-") {
1307+
} else if let Some(subcmd) = cmd.strip_prefix("miri") {
1308+
// Command must be "miri-X".
1309+
let subcmd = subcmd
1310+
.strip_prefix("-")
1311+
.unwrap_or_else(|| panic!("expected `miri-$subcommand`, but got {}", cmd));
13031312
cargo = self.cargo_miri_cmd(compiler);
13041313
cargo.arg("miri").arg(subcmd);
13051314
} else {
@@ -1702,6 +1711,15 @@ impl<'a> Builder<'a> {
17021711
cargo.env("RUSTC_WRAPPER_REAL", existing_wrapper);
17031712
}
17041713

1714+
// If this is for `miri-test`, prepare the sysroots.
1715+
if cmd == "miri-test" {
1716+
self.ensure(compile::Std::new(compiler, compiler.host));
1717+
let host_sysroot = self.sysroot(compiler);
1718+
let miri_sysroot = test::Miri::build_miri_sysroot(self, compiler, target);
1719+
cargo.env("MIRI_SYSROOT", &miri_sysroot);
1720+
cargo.env("MIRI_HOST_SYSROOT", &host_sysroot);
1721+
}
1722+
17051723
cargo.env(profile_var("STRIP"), self.config.rust_strip.to_string());
17061724

17071725
if let Some(stack_protector) = &self.config.rust_stack_protector {

src/bootstrap/src/core/config/config.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -2022,7 +2022,7 @@ impl Config {
20222022
Subcommand::Build { .. } => {
20232023
flags.stage.or(build_stage).unwrap_or(if download_rustc { 2 } else { 1 })
20242024
}
2025-
Subcommand::Test { .. } => {
2025+
Subcommand::Test { .. } | Subcommand::Miri { .. } => {
20262026
flags.stage.or(test_stage).unwrap_or(if download_rustc { 2 } else { 1 })
20272027
}
20282028
Subcommand::Bench { .. } => flags.stage.or(bench_stage).unwrap_or(2),
@@ -2044,6 +2044,7 @@ impl Config {
20442044
if flags.stage.is_none() && crate::CiEnv::current() != crate::CiEnv::None {
20452045
match config.cmd {
20462046
Subcommand::Test { .. }
2047+
| Subcommand::Miri { .. }
20472048
| Subcommand::Doc { .. }
20482049
| Subcommand::Build { .. }
20492050
| Subcommand::Bench { .. }
@@ -2099,7 +2100,9 @@ impl Config {
20992100

21002101
pub(crate) fn test_args(&self) -> Vec<&str> {
21012102
let mut test_args = match self.cmd {
2102-
Subcommand::Test { ref test_args, .. } | Subcommand::Bench { ref test_args, .. } => {
2103+
Subcommand::Test { ref test_args, .. }
2104+
| Subcommand::Bench { ref test_args, .. }
2105+
| Subcommand::Miri { ref test_args, .. } => {
21032106
test_args.iter().flat_map(|s| s.split_whitespace()).collect()
21042107
}
21052108
_ => vec![],

src/bootstrap/src/core/config/flags.rs

+25-3
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,25 @@ pub enum Subcommand {
379379
/// `/<build_base>/rustfix_missing_coverage.txt`
380380
rustfix_coverage: bool,
381381
},
382+
/// Build and run some test suites *in Miri*
383+
Miri {
384+
#[arg(long)]
385+
/// run all tests regardless of failure
386+
no_fail_fast: bool,
387+
#[arg(long, value_name = "ARGS", allow_hyphen_values(true))]
388+
/// extra arguments to be passed for the test tool being used
389+
/// (e.g. libtest, compiletest or rustdoc)
390+
test_args: Vec<String>,
391+
/// extra options to pass the compiler when running tests
392+
#[arg(long, value_name = "ARGS", allow_hyphen_values(true))]
393+
rustc_args: Vec<String>,
394+
#[arg(long)]
395+
/// do not run doc tests
396+
no_doc: bool,
397+
#[arg(long)]
398+
/// only run doc tests
399+
doc: bool,
400+
},
382401
/// Build and run some benchmarks
383402
Bench {
384403
#[arg(long, allow_hyphen_values(true))]
@@ -450,6 +469,7 @@ impl Subcommand {
450469
Subcommand::Fix { .. } => Kind::Fix,
451470
Subcommand::Format { .. } => Kind::Format,
452471
Subcommand::Test { .. } => Kind::Test,
472+
Subcommand::Miri { .. } => Kind::Miri,
453473
Subcommand::Clean { .. } => Kind::Clean,
454474
Subcommand::Dist { .. } => Kind::Dist,
455475
Subcommand::Install { .. } => Kind::Install,
@@ -461,7 +481,7 @@ impl Subcommand {
461481

462482
pub fn rustc_args(&self) -> Vec<&str> {
463483
match *self {
464-
Subcommand::Test { ref rustc_args, .. } => {
484+
Subcommand::Test { ref rustc_args, .. } | Subcommand::Miri { ref rustc_args, .. } => {
465485
rustc_args.iter().flat_map(|s| s.split_whitespace()).collect()
466486
}
467487
_ => vec![],
@@ -470,14 +490,16 @@ impl Subcommand {
470490

471491
pub fn fail_fast(&self) -> bool {
472492
match *self {
473-
Subcommand::Test { no_fail_fast, .. } => !no_fail_fast,
493+
Subcommand::Test { no_fail_fast, .. } | Subcommand::Miri { no_fail_fast, .. } => {
494+
!no_fail_fast
495+
}
474496
_ => false,
475497
}
476498
}
477499

478500
pub fn doc_tests(&self) -> DocTests {
479501
match *self {
480-
Subcommand::Test { doc, no_doc, .. } => {
502+
Subcommand::Test { doc, no_doc, .. } | Subcommand::Miri { no_doc, doc, .. } => {
481503
if doc {
482504
DocTests::Only
483505
} else if no_doc {

src/bootstrap/src/utils/render_tests.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,16 @@ impl<'a> Renderer<'a> {
231231
print!("\ntest result: ");
232232
self.builder.colored_stdout(|stdout| outcome.write_long(stdout)).unwrap();
233233
println!(
234-
". {} passed; {} failed; {} ignored; {} measured; {} filtered out; \
235-
finished in {:.2?}\n",
234+
". {} passed; {} failed; {} ignored; {} measured; {} filtered out{time}\n",
236235
suite.passed,
237236
suite.failed,
238237
suite.ignored,
239238
suite.measured,
240239
suite.filtered_out,
241-
Duration::from_secs_f64(suite.exec_time)
240+
time = match suite.exec_time {
241+
Some(t) => format!("; finished in {:.2?}", Duration::from_secs_f64(t)),
242+
None => format!(""),
243+
}
242244
);
243245
}
244246

@@ -374,7 +376,9 @@ struct SuiteOutcome {
374376
ignored: usize,
375377
measured: usize,
376378
filtered_out: usize,
377-
exec_time: f64,
379+
/// The time it took to execute this test suite, or `None` if time measurement was not possible
380+
/// (e.g. due to running inside Miri).
381+
exec_time: Option<f64>,
378382
}
379383

380384
#[derive(serde_derive::Deserialize)]

src/ci/docker/host-x86_64/x86_64-gnu-tools/checktools.sh

+5
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ case $HOST_TARGET in
6262
exit 1
6363
;;
6464
esac
65+
# Also smoke-test `x.py miri`. This doesn't run any actual tests (that would take too long),
66+
# but it ensures that the crates build properly when tested with Miri.
67+
python3 "$X_PY" miri --stage 2 library/core --test-args notest
68+
python3 "$X_PY" miri --stage 2 library/alloc --test-args notest
69+
python3 "$X_PY" miri --stage 2 library/std --test-args notest

0 commit comments

Comments
 (0)