1- use std:: collections:: { HashMap , HashSet } ;
2- use std:: path:: PathBuf ;
1+ use crate :: metrics;
2+ use crate :: metrics:: { JobMetrics , JobName , get_test_suites} ;
3+ use crate :: utils:: pluralize;
4+ use build_helper:: metrics:: {
5+ BuildStep , JsonRoot , TestOutcome , TestSuite , TestSuiteMetadata , format_build_steps,
6+ } ;
7+ use std:: collections:: { BTreeMap , HashMap , HashSet } ;
8+
9+ pub fn output_bootstrap_stats ( metrics : & JsonRoot ) {
10+ if !metrics. invocations . is_empty ( ) {
11+ println ! ( "# Bootstrap steps" ) ;
12+ record_bootstrap_step_durations ( & metrics) ;
13+ record_test_suites ( & metrics) ;
14+ }
15+ }
316
4- use anyhow:: Context ;
5- use build_helper:: metrics:: { JsonRoot , TestOutcome , TestSuiteMetadata } ;
17+ fn record_bootstrap_step_durations ( metrics : & JsonRoot ) {
18+ for invocation in & metrics. invocations {
19+ let step = BuildStep :: from_invocation ( invocation) ;
20+ let table = format_build_steps ( & step) ;
21+ eprintln ! ( "Step `{}`\n {table}\n " , invocation. cmdline) ;
22+ println ! (
23+ r"<details>
24+ <summary>{}</summary>
25+ <pre><code>{table}</code></pre>
26+ </details>
27+ " ,
28+ invocation. cmdline
29+ ) ;
30+ }
31+ eprintln ! ( "Recorded {} bootstrap invocation(s)" , metrics. invocations. len( ) ) ;
32+ }
633
7- use crate :: jobs :: JobDatabase ;
8- use crate :: metrics:: get_test_suites;
34+ fn record_test_suites ( metrics : & JsonRoot ) {
35+ let suites = metrics:: get_test_suites ( & metrics ) ;
936
10- type Sha = String ;
11- type JobName = String ;
37+ if !suites. is_empty ( ) {
38+ let aggregated = aggregate_test_suites ( & suites) ;
39+ let table = render_table ( aggregated) ;
40+ println ! ( "\n # Test results\n " ) ;
41+ println ! ( "{table}" ) ;
42+ } else {
43+ eprintln ! ( "No test suites found in metrics" ) ;
44+ }
45+ }
1246
13- /// Computes a post merge CI analysis report between the `parent` and `current` commits.
14- pub fn post_merge_report ( job_db : JobDatabase , parent : Sha , current : Sha ) -> anyhow:: Result < ( ) > {
15- let jobs = download_all_metrics ( & job_db, & parent, & current) ?;
16- let aggregated_test_diffs = aggregate_test_diffs ( & jobs) ?;
47+ fn render_table ( suites : BTreeMap < String , TestSuiteRecord > ) -> String {
48+ use std:: fmt:: Write ;
1749
18- println ! ( "Comparing {parent} (base) -> {current} (this PR) \n ") ;
19- report_test_diffs ( aggregated_test_diffs ) ;
50+ let mut table = "| Test suite | Passed ✅ | Ignored 🚫 | Failed ❌ | \n ". to_string ( ) ;
51+ writeln ! ( table , "|:------|------:|------:|------:|" ) . unwrap ( ) ;
2052
21- Ok ( ( ) )
22- }
53+ fn compute_pct ( value : f64 , total : f64 ) -> f64 {
54+ if total == 0.0 { 0.0 } else { value / total }
55+ }
2356
24- struct JobMetrics {
25- parent : Option < JsonRoot > ,
26- current : JsonRoot ,
27- }
57+ fn write_row (
58+ buffer : & mut String ,
59+ name : & str ,
60+ record : & TestSuiteRecord ,
61+ surround : & str ,
62+ ) -> std:: fmt:: Result {
63+ let TestSuiteRecord { passed, ignored, failed } = record;
64+ let total = ( record. passed + record. ignored + record. failed ) as f64 ;
65+ let passed_pct = compute_pct ( * passed as f64 , total) * 100.0 ;
66+ let ignored_pct = compute_pct ( * ignored as f64 , total) * 100.0 ;
67+ let failed_pct = compute_pct ( * failed as f64 , total) * 100.0 ;
68+
69+ write ! ( buffer, "| {surround}{name}{surround} |" ) ?;
70+ write ! ( buffer, " {surround}{passed} ({passed_pct:.0}%){surround} |" ) ?;
71+ write ! ( buffer, " {surround}{ignored} ({ignored_pct:.0}%){surround} |" ) ?;
72+ writeln ! ( buffer, " {surround}{failed} ({failed_pct:.0}%){surround} |" ) ?;
73+
74+ Ok ( ( ) )
75+ }
2876
29- /// Download before/after metrics for all auto jobs in the job database.
30- fn download_all_metrics (
31- job_db : & JobDatabase ,
32- parent : & str ,
33- current : & str ,
34- ) -> anyhow:: Result < HashMap < JobName , JobMetrics > > {
35- let mut jobs = HashMap :: default ( ) ;
36-
37- for job in & job_db. auto_jobs {
38- eprintln ! ( "Downloading metrics of job {}" , job. name) ;
39- let metrics_parent = match download_job_metrics ( & job. name , parent) {
40- Ok ( metrics) => Some ( metrics) ,
41- Err ( error) => {
42- eprintln ! (
43- r#"Did not find metrics for job `{}` at `{}`: {error:?}.
44- Maybe it was newly added?"# ,
45- job. name, parent
46- ) ;
47- None
48- }
49- } ;
50- let metrics_current = download_job_metrics ( & job. name , current) ?;
51- jobs. insert (
52- job. name . clone ( ) ,
53- JobMetrics { parent : metrics_parent, current : metrics_current } ,
54- ) ;
77+ let mut total = TestSuiteRecord :: default ( ) ;
78+ for ( name, record) in suites {
79+ write_row ( & mut table, & name, & record, "" ) . unwrap ( ) ;
80+ total. passed += record. passed ;
81+ total. ignored += record. ignored ;
82+ total. failed += record. failed ;
5583 }
56- Ok ( jobs)
84+ write_row ( & mut table, "Total" , & total, "**" ) . unwrap ( ) ;
85+ table
5786}
5887
59- /// Downloads job metrics of the given job for the given commit.
60- /// Caches the result on the local disk.
61- fn download_job_metrics ( job_name : & str , sha : & str ) -> anyhow:: Result < JsonRoot > {
62- let cache_path = PathBuf :: from ( ".citool-cache" ) . join ( sha) . join ( job_name) . join ( "metrics.json" ) ;
63- if let Some ( cache_entry) =
64- std:: fs:: read_to_string ( & cache_path) . ok ( ) . and_then ( |data| serde_json:: from_str ( & data) . ok ( ) )
65- {
66- return Ok ( cache_entry) ;
67- }
88+ /// Computes a post merge CI analysis report of test differences
89+ /// between the `parent` and `current` commits.
90+ pub fn output_test_diffs ( job_metrics : HashMap < JobName , JobMetrics > ) {
91+ let aggregated_test_diffs = aggregate_test_diffs ( & job_metrics) ;
92+ report_test_diffs ( aggregated_test_diffs) ;
93+ }
6894
69- let url = get_metrics_url ( job_name, sha) ;
70- let mut response = ureq:: get ( & url) . call ( ) ?;
71- if !response. status ( ) . is_success ( ) {
72- return Err ( anyhow:: anyhow!(
73- "Cannot fetch metrics from {url}: {}\n {}" ,
74- response. status( ) ,
75- response. body_mut( ) . read_to_string( ) ?
76- ) ) ;
77- }
78- let data: JsonRoot = response
79- . body_mut ( )
80- . read_json ( )
81- . with_context ( || anyhow:: anyhow!( "cannot deserialize metrics from {url}" ) ) ?;
82-
83- // Ignore errors if cache cannot be created
84- if std:: fs:: create_dir_all ( cache_path. parent ( ) . unwrap ( ) ) . is_ok ( ) {
85- if let Ok ( serialized) = serde_json:: to_string ( & data) {
86- let _ = std:: fs:: write ( & cache_path, & serialized) ;
95+ #[ derive( Default ) ]
96+ struct TestSuiteRecord {
97+ passed : u64 ,
98+ ignored : u64 ,
99+ failed : u64 ,
100+ }
101+
102+ fn test_metadata_name ( metadata : & TestSuiteMetadata ) -> String {
103+ match metadata {
104+ TestSuiteMetadata :: CargoPackage { crates, stage, .. } => {
105+ format ! ( "{} (stage {stage})" , crates. join( ", " ) )
106+ }
107+ TestSuiteMetadata :: Compiletest { suite, stage, .. } => {
108+ format ! ( "{suite} (stage {stage})" )
87109 }
88110 }
89- Ok ( data)
90111}
91112
92- fn get_metrics_url ( job_name : & str , sha : & str ) -> String {
93- let suffix = if job_name. ends_with ( "-alt" ) { "-alt" } else { "" } ;
94- format ! ( "https://ci-artifacts.rust-lang.org/rustc-builds{suffix}/{sha}/metrics-{job_name}.json" )
113+ fn aggregate_test_suites ( suites : & [ & TestSuite ] ) -> BTreeMap < String , TestSuiteRecord > {
114+ let mut records: BTreeMap < String , TestSuiteRecord > = BTreeMap :: new ( ) ;
115+ for suite in suites {
116+ let name = test_metadata_name ( & suite. metadata ) ;
117+ let record = records. entry ( name) . or_default ( ) ;
118+ for test in & suite. tests {
119+ match test. outcome {
120+ TestOutcome :: Passed => {
121+ record. passed += 1 ;
122+ }
123+ TestOutcome :: Failed => {
124+ record. failed += 1 ;
125+ }
126+ TestOutcome :: Ignored { .. } => {
127+ record. ignored += 1 ;
128+ }
129+ }
130+ }
131+ }
132+ records
95133}
96134
97135/// Represents a difference in the outcome of tests between a base and a current commit.
@@ -101,9 +139,7 @@ struct AggregatedTestDiffs {
101139 diffs : HashMap < TestDiff , Vec < JobName > > ,
102140}
103141
104- fn aggregate_test_diffs (
105- jobs : & HashMap < JobName , JobMetrics > ,
106- ) -> anyhow:: Result < AggregatedTestDiffs > {
142+ fn aggregate_test_diffs ( jobs : & HashMap < JobName , JobMetrics > ) -> AggregatedTestDiffs {
107143 let mut diffs: HashMap < TestDiff , Vec < JobName > > = HashMap :: new ( ) ;
108144
109145 // Aggregate test suites
@@ -117,7 +153,7 @@ fn aggregate_test_diffs(
117153 }
118154 }
119155
120- Ok ( AggregatedTestDiffs { diffs } )
156+ AggregatedTestDiffs { diffs }
121157}
122158
123159#[ derive( Eq , PartialEq , Hash , Debug ) ]
@@ -312,7 +348,3 @@ fn report_test_diffs(diff: AggregatedTestDiffs) {
312348 ) ;
313349 }
314350}
315-
316- fn pluralize ( text : & str , count : usize ) -> String {
317- if count == 1 { text. to_string ( ) } else { format ! ( "{text}s" ) }
318- }
0 commit comments