Skip to content

Commit 8da885b

Browse files
committed
Postprocess test suite metrics into GitHub summary
1 parent 1f52195 commit 8da885b

File tree

5 files changed

+170
-9
lines changed

5 files changed

+170
-9
lines changed

.github/workflows/ci.yml

+14-9
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ jobs:
176176
- name: ensure the stable version number is correct
177177
run: src/ci/scripts/verify-stable-version-number.sh
178178

179+
# Pre-build citool before the following step uninstalls rustup
180+
# Build is into the build directory, to avoid modifying sources
181+
- name: build citool
182+
run: |
183+
cd src/ci/citool
184+
CARGO_TARGET_DIR=../../../build/citool cargo build
185+
179186
- name: run the build
180187
# Redirect stderr to stdout to avoid reordering the two streams in the GHA logs.
181188
run: src/ci/scripts/run-build-from-ci.sh 2>&1
@@ -212,16 +219,14 @@ jobs:
212219
# erroring about invalid credentials instead.
213220
if: github.event_name == 'push' || env.DEPLOY == '1' || env.DEPLOY_ALT == '1'
214221

215-
- name: upload job metrics to DataDog
216-
if: needs.calculate_matrix.outputs.run_type != 'pr'
217-
env:
218-
DATADOG_SITE: datadoghq.com
219-
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
220-
DD_GITHUB_JOB_NAME: ${{ matrix.full_name }}
222+
- name: postprocess metrics into the summary
221223
run: |
222-
cd src/ci
223-
npm ci
224-
python3 scripts/upload-build-metrics.py ../../build/cpu-usage.csv
224+
if [ -f build/metrics.json ]; then
225+
export METRICS_FILE=build/metrics.json
226+
else
227+
export METRICS_FILE=obj/build/metrics.json
228+
fi
229+
./build/citool/debug/citool postprocess-metrics ${METRICS_FILE} ${GITHUB_STEP_SUMMARY}
225230
226231
# This job isused to tell bors the final status of the build, as there is no practical way to detect
227232
# when a workflow is successful listening to webhooks only in our current bors implementation (homu).

src/ci/citool/Cargo.lock

+9
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,20 @@ version = "1.0.95"
5858
source = "registry+https://github.com/rust-lang/crates.io-index"
5959
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
6060

61+
[[package]]
62+
name = "build_helper"
63+
version = "0.1.0"
64+
dependencies = [
65+
"serde",
66+
"serde_derive",
67+
]
68+
6169
[[package]]
6270
name = "citool"
6371
version = "0.1.0"
6472
dependencies = [
6573
"anyhow",
74+
"build_helper",
6675
"clap",
6776
"insta",
6877
"serde",

src/ci/citool/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ serde = { version = "1", features = ["derive"] }
1010
serde_yaml = "0.9"
1111
serde_json = "1"
1212

13+
build_helper = { path = "../../build_helper" }
14+
1315
[dev-dependencies]
1416
insta = "1"
1517

src/ci/citool/src/main.rs

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod metrics;
2+
13
use std::collections::BTreeMap;
24
use std::path::{Path, PathBuf};
35
use std::process::Command;
@@ -6,6 +8,8 @@ use anyhow::Context;
68
use clap::Parser;
79
use serde_yaml::Value;
810

11+
use crate::metrics::postprocess_metrics;
12+
913
const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
1014
const DOCKER_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../docker");
1115
const JOBS_YML_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../github-actions/jobs.yml");
@@ -343,6 +347,14 @@ enum Args {
343347
#[clap(long = "type", default_value = "auto")]
344348
job_type: JobType,
345349
},
350+
/// Postprocess the metrics.json file generated by bootstrap.
351+
PostprocessMetrics {
352+
/// Path to the metrics.json file
353+
metrics_path: PathBuf,
354+
/// Path to a file where the postprocessed metrics summary will be stored.
355+
/// Usually, this will be GITHUB_STEP_SUMMARY on CI.
356+
summary_path: PathBuf,
357+
},
346358
}
347359

348360
#[derive(clap::ValueEnum, Clone)]
@@ -372,6 +384,9 @@ fn main() -> anyhow::Result<()> {
372384
Args::RunJobLocally { job_type, name } => {
373385
run_workflow_locally(load_db(default_jobs_file)?, job_type, name)?
374386
}
387+
Args::PostprocessMetrics { metrics_path, summary_path } => {
388+
postprocess_metrics(&metrics_path, &summary_path)?;
389+
}
375390
}
376391

377392
Ok(())

