1
- use std:: cmp :: Reverse ;
2
- use std:: collections :: HashMap ;
1
+ use std:: collections :: { HashMap , HashSet } ;
2
+ use std:: path :: PathBuf ;
3
3
4
4
use anyhow:: Context ;
5
- use build_helper:: metrics:: { JsonRoot , TestOutcome } ;
5
+ use build_helper:: metrics:: { JsonRoot , TestOutcome , TestSuiteMetadata } ;
6
6
7
7
use crate :: jobs:: JobDatabase ;
8
8
use crate :: metrics:: get_test_suites;
@@ -13,8 +13,10 @@ type JobName = String;
13
13
/// Computes a post merge CI analysis report between the `parent` and `current` commits.
14
14
pub fn post_merge_report ( job_db : JobDatabase , parent : Sha , current : Sha ) -> anyhow:: Result < ( ) > {
15
15
let jobs = download_all_metrics ( & job_db, & parent, & current) ?;
16
- let diffs = aggregate_test_diffs ( & jobs) ?;
17
- report_test_changes ( diffs) ;
16
+ let aggregated_test_diffs = aggregate_test_diffs ( & jobs) ?;
17
+
18
+ println ! ( "Comparing {parent} (base) -> {current} (this PR)\n " ) ;
19
+ report_test_diffs ( aggregated_test_diffs) ;
18
20
19
21
Ok ( ( ) )
20
22
}
@@ -54,7 +56,16 @@ Maybe it was newly added?"#,
54
56
Ok ( jobs)
55
57
}
56
58
59
+ /// Downloads job metrics of the given job for the given commit.
60
+ /// Caches the result on the local disk.
57
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
+ }
68
+
58
69
let url = get_metrics_url ( job_name, sha) ;
59
70
let mut response = ureq:: get ( & url) . call ( ) ?;
60
71
if !response. status ( ) . is_success ( ) {
@@ -68,6 +79,13 @@ fn download_job_metrics(job_name: &str, sha: &str) -> anyhow::Result<JsonRoot> {
68
79
. body_mut ( )
69
80
. read_json ( )
70
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) ;
87
+ }
88
+ }
71
89
Ok ( data)
72
90
}
73
91
@@ -76,81 +94,80 @@ fn get_metrics_url(job_name: &str, sha: &str) -> String {
76
94
format ! ( "https://ci-artifacts.rust-lang.org/rustc-builds{suffix}/{sha}/metrics-{job_name}.json" )
77
95
}
78
96
97
+ /// Represents a difference in the outcome of tests between a base and a current commit.
98
+ /// Maps test diffs to jobs that contained them.
99
+ #[ derive( Debug ) ]
100
+ struct AggregatedTestDiffs {
101
+ diffs : HashMap < TestDiff , Vec < JobName > > ,
102
+ }
103
+
79
104
fn aggregate_test_diffs (
80
105
jobs : & HashMap < JobName , JobMetrics > ,
81
- ) -> anyhow:: Result < Vec < AggregatedTestDiffs > > {
82
- let mut job_diffs = vec ! [ ] ;
106
+ ) -> anyhow:: Result < AggregatedTestDiffs > {
107
+ let mut diffs : HashMap < TestDiff , Vec < JobName > > = HashMap :: new ( ) ;
83
108
84
109
// Aggregate test suites
85
110
for ( name, metrics) in jobs {
86
111
if let Some ( parent) = & metrics. parent {
87
112
let tests_parent = aggregate_tests ( parent) ;
88
113
let tests_current = aggregate_tests ( & metrics. current ) ;
89
- let test_diffs = calculate_test_diffs ( tests_parent, tests_current) ;
90
- if !test_diffs. is_empty ( ) {
91
- job_diffs. push ( ( name. clone ( ) , test_diffs) ) ;
114
+ for diff in calculate_test_diffs ( tests_parent, tests_current) {
115
+ diffs. entry ( diff) . or_default ( ) . push ( name. to_string ( ) ) ;
92
116
}
93
117
}
94
118
}
95
119
96
- // Aggregate jobs with the same diff, as often the same diff will appear in many jobs
97
- let job_diffs: HashMap < Vec < ( Test , TestOutcomeDiff ) > , Vec < String > > =
98
- job_diffs. into_iter ( ) . fold ( HashMap :: new ( ) , |mut acc, ( job, diffs) | {
99
- acc. entry ( diffs) . or_default ( ) . push ( job) ;
100
- acc
101
- } ) ;
120
+ Ok ( AggregatedTestDiffs { diffs } )
121
+ }
102
122
103
- Ok ( job_diffs
104
- . into_iter ( )
105
- . map ( |( test_diffs, jobs) | AggregatedTestDiffs { jobs, test_diffs } )
106
- . collect ( ) )
123
+ #[ derive( Eq , PartialEq , Hash , Debug ) ]
124
+ enum TestOutcomeDiff {
125
+ ChangeOutcome { before : TestOutcome , after : TestOutcome } ,
126
+ Missing { before : TestOutcome } ,
127
+ Added ( TestOutcome ) ,
107
128
}
108
129
109
- fn calculate_test_diffs (
110
- reference : TestSuiteData ,
111
- current : TestSuiteData ,
112
- ) -> Vec < ( Test , TestOutcomeDiff ) > {
113
- let mut diffs = vec ! [ ] ;
130
+ #[ derive( Eq , PartialEq , Hash , Debug ) ]
131
+ struct TestDiff {
132
+ test : Test ,
133
+ diff : TestOutcomeDiff ,
134
+ }
135
+
136
+ fn calculate_test_diffs ( parent : TestSuiteData , current : TestSuiteData ) -> HashSet < TestDiff > {
137
+ let mut diffs = HashSet :: new ( ) ;
114
138
for ( test, outcome) in & current. tests {
115
- match reference . tests . get ( test) {
139
+ match parent . tests . get ( test) {
116
140
Some ( before) => {
117
141
if before != outcome {
118
- diffs. push ( (
119
- test. clone ( ) ,
120
- TestOutcomeDiff :: ChangeOutcome {
142
+ diffs. insert ( TestDiff {
143
+ test : test . clone ( ) ,
144
+ diff : TestOutcomeDiff :: ChangeOutcome {
121
145
before : before. clone ( ) ,
122
146
after : outcome. clone ( ) ,
123
147
} ,
124
- ) ) ;
148
+ } ) ;
125
149
}
126
150
}
127
- None => diffs. push ( ( test. clone ( ) , TestOutcomeDiff :: Added ( outcome. clone ( ) ) ) ) ,
151
+ None => {
152
+ diffs. insert ( TestDiff {
153
+ test : test. clone ( ) ,
154
+ diff : TestOutcomeDiff :: Added ( outcome. clone ( ) ) ,
155
+ } ) ;
156
+ }
128
157
}
129
158
}
130
- for ( test, outcome) in & reference . tests {
159
+ for ( test, outcome) in & parent . tests {
131
160
if !current. tests . contains_key ( test) {
132
- diffs. push ( ( test. clone ( ) , TestOutcomeDiff :: Missing { before : outcome. clone ( ) } ) ) ;
161
+ diffs. insert ( TestDiff {
162
+ test : test. clone ( ) ,
163
+ diff : TestOutcomeDiff :: Missing { before : outcome. clone ( ) } ,
164
+ } ) ;
133
165
}
134
166
}
135
167
136
168
diffs
137
169
}
138
170
139
- /// Represents a difference in the outcome of tests between a base and a current commit.
140
- #[ derive( Debug ) ]
141
- struct AggregatedTestDiffs {
142
- /// All jobs that had the exact same test diffs.
143
- jobs : Vec < String > ,
144
- test_diffs : Vec < ( Test , TestOutcomeDiff ) > ,
145
- }
146
-
147
- #[ derive( Eq , PartialEq , Hash , Debug ) ]
148
- enum TestOutcomeDiff {
149
- ChangeOutcome { before : TestOutcome , after : TestOutcome } ,
150
- Missing { before : TestOutcome } ,
151
- Added ( TestOutcome ) ,
152
- }
153
-
154
171
/// Aggregates test suite executions from all bootstrap invocations in a given CI job.
155
172
#[ derive( Default ) ]
156
173
struct TestSuiteData {
@@ -160,6 +177,7 @@ struct TestSuiteData {
160
177
#[ derive( Hash , PartialEq , Eq , Debug , Clone ) ]
161
178
struct Test {
162
179
name : String ,
180
+ is_doctest : bool ,
163
181
}
164
182
165
183
/// Extracts all tests from the passed metrics and map them to their outcomes.
@@ -168,7 +186,10 @@ fn aggregate_tests(metrics: &JsonRoot) -> TestSuiteData {
168
186
let test_suites = get_test_suites ( & metrics) ;
169
187
for suite in test_suites {
170
188
for test in & suite. tests {
171
- let test_entry = Test { name : normalize_test_name ( & test. name ) } ;
189
+ // Poor man's detection of doctests based on the "(line XYZ)" suffix
190
+ let is_doctest = matches ! ( suite. metadata, TestSuiteMetadata :: CargoPackage { .. } )
191
+ && test. name . contains ( "(line" ) ;
192
+ let test_entry = Test { name : normalize_test_name ( & test. name ) , is_doctest } ;
172
193
tests. insert ( test_entry, test. outcome . clone ( ) ) ;
173
194
}
174
195
}
@@ -181,16 +202,13 @@ fn normalize_test_name(name: &str) -> String {
181
202
}
182
203
183
204
/// Prints test changes in Markdown format to stdout.
184
- fn report_test_changes ( mut diffs : Vec < AggregatedTestDiffs > ) {
205
+ fn report_test_diffs ( diff : AggregatedTestDiffs ) {
185
206
println ! ( "## Test differences" ) ;
186
- if diffs. is_empty ( ) {
207
+ if diff . diffs . is_empty ( ) {
187
208
println ! ( "No test diffs found" ) ;
188
209
return ;
189
210
}
190
211
191
- // Sort diffs in decreasing order by diff count
192
- diffs. sort_by_key ( |entry| Reverse ( entry. test_diffs . len ( ) ) ) ;
193
-
194
212
fn format_outcome ( outcome : & TestOutcome ) -> String {
195
213
match outcome {
196
214
TestOutcome :: Passed => "pass" . to_string ( ) ,
@@ -219,36 +237,79 @@ fn report_test_changes(mut diffs: Vec<AggregatedTestDiffs>) {
219
237
}
220
238
}
221
239
222
- let max_diff_count = 10 ;
223
- let max_job_count = 5 ;
224
- let max_test_count = 10 ;
225
-
226
- for diff in diffs. iter ( ) . take ( max_diff_count) {
227
- let mut jobs = diff. jobs . clone ( ) ;
228
- jobs. sort ( ) ;
229
-
230
- let jobs = jobs. iter ( ) . take ( max_job_count) . map ( |j| format ! ( "`{j}`" ) ) . collect :: < Vec < _ > > ( ) ;
240
+ fn format_job_group ( group : u64 ) -> String {
241
+ format ! ( "**J{group}**" )
242
+ }
231
243
232
- let extra_jobs = diff. jobs . len ( ) . saturating_sub ( max_job_count) ;
233
- let suffix = if extra_jobs > 0 {
234
- format ! ( " (and {extra_jobs} {})" , pluralize( "other" , extra_jobs) )
235
- } else {
236
- String :: new ( )
244
+ // It would be quite noisy to repeat the jobs that contained the test changes after/next to
245
+ // every test diff. At the same time, grouping the test diffs by
246
+ // [unique set of jobs that contained them] also doesn't work well, because the test diffs
247
+ // would have to be duplicated several times.
248
+ // Instead, we create a set of unique job groups, and then print a job group after each test.
249
+ // We then print the job groups at the end, as a sort of index.
250
+ let mut grouped_diffs: Vec < ( & TestDiff , u64 ) > = vec ! [ ] ;
251
+ let mut job_list_to_group: HashMap < & [ JobName ] , u64 > = HashMap :: new ( ) ;
252
+ let mut job_index: Vec < & [ JobName ] > = vec ! [ ] ;
253
+
254
+ let original_diff_count = diff. diffs . len ( ) ;
255
+ let diffs = diff
256
+ . diffs
257
+ . into_iter ( )
258
+ . filter ( |( diff, _) | !diff. test . is_doctest )
259
+ . map ( |( diff, mut jobs) | {
260
+ jobs. sort ( ) ;
261
+ ( diff, jobs)
262
+ } )
263
+ . collect :: < Vec < _ > > ( ) ;
264
+ let doctest_count = original_diff_count. saturating_sub ( diffs. len ( ) ) ;
265
+
266
+ let max_diff_count = 100 ;
267
+ for ( diff, jobs) in diffs. iter ( ) . take ( max_diff_count) {
268
+ let jobs = & * jobs;
269
+ let job_group = match job_list_to_group. get ( jobs. as_slice ( ) ) {
270
+ Some ( id) => * id,
271
+ None => {
272
+ let id = job_index. len ( ) as u64 ;
273
+ job_index. push ( jobs) ;
274
+ job_list_to_group. insert ( jobs, id) ;
275
+ id
276
+ }
237
277
} ;
238
- println ! ( "- {}{suffix}" , jobs. join( "," ) ) ;
278
+ grouped_diffs. push ( ( diff, job_group) ) ;
279
+ }
239
280
240
- let extra_tests = diff. test_diffs . len ( ) . saturating_sub ( max_test_count) ;
241
- for ( test, outcome_diff) in diff. test_diffs . iter ( ) . take ( max_test_count) {
242
- println ! ( " - {}: {}" , test. name, format_diff( & outcome_diff) ) ;
243
- }
244
- if extra_tests > 0 {
245
- println ! ( " - (and {extra_tests} additional {})" , pluralize( "tests" , extra_tests) ) ;
246
- }
281
+ // Sort diffs by job group and test name
282
+ grouped_diffs. sort_by ( |( d1, g1) , ( d2, g2) | g1. cmp ( & g2) . then ( d1. test . name . cmp ( & d2. test . name ) ) ) ;
283
+
284
+ for ( diff, job_group) in grouped_diffs {
285
+ println ! (
286
+ "- `{}`: {} ({})" ,
287
+ diff. test. name,
288
+ format_diff( & diff. diff) ,
289
+ format_job_group( job_group)
290
+ ) ;
247
291
}
248
292
249
293
let extra_diffs = diffs. len ( ) . saturating_sub ( max_diff_count) ;
250
294
if extra_diffs > 0 {
251
- println ! ( "\n (and {extra_diffs} additional {})" , pluralize( "diff" , extra_diffs) ) ;
295
+ println ! ( "\n (and {extra_diffs} additional {})" , pluralize( "test diff" , extra_diffs) ) ;
296
+ }
297
+
298
+ if doctest_count > 0 {
299
+ println ! (
300
+ "\n Additionally, {doctest_count} doctest {} were found. These are ignored, as they are noisy." ,
301
+ pluralize( "diff" , doctest_count)
302
+ ) ;
303
+ }
304
+
305
+ // Now print the job group index
306
+ println ! ( "\n **Job group index**\n " ) ;
307
+ for ( group, jobs) in job_index. into_iter ( ) . enumerate ( ) {
308
+ println ! (
309
+ "- {}: {}" ,
310
+ format_job_group( group as u64 ) ,
311
+ jobs. iter( ) . map( |j| format!( "`{j}`" ) ) . collect:: <Vec <_>>( ) . join( ", " )
312
+ ) ;
252
313
}
253
314
}
254
315
0 commit comments