Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(458)

Side by Side Diff: tools/gardening/compare_failures.dart

Issue 2711733005: Add compare_failures gardening utility. (Closed)
Patch Set: Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698