Add lightweight snapshot testing for bootstrap tests

This commit is contained in:
Jakub Beránek 2025-06-12 19:42:03 +02:00
parent 983fe4f265
commit 6feb9b792c
No known key found for this signature in database
GPG key ID: 909CD0D26483516B
2 changed files with 107 additions and 10 deletions

View file

@ -1231,3 +1231,81 @@ fn any_debug() {
// Downcasting to the underlying type should succeed.
assert_eq!(x.downcast_ref::<MyStruct>(), Some(&MyStruct { x: 7 }));
}
/// The staging tests use insta for snapshot testing.
/// See bootstrap's README on how to bless the snapshots.
mod staging {
use crate::core::builder::tests::{
TEST_TRIPLE_1, configure, configure_with_args, render_steps, run_build,
};
#[test]
fn build_compiler_stage_1() {
let mut cache = run_build(
&["compiler".into()],
configure_with_args(&["build", "--stage", "1"], &[TEST_TRIPLE_1], &[TEST_TRIPLE_1]),
);
let steps = cache.into_executed_steps();
insta::assert_snapshot!(render_steps(&steps), @r"
[build] rustc 0 <target1> -> std 0 <target1>
[build] llvm <target1>
[build] rustc 0 <target1> -> rustc 1 <target1>
[build] rustc 0 <target1> -> rustc 1 <target1>
");
}
}
/// Renders the executed bootstrap steps for usage in snapshot tests with insta.
/// Only renders certain important steps.
/// Each value in `steps` should be a tuple of (Step, step output).
fn render_steps(steps: &[(Box<dyn Any>, Box<dyn Any>)]) -> String {
steps
.iter()
.filter_map(|(step, output)| {
// FIXME: implement an optional method on Step to produce metadata for test, instead
// of this downcasting
if let Some((rustc, output)) = downcast_step::<compile::Rustc>(step, output) {
Some(format!(
"[build] {} -> {}",
render_compiler(rustc.build_compiler),
// FIXME: return the correct stage from the `Rustc` step, now it behaves weirdly
render_compiler(Compiler::new(rustc.build_compiler.stage + 1, rustc.target)),
))
} else if let Some((std, output)) = downcast_step::<compile::Std>(step, output) {
Some(format!(
"[build] {} -> std {} <{}>",
render_compiler(std.compiler),
std.compiler.stage,
std.target
))
} else if let Some((llvm, output)) = downcast_step::<llvm::Llvm>(step, output) {
Some(format!("[build] llvm <{}>", llvm.target))
} else {
None
}
})
.map(|line| {
line.replace(TEST_TRIPLE_1, "target1")
.replace(TEST_TRIPLE_2, "target2")
.replace(TEST_TRIPLE_3, "target3")
})
.collect::<Vec<_>>()
.join("\n")
}
fn downcast_step<'a, S: Step>(
step: &'a Box<dyn Any>,
output: &'a Box<dyn Any>,
) -> Option<(&'a S, &'a S::Output)> {
let Some(step) = step.downcast_ref::<S>() else {
return None;
};
let Some(output) = output.downcast_ref::<S::Output>() else {
return None;
};
Some((step, output))
}
fn render_compiler(compiler: Compiler) -> String {
format!("rustc {} <{}>", compiler.stage, compiler.host)
}

View file

@ -17,6 +17,7 @@ use std::borrow::Borrow;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ops::Deref;
@ -208,25 +209,30 @@ pub static INTERNER: LazyLock<Interner> = LazyLock::new(Interner::default);
/// any type in its output. It is a write-once cache; values are never evicted,
/// which means that references to the value can safely be returned from the
/// `get()` method.
#[derive(Debug)]
pub struct Cache(
RefCell<
#[derive(Debug, Default)]
pub struct Cache {
cache: RefCell<
HashMap<
TypeId,
Box<dyn Any>, // actually a HashMap<Step, Interned<Step::Output>>
>,
>,
);
#[cfg(test)]
/// Contains steps in the same order in which they were executed
/// Useful for tests
/// Tuples (step, step output)
executed_steps: RefCell<Vec<(Box<dyn Any>, Box<dyn Any>)>>,
}
impl Cache {
/// Creates a new empty cache.
pub fn new() -> Cache {
Cache(RefCell::new(HashMap::new()))
Cache::default()
}
/// Stores the result of a computation step in the cache.
pub fn put<S: Step>(&self, step: S, value: S::Output) {
let mut cache = self.0.borrow_mut();
let mut cache = self.cache.borrow_mut();
let type_id = TypeId::of::<S>();
let stepcache = cache
.entry(type_id)
@ -234,12 +240,20 @@ impl Cache {
.downcast_mut::<HashMap<S, S::Output>>()
.expect("invalid type mapped");
assert!(!stepcache.contains_key(&step), "processing {step:?} a second time");
#[cfg(test)]
{
let step: Box<dyn Any> = Box::new(step.clone());
let output: Box<dyn Any> = Box::new(value.clone());
self.executed_steps.borrow_mut().push((step, output));
}
stepcache.insert(step, value);
}
/// Retrieves a cached result for the given step, if available.
pub fn get<S: Step>(&self, step: &S) -> Option<S::Output> {
let mut cache = self.0.borrow_mut();
let mut cache = self.cache.borrow_mut();
let type_id = TypeId::of::<S>();
let stepcache = cache
.entry(type_id)
@ -252,8 +266,8 @@ impl Cache {
#[cfg(test)]
impl Cache {
pub fn all<S: Ord + Clone + Step>(&mut self) -> Vec<(S, S::Output)> {
let cache = self.0.get_mut();
pub fn all<S: Ord + Step>(&mut self) -> Vec<(S, S::Output)> {
let cache = self.cache.get_mut();
let type_id = TypeId::of::<S>();
let mut v = cache
.remove(&type_id)
@ -265,7 +279,12 @@ impl Cache {
}
pub fn contains<S: Step>(&self) -> bool {
self.0.borrow().contains_key(&TypeId::of::<S>())
self.cache.borrow().contains_key(&TypeId::of::<S>())
}
#[cfg(test)]
pub fn into_executed_steps(mut self) -> Vec<(Box<dyn Any>, Box<dyn Any>)> {
mem::take(&mut self.executed_steps.borrow_mut())
}
}