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