Postprocess test suite metrics into GitHub summary
This commit is contained in:
parent
fd17deacce
commit
6463590f0c
5 changed files with 181 additions and 0 deletions
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
|
@ -182,6 +182,13 @@ jobs:
|
|||
- name: show the current environment
|
||||
run: src/ci/scripts/dump-environment.sh
|
||||
|
||||
# Pre-build citool before the following step uninstalls rustup
|
||||
# Build is into the build directory, to avoid modifying sources
|
||||
- name: build citool
|
||||
run: |
|
||||
cd src/ci/citool
|
||||
CARGO_TARGET_DIR=../../../build/citool cargo build
|
||||
|
||||
- name: run the build
|
||||
# Redirect stderr to stdout to avoid reordering the two streams in the GHA logs.
|
||||
run: src/ci/scripts/run-build-from-ci.sh 2>&1
|
||||
|
@ -218,6 +225,16 @@ jobs:
|
|||
# erroring about invalid credentials instead.
|
||||
if: github.event_name == 'push' || env.DEPLOY == '1' || env.DEPLOY_ALT == '1'
|
||||
|
||||
- name: postprocess metrics into the summary
|
||||
run: |
|
||||
if [ -f build/metrics.json ]; then
|
||||
./build/citool/debug/citool postprocess-metrics build/metrics.json ${GITHUB_STEP_SUMMARY}
|
||||
elif [ -f obj/build/metrics.json ]; then
|
||||
./build/citool/debug/citool postprocess-metrics obj/build/metrics.json ${GITHUB_STEP_SUMMARY}
|
||||
else
|
||||
echo "No metrics.json found"
|
||||
fi
|
||||
|
||||
- name: upload job metrics to DataDog
|
||||
if: needs.calculate_matrix.outputs.run_type != 'pr'
|
||||
env:
|
||||
|
|
|
@ -58,11 +58,20 @@ version = "1.0.95"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "build_helper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "citool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"build_helper",
|
||||
"clap",
|
||||
"insta",
|
||||
"serde",
|
||||
|
|
|
@ -10,6 +10,8 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_yaml = "0.9"
|
||||
serde_json = "1"
|
||||
|
||||
build_helper = { path = "../../build_helper" }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1"
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
mod metrics;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
@ -6,6 +8,8 @@ use anyhow::Context;
|
|||
use clap::Parser;
|
||||
use serde_yaml::Value;
|
||||
|
||||
use crate::metrics::postprocess_metrics;
|
||||
|
||||
const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
|
||||
const DOCKER_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../docker");
|
||||
const JOBS_YML_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../github-actions/jobs.yml");
|
||||
|
@ -338,6 +342,14 @@ enum Args {
|
|||
#[clap(long = "type", default_value = "auto")]
|
||||
job_type: JobType,
|
||||
},
|
||||
/// Postprocess the metrics.json file generated by bootstrap.
|
||||
PostprocessMetrics {
|
||||
/// Path to the metrics.json file
|
||||
metrics_path: PathBuf,
|
||||
/// Path to a file where the postprocessed metrics summary will be stored.
|
||||
/// Usually, this will be GITHUB_STEP_SUMMARY on CI.
|
||||
summary_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone)]
|
||||
|
@ -369,6 +381,9 @@ fn main() -> anyhow::Result<()> {
|
|||
Args::RunJobLocally { job_type, name } => {
|
||||
run_workflow_locally(load_db(default_jobs_file)?, job_type, name)?
|
||||
}
|
||||
Args::PostprocessMetrics { metrics_path, summary_path } => {
|
||||
postprocess_metrics(&metrics_path, &summary_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
138
src/ci/citool/src/metrics.rs
Normal file
138
src/ci/citool/src/metrics.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use build_helper::metrics::{JsonNode, JsonRoot, TestOutcome, TestSuite, TestSuiteMetadata};
|
||||
|
||||
pub fn postprocess_metrics(metrics_path: &Path, summary_path: &Path) -> anyhow::Result<()> {
|
||||
let metrics = load_metrics(metrics_path)?;
|
||||
|
||||
let mut file = File::options()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(summary_path)
|
||||
.with_context(|| format!("Cannot open summary file at {summary_path:?}"))?;
|
||||
|
||||
record_test_suites(&metrics, &mut file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_test_suites(metrics: &JsonRoot, file: &mut File) -> anyhow::Result<()> {
|
||||
let suites = get_test_suites(&metrics);
|
||||
|
||||
if !suites.is_empty() {
|
||||
let aggregated = aggregate_test_suites(&suites);
|
||||
let table = render_table(aggregated);
|
||||
writeln!(file, "\n# Test results\n")?;
|
||||
writeln!(file, "{table}")?;
|
||||
} else {
|
||||
eprintln!("No test suites found in metrics");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_table(suites: BTreeMap<String, TestSuiteRecord>) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut table = "| Test suite | Passed ✅ | Ignored 🚫 | Failed ❌ |\n".to_string();
|
||||
writeln!(table, "|:------|------:|------:|------:|").unwrap();
|
||||
|
||||
fn write_row(
|
||||
buffer: &mut String,
|
||||
name: &str,
|
||||
record: &TestSuiteRecord,
|
||||
surround: &str,
|
||||
) -> std::fmt::Result {
|
||||
let TestSuiteRecord { passed, ignored, failed } = record;
|
||||
let total = (record.passed + record.ignored + record.failed) as f64;
|
||||
let passed_pct = ((*passed as f64) / total) * 100.0;
|
||||
let ignored_pct = ((*ignored as f64) / total) * 100.0;
|
||||
let failed_pct = ((*failed as f64) / total) * 100.0;
|
||||
|
||||
write!(buffer, "| {surround}{name}{surround} |")?;
|
||||
write!(buffer, " {surround}{passed} ({passed_pct:.0}%){surround} |")?;
|
||||
write!(buffer, " {surround}{ignored} ({ignored_pct:.0}%){surround} |")?;
|
||||
writeln!(buffer, " {surround}{failed} ({failed_pct:.0}%){surround} |")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut total = TestSuiteRecord::default();
|
||||
for (name, record) in suites {
|
||||
write_row(&mut table, &name, &record, "").unwrap();
|
||||
total.passed += record.passed;
|
||||
total.ignored += record.ignored;
|
||||
total.failed += record.failed;
|
||||
}
|
||||
write_row(&mut table, "Total", &total, "**").unwrap();
|
||||
table
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestSuiteRecord {
|
||||
passed: u64,
|
||||
ignored: u64,
|
||||
failed: u64,
|
||||
}
|
||||
|
||||
fn aggregate_test_suites(suites: &[&TestSuite]) -> BTreeMap<String, TestSuiteRecord> {
|
||||
let mut records: BTreeMap<String, TestSuiteRecord> = BTreeMap::new();
|
||||
for suite in suites {
|
||||
let name = match &suite.metadata {
|
||||
TestSuiteMetadata::CargoPackage { crates, stage, .. } => {
|
||||
format!("{} (stage {stage})", crates.join(", "))
|
||||
}
|
||||
TestSuiteMetadata::Compiletest { suite, stage, .. } => {
|
||||
format!("{suite} (stage {stage})")
|
||||
}
|
||||
};
|
||||
let record = records.entry(name).or_default();
|
||||
for test in &suite.tests {
|
||||
match test.outcome {
|
||||
TestOutcome::Passed => {
|
||||
record.passed += 1;
|
||||
}
|
||||
TestOutcome::Failed => {
|
||||
record.failed += 1;
|
||||
}
|
||||
TestOutcome::Ignored { .. } => {
|
||||
record.ignored += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
records
|
||||
}
|
||||
|
||||
fn get_test_suites(metrics: &JsonRoot) -> Vec<&TestSuite> {
|
||||
fn visit_test_suites<'a>(nodes: &'a [JsonNode], suites: &mut Vec<&'a TestSuite>) {
|
||||
for node in nodes {
|
||||
match node {
|
||||
JsonNode::RustbuildStep { children, .. } => {
|
||||
visit_test_suites(&children, suites);
|
||||
}
|
||||
JsonNode::TestSuite(suite) => {
|
||||
suites.push(&suite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut suites = vec![];
|
||||
for invocation in &metrics.invocations {
|
||||
visit_test_suites(&invocation.children, &mut suites);
|
||||
}
|
||||
suites
|
||||
}
|
||||
|
||||
fn load_metrics(path: &Path) -> anyhow::Result<JsonRoot> {
|
||||
let metrics = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Cannot read JSON metrics from {path:?}"))?;
|
||||
let metrics: JsonRoot = serde_json::from_str(&metrics)
|
||||
.with_context(|| format!("Cannot deserialize JSON metrics from {path:?}"))?;
|
||||
Ok(metrics)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue