OLD | NEW |
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// Compares the test log of a build step with previous builds. | 5 /// Compares the test log of a build step with previous builds. |
6 /// | 6 /// |
7 /// Use this to detect flakiness of failures, especially timeouts. | 7 /// Use this to detect flakiness of failures, especially timeouts. |
8 | 8 |
9 import 'dart:async'; | 9 import 'dart:async'; |
10 import 'dart:convert'; | 10 import 'dart:convert'; |
11 import 'dart:io'; | 11 import 'dart:io'; |
12 | 12 |
| 13 import 'src/buildbot_structures.dart'; |
| 14 import 'src/util.dart'; |
| 15 |
13 main(List<String> args) async { | 16 main(List<String> args) async { |
14 if (args.length != 1) { | 17 if (args.length != 1) { |
15 print('Usage: compare_failures <log-uri>'); | 18 print('Usage: compare_failures <log-uri>'); |
16 exit(1); | 19 exit(1); |
17 } | 20 } |
18 String url = args.first; | 21 String url = args.first; |
19 if (!url.endsWith('/text')) { | 22 if (!url.endsWith('/text')) { |
20 // Use the text version of the stdio log. | 23 // Use the text version of the stdio log. |
21 url += '/text'; | 24 url += '/text'; |
22 } | 25 } |
23 Uri uri = Uri.parse(url); | 26 Uri uri = Uri.parse(url); |
24 HttpClient client = new HttpClient(); | 27 HttpClient client = new HttpClient(); |
25 BuildUri buildUri = new BuildUri(uri); | 28 BuildUri buildUri = new BuildUri(uri); |
26 List<BuildResult> results = await readBuildResults(client, buildUri); | 29 List<BuildResult> results = await readBuildResults(client, buildUri); |
27 print(generateBuildResultsSummary(buildUri, results)); | 30 print(generateBuildResultsSummary(buildUri, results)); |
28 client.close(); | 31 client.close(); |
29 } | 32 } |
30 | 33 |
31 /// Creates a [BuildResult] for [buildUri] and, if it contains failures, the | 34 /// Creates a [BuildResult] for [buildUri] and, if it contains failures, the |
32 /// [BuildResult]s for the previous 5 builds. | 35 /// [BuildResult]s for the previous 5 builds. |
33 Future<List<BuildResult>> readBuildResults( | 36 Future<List<BuildResult>> readBuildResults( |
34 HttpClient client, BuildUri buildUri) async { | 37 HttpClient client, BuildUri buildUri) async { |
35 List<BuildResult> summaries = <BuildResult>[]; | 38 List<BuildResult> summaries = <BuildResult>[]; |
36 BuildResult firstSummary = await readBuildResult(client, buildUri); | 39 BuildResult firstSummary = await readBuildResult(client, buildUri); |
37 summaries.add(firstSummary); | 40 summaries.add(firstSummary); |
38 if (firstSummary.hasFailures) { | 41 if (firstSummary.hasFailures) { |
39 for (int i = 0; i < 5; i++) { | 42 for (int i = 0; i < 10; i++) { |
40 buildUri = buildUri.prev(); | 43 buildUri = buildUri.prev(); |
41 summaries.add(await readBuildResult(client, buildUri)); | 44 summaries.add(await readBuildResult(client, buildUri)); |
42 } | 45 } |
43 } | 46 } |
44 return summaries; | 47 return summaries; |
45 } | 48 } |
46 | 49 |
47 /// Reads the content of [uri] as text. | 50 /// Reads the content of [uri] as text. |
48 Future<String> readUriAsText(HttpClient client, Uri uri) async { | 51 Future<String> readUriAsText(HttpClient client, Uri uri) async { |
49 HttpClientRequest request = await client.getUrl(uri); | 52 HttpClientRequest request = await client.getUrl(uri); |
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
211 | 214 |
212 String toString() { | 215 String toString() { |
213 StringBuffer sb = new StringBuffer(); | 216 StringBuffer sb = new StringBuffer(); |
214 sb.write('$buildUri\n'); | 217 sb.write('$buildUri\n'); |
215 sb.write('Failures:\n${_failures.join('\n-----\n')}\n'); | 218 sb.write('Failures:\n${_failures.join('\n-----\n')}\n'); |
216 sb.write('\nTimings:\n${_timings.join('\n')}'); | 219 sb.write('\nTimings:\n${_timings.join('\n')}'); |
217 return sb.toString(); | 220 return sb.toString(); |
218 } | 221 } |
219 } | 222 } |
220 | 223 |
221 /// The [Uri] of a build step stdio log split into its subparts. | |
222 class BuildUri { | |
223 final String scheme; | |
224 final String host; | |
225 final String prefix; | |
226 final String botName; | |
227 final int buildNumber; | |
228 final String stepName; | |
229 final String suffix; | |
230 | |
231 factory BuildUri(Uri uri) { | |
232 String scheme = uri.scheme; | |
233 String host = uri.host; | |
234 List<String> parts = | |
235 split(uri.path, ['/builders/', '/builds/', '/steps/', '/logs/']); | |
236 String prefix = parts[0]; | |
237 String botName = parts[1]; | |
238 int buildNumber = int.parse(parts[2]); | |
239 String stepName = parts[3]; | |
240 String suffix = parts[4]; | |
241 return new BuildUri.internal( | |
242 scheme, host, prefix, botName, buildNumber, stepName, suffix); | |
243 } | |
244 | |
245 BuildUri.internal(this.scheme, this.host, this.prefix, this.botName, | |
246 this.buildNumber, this.stepName, this.suffix); | |
247 | |
248 String get buildName => | |
249 '/builders/$botName/builds/$buildNumber/steps/$stepName'; | |
250 | |
251 String get path => '$prefix$buildName/logs/$suffix'; | |
252 | |
253 /// Creates the [Uri] for this build step stdio log. | |
254 Uri toUri() { | |
255 return new Uri(scheme: scheme, host: host, path: path); | |
256 } | |
257 | |
258 /// Returns the [BuildUri] the previous build of this build step. | |
259 BuildUri prev() { | |
260 return new BuildUri.internal( | |
261 scheme, host, prefix, botName, buildNumber - 1, stepName, suffix); | |
262 } | |
263 | |
264 String toString() { | |
265 return buildName; | |
266 } | |
267 } | |
268 | |
269 /// Id for a test on a specific configuration, for instance | |
270 /// `dart2js-chrome release_x64/co19/Language/Metadata/before_function_t07`. | |
271 class TestConfiguration { | |
272 final String configName; | |
273 final String testName; | |
274 | |
275 TestConfiguration(this.configName, this.testName); | |
276 | |
277 String toString() { | |
278 return '$configName $testName'; | |
279 } | |
280 | |
281 int get hashCode => configName.hashCode * 17 + testName.hashCode * 19; | |
282 | |
283 bool operator ==(other) { | |
284 if (identical(this, other)) return true; | |
285 if (other is! TestConfiguration) return false; | |
286 return configName == other.configName && testName == other.testName; | |
287 } | |
288 } | |
289 | |
290 /// Test failure data derived from the test failure summary in the build step | 224 /// Test failure data derived from the test failure summary in the build step |
291 /// stdio log. | 225 /// stdio log. |
292 class TestFailure { | 226 class TestFailure { |
293 final BuildUri uri; | 227 final BuildUri uri; |
294 final TestConfiguration id; | 228 final TestConfiguration id; |
295 final String expected; | 229 final String expected; |
296 final String actual; | 230 final String actual; |
297 final String text; | 231 final String text; |
298 | 232 |
299 factory TestFailure(BuildUri uri, List<String> lines) { | 233 factory TestFailure(BuildUri uri, List<String> lines) { |
300 List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']); | 234 List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']); |
301 String configName = parts[1]; | 235 String configName = parts[1]; |
302 String archName = parts[2]; | 236 String archName = parts[2]; |
303 String testName = parts[3]; | 237 String testName = parts[3]; |
304 TestConfiguration id = | 238 TestConfiguration id = |
305 new TestConfiguration(configName, '$archName/$testName'); | 239 new TestConfiguration(configName, archName, testName); |
306 String expected = split(lines[1], ['Expected: '])[1]; | 240 String expected = split(lines[1], ['Expected: '])[1]; |
307 String actual = split(lines[2], ['Actual: '])[1]; | 241 String actual = split(lines[2], ['Actual: '])[1]; |
308 return new TestFailure.internal( | 242 return new TestFailure.internal( |
309 uri, id, expected, actual, lines.skip(3).join('\n')); | 243 uri, id, expected, actual, lines.skip(3).join('\n')); |
310 } | 244 } |
311 | 245 |
312 TestFailure.internal( | 246 TestFailure.internal( |
313 this.uri, this.id, this.expected, this.actual, this.text); | 247 this.uri, this.id, this.expected, this.actual, this.text); |
314 | 248 |
315 String toString() { | 249 String toString() { |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
358 | 292 |
359 /// Create the [Timing]s for the [line] as found in the top-20 timings of a | 293 /// Create the [Timing]s for the [line] as found in the top-20 timings of a |
360 /// build step stdio log. | 294 /// build step stdio log. |
361 List<Timing> parseTimings(BuildUri uri, String line) { | 295 List<Timing> parseTimings(BuildUri uri, String line) { |
362 List<String> parts = split(line, [' - ', ' - ', ' ']); | 296 List<String> parts = split(line, [' - ', ' - ', ' ']); |
363 String time = parts[0]; | 297 String time = parts[0]; |
364 String stepName = parts[1]; | 298 String stepName = parts[1]; |
365 String configName = parts[2]; | 299 String configName = parts[2]; |
366 String testNames = parts[3]; | 300 String testNames = parts[3]; |
367 List<Timing> timings = <Timing>[]; | 301 List<Timing> timings = <Timing>[]; |
368 for (String testName in testNames.split(',')) { | 302 for (String name in testNames.split(',')) { |
| 303 name = name.trim(); |
| 304 int slashPos = name.indexOf('/'); |
| 305 String archName = name.substring(0, slashPos); |
| 306 String testName = name.substring(slashPos + 1); |
369 timings.add(new Timing( | 307 timings.add(new Timing( |
370 uri, | 308 uri, |
371 time, | 309 time, |
372 new TestStep( | 310 new TestStep( |
373 stepName, new TestConfiguration(configName, testName.trim())))); | 311 stepName, new TestConfiguration(configName, archName, testName)))); |
374 } | 312 } |
375 return timings; | 313 return timings; |
376 } | 314 } |
377 | |
378 /// Split [text] using [infixes] as infix markers. | |
379 List<String> split(String text, List<String> infixes) { | |
380 List<String> result = <String>[]; | |
381 int start = 0; | |
382 for (String infix in infixes) { | |
383 int index = text.indexOf(infix, start); | |
384 if (index == -1) | |
385 throw "'$infix' not found in '$text' from offset ${start}."; | |
386 result.add(text.substring(start, index)); | |
387 start = index + infix.length; | |
388 } | |
389 result.add(text.substring(start)); | |
390 return result; | |
391 } | |
392 | |
393 /// Pad [text] with spaces to the right to fit [length]. | |
394 String padRight(String text, int length) { | |
395 if (text.length < length) return '${text}${' ' * (length - text.length)}'; | |
396 return text; | |
397 } | |
398 | |
399 void log(String text) { | |
400 print(text); | |
401 } | |
OLD | NEW |