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

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

Issue 2711733005: Add compare_failures gardening utility. (Closed)
Patch Set: Updated cf. comments 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 | « tools/gardening/README.md ('k') | 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 parsingTimingBlock = 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 parsingTimingBlock = true;
85 } else if (parsingTimingBlock) {
86 if (line.startsWith('0:')) {
87 timings.addAll(parseTimings(buildUri, line));
88 } else {
89 parsingTimingBlock = 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<TestConfiguration> timeoutIds = new Set<TestConfiguration>();
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<TestConfiguration, Map<int, Map<String, Timing>>> map =
109 <TestConfiguration, 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((TestConfiguration 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<TestConfiguration> errorIds = new Set<TestConfiguration>();
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<TestConfiguration, Map<int, TestFailure>> map = <TestConfiguration, 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((TestConfiguration 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 that did not time out.
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 String get buildName =>
248 '/builders/$botName/builds/$buildNumber/steps/$stepName';
249
250 String get path => '$prefix$buildName/logs/$suffix';
251
252 /// Creates the [Uri] for this build step stdio log.
253 Uri toUri() {
254 return new Uri(
255 scheme: scheme,
256 host: host,
257 path: path);
258 }
259
260 /// Returns the [BuildUri] the previous build of this build step.
261 BuildUri prev() {
262 return new BuildUri.internal(
263 scheme, host, prefix, botName, buildNumber - 1, stepName, suffix);
264 }
265
266 String toString() {
267 return buildName;
268 }
269 }
270
271 /// Id for a test on a specific configuration, for instance
272 /// `dart2js-chrome release_x64/co19/Language/Metadata/before_function_t07`.
273 class TestConfiguration {
274 final String configName;
275 final String testName;
276
277 TestConfiguration(this.configName, this.testName);
278
279 String toString() {
280 return '$configName $testName';
281 }
282
283 int get hashCode => configName.hashCode * 17 + testName.hashCode * 19;
284
285 bool operator ==(other) {
286 if (identical(this, other)) return true;
287 if (other is! TestConfiguration) return false;
288 return configName == other.configName && testName == other.testName;
289 }
290 }
291
292 /// Test failure data derived from the test failure summary in the build step
293 /// stdio log.
294 class TestFailure {
295 final BuildUri uri;
296 final TestConfiguration id;
297 final String expected;
298 final String actual;
299 final String text;
300
301 factory TestFailure(BuildUri uri, List<String> lines) {
302 List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']);
303 String configName = parts[1];
304 String archName = parts[2];
305 String testName = parts[3];
306 TestConfiguration id =
307 new TestConfiguration(configName, '$archName/$testName');
308 String expected = split(lines[1], ['Expected: '])[1];
309 String actual = split(lines[2], ['Actual: '])[1];
310 return new TestFailure.internal(
311 uri, id, expected, actual, lines.skip(3).join('\n'));
312 }
313
314 TestFailure.internal(
315 this.uri, this.id, this.expected, this.actual, this.text);
316
317 String toString() {
318 StringBuffer sb = new StringBuffer();
319 sb.write('FAILED: $id\n');
320 sb.write('Expected: $expected\n');
321 sb.write('Actual: $actual\n');
322 sb.write(text);
323 return sb.toString();
324 }
325 }
326
327 /// Id for a single test step, for instance the compilation and run steps of
328 /// a test.
329 class TestStep {
330 final String stepName;
331 final TestConfiguration id;
332
333 TestStep(this.stepName, this.id);
334
335 String toString() {
336 return '$stepName - $id';
337 }
338
339 int get hashCode => stepName.hashCode * 13 + id.hashCode * 17;
340
341 bool operator ==(other) {
342 if (identical(this, other)) return true;
343 if (other is! TestStep) return false;
344 return stepName == other.stepName && id == other.id;
345 }
346 }
347
348 /// The timing result for a single test step.
349 class Timing {
350 final BuildUri uri;
351 final String time;
352 final TestStep step;
353
354 Timing(this.uri, this.time, this.step);
355
356 String toString() {
357 return '$time - $step';
358 }
359 }
360
361 /// Create the [Timing]s for the [line] as found in the top-20 timings of a
362 /// build step stdio log.
363 List<Timing> parseTimings(BuildUri uri, String line) {
364 List<String> parts = split(line, [' - ', ' - ', ' ']);
365 String time = parts[0];
366 String stepName = parts[1];
367 String configName = parts[2];
368 String testNames = parts[3];
369 List<Timing> timings = <Timing>[];
370 for (String testName in testNames.split(',')) {
371 timings.add(new Timing(uri, time,
372 new TestStep(stepName,
373 new TestConfiguration(configName, testName.trim()))));
374 }
375 return timings;
376 }
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 }
OLDNEW
« no previous file with comments | « tools/gardening/README.md ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698