Add command to citool
for generating a test dashboard
This commit is contained in:
parent
111c15c48e
commit
c8a882b7b5
7 changed files with 433 additions and 1 deletions
|
@ -64,12 +64,63 @@ version = "1.0.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7"
|
||||||
|
dependencies = [
|
||||||
|
"askama_derive",
|
||||||
|
"itoa",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_derive"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac"
|
||||||
|
dependencies = [
|
||||||
|
"askama_parser",
|
||||||
|
"basic-toml",
|
||||||
|
"memchr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_parser"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "basic-toml"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "build_helper"
|
name = "build_helper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -104,6 +155,7 @@ name = "citool"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askama",
|
||||||
"build_helper",
|
"build_helper",
|
||||||
"clap",
|
"clap",
|
||||||
"csv",
|
"csv",
|
||||||
|
@ -646,6 +698,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.23"
|
version = "0.23.23"
|
||||||
|
@ -1026,6 +1084,15 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
|
@ -5,6 +5,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
askama = "0.13"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
csv = "1"
|
csv = "1"
|
||||||
diff = "0.1"
|
diff = "0.1"
|
||||||
|
|
|
@ -4,6 +4,7 @@ mod datadog;
|
||||||
mod github;
|
mod github;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
|
mod test_dashboard;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
@ -22,6 +23,7 @@ use crate::datadog::upload_datadog_metric;
|
||||||
use crate::github::JobInfoResolver;
|
use crate::github::JobInfoResolver;
|
||||||
use crate::jobs::RunType;
|
use crate::jobs::RunType;
|
||||||
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
|
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
|
||||||
|
use crate::test_dashboard::generate_test_dashboard;
|
||||||
use crate::utils::load_env_var;
|
use crate::utils::load_env_var;
|
||||||
|
|
||||||
const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
|
const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
|
||||||
|
@ -234,6 +236,14 @@ enum Args {
|
||||||
/// Current commit that will be compared to `parent`.
|
/// Current commit that will be compared to `parent`.
|
||||||
current: String,
|
current: String,
|
||||||
},
|
},
|
||||||
|
/// Generate a directory containing a HTML dashboard of test results from a CI run.
|
||||||
|
TestDashboard {
|
||||||
|
/// Commit SHA that was tested on CI to analyze.
|
||||||
|
current: String,
|
||||||
|
/// Output path for the HTML directory.
|
||||||
|
#[clap(long)]
|
||||||
|
output_dir: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Clone)]
|
#[derive(clap::ValueEnum, Clone)]
|
||||||
|
@ -275,7 +285,11 @@ fn main() -> anyhow::Result<()> {
|
||||||
postprocess_metrics(metrics_path, parent, job_name)?;
|
postprocess_metrics(metrics_path, parent, job_name)?;
|
||||||
}
|
}
|
||||||
Args::PostMergeReport { current, parent } => {
|
Args::PostMergeReport { current, parent } => {
|
||||||
post_merge_report(load_db(default_jobs_file)?, current, parent)?;
|
post_merge_report(load_db(&default_jobs_file)?, current, parent)?;
|
||||||
|
}
|
||||||
|
Args::TestDashboard { current, output_dir } => {
|
||||||
|
let db = load_db(&default_jobs_file)?;
|
||||||
|
generate_test_dashboard(db, ¤t, &output_dir)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
239
src/ci/citool/src/test_dashboard/mod.rs
Normal file
239
src/ci/citool/src/test_dashboard/mod.rs
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
pub struct TestInfo {
|
||||||
|
name: String,
|
||||||
|
jobs: Vec<JobTestResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct JobTestResult {
|
||||||
|
job_name: String,
|
||||||
|
outcome: TestOutcome,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestSuiteInfo {
|
||||||
|
name: String,
|
||||||
|
tests: BTreeMap<String, TestInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
kind: TestSuiteKind,
|
||||||
|
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
|
||||||
|
for (job, metrics) in job_metrics {
|
||||||
|
let test_suites = get_test_suites(&metrics.current);
|
||||||
|
for suite in test_suites {
|
||||||
|
let (suite_name, stage, target, kind) = match &suite.metadata {
|
||||||
|
TestSuiteMetadata::CargoPackage { crates, stage, target, .. } => {
|
||||||
|
(crates.join(","), *stage, target, TestSuiteKind::Cargo)
|
||||||
|
}
|
||||||
|
TestSuiteMetadata::Compiletest { suite, stage, target, .. } => {
|
||||||
|
(suite.clone(), *stage, target, TestSuiteKind::Compiletest)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let suite_entry = suites
|
||||||
|
.entry(suite_name.clone())
|
||||||
|
.or_insert_with(|| CoarseTestSuite { kind, 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_entry = suite_entry
|
||||||
|
.tests
|
||||||
|
.entry(test_name.clone())
|
||||||
|
.or_insert_with(|| Test { name: test_name, passed: vec![], ignored: vec![] });
|
||||||
|
match test.outcome {
|
||||||
|
TestOutcome::Passed => {
|
||||||
|
test_entry.passed.push(test_metadata);
|
||||||
|
}
|
||||||
|
TestOutcome::Ignored { ignore_reason: _ } => {
|
||||||
|
test_entry.ignored.push(test_metadata);
|
||||||
|
}
|
||||||
|
TestOutcome::Failed => {
|
||||||
|
eprintln!("Warning: failed test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, split the suites per directory
|
||||||
|
let mut suites = suites.into_iter().collect::<Vec<_>>();
|
||||||
|
suites.sort_by(|a, b| a.1.kind.cmp(&b.1.kind).then_with(|| a.0.cmp(&b.0)));
|
||||||
|
|
||||||
|
let mut target_suites = vec![];
|
||||||
|
for (suite_name, suite) in suites {
|
||||||
|
let suite = match suite.kind {
|
||||||
|
TestSuiteKind::Compiletest => TestSuite {
|
||||||
|
name: suite_name.clone(),
|
||||||
|
kind: TestSuiteKind::Compiletest,
|
||||||
|
group: build_test_group(&suite_name, suite.tests),
|
||||||
|
},
|
||||||
|
TestSuiteKind::Cargo => {
|
||||||
|
let mut tests: Vec<_> = suite.tests.into_iter().collect();
|
||||||
|
tests.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
TestSuite {
|
||||||
|
name: format!("[cargo] {}", suite_name.clone()),
|
||||||
|
kind: TestSuiteKind::Cargo,
|
||||||
|
group: TestGroup {
|
||||||
|
name: suite_name,
|
||||||
|
root_tests: tests.into_iter().map(|t| t.1).collect(),
|
||||||
|
groups: vec![],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
target_suites.push(suite);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestSuites { suites: target_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(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
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>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct TestSuite<'a> {
|
||||||
|
name: String,
|
||||||
|
kind: TestSuiteKind,
|
||||||
|
group: TestGroup<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
struct Test<'a> {
|
||||||
|
name: String,
|
||||||
|
passed: Vec<TestMetadata<'a>>,
|
||||||
|
ignored: Vec<TestMetadata<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, serde::Serialize)]
|
||||||
|
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, serde::Serialize)]
|
||||||
|
#[template(path = "test_group.askama")]
|
||||||
|
/// Represents a group of tests
|
||||||
|
struct TestGroup<'a> {
|
||||||
|
name: String,
|
||||||
|
/// Tests located directly in this directory
|
||||||
|
root_tests: Vec<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(PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
|
||||||
|
enum TestSuiteKind {
|
||||||
|
Compiletest,
|
||||||
|
Cargo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "test_suites.askama")]
|
||||||
|
struct TestSuitesPage<'a> {
|
||||||
|
suites: TestSuites<'a>,
|
||||||
|
test_count: u64,
|
||||||
|
}
|
71
src/ci/citool/templates/layout.askama
Normal file
71
src/ci/citool/templates/layout.askama
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rust CI Test Dashboard</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 1500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-suites {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #F4F4F4;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
summary:hover {
|
||||||
|
background-color: #CFCFCF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the disclosure triangles */
|
||||||
|
details > summary {
|
||||||
|
list-style: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
details > summary::before {
|
||||||
|
content: "▶";
|
||||||
|
position: absolute;
|
||||||
|
left: -15px;
|
||||||
|
transform: rotate(0);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
22
src/ci/citool/templates/test_group.askama
Normal file
22
src/ci/citool/templates/test_group.askama
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary>{{ name }} ({{ test_count() }} test{{ test_count() | pluralize }})</summary>
|
||||||
|
|
||||||
|
{% if !groups.is_empty() %}
|
||||||
|
<ul>
|
||||||
|
{% for (dir_name, subgroup) in groups %}
|
||||||
|
{{ subgroup|safe }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if !root_tests.is_empty() %}
|
||||||
|
<ul>
|
||||||
|
{% for test in root_tests %}
|
||||||
|
<li><b>{{ test.name }}</b> ({{ test.passed.len() }} passed, {{ test.ignored.len() }} ignored)</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</li>
|
18
src/ci/citool/templates/test_suites.askama
Normal file
18
src/ci/citool/templates/test_suites.askama
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "layout.askama" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Rust CI Test Dashboard</h1>
|
||||||
|
<div class="test-suites">
|
||||||
|
<div class="summary">
|
||||||
|
Total tests: {{ test_count }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for suite in suites.suites %}
|
||||||
|
{% if suite.kind == TestSuiteKind::Compiletest %}
|
||||||
|
{{ suite.group|safe }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue