Version 2.15.0-162.0.dev

Merge commit 'b846a20e2637d54757251f7f42bc9ad423632cea' into 'dev'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 036e134..e61db67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -388,6 +388,17 @@
   `dart pub get/upgrade/downgrade/add/remove` that will result in the `example/`
   folder dependencies to be updated after operating in the current directory.
 
+## 2.14.3 - 2021-09-30
+
+This is a patch release that fixes:
+
+- a code completion performance regression [flutter/flutter-intellij#5761][].
+- debug information emitted by the Dart VM [#47289][].
+
+[flutter/flutter-intellij#5761]:
+  https://github.com/flutter/flutter-intellij/issues/5761
+[#47289]: https://github.com/dart-lang/sdk/issues/47289
+
 ## 2.14.2 - 2021-09-16
 
 This is a patch release that fixes:
diff --git a/pkg/test_runner/bin/compare_results.dart b/pkg/test_runner/bin/compare_results.dart
new file mode 100755
index 0000000..39eb614
--- /dev/null
+++ b/pkg/test_runner/bin/compare_results.dart
@@ -0,0 +1,331 @@
+#!/usr/bin/env dart
+// Copyright (c) 2018, 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.
+
+// Compare the old and new test results and list tests that pass the filters.
+// The output contains additional details in the verbose mode. There is a human
+// readable mode that explains the results and how they changed.
+
+// @dart = 2.9
+
+import 'dart:collection';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:test_runner/bot_results.dart';
+
+class Event {
+  final Result before;
+  final Result after;
+
+  Event(this.before, this.after);
+
+  bool get isNew => before == null;
+  bool get isNewPassing => before == null && after.matches;
+  bool get isNewFailing => before == null && !after.matches;
+  bool get changed => !unchanged;
+  bool get unchanged =>
+      before != null &&
+      before.outcome == after.outcome &&
+      before.expectation == after.expectation;
+  bool get remainedPassing => before.matches && after.matches;
+  bool get remainedFailing => !before.matches && !after.matches;
+  bool get flaked => after.flaked;
+  bool get fixed => !before.matches && after.matches;
+  bool get broke => before.matches && !after.matches;
+
+  String get description {
+    if (isNewPassing) {
+      return "is new and succeeded";
+    } else if (isNewFailing) {
+      return "is new and failed";
+    } else if (remainedPassing) {
+      return "succeeded again";
+    } else if (remainedFailing) {
+      return "failed again";
+    } else if (fixed) {
+      return "was fixed";
+    } else if (broke) {
+      return "broke";
+    } else {
+      throw Exception("Unreachable");
+    }
+  }
+}
+
+class Options {
+  Options(this._options);
+
+  final ArgResults _options;
+
+  bool get changed => _options["changed"] as bool;
+  int get count => _options["count"] is String
+      ? int.parse(_options["count"] as String)
+      : null;
+  String get flakinessData => _options["flakiness-data"] as String;
+  bool get help => _options["help"] as bool;
+  bool get human => _options["human"] as bool;
+  bool get judgement => _options["judgement"] as bool;
+  String get logs => _options["logs"] as String;
+  bool get logsOnly => _options["logs-only"] as bool;
+  Iterable<String> get statusFilter => ["passing", "flaky", "failing"]
+      .where((option) => _options[option] as bool);
+  bool get unchanged => _options["unchanged"] as bool;
+  bool get verbose => _options["verbose"] as bool;
+  List<String> get rest => _options.rest;
+}
+
+bool firstSection = true;
+
+bool search(
+    String description,
+    String searchForStatus,
+    List<Event> events,
+    Options options,
+    Map<String, Map<String, dynamic>> logs,
+    List<String> logSection) {
+  var judgement = false;
+  var beganSection = false;
+  var count = options.count;
+  final configurations =
+      events.map((event) => event.after.configuration).toSet();
+  for (final event in events) {
+    if (searchForStatus == "passing" &&
+        (event.after.flaked || !event.after.matches)) {
+      continue;
+    }
+    if (searchForStatus == "flaky" && !event.after.flaked) {
+      continue;
+    }
+    if (searchForStatus == "failing" &&
+        (event.after.flaked || event.after.matches)) {
+      continue;
+    }
+    if (options.unchanged && !event.unchanged) continue;
+    if (options.changed && !event.changed) continue;
+    if (!beganSection) {
+      if (options.human && !options.logsOnly) {
+        if (!firstSection) {
+          print("");
+        }
+        firstSection = false;
+        print("$description\n");
+      }
+    }
+    beganSection = true;
+    final before = event.before;
+    final after = event.after;
+    // The --flaky option is used to get a list of tests to deflake within a
+    // single named configuration. Therefore we can't right now always emit
+    // the configuration name, so only do it if there's more than one in the
+    // results being compared (that won't happen during deflaking.
+    final name =
+        configurations.length == 1 ? event.after.name : event.after.key;
+    if (!after.flaked && !after.matches) {
+      judgement = true;
+    }
+    if (count != null) {
+      if (--count <= 0) {
+        if (options.human) {
+          print("(And more)");
+        }
+        break;
+      }
+    }
+    String output;
+    if (options.verbose) {
+      if (options.human) {
+        final expect = after.matches ? "" : ", expected ${after.expectation}";
+        if (before == null || before.outcome == after.outcome) {
+          output = "$name ${event.description} "
+              "(${event.after.outcome}$expect)";
+        } else {
+          output = "$name ${event.description} "
+              "(${event.before?.outcome} -> ${event.after.outcome}$expect)";
+        }
+      } else {
+        output = "$name ${before?.outcome} ${after.outcome} "
+            "${before?.expectation} ${after.expectation} "
+            "${before?.matches} ${after.matches} "
+            "${before?.flaked} ${after.flaked}";
+      }
+    } else {
+      output = name;
+    }
+    final log = logs[event.after.key];
+    final bar = '=' * (output.length + 2);
+    if (log != null) {
+      logSection?.add("\n\n/$bar\\\n| $output |\n\\$bar/\n\n${log["log"]}");
+    }
+    if (!options.logsOnly) {
+      print(output);
+    }
+  }
+
+  return judgement;
+}
+
+main(List<String> args) async {
+  final parser = ArgParser();
+  parser.addFlag("changed",
+      abbr: 'c',
+      negatable: false,
+      help: "Show only tests that changed results.");
+  parser.addOption("count",
+      abbr: "C",
+      help: "Upper limit on how many tests to report in each section");
+  parser.addFlag("failing",
+      abbr: 'f', negatable: false, help: "Show failing tests.");
+  parser.addOption("flakiness-data",
+      abbr: 'd', help: "File containing flakiness data");
+  parser.addFlag("judgement",
+      abbr: 'j',
+      negatable: false,
+      help: "Exit 1 only if any of the filtered results failed.");
+  parser.addFlag("flaky",
+      abbr: 'F', negatable: false, help: "Show flaky tests.");
+  parser.addFlag("help", help: "Show the program usage.", negatable: false);
+  parser.addFlag("human", abbr: "h", negatable: false);
+  parser.addFlag("passing",
+      abbr: 'p', negatable: false, help: "Show passing tests.");
+  parser.addFlag("unchanged",
+      abbr: 'u',
+      negatable: false,
+      help: "Show only tests with unchanged results.");
+  parser.addFlag("verbose",
+      abbr: "v",
+      help: "Show the old and new result for each test",
+      negatable: false);
+  parser.addOption("logs",
+      abbr: "l", help: "Path to file holding logs of failing and flaky tests.");
+  parser.addFlag("logs-only",
+      help: "Only print logs of failing and flaky tests, no other output",
+      negatable: false);
+
+  final options = Options(parser.parse(args));
+  if (options.help) {
+    print("""
+Usage: compare_results.dart [OPTION]... BEFORE AFTER
+Compare the old and new test results and list tests that pass the filters.
+All tests are listed if no filters are given.
+
+The options are as follows:
+
+${parser.usage}""");
+    return;
+  }
+
+  if (options.changed && options.unchanged) {
+    print(
+        "error: The options --changed and --unchanged are mutually exclusive");
+    exitCode = 2;
+    return;
+  }
+
+  final parameters = options.rest;
+  if (parameters.length != 2) {
+    print("error: Expected two parameters "
+        "(results before, results after)");
+    exitCode = 2;
+    return;
+  }
+
+  // Load the input and the flakiness data if specified.
+  final before = await loadResultsMap(parameters[0]);
+  final after = await loadResultsMap(parameters[1]);
+  final logs = options.logs == null
+      ? <String, Map<String, dynamic>>{}
+      : await loadResultsMap(options.logs);
+  final flakinessData = options.flakinessData != null
+      ? await loadResultsMap(options.flakinessData)
+      : <String, Map<String, dynamic>>{};
+
+  // The names of every test that has a data point in the new data set.
+  final names = SplayTreeSet<String>.from(after.keys);
+
+  final events = <Event>[];
+  for (final name in names) {
+    final mapBefore = before[name];
+    final mapAfter = after[name];
+    final resultBefore = mapBefore != null
+        ? Result.fromMap(mapBefore, flakinessData[name])
+        : null;
+    final resultAfter = Result.fromMap(mapAfter, flakinessData[name]);
+    final event = Event(resultBefore, resultAfter);
+    events.add(event);
+  }
+
+  final filterDescriptions = {
+    "passing": {
+      "unchanged": "continued to pass",
+      "changed": "began passing",
+      null: "passed",
+    },
+    "flaky": {
+      "unchanged": "are known to flake but didn't",
+      "changed": "flaked",
+      null: "are known to flake",
+    },
+    "failing": {
+      "unchanged": "continued to fail",
+      "changed": "began failing",
+      null: "failed",
+    },
+    "any": {
+      "unchanged": "had the same result",
+      "changed": "changed result",
+      null: "ran",
+    },
+  };
+
+  final searchForStatuses = options.statusFilter;
+
+  // Report tests matching the filters.
+  final logSection = <String>[];
+  var judgement = false;
+  for (final searchForStatus
+      in searchForStatuses.isNotEmpty ? searchForStatuses : <String>["any"]) {
+    final searchForChanged = options.unchanged
+        ? "unchanged"
+        : options.changed
+            ? "changed"
+            : null;
+    final aboutStatus = filterDescriptions[searchForStatus][searchForChanged];
+    final sectionHeader = "The following tests $aboutStatus:";
+    final logSectionArg =
+        searchForStatus == "failing" || searchForStatus == "flaky"
+            ? logSection
+            : null;
+    final possibleJudgement = search(
+        sectionHeader, searchForStatus, events, options, logs, logSectionArg);
+    if (searchForStatus == null || searchForStatus == "failing") {
+      judgement = possibleJudgement;
+    }
+  }
+
+  if (logSection.isNotEmpty) {
+    print(logSection.join());
+  }
+  // Exit 1 only if --judgement and any test failed.
+  if (options.judgement) {
+    if (options.human && !options.logsOnly && !firstSection) {
+      print("");
+    }
+    var oldNew = options.unchanged
+        ? "old "
+        : options.changed
+            ? "new "
+            : "";
+    if (judgement) {
+      if (options.human && !options.logsOnly) {
+        print("There were ${oldNew}test failures.");
+      }
+      exitCode = 1;
+    } else {
+      if (options.human && !options.logsOnly) {
+        print("No ${oldNew}test failures were found.");
+      }
+    }
+  }
+}
diff --git a/pkg/test_runner/test/compare_results/compare_results_test.dart b/pkg/test_runner/test/compare_results/compare_results_test.dart
new file mode 100644
index 0000000..414ac6f
--- /dev/null
+++ b/pkg/test_runner/test/compare_results/compare_results_test.dart
@@ -0,0 +1,205 @@
+// Copyright (c) 2021, 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.
+
+// Test that compare results works as expected.
+
+// @dart = 2.9
+
+import 'package:expect/expect.dart';
+import 'package:test_runner/bot_results.dart';
+import '../../bin/compare_results.dart';
+
+void main() {
+  testEvent();
+}
+
+void testEvent() {
+  var passingResult = _result();
+  var sameResult = Event(passingResult, passingResult);
+  _expectEvent(sameResult,
+      isNew: false,
+      isNewPassing: false,
+      isNewFailing: false,
+      changed: false,
+      unchanged: true,
+      remainedPassing: true,
+      remainedFailing: false,
+      fixed: false,
+      broke: false,
+      description: 'succeeded again');
+
+  var failingResult =
+      _result(matches: false, outcome: 'Fail', previousOutcome: 'Fail');
+  var sameFailingResult = Event(failingResult, failingResult);
+  _expectEvent(sameFailingResult,
+      isNew: false,
+      isNewPassing: false,
+      isNewFailing: false,
+      changed: false,
+      unchanged: true,
+      remainedPassing: false,
+      remainedFailing: true,
+      fixed: false,
+      broke: false,
+      description: 'failed again');
+
+  var regression = Event(passingResult, failingResult);
+  _expectEvent(regression,
+      isNew: false,
+      isNewPassing: false,
+      isNewFailing: false,
+      changed: true,
+      unchanged: false,
+      remainedPassing: false,
+      remainedFailing: false,
+      fixed: false,
+      broke: true,
+      description: 'broke');
+
+  var differentFailingResult =
+      _result(matches: false, outcome: 'Error', previousOutcome: 'Error');
+  var differentFailure = Event(failingResult, differentFailingResult);
+  _expectEvent(differentFailure,
+      isNew: false,
+      isNewPassing: false,
+      isNewFailing: false,
+      changed: true,
+      unchanged: false,
+      remainedPassing: false,
+      remainedFailing: true,
+      fixed: false,
+      broke: false,
+      description: 'failed again');
+
+  var fixed = Event(failingResult, passingResult);
+  _expectEvent(fixed,
+      isNew: false,
+      isNewPassing: false,
+      isNewFailing: false,
+      changed: true,
+      unchanged: false,
+      remainedPassing: false,
+      remainedFailing: false,
+      fixed: true,
+      broke: false,
+      description: 'was fixed');
+
+  var newPass = Event(null, passingResult);
+  _expectEvent(newPass,
+      isNew: true,
+      isNewPassing: true,
+      isNewFailing: false,
+      changed: true,
+      unchanged: false,
+      description: 'is new and succeeded');
+
+  var newFailure = Event(null, failingResult);
+  _expectEvent(newFailure,
+      isNew: true,
+      isNewPassing: false,
+      isNewFailing: true,
+      changed: true,
+      unchanged: false,
+      description: 'is new and failed');
+
+  var flakyResult = _result(flaked: true);
+  var becameFlaky = Event(passingResult, flakyResult);
+  _expectEvent(becameFlaky, flaked: true);
+
+  var noLongerFlaky = Event(flakyResult, passingResult);
+  _expectEvent(noLongerFlaky, flaked: false);
+
+  var failingExpectedToFailResult = _result(
+      matches: true,
+      outcome: 'Fail',
+      previousOutcome: 'Fail',
+      expectation: 'Fail');
+  var nowMeetingExpectation = Event(failingResult, failingExpectedToFailResult);
+  _expectEvent(nowMeetingExpectation,
+      changed: true,
+      unchanged: false,
+      remainedPassing: false,
+      remainedFailing: false,
+      broke: false,
+      description: 'was fixed');
+
+  var passingExpectedToFailResult = _result(
+      matches: false,
+      outcome: 'Pass',
+      previousOutcome: 'Pass',
+      expectation: 'Fail');
+  var noLongerMeetingExpectation =
+      Event(passingResult, passingExpectedToFailResult);
+  _expectEvent(noLongerMeetingExpectation,
+      changed: true,
+      unchanged: false,
+      remainedPassing: false,
+      remainedFailing: false,
+      broke: true,
+      description: 'broke');
+}
+
+void _expectEvent(Event actual,
+    {bool isNew,
+    bool isNewPassing,
+    bool isNewFailing,
+    bool changed,
+    bool unchanged,
+    bool remainedPassing,
+    bool remainedFailing,
+    bool flaked,
+    bool fixed,
+    bool broke,
+    String description}) {
+  if (isNew != null) {
+    Expect.equals(isNew, actual.isNew, 'isNew mismatch');
+  }
+  if (isNewPassing != null) {
+    Expect.equals(isNewPassing, actual.isNewPassing, 'isNewPassing mismatch');
+  }
+  if (isNewFailing != null) {
+    Expect.equals(isNewFailing, actual.isNewFailing, 'isNewFailing mismatch');
+  }
+  if (changed != null) {
+    Expect.equals(changed, actual.changed, 'changed mismatch');
+  }
+  if (unchanged != null) {
+    Expect.equals(unchanged, actual.unchanged, 'unchanged mismatch');
+  }
+  if (remainedPassing != null) {
+    Expect.equals(
+        remainedPassing, actual.remainedPassing, 'remainedPassing mismatch');
+  }
+  if (remainedFailing != null) {
+    Expect.equals(
+        remainedFailing, actual.remainedFailing, 'remainedFailing mismatch');
+  }
+  if (flaked != null) {
+    Expect.equals(flaked, actual.flaked, 'flaked mismatch');
+  }
+  if (fixed != null) {
+    Expect.equals(fixed, actual.fixed, 'fixed mismatch');
+  }
+  if (broke != null) {
+    Expect.equals(broke, actual.broke, 'broke mismatch');
+  }
+  if (description != null) {
+    Expect.equals(description, actual.description, 'description mismatch');
+  }
+}
+
+Result _result(
+    {String configuration = 'config',
+    String expectation = 'Pass',
+    bool matches = true,
+    String name = 'test1',
+    String outcome = 'Pass',
+    bool changed = false,
+    String commitHash = 'abcdabcd',
+    bool flaked = false,
+    bool isFlaky = false,
+    String previousOutcome = 'Pass'}) {
+  return Result(configuration, name, outcome, expectation, matches, changed,
+      commitHash, isFlaky, previousOutcome, flaked);
+}
diff --git a/tools/VERSION b/tools/VERSION
index 23ba9f3..25ca929 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 161
+PRERELEASE 162
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/bots/compare_results.dart b/tools/bots/compare_results.dart
index 9edd0f8..bcf9abe52 100755
--- a/tools/bots/compare_results.dart
+++ b/tools/bots/compare_results.dart
@@ -9,299 +9,8 @@
 
 // @dart = 2.9
 
-import 'dart:collection';
-import 'dart:io';
+import '../../pkg/test_runner/bin/compare_results.dart' as compareResults;
 
-import 'package:args/args.dart';
-import 'package:test_runner/bot_results.dart';
-
-class Event {
-  final Result before;
-  final Result after;
-
-  Event(this.before, this.after);
-
-  bool get isNew => before == null;
-  bool get isNewPassing => before == null && after.matches;
-  bool get isNewFailing => before == null && !after.matches;
-  bool get changed => !unchanged;
-  bool get unchanged => before != null && before.outcome == after.outcome;
-  bool get remainedPassing => before.matches && after.matches;
-  bool get remainedFailing => !before.matches && !after.matches;
-  bool get flaked => after.flaked;
-  bool get fixed => !before.matches && after.matches;
-  bool get broke => before.matches && !after.matches;
-
-  String get description {
-    if (isNewPassing) {
-      return "is new and succeeded";
-    } else if (isNewFailing) {
-      return "is new and failed";
-    } else if (remainedPassing) {
-      return "succeeded again";
-    } else if (remainedFailing) {
-      return "failed again";
-    } else if (fixed) {
-      return "was fixed";
-    } else if (broke) {
-      return "broke";
-    } else {
-      throw new Exception("Unreachable");
-    }
-  }
-}
-
-bool firstSection = true;
-
-bool search(
-    String description,
-    String searchForStatus,
-    List<Event> events,
-    ArgResults options,
-    Map<String, Map<String, dynamic>> logs,
-    List<String> logSection) {
-  bool judgement = false;
-  bool beganSection = false;
-  int count = options["count"] != null ? int.parse(options["count"]) : null;
-  final configurations =
-      events.map((event) => event.after.configuration).toSet();
-  for (final event in events) {
-    if (searchForStatus == "passing" &&
-        (event.after.flaked || !event.after.matches)) {
-      continue;
-    }
-    if (searchForStatus == "flaky" && !event.after.flaked) {
-      continue;
-    }
-    if (searchForStatus == "failing" &&
-        (event.after.flaked || event.after.matches)) {
-      continue;
-    }
-    if (options["unchanged"] && !event.unchanged) continue;
-    if (options["changed"] && !event.changed) continue;
-    if (!beganSection) {
-      if (options["human"] && !options["logs-only"]) {
-        if (!firstSection) {
-          print("");
-        }
-        firstSection = false;
-        print("$description\n");
-      }
-    }
-    beganSection = true;
-    final before = event.before;
-    final after = event.after;
-    // The --flaky option is used to get a list of tests to deflake within a
-    // single named configuration. Therefore we can't right now always emit
-    // the configuration name, so only do it if there's more than one in the
-    // results being compared (that won't happen during deflaking.
-    final name =
-        configurations.length == 1 ? event.after.name : event.after.key;
-    if (!after.flaked && !after.matches) {
-      judgement = true;
-    }
-    if (count != null) {
-      if (--count <= 0) {
-        if (options["human"]) {
-          print("(And more)");
-        }
-        break;
-      }
-    }
-    String output;
-    if (options["verbose"]) {
-      if (options["human"]) {
-        String expect = after.matches ? "" : ", expected ${after.expectation}";
-        if (before == null || before.outcome == after.outcome) {
-          output = "$name ${event.description} "
-              "(${event.after.outcome}${expect})";
-        } else {
-          output = "$name ${event.description} "
-              "(${event.before?.outcome} -> ${event.after.outcome}${expect})";
-        }
-      } else {
-        output = "$name ${before?.outcome} ${after.outcome} "
-            "${before?.expectation} ${after.expectation} "
-            "${before?.matches} ${after.matches} "
-            "${before?.flaked} ${after.flaked}";
-      }
-    } else {
-      output = name;
-    }
-    final log = logs[event.after.key];
-    final bar = '=' * (output.length + 2);
-    if (log != null) {
-      logSection?.add("\n\n/$bar\\\n| $output |\n\\$bar/\n\n${log["log"]}");
-    }
-    if (!options["logs-only"]) {
-      print(output);
-    }
-  }
-
-  return judgement;
-}
-
-main(List<String> args) async {
-  final parser = new ArgParser();
-  parser.addFlag("changed",
-      abbr: 'c',
-      negatable: false,
-      help: "Show only tests that changed results.");
-  parser.addOption("count",
-      abbr: "C",
-      help: "Upper limit on how many tests to report in each section");
-  parser.addFlag("failing",
-      abbr: 'f', negatable: false, help: "Show failing tests.");
-  parser.addOption("flakiness-data",
-      abbr: 'd', help: "File containing flakiness data");
-  parser.addFlag("judgement",
-      abbr: 'j',
-      negatable: false,
-      help: "Exit 1 only if any of the filtered results failed.");
-  parser.addFlag("flaky",
-      abbr: 'F', negatable: false, help: "Show flaky tests.");
-  parser.addFlag("help", help: "Show the program usage.", negatable: false);
-  parser.addFlag("human", abbr: "h", negatable: false);
-  parser.addFlag("passing",
-      abbr: 'p', negatable: false, help: "Show passing tests.");
-  parser.addFlag("unchanged",
-      abbr: 'u',
-      negatable: false,
-      help: "Show only tests with unchanged results.");
-  parser.addFlag("verbose",
-      abbr: "v",
-      help: "Show the old and new result for each test",
-      negatable: false);
-  parser.addOption("logs",
-      abbr: "l", help: "Path to file holding logs of failing and flaky tests.");
-  parser.addFlag("logs-only",
-      help: "Only print logs of failing and flaky tests, no other output",
-      negatable: false);
-
-  final options = parser.parse(args);
-  if (options["help"]) {
-    print("""
-Usage: compare_results.dart [OPTION]... BEFORE AFTER
-Compare the old and new test results and list tests that pass the filters.
-All tests are listed if no filters are given.
-
-The options are as follows:
-
-${parser.usage}""");
-    return;
-  }
-
-  if (options["changed"] && options["unchanged"]) {
-    print(
-        "error: The options --changed and --unchanged are mutually exclusive");
-    exitCode = 2;
-    return;
-  }
-
-  final parameters = options.rest;
-  if (parameters.length != 2) {
-    print("error: Expected two parameters "
-        "(results before, results after)");
-    exitCode = 2;
-    return;
-  }
-
-  // Load the input and the flakiness data if specified.
-  final before = await loadResultsMap(parameters[0]);
-  final after = await loadResultsMap(parameters[1]);
-  final logs = options['logs'] == null
-      ? <String, Map<String, dynamic>>{}
-      : await loadResultsMap(options['logs']);
-  final flakinessData = options["flakiness-data"] != null
-      ? await loadResultsMap(options["flakiness-data"])
-      : <String, Map<String, dynamic>>{};
-
-  // The names of every test that has a data point in the new data set.
-  final names = new SplayTreeSet<String>.from(after.keys);
-
-  final events = <Event>[];
-  for (final name in names) {
-    final mapBefore = before[name];
-    final mapAfter = after[name];
-    final resultBefore = mapBefore != null
-        ? new Result.fromMap(mapBefore, flakinessData[name])
-        : null;
-    final resultAfter = new Result.fromMap(mapAfter, flakinessData[name]);
-    final event = new Event(resultBefore, resultAfter);
-    events.add(event);
-  }
-
-  final filterDescriptions = {
-    "passing": {
-      "unchanged": "continued to pass",
-      "changed": "began passing",
-      null: "passed",
-    },
-    "flaky": {
-      "unchanged": "are known to flake but didn't",
-      "changed": "flaked",
-      null: "are known to flake",
-    },
-    "failing": {
-      "unchanged": "continued to fail",
-      "changed": "began failing",
-      null: "failed",
-    },
-    "any": {
-      "unchanged": "had the same result",
-      "changed": "changed result",
-      null: "ran",
-    },
-  };
-
-  final searchForStatuses =
-      ["passing", "flaky", "failing"].where((option) => options[option]);
-
-  // Report tests matching the filters.
-  final logSection = <String>[];
-  bool judgement = false;
-  for (final searchForStatus
-      in searchForStatuses.isNotEmpty ? searchForStatuses : <String>["any"]) {
-    final searchForChanged = options["unchanged"]
-        ? "unchanged"
-        : options["changed"]
-            ? "changed"
-            : null;
-    final aboutStatus = filterDescriptions[searchForStatus][searchForChanged];
-    final sectionHeader = "The following tests $aboutStatus:";
-    final logSectionArg =
-        searchForStatus == "failing" || searchForStatus == "flaky"
-            ? logSection
-            : null;
-    bool possibleJudgement = search(
-        sectionHeader, searchForStatus, events, options, logs, logSectionArg);
-    if ((searchForStatus == null || searchForStatus == "failing")) {
-      judgement = possibleJudgement;
-    }
-  }
-
-  if (logSection.isNotEmpty) {
-    print(logSection.join());
-  }
-  // Exit 1 only if --judgement and any test failed.
-  if (options["judgement"]) {
-    if (options["human"] && !options["logs-only"] && !firstSection) {
-      print("");
-    }
-    String oldNew = options["unchanged"]
-        ? "old "
-        : options["changed"]
-            ? "new "
-            : "";
-    if (judgement) {
-      if (options["human"] && !options["logs-only"]) {
-        print("There were ${oldNew}test failures.");
-      }
-      exitCode = 1;
-    } else {
-      if (options["human"] && !options["logs-only"]) {
-        print("No ${oldNew}test failures were found.");
-      }
-    }
-  }
+main(List<String> args) {
+  compareResults.main(args);
 }
diff --git a/tools/bots/extend_results.dart b/tools/bots/extend_results.dart
index 7dc7743..4ab1d2c 100644
--- a/tools/bots/extend_results.dart
+++ b/tools/bots/extend_results.dart
@@ -56,9 +56,12 @@
     }
     if (priorResult != null) {
       result['previous_result'] = priorResult['result'];
+      result['changed'] = !(result['result'] == result['previous_result'] &&
+          result['flaky'] == result['previous_flaky'] &&
+          result['expected'] == priorResult['expected']);
+    } else {
+      result['changed'] = true;
     }
-    result['changed'] = (result['result'] != result['previous_result'] ||
-        result['flaky'] != result['previous_flaky']);
   }
   final sink = new File(newResultsPath).openWrite();
   final sorted = results.keys.toList()..sort();