|
| 1 | +use std::collections::{BTreeMap, HashMap}; |
| 2 | +use std::fs::File; |
| 3 | +use std::io::BufWriter; |
| 4 | +use std::path::{Path, PathBuf}; |
| 5 | + |
| 6 | +use askama::Template; |
| 7 | +use build_helper::metrics::{TestOutcome, TestSuiteMetadata}; |
| 8 | + |
| 9 | +use crate::jobs::JobDatabase; |
| 10 | +use crate::metrics::{JobMetrics, JobName, download_auto_job_metrics, get_test_suites}; |
| 11 | +use crate::utils::normalize_path_delimiters; |
| 12 | + |
| 13 | +pub struct TestInfo { |
| 14 | + name: String, |
| 15 | + jobs: Vec<JobTestResult>, |
| 16 | +} |
| 17 | + |
| 18 | +struct JobTestResult { |
| 19 | + job_name: String, |
| 20 | + outcome: TestOutcome, |
| 21 | +} |
| 22 | + |
| 23 | +#[derive(Default)] |
| 24 | +struct TestSuiteInfo { |
| 25 | + name: String, |
| 26 | + tests: BTreeMap<String, TestInfo>, |
| 27 | +} |
| 28 | + |
| 29 | +/// Generate a set of HTML files into a directory that contain a dashboard of test results. |
| 30 | +pub fn generate_test_dashboard( |
| 31 | + db: JobDatabase, |
| 32 | + current: &str, |
| 33 | + output_dir: &Path, |
| 34 | +) -> anyhow::Result<()> { |
| 35 | + let metrics = download_auto_job_metrics(&db, None, current)?; |
| 36 | + |
| 37 | + let suites = gather_test_suites(&metrics); |
| 38 | + |
| 39 | + std::fs::create_dir_all(output_dir)?; |
| 40 | + |
| 41 | + let test_count = suites.test_count(); |
| 42 | + write_page(output_dir, "index.html", &TestSuitesPage { suites, test_count })?; |
| 43 | + |
| 44 | + Ok(()) |
| 45 | +} |
| 46 | + |
| 47 | +fn write_page<T: Template>(dir: &Path, name: &str, template: &T) -> anyhow::Result<()> { |
| 48 | + let mut file = BufWriter::new(File::create(dir.join(name))?); |
| 49 | + Template::write_into(template, &mut file)?; |
| 50 | + Ok(()) |
| 51 | +} |
| 52 | + |
| 53 | +fn gather_test_suites(job_metrics: &HashMap<JobName, JobMetrics>) -> TestSuites { |
| 54 | + struct CoarseTestSuite<'a> { |
| 55 | + kind: TestSuiteKind, |
| 56 | + tests: BTreeMap<String, Test<'a>>, |
| 57 | + } |
| 58 | + |
| 59 | + let mut suites: HashMap<String, CoarseTestSuite> = HashMap::new(); |
| 60 | + |
| 61 | + // First, gather tests from all jobs, stages and targets, and aggregate them per suite |
| 62 | + for (job, metrics) in job_metrics { |
| 63 | + let test_suites = get_test_suites(&metrics.current); |
| 64 | + for suite in test_suites { |
| 65 | + let (suite_name, stage, target, kind) = match &suite.metadata { |
| 66 | + TestSuiteMetadata::CargoPackage { crates, stage, target, .. } => { |
| 67 | + (crates.join(","), *stage, target, TestSuiteKind::Cargo) |
| 68 | + } |
| 69 | + TestSuiteMetadata::Compiletest { suite, stage, target, .. } => { |
| 70 | + (suite.clone(), *stage, target, TestSuiteKind::Compiletest) |
| 71 | + } |
| 72 | + }; |
| 73 | + let suite_entry = suites |
| 74 | + .entry(suite_name.clone()) |
| 75 | + .or_insert_with(|| CoarseTestSuite { kind, tests: Default::default() }); |
| 76 | + let test_metadata = TestMetadata { job, stage, target }; |
| 77 | + |
| 78 | + for test in &suite.tests { |
| 79 | + let test_name = normalize_test_name(&test.name, &suite_name); |
| 80 | + let test_entry = suite_entry |
| 81 | + .tests |
| 82 | + .entry(test_name.clone()) |
| 83 | + .or_insert_with(|| Test { name: test_name, passed: vec![], ignored: vec![] }); |
| 84 | + match test.outcome { |
| 85 | + TestOutcome::Passed => { |
| 86 | + test_entry.passed.push(test_metadata); |
| 87 | + } |
| 88 | + TestOutcome::Ignored { ignore_reason: _ } => { |
| 89 | + test_entry.ignored.push(test_metadata); |
| 90 | + } |
| 91 | + TestOutcome::Failed => { |
| 92 | + eprintln!("Warning: failed test"); |
| 93 | + } |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // Then, split the suites per directory |
| 100 | + let mut suites = suites.into_iter().collect::<Vec<_>>(); |
| 101 | + suites.sort_by(|a, b| a.1.kind.cmp(&b.1.kind).then_with(|| a.0.cmp(&b.0))); |
| 102 | + |
| 103 | + let mut target_suites = vec![]; |
| 104 | + for (suite_name, suite) in suites { |
| 105 | + let suite = match suite.kind { |
| 106 | + TestSuiteKind::Compiletest => TestSuite { |
| 107 | + name: suite_name.clone(), |
| 108 | + kind: TestSuiteKind::Compiletest, |
| 109 | + group: build_test_group(&suite_name, suite.tests), |
| 110 | + }, |
| 111 | + TestSuiteKind::Cargo => { |
| 112 | + let mut tests: Vec<_> = suite.tests.into_iter().collect(); |
| 113 | + tests.sort_by(|a, b| a.0.cmp(&b.0)); |
| 114 | + TestSuite { |
| 115 | + name: format!("[cargo] {}", suite_name.clone()), |
| 116 | + kind: TestSuiteKind::Cargo, |
| 117 | + group: TestGroup { |
| 118 | + name: suite_name, |
| 119 | + root_tests: tests.into_iter().map(|t| t.1).collect(), |
| 120 | + groups: vec![], |
| 121 | + }, |
| 122 | + } |
| 123 | + } |
| 124 | + }; |
| 125 | + target_suites.push(suite); |
| 126 | + } |
| 127 | + |
| 128 | + TestSuites { suites: target_suites } |
| 129 | +} |
| 130 | + |
| 131 | +/// Recursively expand a test group based on filesystem hierarchy. |
| 132 | +fn build_test_group<'a>(name: &str, tests: BTreeMap<String, Test<'a>>) -> TestGroup<'a> { |
| 133 | + let mut root_tests = vec![]; |
| 134 | + let mut subdirs: BTreeMap<String, BTreeMap<String, Test<'a>>> = Default::default(); |
| 135 | + |
| 136 | + // Split tests into root tests and tests located in subdirectories |
| 137 | + for (name, test) in tests { |
| 138 | + let mut components = Path::new(&name).components().peekable(); |
| 139 | + let subdir = components.next().unwrap(); |
| 140 | + |
| 141 | + if components.peek().is_none() { |
| 142 | + // This is a root test |
| 143 | + root_tests.push(test); |
| 144 | + } else { |
| 145 | + // This is a test in a nested directory |
| 146 | + let subdir_tests = |
| 147 | + subdirs.entry(subdir.as_os_str().to_str().unwrap().to_string()).or_default(); |
| 148 | + let test_name = |
| 149 | + components.into_iter().collect::<PathBuf>().to_str().unwrap().to_string(); |
| 150 | + subdir_tests.insert(test_name, test); |
| 151 | + } |
| 152 | + } |
| 153 | + let dirs = subdirs |
| 154 | + .into_iter() |
| 155 | + .map(|(name, tests)| { |
| 156 | + let group = build_test_group(&name, tests); |
| 157 | + (name, group) |
| 158 | + }) |
| 159 | + .collect(); |
| 160 | + |
| 161 | + TestGroup { name: name.to_string(), root_tests, groups: dirs } |
| 162 | +} |
| 163 | + |
| 164 | +/// Compiletest tests start with `[suite] tests/[suite]/a/b/c...`. |
| 165 | +/// Remove the `[suite] tests/[suite]/` prefix so that we can find the filesystem path. |
| 166 | +/// Also normalizes path delimiters. |
| 167 | +fn normalize_test_name(name: &str, suite_name: &str) -> String { |
| 168 | + let name = normalize_path_delimiters(name); |
| 169 | + let name = name.as_ref(); |
| 170 | + let name = name.strip_prefix(&format!("[{suite_name}]")).unwrap_or(name).trim(); |
| 171 | + let name = name.strip_prefix("tests/").unwrap_or(name); |
| 172 | + let name = name.strip_prefix(suite_name).unwrap_or(name); |
| 173 | + name.trim_start_matches("/").to_string() |
| 174 | +} |
| 175 | + |
| 176 | +#[derive(serde::Serialize)] |
| 177 | +struct TestSuites<'a> { |
| 178 | + suites: Vec<TestSuite<'a>>, |
| 179 | +} |
| 180 | + |
| 181 | +impl<'a> TestSuites<'a> { |
| 182 | + fn test_count(&self) -> u64 { |
| 183 | + self.suites.iter().map(|suite| suite.group.test_count()).sum::<u64>() |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +#[derive(serde::Serialize)] |
| 188 | +struct TestSuite<'a> { |
| 189 | + name: String, |
| 190 | + kind: TestSuiteKind, |
| 191 | + group: TestGroup<'a>, |
| 192 | +} |
| 193 | + |
| 194 | +#[derive(Debug, serde::Serialize)] |
| 195 | +struct Test<'a> { |
| 196 | + name: String, |
| 197 | + passed: Vec<TestMetadata<'a>>, |
| 198 | + ignored: Vec<TestMetadata<'a>>, |
| 199 | +} |
| 200 | + |
| 201 | +#[derive(Clone, Copy, Debug, serde::Serialize)] |
| 202 | +struct TestMetadata<'a> { |
| 203 | + job: &'a str, |
| 204 | + stage: u32, |
| 205 | + target: &'a str, |
| 206 | +} |
| 207 | + |
| 208 | +// We have to use a template for the TestGroup instead of a macro, because |
| 209 | +// macros cannot be recursive in askama at the moment. |
| 210 | +#[derive(Template, serde::Serialize)] |
| 211 | +#[template(path = "test_group.askama")] |
| 212 | +/// Represents a group of tests |
| 213 | +struct TestGroup<'a> { |
| 214 | + name: String, |
| 215 | + /// Tests located directly in this directory |
| 216 | + root_tests: Vec<Test<'a>>, |
| 217 | + /// Nested directories with additional tests |
| 218 | + groups: Vec<(String, TestGroup<'a>)>, |
| 219 | +} |
| 220 | + |
| 221 | +impl<'a> TestGroup<'a> { |
| 222 | + fn test_count(&self) -> u64 { |
| 223 | + let root = self.root_tests.len() as u64; |
| 224 | + self.groups.iter().map(|(_, group)| group.test_count()).sum::<u64>() + root |
| 225 | + } |
| 226 | +} |
| 227 | + |
| 228 | +#[derive(PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] |
| 229 | +enum TestSuiteKind { |
| 230 | + Compiletest, |
| 231 | + Cargo, |
| 232 | +} |
| 233 | + |
| 234 | +#[derive(Template)] |
| 235 | +#[template(path = "test_suites.askama")] |
| 236 | +struct TestSuitesPage<'a> { |
| 237 | + suites: TestSuites<'a>, |
| 238 | + test_count: u64, |
| 239 | +} |
0 commit comments