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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « tools/gardening/README.md ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tools/gardening/compare_failures.dart
diff --git a/tools/gardening/compare_failures.dart b/tools/gardening/compare_failures.dart
new file mode 100644
index 0000000000000000000000000000000000000000..94f70dd5be762bfa0fe2a4af3bb1d3f387a75dd4
--- /dev/null
+++ b/tools/gardening/compare_failures.dart
@@ -0,0 +1,401 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Compares the test log of a build step with previous builds.
+///
+/// Use this to detect flakiness of failures, especially timeouts.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+main(List<String> args) async {
+ if (args.length != 1) {
+ print('Usage: compare_failures <log-uri>');
+ exit(1);
+ }
+ String url = args.first;
+ if (!url.endsWith('/text')) {
+ // Use the text version of the stdio log.
+ url += '/text';
+ }
+ Uri uri = Uri.parse(url);
+ HttpClient client = new HttpClient();
+ BuildUri buildUri = new BuildUri(uri);
+ List<BuildResult> results = await readBuildResults(client, buildUri);
+ print(generateBuildResultsSummary(buildUri, results));
+ client.close();
+}
+
+/// Creates a [BuildResult] for [buildUri] and, if it contains failures, the
+/// [BuildResult]s for the previous 5 builds.
+Future<List<BuildResult>> readBuildResults(
+ HttpClient client, BuildUri buildUri) async {
+ List<BuildResult> summaries = <BuildResult>[];
+ BuildResult firstSummary = await readBuildResult(client, buildUri);
+ summaries.add(firstSummary);
+ if (firstSummary.hasFailures) {
+ for (int i = 0; i < 5; i++) {
+ buildUri = buildUri.prev();
+ summaries.add(await readBuildResult(client, buildUri));
+ }
+ }
+ return summaries;
+}
+
+/// Reads the content of [uri] as text.
+Future<String> readUriAsText(HttpClient client, Uri uri) async {
+ HttpClientRequest request = await client.getUrl(uri);
+ HttpClientResponse response = await request.close();
+ return UTF8.decode(await response.expand((l) => l).toList());
+}
+
+/// Parses the [buildUri] test log and creates a [BuildResult] for it.
+Future<BuildResult> readBuildResult(
+ HttpClient client, BuildUri buildUri) async {
+ Uri uri = buildUri.toUri();
+ log('Reading $uri');
+ String text = await readUriAsText(client, uri);
+
+ bool inFailure = false;
+ List<String> currentFailure;
+ bool parsingTimingBlock = false;
+
+ List<TestFailure> failures = <TestFailure>[];
+ List<Timing> timings = <Timing>[];
+ for (String line in text.split('\n')) {
+ if (currentFailure != null) {
+ if (line.startsWith('!@@@STEP_CLEAR@@@')) {
+ failures.add(new TestFailure(buildUri, currentFailure));
+ currentFailure = null;
+ } else {
+ currentFailure.add(line);
+ }
+ } else if (inFailure && line.startsWith('@@@STEP_FAILURE@@@')) {
+ inFailure = false;
+ } else if (line.startsWith('!@@@STEP_FAILURE@@@')) {
+ inFailure = true;
+ } else if (line.startsWith('FAILED:')) {
+ currentFailure = <String>[];
+ currentFailure.add(line);
+ }
+ if (line.startsWith('--- Total time:')) {
+ parsingTimingBlock = true;
+ } else if (parsingTimingBlock) {
+ if (line.startsWith('0:')) {
+ timings.addAll(parseTimings(buildUri, line));
+ } else {
+ parsingTimingBlock = false;
+ }
+ }
+ }
+ return new BuildResult(buildUri, failures, timings);
+}
+
+/// Generate a summary of the timeouts and other failures in [results].
+String generateBuildResultsSummary(
+ BuildUri buildUri, List<BuildResult> results) {
+ StringBuffer sb = new StringBuffer();
+ sb.write('Results for $buildUri:\n');
+ Set<TestConfiguration> timeoutIds = new Set<TestConfiguration>();
+ for (BuildResult result in results) {
+ timeoutIds.addAll(result.timeouts.map((TestFailure failure) => failure.id));
+ }
+ if (timeoutIds.isNotEmpty) {
+ int firstBuildNumber = results.first.buildUri.buildNumber;
+ int lastBuildNumber = results.last.buildUri.buildNumber;
+ Map<TestConfiguration, Map<int, Map<String, Timing>>> map =
+ <TestConfiguration, Map<int, Map<String, Timing>>>{};
+ Set<String> stepNames = new Set<String>();
+ for (BuildResult result in results) {
+ for (Timing timing in result.timings) {
+ Map<int, Map<String, Timing>> builds =
+ map.putIfAbsent(timing.step.id, () => <int, Map<String, Timing>>{});
+ stepNames.add(timing.step.stepName);
+ builds.putIfAbsent(timing.uri.buildNumber, () => <String, Timing>{})[
+ timing.step.stepName] = timing;
+ }
+ }
+ sb.write('Timeouts for ${buildUri} :\n');
+ map.forEach((TestConfiguration id, Map<int, Map<String, Timing>> timings) {
+ if (!timeoutIds.contains(id)) return;
+ sb.write('$id\n');
+ sb.write(
+ '${' ' * 8} ${stepNames.map((t) => padRight(t, 14)).join(' ')}\n');
+ for (int buildNumber = firstBuildNumber;
+ buildNumber >= lastBuildNumber;
+ buildNumber--) {
+ Map<String, Timing> steps = timings[buildNumber] ?? const {};
+ sb.write(padRight(' ${buildNumber}: ', 8));
+ for (String stepName in stepNames) {
+ Timing timing = steps[stepName];
+ if (timing != null) {
+ sb.write(' ${timing.time}');
+ } else {
+ sb.write(' --------------');
+ }
+ }
+ sb.write('\n');
+ }
+ sb.write('\n');
+ });
+ }
+ Set<TestConfiguration> errorIds = new Set<TestConfiguration>();
+ for (BuildResult result in results) {
+ errorIds.addAll(result.errors.map((TestFailure failure) => failure.id));
+ }
+ if (errorIds.isNotEmpty) {
+ int firstBuildNumber = results.first.buildUri.buildNumber;
+ int lastBuildNumber = results.last.buildUri.buildNumber;
+ Map<TestConfiguration, Map<int, TestFailure>> map = <TestConfiguration, Map<int, TestFailure>>{};
+ for (BuildResult result in results) {
+ for (TestFailure failure in result.errors) {
+ map.putIfAbsent(failure.id, () => <int, TestFailure>{})[
+ failure.uri.buildNumber] = failure;
+ }
+ }
+ sb.write('Errors for ${buildUri} :\n');
+ // TODO(johnniwinther): Improve comparison of non-timeouts.
+ map.forEach((TestConfiguration id, Map<int, TestFailure> failures) {
+ if (!errorIds.contains(id)) return;
+ sb.write('$id\n');
+ for (int buildNumber = firstBuildNumber;
+ buildNumber >= lastBuildNumber;
+ buildNumber--) {
+ TestFailure failure = failures[buildNumber];
+ sb.write(padRight(' ${buildNumber}: ', 8));
+ if (failure != null) {
+ sb.write(padRight(failure.expected, 10));
+ sb.write(' / ');
+ sb.write(padRight(failure.actual, 10));
+ } else {
+ sb.write(' ' * 10);
+ sb.write(' / ');
+ sb.write(padRight('-- OK --', 10));
+ }
+ sb.write('\n');
+ }
+ sb.write('\n');
+ });
+ }
+ return sb.toString();
+}
+
+/// The results of a build step.
+class BuildResult {
+ final BuildUri buildUri;
+ final List<TestFailure> _failures;
+ final List<Timing> _timings;
+
+ BuildResult(this.buildUri, this._failures, this._timings);
+
+ /// `true` of the build result has test failures.
+ bool get hasFailures => _failures.isNotEmpty;
+
+ /// Returns the top-20 timings found in the build log.
+ Iterable<Timing> get timings => _timings;
+
+ /// Returns the [TestFailure]s for tests that timed out.
+ Iterable<TestFailure> get timeouts {
+ return _failures
+ .where((TestFailure failure) => failure.actual == 'Timeout');
+ }
+
+ /// Returns the [TestFailure]s for failing tests that did not time out.
+ Iterable<TestFailure> get errors {
+ return _failures
+ .where((TestFailure failure) => failure.actual != 'Timeout');
+ }
+
+ String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.write('$buildUri\n');
+ sb.write('Failures:\n${_failures.join('\n-----\n')}\n');
+ sb.write('\nTimings:\n${_timings.join('\n')}');
+ return sb.toString();
+ }
+}
+
+/// The [Uri] of a build step stdio log split into its subparts.
+class BuildUri {
+ final String scheme;
+ final String host;
+ final String prefix;
+ final String botName;
+ final int buildNumber;
+ final String stepName;
+ final String suffix;
+
+ factory BuildUri(Uri uri) {
+ String scheme = uri.scheme;
+ String host = uri.host;
+ List<String> parts =
+ split(uri.path, ['/builders/', '/builds/', '/steps/', '/logs/']);
+ String prefix = parts[0];
+ String botName = parts[1];
+ int buildNumber = int.parse(parts[2]);
+ String stepName = parts[3];
+ String suffix = parts[4];
+ return new BuildUri.internal(
+ scheme, host, prefix, botName, buildNumber, stepName, suffix);
+ }
+
+ BuildUri.internal(this.scheme, this.host, this.prefix, this.botName,
+ this.buildNumber, this.stepName, this.suffix);
+
+ String get buildName =>
+ '/builders/$botName/builds/$buildNumber/steps/$stepName';
+
+ String get path => '$prefix$buildName/logs/$suffix';
+
+ /// Creates the [Uri] for this build step stdio log.
+ Uri toUri() {
+ return new Uri(
+ scheme: scheme,
+ host: host,
+ path: path);
+ }
+
+ /// Returns the [BuildUri] the previous build of this build step.
+ BuildUri prev() {
+ return new BuildUri.internal(
+ scheme, host, prefix, botName, buildNumber - 1, stepName, suffix);
+ }
+
+ String toString() {
+ return buildName;
+ }
+}
+
+/// Id for a test on a specific configuration, for instance
+/// `dart2js-chrome release_x64/co19/Language/Metadata/before_function_t07`.
+class TestConfiguration {
+ final String configName;
+ final String testName;
+
+ TestConfiguration(this.configName, this.testName);
+
+ String toString() {
+ return '$configName $testName';
+ }
+
+ int get hashCode => configName.hashCode * 17 + testName.hashCode * 19;
+
+ bool operator ==(other) {
+ if (identical(this, other)) return true;
+ if (other is! TestConfiguration) return false;
+ return configName == other.configName && testName == other.testName;
+ }
+}
+
+/// Test failure data derived from the test failure summary in the build step
+/// stdio log.
+class TestFailure {
+ final BuildUri uri;
+ final TestConfiguration id;
+ final String expected;
+ final String actual;
+ final String text;
+
+ factory TestFailure(BuildUri uri, List<String> lines) {
+ List<String> parts = split(lines.first, ['FAILED: ', ' ', ' ']);
+ String configName = parts[1];
+ String archName = parts[2];
+ String testName = parts[3];
+ TestConfiguration id =
+ new TestConfiguration(configName, '$archName/$testName');
+ String expected = split(lines[1], ['Expected: '])[1];
+ String actual = split(lines[2], ['Actual: '])[1];
+ return new TestFailure.internal(
+ uri, id, expected, actual, lines.skip(3).join('\n'));
+ }
+
+ TestFailure.internal(
+ this.uri, this.id, this.expected, this.actual, this.text);
+
+ String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.write('FAILED: $id\n');
+ sb.write('Expected: $expected\n');
+ sb.write('Actual: $actual\n');
+ sb.write(text);
+ return sb.toString();
+ }
+}
+
+/// Id for a single test step, for instance the compilation and run steps of
+/// a test.
+class TestStep {
+ final String stepName;
+ final TestConfiguration id;
+
+ TestStep(this.stepName, this.id);
+
+ String toString() {
+ return '$stepName - $id';
+ }
+
+ int get hashCode => stepName.hashCode * 13 + id.hashCode * 17;
+
+ bool operator ==(other) {
+ if (identical(this, other)) return true;
+ if (other is! TestStep) return false;
+ return stepName == other.stepName && id == other.id;
+ }
+}
+
+/// The timing result for a single test step.
+class Timing {
+ final BuildUri uri;
+ final String time;
+ final TestStep step;
+
+ Timing(this.uri, this.time, this.step);
+
+ String toString() {
+ return '$time - $step';
+ }
+}
+
+/// Create the [Timing]s for the [line] as found in the top-20 timings of a
+/// build step stdio log.
+List<Timing> parseTimings(BuildUri uri, String line) {
+ List<String> parts = split(line, [' - ', ' - ', ' ']);
+ String time = parts[0];
+ String stepName = parts[1];
+ String configName = parts[2];
+ String testNames = parts[3];
+ List<Timing> timings = <Timing>[];
+ for (String testName in testNames.split(',')) {
+ timings.add(new Timing(uri, time,
+ new TestStep(stepName,
+ new TestConfiguration(configName, testName.trim()))));
+ }
+ return timings;
+}
+
+/// Split [text] using [infixes] as infix markers.
+List<String> split(String text, List<String> infixes) {
+ List<String> result = <String>[];
+ int start = 0;
+ for (String infix in infixes) {
+ int index = text.indexOf(infix, start);
+ if (index == -1)
+ throw "'$infix' not found in '$text' from offset ${start}.";
+ result.add(text.substring(start, index));
+ start = index + infix.length;
+ }
+ result.add(text.substring(start));
+ return result;
+}
+
+/// Pad [text] with spaces to the right to fit [length].
+String padRight(String text, int length) {
+ if (text.length < length) return '${text}${' ' * (length - text.length)}';
+ return text;
+}
+
+void log(String text) {
+ print(text);
+}
« 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