blob: c9de38852e5a8f1e3248566e5bf05570cb818f15 [file] [log] [blame] [edit]
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use askama::Template;
use build_helper::metrics::{TestOutcome, TestSuiteMetadata};
use crate::jobs::JobDatabase;
use crate::metrics::{JobMetrics, JobName, download_auto_job_metrics, get_test_suites};
use crate::utils::normalize_path_delimiters;
/// Generate a set of HTML files into a directory that contain a dashboard of test results.
pub fn generate_test_dashboard(
db: JobDatabase,
current: &str,
output_dir: &Path,
) -> anyhow::Result<()> {
let metrics = download_auto_job_metrics(&db, None, current)?;
let suites = gather_test_suites(&metrics);
std::fs::create_dir_all(output_dir)?;
let test_count = suites.test_count();
write_page(output_dir, "index.html", &TestSuitesPage { suites, test_count })?;
Ok(())
}
fn write_page<T: Template>(dir: &Path, name: &str, template: &T) -> anyhow::Result<()> {
let mut file = BufWriter::new(File::create(dir.join(name))?);
Template::write_into(template, &mut file)?;
Ok(())
}
fn gather_test_suites(job_metrics: &HashMap<JobName, JobMetrics>) -> TestSuites<'_> {
struct CoarseTestSuite<'a> {
tests: BTreeMap<String, Test<'a>>,
}
let mut suites: HashMap<String, CoarseTestSuite> = HashMap::new();
// First, gather tests from all jobs, stages and targets, and aggregate them per suite
// Only work with compiletest suites.
for (job, metrics) in job_metrics {
let test_suites = get_test_suites(&metrics.current);
for suite in test_suites {
let (suite_name, stage, target) = match &suite.metadata {
TestSuiteMetadata::CargoPackage { .. } => {
continue;
}
TestSuiteMetadata::Compiletest { suite, stage, target, .. } => {
(suite.clone(), *stage, target)
}
};
let suite_entry = suites
.entry(suite_name.clone())
.or_insert_with(|| CoarseTestSuite { tests: Default::default() });
let test_metadata = TestMetadata { job, stage, target };
for test in &suite.tests {
let test_name = normalize_test_name(&test.name, &suite_name);
let (test_name, variant_name) = match test_name.rsplit_once('#') {
Some((name, variant)) => (name.to_string(), variant.to_string()),
None => (test_name, "".to_string()),
};
let test_entry = suite_entry
.tests
.entry(test_name.clone())
.or_insert_with(|| Test { revisions: Default::default() });
let variant_entry = test_entry
.revisions
.entry(variant_name)
.or_insert_with(|| TestResults { passed: vec![], ignored: vec![] });
match test.outcome {
TestOutcome::Passed => {
variant_entry.passed.push(test_metadata);
}
TestOutcome::Ignored { ignore_reason: _ } => {
variant_entry.ignored.push(test_metadata);
}
TestOutcome::Failed => {
eprintln!("Warning: failed test {test_name}");
}
}
}
}
}
// Then, split the suites per directory
let mut suites = suites.into_iter().collect::<Vec<_>>();
suites.sort_by(|a, b| a.0.cmp(&b.0));
let suites = suites
.into_iter()
.map(|(suite_name, suite)| TestSuite { group: build_test_group(&suite_name, suite.tests) })
.collect();
TestSuites { suites }
}
/// Recursively expand a test group based on filesystem hierarchy.
fn build_test_group<'a>(name: &str, tests: BTreeMap<String, Test<'a>>) -> TestGroup<'a> {
let mut root_tests = vec![];
let mut subdirs: BTreeMap<String, BTreeMap<String, Test<'a>>> = Default::default();
// Split tests into root tests and tests located in subdirectories
for (name, test) in tests {
let mut components = Path::new(&name).components().peekable();
let subdir = components.next().unwrap();
if components.peek().is_none() {
// This is a root test
root_tests.push((name, test));
} else {
// This is a test in a nested directory
let subdir_tests =
subdirs.entry(subdir.as_os_str().to_str().unwrap().to_string()).or_default();
let test_name =
components.into_iter().collect::<PathBuf>().to_str().unwrap().to_string();
subdir_tests.insert(test_name, test);
}
}
let dirs = subdirs
.into_iter()
.map(|(name, tests)| {
let group = build_test_group(&name, tests);
(name, group)
})
.collect();
TestGroup { name: name.to_string(), root_tests, groups: dirs }
}
/// Compiletest tests start with `[suite] tests/[suite]/a/b/c...`.
/// Remove the `[suite] tests/[suite]/` prefix so that we can find the filesystem path.
/// Also normalizes path delimiters.
fn normalize_test_name(name: &str, suite_name: &str) -> String {
let name = normalize_path_delimiters(name);
let name = name.as_ref();
let name = name.strip_prefix(&format!("[{suite_name}]")).unwrap_or(name).trim();
let name = name.strip_prefix("tests/").unwrap_or(name);
let name = name.strip_prefix(suite_name).unwrap_or(name);
name.trim_start_matches("/").to_string()
}
struct TestSuites<'a> {
suites: Vec<TestSuite<'a>>,
}
impl<'a> TestSuites<'a> {
fn test_count(&self) -> u64 {
self.suites.iter().map(|suite| suite.group.test_count()).sum::<u64>()
}
}
struct TestSuite<'a> {
group: TestGroup<'a>,
}
struct TestResults<'a> {
passed: Vec<TestMetadata<'a>>,
ignored: Vec<TestMetadata<'a>>,
}
struct Test<'a> {
revisions: BTreeMap<String, TestResults<'a>>,
}
impl<'a> Test<'a> {
/// If this is a test without revisions, it will have a single entry in `revisions` with
/// an empty string as the revision name.
fn single_test(&self) -> Option<&TestResults<'a>> {
if self.revisions.len() == 1 {
self.revisions.iter().next().take_if(|e| e.0.is_empty()).map(|e| e.1)
} else {
None
}
}
}
#[derive(Clone, Copy)]
#[allow(dead_code)]
struct TestMetadata<'a> {
job: &'a str,
stage: u32,
target: &'a str,
}
// We have to use a template for the TestGroup instead of a macro, because
// macros cannot be recursive in askama at the moment.
#[derive(Template)]
#[template(path = "test_group.askama")]
/// Represents a group of tests
struct TestGroup<'a> {
name: String,
/// Tests located directly in this directory
root_tests: Vec<(String, Test<'a>)>,
/// Nested directories with additional tests
groups: Vec<(String, TestGroup<'a>)>,
}
impl<'a> TestGroup<'a> {
fn test_count(&self) -> u64 {
let root = self.root_tests.len() as u64;
self.groups.iter().map(|(_, group)| group.test_count()).sum::<u64>() + root
}
}
#[derive(Template)]
#[template(path = "test_suites.askama")]
struct TestSuitesPage<'a> {
suites: TestSuites<'a>,
test_count: u64,
}