src/ci/citool/src/metrics.rs

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use std::collections::BTreeMap;
2+
use std::io::Write;
3+
use std::path::Path;
4+
5+
use anyhow::Context;
6+
use build_helper::metrics::{JsonNode, JsonRoot, TestOutcome, TestSuite, TestSuiteMetadata};
7+
8+
pub fn postprocess_metrics(metrics_path: &Path, summary_path: &Path) -> anyhow::Result<()> {
9+
let metrics = load_metrics(metrics_path)?;
10+
let suites = get_test_suites(&metrics);
11+
12+
if suites.is_empty() {
13+
return Ok(());
14+
}
15+
16+
let aggregated = aggregate_test_suites(&suites);
17+
let table = render_table(aggregated);
18+
19+
let mut file = std::fs::File::options()
20+
.append(true)
21+
.create(true)
22+
.open(summary_path)
23+
.with_context(|| format!("Cannot open summary file at {summary_path:?}"))?;
24+
writeln!(file, "\n# Test results\n")?;
25+
writeln!(file, "{table}")?;
26+
27+
Ok(())
28+
}
29+
30+
fn render_table(suites: BTreeMap<String, TestSuiteRecord>) -> String {
31+
use std::fmt::Write;
32+
33+
let mut table = "| Test suite | Passed ✅ | Ignored 🚫 | Failed ❌ |\n".to_string();
34+
writeln!(table, "|:------|------:|------:|------:|").unwrap();
35+
36+
fn write_row(
37+
buffer: &mut String,
38+
name: &str,
39+
record: &TestSuiteRecord,
40+
surround: &str,
41+
) -> std::fmt::Result {
42+
let TestSuiteRecord { passed, ignored, failed } = record;
43+
let total = (record.passed + record.ignored + record.failed) as f64;
44+
let passed_pct = ((*passed as f64) / total) * 100.0;
45+
let ignored_pct = ((*ignored as f64) / total) * 100.0;
46+
let failed_pct = ((*failed as f64) / total) * 100.0;
47+
48+
write!(buffer, "| {surround}{name}{surround} |")?;
49+
write!(buffer, " {surround}{passed} ({passed_pct:.0}%){surround} |")?;
50+
write!(buffer, " {surround}{ignored} ({ignored_pct:.0}%){surround} |")?;
51+
writeln!(buffer, " {surround}{failed} ({failed_pct:.0}%){surround} |")?;
52+
53+
Ok(())
54+
}
55+
56+
let mut total = TestSuiteRecord::default();
57+
for (name, record) in suites {
58+
write_row(&mut table, &name, &record, "").unwrap();
59+
total.passed += record.passed;
60+
total.ignored += record.ignored;
61+
total.failed += record.failed;
62+
}
63+
write_row(&mut table, "Total", &total, "**").unwrap();
64+
table
65+
}
66+
67+
#[derive(Default)]
68+
struct TestSuiteRecord {
69+
passed: u64,
70+
ignored: u64,
71+
failed: u64,
72+
}
73+
74+
fn aggregate_test_suites(suites: &[&TestSuite]) -> BTreeMap<String, TestSuiteRecord> {
75+
let mut records: BTreeMap<String, TestSuiteRecord> = BTreeMap::new();
76+
for suite in suites {
77+
let name = match &suite.metadata {
78+
TestSuiteMetadata::CargoPackage { crates, stage, .. } => {
79+
format!("{} (stage {stage})", crates.join(", "))
80+
}
81+
TestSuiteMetadata::Compiletest { suite, stage, .. } => {
82+
format!("{suite} (stage {stage})")
83+
}
84+
};
85+
let record = records.entry(name).or_default();
86+
for test in &suite.tests {
87+
match test.outcome {
88+
TestOutcome::Passed => {
89+
record.passed += 1;
90+
}
91+
TestOutcome::Failed => {
92+
record.failed += 1;
93+
}
94+
TestOutcome::Ignored { .. } => {
95+
record.ignored += 1;
96+
}
97+
}
98+
}
99+
}
100+
records
101+
}
102+
103+
fn get_test_suites(metrics: &JsonRoot) -> Vec<&TestSuite> {
104+
fn visit_test_suites<'a>(nodes: &'a [JsonNode], suites: &mut Vec<&'a TestSuite>) {
105+
for node in nodes {
106+
match node {
107+
JsonNode::RustbuildStep { children, .. } => {
108+
visit_test_suites(&children, suites);
109+
}
110+
JsonNode::TestSuite(suite) => {
111+
suites.push(&suite);
112+
}
113+
}
114+
}
115+
}
116+
117+
let mut suites = vec![];
118+
for invocation in &metrics.invocations {
119+
visit_test_suites(&invocation.children, &mut suites);
120+
}
121+
suites
122+
}
123+
124+
fn load_metrics(path: &Path) -> anyhow::Result<JsonRoot> {
125+
let metrics = std::fs::read_to_string(path)
126+
.with_context(|| format!("Cannot read JSON metrics from {path:?}"))?;
127+
let metrics: JsonRoot = serde_json::from_str(&metrics)
128+
.with_context(|| format!("Cannot deserialize JSON metrics from {path:?}"))?;
129+
Ok(metrics)
130+
}

0 commit comments

Comments
 (0)