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:io'; | 10 import 'dart:io'; |
11 | 11 |
| 12 import 'package:args/args.dart'; |
12 import 'package:gardening/src/buildbot_structures.dart'; | 13 import 'package:gardening/src/buildbot_structures.dart'; |
| 14 import 'package:gardening/src/buildbot_loading.dart'; |
13 import 'package:gardening/src/util.dart'; | 15 import 'package:gardening/src/util.dart'; |
14 | 16 |
15 main(List<String> args) async { | 17 main(List<String> args) async { |
16 if (args.length != 1) { | 18 ArgParser argParser = createArgParser(); |
17 print('Usage: compare_failures <log-uri>'); | 19 ArgResults argResults = argParser.parse(args); |
| 20 processArgResults(argResults); |
| 21 if (argResults.rest.length != 1) { |
| 22 print('Usage: compare_failures [options] <log-uri>'); |
| 23 print('where <log-uri> is the uri the stdio output of a failing test step'); |
| 24 print('and options are:'); |
| 25 print(argParser.usage); |
18 exit(1); | 26 exit(1); |
19 } | 27 } |
20 String url = args.first; | 28 String url = argResults.rest.first; |
21 if (!url.endsWith('/text')) { | 29 if (!url.endsWith('/text')) { |
22 // Use the text version of the stdio log. | 30 // Use the text version of the stdio log. |
23 url += '/text'; | 31 url += '/text'; |
24 } | 32 } |
25 Uri uri = Uri.parse(url); | 33 Uri uri = Uri.parse(url); |
26 HttpClient client = new HttpClient(); | 34 HttpClient client = new HttpClient(); |
27 BuildUri buildUri = new BuildUri(uri); | 35 BuildUri buildUri = new BuildUri(uri); |
28 List<BuildResult> results = await readBuildResults(client, buildUri); | 36 List<BuildResult> results = await readBuildResults(client, buildUri); |
29 print(generateBuildResultsSummary(buildUri, results)); | 37 print(generateBuildResultsSummary(buildUri, results)); |
30 client.close(); | 38 client.close(); |
31 } | 39 } |
32 | 40 |
33 /// Creates a [BuildResult] for [buildUri] and, if it contains failures, the | 41 /// Creates a [BuildResult] for [buildUri] and, if it contains failures, the |
34 /// [BuildResult]s for the previous 5 builds. | 42 /// [BuildResult]s for the previous 5 builds. |
35 Future<List<BuildResult>> readBuildResults( | 43 Future<List<BuildResult>> readBuildResults( |
36 HttpClient client, BuildUri buildUri) async { | 44 HttpClient client, BuildUri buildUri) async { |
37 List<BuildResult> summaries = <BuildResult>[]; | 45 List<BuildResult> summaries = <BuildResult>[]; |
38 BuildResult firstSummary = await readBuildResult(client, buildUri); | 46 BuildResult firstSummary = await readBuildResult(client, buildUri); |
39 summaries.add(firstSummary); | 47 summaries.add(firstSummary); |
40 if (firstSummary.hasFailures) { | 48 if (firstSummary.hasFailures) { |
41 for (int i = 0; i < 10; i++) { | 49 for (int i = 0; i < 10; i++) { |
42 buildUri = buildUri.prev(); | 50 buildUri = buildUri.prev(); |
43 summaries.add(await readBuildResult(client, buildUri)); | 51 summaries.add(await readBuildResult(client, buildUri)); |
44 } | 52 } |
45 } | 53 } |
46 return summaries; | 54 return summaries; |
47 } | 55 } |
48 | 56 |
49 /// Parses the [buildUri] test log and creates a [BuildResult] for it. | |
50 Future<BuildResult> readBuildResult( | |
51 HttpClient client, BuildUri buildUri) async { | |
52 Uri uri = buildUri.toUri(); | |
53 log('Reading $uri'); | |
54 String text = await readUriAsText(client, uri); | |
55 | |
56 bool inFailure = false; | |
57 List<String> currentFailure; | |
58 bool parsingTimingBlock = false; | |
59 | |
60 List<TestFailure> failures = <TestFailure>[]; | |
61 List<Timing> timings = <Timing>[]; | |
62 for (String line in text.split('\n')) { | |
63 if (currentFailure != null) { | |
64 if (line.startsWith('!@@@STEP_CLEAR@@@')) { | |
65 failures.add(new TestFailure(buildUri, currentFailure)); | |
66 currentFailure = null; | |
67 } else { | |
68 currentFailure.add(line); | |
69 } | |
70 } else if (inFailure && line.startsWith('@@@STEP_FAILURE@@@')) { | |
71 inFailure = false; | |
72 } else if (line.startsWith('!@@@STEP_FAILURE@@@')) { | |
73 inFailure = true; | |
74 } else if (line.startsWith('FAILED:')) { | |
75 currentFailure = <String>[]; | |
76 currentFailure.add(line); | |
77 } | |
78 if (line.startsWith('--- Total time:')) { | |
79 parsingTimingBlock = true; | |
80 } else if (parsingTimingBlock) { | |
81 if (line.startsWith('0:')) { | |
82 timings.addAll(parseTimings(buildUri, line)); | |
83 } else { | |
84 parsingTimingBlock = false; | |
85 } | |
86 } | |
87 } | |
88 return new BuildResult(buildUri, failures, timings); | |
89 } | |
90 | |
91 /// Generate a summary of the timeouts and other failures in [results]. | 57 /// Generate a summary of the timeouts and other failures in [results]. |
92 String generateBuildResultsSummary( | 58 String generateBuildResultsSummary( |
93 BuildUri buildUri, List<BuildResult> results) { | 59 BuildUri buildUri, List<BuildResult> results) { |
94 StringBuffer sb = new StringBuffer(); | 60 StringBuffer sb = new StringBuffer(); |
95 sb.write('Results for $buildUri:\n'); | 61 sb.write('Results for $buildUri:\n'); |
96 Set<TestConfiguration> timeoutIds = new Set<TestConfiguration>(); | 62 Set<TestConfiguration> timeoutIds = new Set<TestConfiguration>(); |
97 for (BuildResult result in results) { | 63 for (BuildResult result in results) { |
98 timeoutIds.addAll(result.timeouts.map((TestFailure failure) => failure.id)); | 64 timeoutIds.addAll(result.timeouts.map((TestFailure failure) => failure.id)); |
99 } | 65 } |
100 if (timeoutIds.isNotEmpty) { | 66 if (timeoutIds.isNotEmpty) { |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
170 sb.write(' / '); | 136 sb.write(' / '); |
171 sb.write(padRight('-- OK --', 10)); | 137 sb.write(padRight('-- OK --', 10)); |
172 } | 138 } |
173 sb.write('\n'); | 139 sb.write('\n'); |
174 } | 140 } |
175 sb.write('\n'); | 141 sb.write('\n'); |
176 }); | 142 }); |
177 } | 143 } |
178 return sb.toString(); | 144 return sb.toString(); |
179 } | 145 } |
180 | |
181 /// The results of a build step. | |
182 class BuildResult { | |
183 final BuildUri buildUri; | |
184 final List<TestFailure> _failures; | |
185 final List<Timing> _timings; | |
186 | |
187 BuildResult(this.buildUri, this._failures, this._timings); | |
188 | |
189 /// `true` of the build result has test failures. | |
190 bool get hasFailures => _failures.isNotEmpty; | |
191 | |
192 /// Returns the top-20 timings found in the build log. | |
193 Iterable<Timing> get timings => _timings; | |
194 | |
195 /// Returns the [TestFailure]s for tests that timed out. | |
196 Iterable<TestFailure> get timeouts { | |
197 return _failures | |
198 .where((TestFailure failure) => failure.actual == 'Timeout'); | |
199 } | |
200 | |
201 /// Returns the [TestFailure]s for failing tests that did not time out. | |
202 Iterable<TestFailure> get errors { | |
203 return _failures | |
204 .where((TestFailure failure) => failure.actual != 'Timeout'); | |
205 } | |
206 | |
207 String toString() { | |
208 StringBuffer sb = new StringBuffer(); | |
209 sb.write('$buildUri\n'); | |
210 sb.write('Failures:\n${_failures.join('\n-----\n')}\n'); | |
211 sb.write('\nTimings:\n${_timings.join('\n')}'); | |
212 return sb.toString(); | |
213 } | |
214 } | |
215 | |
216 /// Test failure data derived from the test failure summary in the build step | |
217 /// stdio log. | |
218 class TestFailure { | |
219 final BuildUri uri; | |
220 final TestConfiguration id; | |
221 final String expected; | |
222 final String actual; | |
223 final String text; | |
224 | |
225 factory TestFailure(BuildUri uri, List<String> lines) { | |
226 List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']); | |
227 String configName = parts[1]; | |
228 String archName = parts[2]; | |
229 String testName = parts[3]; | |
230 TestConfiguration id = | |
231 new TestConfiguration(configName, archName, testName); | |
232 String expected = split(lines[1], ['Expected: '])[1]; | |
233 String actual = split(lines[2], ['Actual: '])[1]; | |
234 return new TestFailure.internal( | |
235 uri, id, expected, actual, lines.skip(3).join('\n')); | |
236 } | |
237 | |
238 TestFailure.internal( | |
239 this.uri, this.id, this.expected, this.actual, this.text); | |
240 | |
241 String toString() { | |
242 StringBuffer sb = new StringBuffer(); | |
243 sb.write('FAILED: $id\n'); | |
244 sb.write('Expected: $expected\n'); | |
245 sb.write('Actual: $actual\n'); | |
246 sb.write(text); | |
247 return sb.toString(); | |
248 } | |
249 } | |
250 | |
251 /// Id for a single test step, for instance the compilation and run steps of | |
252 /// a test. | |
253 class TestStep { | |
254 final String stepName; | |
255 final TestConfiguration id; | |
256 | |
257 TestStep(this.stepName, this.id); | |
258 | |
259 String toString() { | |
260 return '$stepName - $id'; | |
261 } | |
262 | |
263 int get hashCode => stepName.hashCode * 13 + id.hashCode * 17; | |
264 | |
265 bool operator ==(other) { | |
266 if (identical(this, other)) return true; | |
267 if (other is! TestStep) return false; | |
268 return stepName == other.stepName && id == other.id; | |
269 } | |
270 } | |
271 | |
272 /// The timing result for a single test step. | |
273 class Timing { | |
274 final BuildUri uri; | |
275 final String time; | |
276 final TestStep step; | |
277 | |
278 Timing(this.uri, this.time, this.step); | |
279 | |
280 String toString() { | |
281 return '$time - $step'; | |
282 } | |
283 } | |
284 | |
285 /// Create the [Timing]s for the [line] as found in the top-20 timings of a | |
286 /// build step stdio log. | |
287 List<Timing> parseTimings(BuildUri uri, String line) { | |
288 List<String> parts = split(line, [' - ', ' - ', ' ']); | |
289 String time = parts[0]; | |
290 String stepName = parts[1]; | |
291 String configName = parts[2]; | |
292 String testNames = parts[3]; | |
293 List<Timing> timings = <Timing>[]; | |
294 for (String name in testNames.split(',')) { | |
295 name = name.trim(); | |
296 int slashPos = name.indexOf('/'); | |
297 String archName = name.substring(0, slashPos); | |
298 String testName = name.substring(slashPos + 1); | |
299 timings.add(new Timing( | |
300 uri, | |
301 time, | |
302 new TestStep( | |
303 stepName, new TestConfiguration(configName, archName, testName)))); | |
304 } | |
305 return timings; | |
306 } | |
OLD | NEW |