| 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);
|
| +}
|
|
|