Skip to content

Commit d6a465b

Browse files
authored
Unrolled build for #142431
Rollup merge of #142431 - Kobzol:bootstrap-snapshot-tests, r=jieyouxu Add initial version of snapshot tests to bootstrap When making any changes to bootstrap (steps), it is very difficult to realize how does it affect various common bootstrap commands, and if everything still works as we expect it to. We are far away from having actual end-to-end tests, but what we could at least do is have a way of testing what steps does bootstrap execute in dry run mode. Now, we already have something like this in `src/bootstrap/src/core/builder/tests.rs`, however that is quite limited, because it only checks executed steps for a specific impl of `Step` and it does not consider step order. Recently, when working on what I thought was one of the simplest possible step untanglings in bootstrap (#142357), I ran into errors in tests that were quite hard to debug. Partly also because the current staging test diffs are multiline and use `Debug` output, so it's quite difficult for me to make sense of them. In this PR, I introduce `insta`, which allows writing snapshot tests in a very simple way. With it, I want to allow writing tests that will clearly show us what is going on during bootstrap execution, and then write golden tests for `build/check/test` stage `0/1/2` for compiler/std/tools etc., to make sure that we don't regress something, and also to help with [#t-infra/bootstrap > Proposal to cleanup stages and steps after the redesign](https://rust-lang.zulipchat.com/#narrow/channel/326414-t-infra.2Fbootstrap/topic/Proposal.20to.20cleanup.20stages.20and.20steps.20after.20the.20redesign/with/523488806), to help avoid a situation where we would (again) have to make a flurry of staging changes because of unexpected consequences. In the snapshot tests, we currently render the build of rustc, std and LLVM. Currently I render the executed steps using downcasting, which is not super pretty, but it allows us to make the test rendering localized in one place, and it's IMO enough for now. I implemented only a single test using the new machinery. Maybe if you take a look at it, you will understand why 😆 Bootstrap currently does some peculiar things, such as running a stage 0 std step (even though stage 0 std no longer exists) and running the Rustc stage 0 -> 1 step twice, once with a single crates, once with all rustc crates. So I think that even with this single step, there will be a bunch of things to fix in the near future... The way we currently prepare the Config test fixtures is far from ideal, this is something I think ``@Shourya742`` could work on as a part of their GSoC project (remove as much command execution from Config construction as possible, actually run bootstrap on a temporary directory instead of running it on the rustc checkout, create a Builder-like API for creating the Config test fixtures). r? ``@jieyouxu``
2 parents d9ca9bd + 6feb9b7 commit d6a465b

File tree

6 files changed

+158
-20
lines changed

6 files changed

+158
-20
lines changed

