OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /// Compares the test log of a build step with previous builds. | |
6 /// | |
7 /// Use this to detect flakiness of failures, especially timeouts. | |
8 | |
9 import 'dart:async'; | |
10 import 'dart:convert'; | |
11 import 'dart:io'; | |
12 | |
13 main(List<String> args) async { | |
14 if (args.length != 1) { | |
15 print('Usage: compare_failures <log-uri>'); | |
16 exit(1); | |
17 } | |
18 String url = args.first; | |
19 if (!url.endsWith('/text')) { | |
20 // Use the text version of the stdio log. | |
21 url += '/text'; | |
22 } | |
23 Uri uri = Uri.parse(url); | |
24 HttpClient client = new HttpClient(); | |
25 BuildUri buildUri = new BuildUri(uri); | |
26 List<BuildResult> results = await readBuildResults(client, buildUri); | |
27 print(generateBuildResultsSummary(buildUri, results)); | |
28 client.close(); | |
29 } | |
30 | |
31 /// Creates a [BuildResult] for [buildUri] and, if it contains failures, the | |
32 /// [BuildResult]s for the previous 5 builds. | |
33 Future<List<BuildResult>> readBuildResults( | |
34 HttpClient client, BuildUri buildUri) async { | |
35 List<BuildResult> summaries = <BuildResult>[]; | |
36 BuildResult firstSummary = await readBuildResult(client, buildUri); | |
37 summaries.add(firstSummary); | |
38 if (firstSummary.hasFailures) { | |
39 for (int i = 0; i < 5; i++) { | |
40 buildUri = buildUri.prev(); | |
41 summaries.add(await readBuildResult(client, buildUri)); | |
42 } | |
43 } | |
44 return summaries; | |
45 } | |
46 | |
47 /// Reads the content of [uri] as text. | |
48 Future<String> readUriAsText(HttpClient client, Uri uri) async { | |
49 HttpClientRequest request = await client.getUrl(uri); | |
50 HttpClientResponse response = await request.close(); | |
51 return UTF8.decode(await response.expand((l) => l).toList()); | |
52 } | |
53 | |
54 /// Parses the [buildUri] test log and creates a [BuildResult] for it. | |
55 Future<BuildResult> readBuildResult( | |
56 HttpClient client, BuildUri buildUri) async { | |
57 Uri uri = buildUri.toUri(); | |
58 log('Reading $uri'); | |
59 String text = await readUriAsText(client, uri); | |
60 | |
61 bool inFailure = false; | |
62 List<String> currentFailure; | |
63 bool inTiming = false; | |
64 | |
65 List<TestFailure> failures = <TestFailure>[]; | |
66 List<Timing> timings = <Timing>[]; | |
67 for (String line in text.split('\n')) { | |
68 if (currentFailure != null) { | |
69 if (line.startsWith('!@@@STEP_CLEAR@@@')) { | |
70 failures.add(new TestFailure(buildUri, currentFailure)); | |
71 currentFailure = null; | |
72 } else { | |
73 currentFailure.add(line); | |
74 } | |
75 } else if (inFailure && line.startsWith('@@@STEP_FAILURE@@@')) { | |
76 inFailure = false; | |
77 } else if (line.startsWith('!@@@STEP_FAILURE@@@')) { | |
78 inFailure = true; | |
79 } else if (line.startsWith('FAILED:')) { | |
80 currentFailure = <String>[]; | |
81 currentFailure.add(line); | |
82 } | |
83 if (line.startsWith('--- Total time:')) { | |
84 inTiming = true; | |
karlklose
2017/02/24 06:33:34
'inTiming' -> 'parsingTimingBlock'?
Johnni Winther
2017/03/15 09:49:54
Done.
| |
85 } else if (inTiming) { | |
86 if (line.startsWith('0:')) { | |
87 timings.addAll(parseTimings(buildUri, line)); | |
88 } else { | |
89 inTiming = false; | |
90 } | |
91 } | |
92 } | |
93 return new BuildResult(buildUri, failures, timings); | |
94 } | |
95 | |
96 /// Generate a summary of the timeouts and other failures in [results]. | |
97 String generateBuildResultsSummary( | |
98 BuildUri buildUri, List<BuildResult> results) { | |
99 StringBuffer sb = new StringBuffer(); | |
100 sb.write('Results for $buildUri:\n'); | |
101 Set<TestId> timeoutIds = new Set<TestId>(); | |
102 for (BuildResult result in results) { | |
103 timeoutIds.addAll(result.timeouts.map((TestFailure failure) => failure.id)); | |
104 } | |
105 if (timeoutIds.isNotEmpty) { | |
106 int firstBuildNumber = results.first.buildUri.buildNumber; | |
107 int lastBuildNumber = results.last.buildUri.buildNumber; | |
108 Map<TestId, Map<int, Map<String, Timing>>> map = | |
109 <TestId, Map<int, Map<String, Timing>>>{}; | |
110 Set<String> stepNames = new Set<String>(); | |
111 for (BuildResult result in results) { | |
112 for (Timing timing in result.timings) { | |
113 Map<int, Map<String, Timing>> builds = | |
114 map.putIfAbsent(timing.step.id, () => <int, Map<String, Timing>>{}); | |
115 stepNames.add(timing.step.stepName); | |
116 builds.putIfAbsent(timing.uri.buildNumber, () => <String, Timing>{})[ | |
117 timing.step.stepName] = timing; | |
118 } | |
119 } | |
120 sb.write('Timeouts for ${buildUri} :\n'); | |
121 map.forEach((TestId id, Map<int, Map<String, Timing>> timings) { | |
122 if (!timeoutIds.contains(id)) return; | |
123 sb.write('$id\n'); | |
124 sb.write( | |
125 '${' ' * 8} ${stepNames.map((t) => padRight(t, 14)).join(' ')}\n'); | |
126 for (int buildNumber = firstBuildNumber; | |
127 buildNumber >= lastBuildNumber; | |
128 buildNumber--) { | |
129 Map<String, Timing> steps = timings[buildNumber] ?? const {}; | |
130 sb.write(padRight(' ${buildNumber}: ', 8)); | |
131 for (String stepName in stepNames) { | |
132 Timing timing = steps[stepName]; | |
133 if (timing != null) { | |
134 sb.write(' ${timing.time}'); | |
135 } else { | |
136 sb.write(' --------------'); | |
137 } | |
138 } | |
139 sb.write('\n'); | |
140 } | |
141 sb.write('\n'); | |
142 }); | |
143 } | |
144 Set<TestId> errorIds = new Set<TestId>(); | |
145 for (BuildResult result in results) { | |
146 errorIds.addAll(result.errors.map((TestFailure failure) => failure.id)); | |
147 } | |
148 if (errorIds.isNotEmpty) { | |
149 int firstBuildNumber = results.first.buildUri.buildNumber; | |
150 int lastBuildNumber = results.last.buildUri.buildNumber; | |
151 Map<TestId, Map<int, TestFailure>> map = <TestId, Map<int, TestFailure>>{}; | |
152 for (BuildResult result in results) { | |
153 for (TestFailure failure in result.errors) { | |
154 map.putIfAbsent(failure.id, () => <int, TestFailure>{})[ | |
155 failure.uri.buildNumber] = failure; | |
156 } | |
157 } | |
158 sb.write('Errors for ${buildUri} :\n'); | |
159 // TODO(johnniwinther): Improve comparison of non-timeouts. | |
160 map.forEach((TestId id, Map<int, TestFailure> failures) { | |
161 if (!errorIds.contains(id)) return; | |
162 sb.write('$id\n'); | |
163 for (int buildNumber = firstBuildNumber; | |
164 buildNumber >= lastBuildNumber; | |
165 buildNumber--) { | |
166 TestFailure failure = failures[buildNumber]; | |
167 sb.write(padRight(' ${buildNumber}: ', 8)); | |
168 if (failure != null) { | |
169 sb.write(padRight(failure.expected, 10)); | |
170 sb.write(' / '); | |
171 sb.write(padRight(failure.actual, 10)); | |
172 } else { | |
173 sb.write(' ' * 10); | |
174 sb.write(' / '); | |
175 sb.write(padRight('-- OK --', 10)); | |
176 } | |
177 sb.write('\n'); | |
178 } | |
179 sb.write('\n'); | |
180 }); | |
181 } | |
182 return sb.toString(); | |
183 } | |
184 | |
185 /// The results of a build step. | |
186 class BuildResult { | |
187 final BuildUri buildUri; | |
188 final List<TestFailure> _failures; | |
189 final List<Timing> _timings; | |
190 | |
191 BuildResult(this.buildUri, this._failures, this._timings); | |
192 | |
193 /// `true` of the build result has test failures. | |
194 bool get hasFailures => _failures.isNotEmpty; | |
195 | |
196 /// Returns the top-20 timings found in the build log. | |
197 Iterable<Timing> get timings => _timings; | |
198 | |
199 /// Returns the [TestFailure]s for tests that timed out. | |
200 Iterable<TestFailure> get timeouts { | |
201 return _failures | |
202 .where((TestFailure failure) => failure.actual == 'Timeout'); | |
203 } | |
204 | |
205 /// Returns the [TestFailure]s for failing tests did not time out. | |
karlklose
2017/02/24 06:33:34
'for failing tests *that* did not time out.'
Johnni Winther
2017/03/15 09:49:54
Done.
| |
206 Iterable<TestFailure> get errors { | |
207 return _failures | |
208 .where((TestFailure failure) => failure.actual != 'Timeout'); | |
209 } | |
210 | |
211 String toString() { | |
212 StringBuffer sb = new StringBuffer(); | |
213 sb.write('$buildUri\n'); | |
214 sb.write('Failures:\n${_failures.join('\n-----\n')}\n'); | |
215 sb.write('\nTimings:\n${_timings.join('\n')}'); | |
216 return sb.toString(); | |
217 } | |
218 } | |
219 | |
220 /// The [Uri] of a build step stdio log split into its subparts. | |
221 class BuildUri { | |
222 final String scheme; | |
223 final String host; | |
224 final String prefix; | |
225 final String botName; | |
226 final int buildNumber; | |
227 final String stepName; | |
228 final String suffix; | |
229 | |
230 factory BuildUri(Uri uri) { | |
231 String scheme = uri.scheme; | |
232 String host = uri.host; | |
233 List<String> parts = | |
234 split(uri.path, ['/builders/', '/builds/', '/steps/', '/logs/']); | |
235 String prefix = parts[0]; | |
236 String botName = parts[1]; | |
237 int buildNumber = int.parse(parts[2]); | |
238 String stepName = parts[3]; | |
239 String suffix = parts[4]; | |
240 return new BuildUri.internal( | |
241 scheme, host, prefix, botName, buildNumber, stepName, suffix); | |
242 } | |
243 | |
244 BuildUri.internal(this.scheme, this.host, this.prefix, this.botName, | |
245 this.buildNumber, this.stepName, this.suffix); | |
246 | |
247 /// Creates the [Uri] for this build step stdio log. | |
248 Uri toUri() { | |
249 return new Uri( | |
250 scheme: scheme, | |
251 host: host, | |
252 path: | |
253 '$prefix/builders/$botName/builds/$buildNumber/steps/$stepName/logs/ $suffix'); | |
karlklose
2017/02/24 06:33:34
Long line. Consider putting this into a getter. It
Johnni Winther
2017/03/15 09:49:54
Done.
| |
254 } | |
255 | |
256 /// Returns the [BuildUri] the previous build of this build step. | |
257 BuildUri prev() { | |
258 return new BuildUri.internal( | |
259 scheme, host, prefix, botName, buildNumber - 1, stepName, suffix); | |
260 } | |
261 | |
262 String toString() { | |
263 return '/builders/$botName/builds/$buildNumber/steps/$stepName'; | |
264 } | |
265 } | |
266 | |
267 /// Id for a test. | |
268 class TestId { | |
karlklose
2017/02/24 06:33:34
Ho about 'TestConfiguration'?
Johnni Winther
2017/03/15 09:49:54
Done.
| |
269 final String configName; | |
270 final String testName; | |
271 | |
272 TestId(this.configName, this.testName); | |
273 | |
274 String toString() { | |
275 return '$configName $testName'; | |
276 } | |
277 | |
278 int get hashCode => configName.hashCode * 17 + testName.hashCode * 19; | |
279 | |
280 bool operator ==(other) { | |
281 if (identical(this, other)) return true; | |
282 if (other is! TestId) return false; | |
283 return configName == other.configName && testName == other.testName; | |
284 } | |
285 } | |
286 | |
287 /// Test failure data derived from the test failure summary in the build step | |
288 /// stdio log. | |
289 class TestFailure { | |
290 final BuildUri uri; | |
291 final TestId id; | |
292 final String expected; | |
293 final String actual; | |
294 final String text; | |
295 | |
296 factory TestFailure(BuildUri uri, List<String> lines) { | |
297 List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']); | |
298 String configName = parts[1]; | |
299 String archName = parts[2]; | |
300 String testName = parts[3]; | |
301 TestId id = new TestId(configName, '$archName/$testName'); | |
302 String expected = split(lines[1], ['Expected: '])[1]; | |
303 String actual = split(lines[2], ['Actual: '])[1]; | |
304 return new TestFailure.internal( | |
305 uri, id, expected, actual, lines.skip(3).join('\n')); | |
306 } | |
307 | |
308 TestFailure.internal( | |
309 this.uri, this.id, this.expected, this.actual, this.text); | |
310 | |
311 String toString() { | |
312 StringBuffer sb = new StringBuffer(); | |
313 sb.write('FAILED: $id\n'); | |
314 sb.write('Expected: $expected\n'); | |
315 sb.write('Actual: $actual\n'); | |
316 sb.write(text); | |
317 return sb.toString(); | |
318 } | |
319 } | |
320 | |
321 /// Id for a single test step, for instance the compilation and run steps of | |
322 /// a test. | |
323 class TestStep { | |
324 final String stepName; | |
325 final TestId id; | |
326 | |
327 TestStep(this.stepName, this.id); | |
328 | |
329 String toString() { | |
330 return '$stepName - $id'; | |
331 } | |
332 | |
333 int get hashCode => stepName.hashCode * 13 + id.hashCode * 17; | |
334 | |
335 bool operator ==(other) { | |
336 if (identical(this, other)) return true; | |
337 if (other is! TestStep) return false; | |
338 return stepName == other.stepName && id == other.id; | |
339 } | |
340 } | |
341 | |
342 /// The timing result for a single test step. | |
343 class Timing { | |
344 final BuildUri uri; | |
345 final String time; | |
346 final TestStep step; | |
347 | |
348 Timing(this.uri, this.time, this.step); | |
349 | |
350 String toString() { | |
351 return '$time - $step'; | |
352 } | |
353 } | |
354 | |
355 /// Create the [Timing]s for the [line] as found in the top-20 timings of a | |
356 /// build step stdio log. | |
357 List<Timing> parseTimings(BuildUri uri, String line) { | |
358 List<String> parts = split(line, [' - ', ' - ', ' ']); | |
359 String time = parts[0]; | |
360 String stepName = parts[1]; | |
361 String configName = parts[2]; | |
362 String testNames = parts[3]; | |
363 List<Timing> timings = <Timing>[]; | |
364 for (String testName in testNames.split(',')) { | |
365 timings.add(new Timing(uri, time, | |
366 new TestStep(stepName, new TestId(configName, testName.trim())))); | |
367 } | |
368 return timings; | |
369 } | |
370 | |
371 /// Split [text] using [infixes] as infix markers. | |
372 List<String> split(String text, List<String> infixes) { | |
373 List<String> result = <String>[]; | |
374 int start = 0; | |
375 for (String infix in infixes) { | |
376 int index = text.indexOf(infix, start); | |
377 if (index == -1) | |
378 throw "'$infix' not found in '$text' from offset ${start}."; | |
379 result.add(text.substring(start, index)); | |
380 start = index + infix.length; | |
381 } | |
382 result.add(text.substring(start)); | |
383 return result; | |
384 } | |
385 | |
386 /// Pad [text] with spaces to the right to fit [length]. | |
387 String padRight(String text, int length) { | |
388 if (text.length < length) return '${text}${' ' * (length - text.length)}'; | |
389 return text; | |
390 } | |
391 | |
392 void log(String text) { | |
393 print(text); | |
394 } | |
OLD | NEW |