add a line to re-run the failed test after each failure in the compact reporter (#1736)

Closes https://github.com/dart-lang/test/issues/1734

Example output:

<img width="906" alt="Screen Shot 2022-06-28 at 11 00 32 AM" src="https://user-images.githubusercontent.com/984921/176251403-63a99464-6de2-42ad-a85e-1c794d434c00.png">
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index 0626919..c613763 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 * Make the labels for test loading more readable in the compact and expanded
   reporters, use gray instead of black.
+* Print a command to re-run the failed test after each failure in the compact
+  reporter.
 * Fix the package config path used when running pre-compiled vm tests.
 
 ## 1.21.3
diff --git a/pkgs/test/test/runner/compact_reporter_test.dart b/pkgs/test/test/runner/compact_reporter_test.dart
index b8ef11a..ba0ff02 100644
--- a/pkgs/test/test/runner/compact_reporter_test.dart
+++ b/pkgs/test/test/runner/compact_reporter_test.dart
@@ -5,7 +5,9 @@
 @TestOn('vm')
 
 import 'dart:async';
+import 'dart:io';
 
+import 'package:path/path.dart' as p;
 import 'package:test/test.dart';
 import 'package:test_descriptor/test_descriptor.dart' as d;
 
@@ -426,6 +428,42 @@
         For example, 'dart test --chain-stack-traces'.''',
         chainStackTraces: false);
   });
+
+  group('gives users a way to re-run failed tests', () {
+    final executablePath = p.absolute(Platform.resolvedExecutable);
+
+    test('with simple names', () {
+      return _expectReport('''
+        test('failure', () {
+          expect(1, equals(2));
+        });''', '''
+        +0: loading test.dart
+        +0: failure
+        +0 -1: failure [E]
+          Expected: <2>
+            Actual: <1>
+
+        To run this test again: $executablePath test test.dart -p vm --plain-name 'failure'
+
+        +0 -1: Some tests failed.''');
+    });
+
+    test('escapes names containing single quotes', () {
+      return _expectReport('''
+        test("failure with a ' in the name", () {
+          expect(1, equals(2));
+        });''', '''
+        +0: loading test.dart
+        +0: failure with a ' in the name
+        +0 -1: failure with a ' in the name [E]
+          Expected: <2>
+            Actual: <1>
+
+        To run this test again: $executablePath test test.dart -p vm --plain-name 'failure with a '\\'' in the name'
+
+        +0 -1: Some tests failed.''');
+    });
+  });
 }
 
 Future<void> _expectReport(String tests, String expected,
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index 6728e44..f94fbd1 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 * Make the labels for test loading more readable in the compact and expanded
   reporters, use gray instead of black.
+* Print a command to re-run the failed test after each failure in the compact
+  reporter.
 * Fix the package config path used when running pre-compiled vm tests.
 
 ## 0.4.15
diff --git a/pkgs/test_core/lib/src/runner/reporter/compact.dart b/pkgs/test_core/lib/src/runner/reporter/compact.dart
index 509ff36..d4a1ba7 100644
--- a/pkgs/test_core/lib/src/runner/reporter/compact.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/compact.dart
@@ -40,6 +40,10 @@
   /// Windows or not outputting to a terminal.
   final String _gray;
 
+  /// The terminal escape code for cyan text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _cyan;
+
   /// The terminal escape for bold text, or the empty string if this is
   /// Windows or not outputting to a terminal.
   final String _bold;
@@ -132,6 +136,7 @@
         _red = color ? '\u001b[31m' : '',
         _yellow = color ? '\u001b[33m' : '',
         _gray = color ? '\u001b[90m' : '',
+        _cyan = color ? '\u001b[36m' : '',
         _bold = color ? '\u001b[1m' : '',
         _noColor = color ? '\u001b[0m' : '' {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
@@ -214,6 +219,16 @@
       if (message.type == MessageType.skip) text = '  $_yellow$text$_noColor';
       _sink.writeln(text);
     }));
+
+    liveTest.onComplete.then((_) {
+      var result = liveTest.state.result;
+      if (result != Result.error && result != Result.failure) return;
+      _sink.writeln('');
+      _sink.writeln('$_bold${_cyan}To run this test again:$_noColor '
+          '${Platform.executable} test ${liveTest.suite.path} '
+          '-p ${liveTest.suite.platform.runtime.identifier} '
+          "--plain-name '${liveTest.test.name.replaceAll("'", r"'\''")}'");
+    });
   }
 
   /// A callback called when [liveTest]'s state becomes [state].