src/bootstrap/Cargo.lock

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies = [
4444
"fd-lock",
4545
"home",
4646
"ignore",
47+
"insta",
4748
"junction",
4849
"libc",
4950
"object",
@@ -158,6 +159,18 @@ dependencies = [
158159
"cc",
159160
]
160161

162+
[[package]]
163+
name = "console"
164+
version = "0.15.11"
165+
source = "registry+https://github.com/rust-lang/crates.io-index"
166+
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
167+
dependencies = [
168+
"encode_unicode",
169+
"libc",
170+
"once_cell",
171+
"windows-sys 0.59.0",
172+
]
173+
161174
[[package]]
162175
name = "cpufeatures"
163176
version = "0.2.15"
@@ -218,6 +231,12 @@ dependencies = [
218231
"crypto-common",
219232
]
220233

234+
[[package]]
235+
name = "encode_unicode"
236+
version = "1.0.0"
237+
source = "registry+https://github.com/rust-lang/crates.io-index"
238+
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
239+
221240
[[package]]
222241
name = "errno"
223242
version = "0.3.11"
@@ -323,6 +342,17 @@ dependencies = [
323342
"winapi-util",
324343
]
325344

345+
[[package]]
346+
name = "insta"
347+
version = "1.43.1"
348+
source = "registry+https://github.com/rust-lang/crates.io-index"
349+
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
350+
dependencies = [
351+
"console",
352+
"once_cell",
353+
"similar",
354+
]
355+
326356
[[package]]
327357
name = "itoa"
328358
version = "1.0.11"
@@ -675,6 +705,12 @@ version = "1.3.0"
675705
source = "registry+https://github.com/rust-lang/crates.io-index"
676706
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
677707

708+
[[package]]
709+
name = "similar"
710+
version = "2.7.0"
711+
source = "registry+https://github.com/rust-lang/crates.io-index"
712+
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
713+
678714
[[package]]
679715
name = "smallvec"
680716
version = "1.13.2"

src/bootstrap/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ features = [
8484
[dev-dependencies]
8585
pretty_assertions = "1.4"
8686
tempfile = "3.15.0"
87+
insta = "1.43"
8788

8889
# We care a lot about bootstrap's compile times, so don't include debuginfo for
8990
# dependencies, only bootstrap itself.

src/bootstrap/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ please file issues on the [Rust issue tracker][rust-issue-tracker].
200200
[rust-bootstrap-zulip]: https://rust-lang.zulipchat.com/#narrow/stream/t-infra.2Fbootstrap
201201
[rust-issue-tracker]: https://github.com/rust-lang/rust/issues
202202

203+
## Testing
204+
205+
To run bootstrap tests, execute `x test bootstrap`. If you want to bless snapshot tests, then install `cargo-insta` (`cargo install cargo-insta`) and then run `cargo insta review --manifest-path src/bootstrap/Cargo.toml`.
206+
203207
## Changelog
204208

205209
Because we do not release bootstrap with versions, we also do not maintain CHANGELOG files. To

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3059,6 +3059,8 @@ impl Step for Bootstrap {
30593059
cargo
30603060
.rustflag("-Cdebuginfo=2")
30613061
.env("CARGO_TARGET_DIR", builder.out.join("bootstrap"))
3062+
// Needed for insta to correctly write pending snapshots to the right directories.
3063+
.env("INSTA_WORKSPACE_ROOT", &builder.src)
30623064
.env("RUSTC_BOOTSTRAP", "1");
30633065

30643066
// bootstrap tests are racy on directory creation so just run them one at a time.

src/bootstrap/src/core/builder/tests.rs

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ static TEST_TRIPLE_2: &str = "i686-unknown-hurd-gnu";
1515
static TEST_TRIPLE_3: &str = "i686-unknown-netbsd";
1616

1717
fn configure(cmd: &str, host: &[&str], target: &[&str]) -> Config {
18-
configure_with_args(&[cmd.to_owned()], host, target)
18+
configure_with_args(&[cmd], host, target)
1919
}
2020

21-
fn configure_with_args(cmd: &[String], host: &[&str], target: &[&str]) -> Config {
22-
let mut config = Config::parse(Flags::parse(cmd));
21+
fn configure_with_args(cmd: &[&str], host: &[&str], target: &[&str]) -> Config {
22+
let cmd = cmd.iter().copied().map(String::from).collect::<Vec<_>>();
23+
let mut config = Config::parse(Flags::parse(&cmd));
2324
// don't save toolstates
2425
config.save_toolstates = None;
2526
config.set_dry_run(DryRun::SelfCheck);
@@ -67,7 +68,7 @@ fn run_build(paths: &[PathBuf], config: Config) -> Cache {
6768
fn check_cli<const N: usize>(paths: [&str; N]) {
6869
run_build(
6970
&paths.map(PathBuf::from),
70-
configure_with_args(&paths.map(String::from), &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]),
71+
configure_with_args(&paths, &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]),
7172
);
7273
}
7374

@@ -1000,8 +1001,7 @@ mod sysroot_target_dirs {
10001001
/// cg_gcc tests instead.
10011002
#[test]
10021003
fn test_test_compiler() {
1003-
let cmd = &["test", "compiler"].map(str::to_owned);
1004-
let config = configure_with_args(cmd, &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]);
1004+
let config = configure_with_args(&["test", "compiler"], &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]);
10051005
let cache = run_build(&config.paths.clone(), config);
10061006

10071007
let compiler = cache.contains::<test::CrateLibrustc>();
@@ -1034,8 +1034,7 @@ fn test_test_coverage() {
10341034
// Print each test case so that if one fails, the most recently printed
10351035
// case is the one that failed.
10361036
println!("Testing case: {cmd:?}");
1037-
let cmd = cmd.iter().copied().map(str::to_owned).collect::<Vec<_>>();
1038-
let config = configure_with_args(&cmd, &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]);
1037+
let config = configure_with_args(cmd, &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]);
10391038
let mut cache = run_build(&config.paths.clone(), config);
10401039

10411040
let modes =
@@ -1207,8 +1206,7 @@ fn test_get_tool_rustc_compiler() {
12071206
/// of `Any { .. }`.
12081207
#[test]
12091208
fn step_cycle_debug() {
1210-
let cmd = ["run", "cyclic-step"].map(str::to_owned);
1211-
let config = configure_with_args(&cmd, &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]);
1209+
let config = configure_with_args(&["run", "cyclic-step"], &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]);
12121210

12131211
let err = panic::catch_unwind(|| run_build(&config.paths.clone(), config)).unwrap_err();
12141212
let err = err.downcast_ref::<String>().unwrap().as_str();
@@ -1233,3 +1231,81 @@ fn any_debug() {
12331231
// Downcasting to the underlying type should succeed.
12341232
assert_eq!(x.downcast_ref::<MyStruct>(), Some(&MyStruct { x: 7 }));
12351233
}
1234+
1235+
/// The staging tests use insta for snapshot testing.
1236+
/// See bootstrap's README on how to bless the snapshots.
1237+
mod staging {
1238+
use crate::core::builder::tests::{
1239+
TEST_TRIPLE_1, configure, configure_with_args, render_steps, run_build,
1240+
};
1241+
1242+
#[test]
1243+
fn build_compiler_stage_1() {
1244+
let mut cache = run_build(
1245+
&["compiler".into()],
1246+
configure_with_args(&["build", "--stage", "1"], &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]),
1247+
);
1248+
let steps = cache.into_executed_steps();
1249+
insta::assert_snapshot!(render_steps(&steps), @r"
1250+
[build] rustc 0 <target1> -> std 0 <target1>
1251+
[build] llvm <target1>
1252+
[build] rustc 0 <target1> -> rustc 1 <target1>
1253+
[build] rustc 0 <target1> -> rustc 1 <target1>
1254+
");
1255+
}
1256+
}
1257+
1258+
/// Renders the executed bootstrap steps for usage in snapshot tests with insta.
1259+
/// Only renders certain important steps.
1260+
/// Each value in `steps` should be a tuple of (Step, step output).
1261+
fn render_steps(steps: &[(Box<dyn Any>, Box<dyn Any>)]) -> String {
1262+
steps
1263+
.iter()
1264+
.filter_map(|(step, output)| {
1265+
// FIXME: implement an optional method on Step to produce metadata for test, instead
1266+
// of this downcasting
1267+
if let Some((rustc, output)) = downcast_step::<compile::Rustc>(step, output) {
1268+
Some(format!(
1269+
"[build] {} -> {}",
1270+
render_compiler(rustc.build_compiler),
1271+
// FIXME: return the correct stage from the `Rustc` step, now it behaves weirdly
1272+
render_compiler(Compiler::new(rustc.build_compiler.stage + 1, rustc.target)),
1273+
))
1274+
} else if let Some((std, output)) = downcast_step::<compile::Std>(step, output) {
1275+
Some(format!(
1276+
"[build] {} -> std {} <{}>",
1277+
render_compiler(std.compiler),
1278+
std.compiler.stage,
1279+
std.target
1280+
))
1281+
} else if let Some((llvm, output)) = downcast_step::<llvm::Llvm>(step, output) {
1282+
Some(format!("[build] llvm <{}>", llvm.target))
1283+
} else {
1284+
None
1285+
}
1286+
})
1287+
.map(|line| {
1288+
line.replace(TEST_TRIPLE_1, "target1")
1289+
.replace(TEST_TRIPLE_2, "target2")
1290+
.replace(TEST_TRIPLE_3, "target3")
1291+
})
1292+
.collect::<Vec<_>>()
1293+
.join("\n")
1294+
}
1295+
1296+
fn downcast_step<'a, S: Step>(
1297+
step: &'a Box<dyn Any>,
1298+
output: &'a Box<dyn Any>,
1299+
) -> Option<(&'a S, &'a S::Output)> {
1300+
let Some(step) = step.downcast_ref::<S>() else {
1301+
return None;
1302+
};
1303+
let Some(output) = output.downcast_ref::<S::Output>() else {
1304+
return None;
1305+
};
1306+
Some((step, output))
1307+
}
1308+
1309+
fn render_compiler(compiler: Compiler) -> String {
1310+
format!("rustc {} <{}>", compiler.stage, compiler.host)
1311+
}

src/bootstrap/src/utils/cache.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use std::borrow::Borrow;
1717
use std::cell::RefCell;
1818
use std::cmp::Ordering;
1919
use std::collections::HashMap;
20+
use std::fmt::Debug;
2021
use std::hash::{Hash, Hasher};
2122
use std::marker::PhantomData;
2223
use std::ops::Deref;
@@ -208,38 +209,51 @@ pub static INTERNER: LazyLock<Interner> = LazyLock::new(Interner::default);
208209
/// any type in its output. It is a write-once cache; values are never evicted,
209210
/// which means that references to the value can safely be returned from the
210211
/// `get()` method.
211-
#[derive(Debug)]
212-
pub struct Cache(
213-
RefCell<
212+
#[derive(Debug, Default)]
213+
pub struct Cache {
214+
cache: RefCell<
214215
HashMap<
215216
TypeId,
216217
Box<dyn Any>, // actually a HashMap<Step, Interned<Step::Output>>
217218
>,
218219
>,
219-
);
220+
#[cfg(test)]
221+
/// Contains steps in the same order in which they were executed
222+
/// Useful for tests
223+
/// Tuples (step, step output)
224+
executed_steps: RefCell<Vec<(Box<dyn Any>, Box<dyn Any>)>>,
225+
}
220226

221227
impl Cache {
222228
/// Creates a new empty cache.
223229
pub fn new() -> Cache {
224-
Cache(RefCell::new(HashMap::new()))
230+
Cache::default()
225231
}
226232

227233
/// Stores the result of a computation step in the cache.
228234
pub fn put<S: Step>(&self, step: S, value: S::Output) {
229-
let mut cache = self.0.borrow_mut();
235+
let mut cache = self.cache.borrow_mut();
230236
let type_id = TypeId::of::<S>();
231237
let stepcache = cache
232238
.entry(type_id)
233239
.or_insert_with(|| Box::<HashMap<S, S::Output>>::default())
234240
.downcast_mut::<HashMap<S, S::Output>>()
235241
.expect("invalid type mapped");
236242
assert!(!stepcache.contains_key(&step), "processing {step:?} a second time");
243+
244+
#[cfg(test)]
245+
{
246+
let step: Box<dyn Any> = Box::new(step.clone());
247+
let output: Box<dyn Any> = Box::new(value.clone());
248+
self.executed_steps.borrow_mut().push((step, output));
249+
}
250+
237251
stepcache.insert(step, value);
238252
}
239253

240254
/// Retrieves a cached result for the given step, if available.
241255
pub fn get<S: Step>(&self, step: &S) -> Option<S::Output> {
242-
let mut cache = self.0.borrow_mut();
256+
let mut cache = self.cache.borrow_mut();
243257
let type_id = TypeId::of::<S>();
244258
let stepcache = cache
245259
.entry(type_id)
@@ -252,8 +266,8 @@ impl Cache {
252266

253267
#[cfg(test)]
254268
impl Cache {
255-
pub fn all<S: Ord + Clone + Step>(&mut self) -> Vec<(S, S::Output)> {
256-
let cache = self.0.get_mut();
269+
pub fn all<S: Ord + Step>(&mut self) -> Vec<(S, S::Output)> {
270+
let cache = self.cache.get_mut();
257271
let type_id = TypeId::of::<S>();
258272
let mut v = cache
259273
.remove(&type_id)
@@ -265,7 +279,12 @@ impl Cache {
265279
}
266280

267281
pub fn contains<S: Step>(&self) -> bool {
268-
self.0.borrow().contains_key(&TypeId::of::<S>())
282+
self.cache.borrow().contains_key(&TypeId::of::<S>())
283+
}
284+
285+
#[cfg(test)]
286+
pub fn into_executed_steps(mut self) -> Vec<(Box<dyn Any>, Box<dyn Any>)> {
287+
mem::take(&mut self.executed_steps.borrow_mut())
269288
}
270289
}
271290

0 commit comments

Comments
 (0)