Version 2.14.0-247.0.dev

Merge commit '2f8cc223d58c7a3a0ace57e09f27766d2ca5904e' into 'dev'
diff --git a/pkg/front_end/test/dartdoc_test_test.dart b/pkg/front_end/test/dartdoc_test_test.dart
new file mode 100644
index 0000000..4cb7814
--- /dev/null
+++ b/pkg/front_end/test/dartdoc_test_test.dart
@@ -0,0 +1,462 @@
+// @dart = 2.9
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:_fe_analyzer_shared/src/scanner/token.dart';
+import 'package:front_end/src/api_prototype/memory_file_system.dart';
+import 'package:front_end/src/fasta/hybrid_file_system.dart';
+
+import "../tool/dart_doctest_impl.dart" as impl;
+
+main() async {
+  expectCategory = "comment extraction";
+  testCommentExtraction();
+
+  expectCategory = "test extraction";
+  testTestExtraction();
+
+  expectCategory = "test runs";
+  await testRunningTests();
+}
+
+void testRunningTests() async {
+  MemoryFileSystem memoryFileSystem =
+      new MemoryFileSystem(new Uri(scheme: "darttest", path: "/"));
+  HybridFileSystem hybridFileSystem = new HybridFileSystem(memoryFileSystem);
+  impl.DartDocTest dartDocTest = new impl.DartDocTest(
+      underlyingFileSystem: hybridFileSystem, silent: true);
+
+  // Good test
+  Uri test1 = new Uri(scheme: "darttest", path: "/test1.dart");
+  String test = """
+  // DartDocTest(1+1, 2)
+  main() {
+    print("Hello from main");
+  }
+
+  // DartDocTest(_internal(), 42)
+  int _internal() {
+    return 42;
+  }
+  """;
+  List<impl.Test> tests = extractTests(test, test1);
+  expect(tests.length, 2);
+  List<impl.TestResult> expected = [
+    new impl.TestResult(tests[0], impl.TestOutcome.Pass),
+    new impl.TestResult(tests[1], impl.TestOutcome.Pass),
+  ];
+  memoryFileSystem.entityForUri(test1).writeAsStringSync(test);
+  expect(await dartDocTest.process(test1), expected);
+
+// Mixed good/bad.
+  Uri test2 = new Uri(scheme: "darttest", path: "/test2.dart");
+  test = """
+// DartDocTest(1+1, 3)
+main() {
+  print("Hello from main");
+}
+
+// DartDocTest(_internal(), 43)
+// DartDocTest(_internal(), 42)
+int _internal() {
+  return 42;
+}
+""";
+  tests = extractTests(test, test2);
+  expect(tests.length, 3);
+  expected = [
+    new impl.TestResult(tests[0], impl.TestOutcome.Failed)
+      ..message = "Expected '3'; got '2'.",
+    new impl.TestResult(tests[1], impl.TestOutcome.Failed)
+      ..message = "Expected '43'; got '42'.",
+    new impl.TestResult(tests[2], impl.TestOutcome.Pass),
+  ];
+  memoryFileSystem.entityForUri(test2).writeAsStringSync(test);
+  expect(await dartDocTest.process(test2), expected);
+
+// Good case using await.
+  Uri test3 = new Uri(scheme: "darttest", path: "/test3.dart");
+  test = """
+// DartDocTest(await _internal(), 42)
+Future<int> _internal() async {
+  await Future.delayed(new Duration(milliseconds: 1));
+  return 42;
+}
+""";
+  tests = extractTests(test, test3);
+  expect(tests.length, 1);
+  expected = [
+    new impl.TestResult(tests[0], impl.TestOutcome.Pass),
+  ];
+  memoryFileSystem.entityForUri(test3).writeAsStringSync(test);
+  expect(await dartDocTest.process(test3), expected);
+
+// One test parse error and one good case.
+  Uri test4 = new Uri(scheme: "darttest", path: "/test4.dart");
+  test = """
+// DartDocTest(_internal() 42)
+// DartDocTest(_internal(), 42)
+int _internal() {
+  return 42;
+}
+""";
+  tests = extractTests(test, test4);
+  expect(tests.length, 2);
+  expected = [
+    new impl.TestResult(tests[0], impl.TestOutcome.TestCompilationError)
+      ..message = "Parse error @ 27",
+    new impl.TestResult(tests[1], impl.TestOutcome.Pass),
+  ];
+  memoryFileSystem.entityForUri(test4).writeAsStringSync(test);
+  expect(await dartDocTest.process(test4), expected);
+
+// Test with compile-time error. Note that this means no tests are compiled at
+// all and that while the error messages are passed it spills the internals of
+// the dartdocs stuff (e.g. points to lines that doesn't exist because we added
+// them, specifies 'dartDocTest.test' etc).
+  Uri test5 = new Uri(scheme: "darttest", path: "/test5.dart");
+  test = """
+// DartDocTest(_internal() + 2, 42)
+// // DartDocTest(2+2, 4)
+void _internal() {
+  return;
+}
+""";
+  tests = extractTests(test);
+  expect(tests.length, 2);
+  expected = [
+    new impl.TestResult(null, impl.TestOutcome.CompilationError)
+      ..message =
+          """darttest:/test5.dart:9:20: Error: This expression has type 'void' and can't be used.
+  dartDocTest.test(_internal() + 2, 42);
+                   ^
+darttest:/test5.dart:9:32: Error: The operator '+' isn't defined for the class 'void'.
+Try correcting the operator to an existing operator, or defining a '+' operator.
+  dartDocTest.test(_internal() + 2, 42);
+                               ^""",
+  ];
+  memoryFileSystem.entityForUri(test5).writeAsStringSync(test);
+  expect(await dartDocTest.process(test5), expected);
+
+  // Test with runtime error.
+  Uri test6 = new Uri(scheme: "darttest", path: "/test6.dart");
+  test = """
+// DartDocTest(_internal() + 2, 42)
+// // DartDocTest(2+2, 4)
+dynamic _internal() {
+  return "hello";
+}
+""";
+  tests = extractTests(test);
+  expect(tests.length, 2);
+  expected = [
+    new impl.TestResult(tests[0], impl.TestOutcome.Crash)
+      // this weird message is from the VM!
+      ..message = "type 'int' is not a subtype of type 'String' of 'other'",
+    new impl.TestResult(tests[1], impl.TestOutcome.Pass),
+  ];
+  memoryFileSystem.entityForUri(test6).writeAsStringSync(test);
+  expect(await dartDocTest.process(test6), expected);
+}
+
+void testTestExtraction() {
+  // No tests.
+  expect(extractTests(""), <impl.Test>[]);
+
+  // One test.
+  expect(extractTests("// DartDocTest(1+1, 2)"), <impl.Test>[
+    new impl.ExpectTest("1+1", "2"),
+  ]);
+
+  // Two tests.
+  expect(extractTests("""
+// DartDocTest(1+1, 2)
+// DartDocTest(2+40, 42)
+"""), <impl.Test>[
+    new impl.ExpectTest("1+1", "2"),
+    new impl.ExpectTest("2+40", "42"),
+  ]);
+
+  // Two valid tests. Four invalid ones.
+  expect(extractTests("""
+// DartDocTest(1+1, 2)
+// DartDocTest(2+40; 42]
+// DartDocTest(2+40+, 42]
+// DartDocTest(2+40, 42]
+// DartDocTest(2+40, 42)
+// DartDocTest(2+40, 42+)
+"""), <impl.Test>[
+    new impl.ExpectTest("1+1", "2"),
+    new impl.TestParseError(
+        """darttest:/foo.dart:2:20: Expected ',' before this.
+// DartDocTest(2+40; 42]
+                   ^""", 42),
+    new impl.TestParseError("""Parse error(s):
+
+darttest:/foo.dart:3:21: Expected an identifier, but got ','.
+// DartDocTest(2+40+, 42]
+                    ^""", 68),
+    new impl.TestParseError(
+        """darttest:/foo.dart:4:24: Expected ')' before this.
+// DartDocTest(2+40, 42]
+                       ^""", 97),
+    new impl.ExpectTest("2+40", "42"),
+    new impl.TestParseError("""Parse error(s):
+
+darttest:/foo.dart:6:25: Expected an identifier, but got ')'.
+// DartDocTest(2+40, 42+)
+                        ^""", 148),
+  ]);
+
+  // Two tests in block comments with back-ticks around tests.
+  expect(extractTests("""
+/**
+ * This is a test:
+ * ```
+ * DartDocTest(1+1, 2)
+ * ```
+ * and so is this:
+ * ```
+ * DartDocTest(2+40, 42)
+ * ```
+ */
+"""), <impl.Test>[
+    new impl.ExpectTest("1+1", "2"),
+    new impl.ExpectTest("2+40", "42"),
+  ]);
+
+  // Two tests --- include linebreaks.
+  expect(extractTests("""
+/*
+  This is a test:
+  DartDocTest(1+1,
+    2)
+  and so is this:
+  DartDocTest(2+
+  40,
+  42)
+ */
+"""), <impl.Test>[
+    new impl.ExpectTest("1+1", "2"),
+    // The linebreak etc here is not stripped (at the moment at least)
+    new impl.ExpectTest("2+\n  40", "42"),
+  ]);
+
+  // Two tests --- with parens and commas as string...
+  expect(extractTests("""
+// DartDocTest("(", "(")
+// and so is this:
+// DartDocTest(",)", ",)")
+"""), <impl.Test>[
+    new impl.ExpectTest('"("', '"("'),
+    new impl.ExpectTest('",)"', '",)"'),
+  ]);
+
+  // Await expression.
+  expect(extractTests("""
+// DartDocTest(await foo(), 42)
+"""), <impl.Test>[
+    new impl.ExpectTest('await foo()', '42'),
+  ]);
+}
+
+void testCommentExtraction() {
+  // No comment
+  expect(extractFirstComment(""), null);
+
+  // Simple line comment at position 0.
+  expect(
+      extractFirstComment("// Hello"), new impl.CommentString("   Hello", 0));
+
+  // Simple line comment at position 5.
+  expect(extractFirstComment("     // Hello"),
+      new impl.CommentString("   Hello", 5));
+
+  // Multiline simple comment at position 20.
+  expect(extractFirstComment("""
+import 'foo.dart';
+
+// This is
+// a
+// multiline
+// comment
+
+import 'bar.dart'"""), new impl.CommentString("""
+   This is
+   a
+   multiline
+   comment""", 20));
+
+  // Multiline simple comment (with 3 slashes) at position 20.
+  expect(extractFirstComment("""
+import 'foo.dart';
+
+/// This is
+/// a
+/// multiline
+/// comment
+
+import 'bar.dart'"""), new impl.CommentString("""
+    This is
+    a
+    multiline
+    comment""", 20));
+
+  // Multiline comments with /* at position 20.
+  expect(extractFirstComment("""
+import 'foo.dart';
+
+/* This is
+a
+ multiline
+comment
+*/
+
+import 'bar.dart'"""), new impl.CommentString("""
+   This is
+a
+ multiline
+comment
+  """, 20));
+
+  // Multiline comments with /* at position 20. Note that the comment has
+  // * at the start of the line but that they aren't stripped because the
+  // comments starts with /* and NOT with /**.
+  expect(extractFirstComment("""
+import 'foo.dart';
+
+/* This is
+*a
+* multiline
+*comment
+*/
+
+import 'bar.dart'"""), new impl.CommentString("""
+   This is
+*a
+* multiline
+*comment
+  """, 20));
+
+  // Multiline comments with /** */ at position 20. Note that the comment has
+  // * at the start of the line and that they are stripped because the
+  // comments starts with /** and NOT with only /*.
+  expect(extractFirstComment("""
+import 'foo.dart';
+
+/** This is
+*a
+* multiline
+*comment
+*/
+
+import 'bar.dart'"""), new impl.CommentString("""
+    This is
+ a
+  multiline
+ comment
+  """, 20));
+
+  // Multiline comment with block comment inside at position 20.
+  // The block comment data is not stripped.
+  expect(extractFirstComment("""
+import 'foo.dart';
+
+/// This is
+/// /*a*/
+/// multiline comment"""), new impl.CommentString("""
+    This is
+    /*a*/
+    multiline comment""", 20));
+}
+
+int expectCalls = 0;
+String expectCategory;
+
+void expect(dynamic actual, dynamic expected) {
+  expectCalls++;
+  StringBuffer sb = new StringBuffer();
+  if (!_expectImpl(actual, expected, sb)) {
+    if (sb.isNotEmpty) {
+      throw "Error! Expected '$expected' but got '$actual'"
+          "\n\n"
+          "Explanation: $sb.";
+    } else {
+      throw "Error! Expected '$expected' but got '$actual'";
+    }
+  }
+  print("Expect #$expectCalls ($expectCategory) OK.");
+}
+
+bool _expectImpl(dynamic actual, dynamic expected, StringBuffer explainer) {
+  if (identical(actual, expected)) return true;
+  if (actual == expected) return true;
+  if (actual == null || expected == null) return false;
+  if (actual is List && expected is List) {
+    if (actual.runtimeType != expected.runtimeType) {
+      explainer.write("List runtimeType difference: "
+          "${actual.runtimeType} vs ${expected.runtimeType}");
+      return false;
+    }
+    if (actual.length != expected.length) {
+      explainer.write("List length difference: "
+          "${actual.length} vs ${expected.length}");
+      return false;
+    }
+    for (int i = 0; i < actual.length; i++) {
+      if (actual[i] != expected[i]) {
+        explainer.write("List difference at index $i: "
+            "${actual[i]} vs ${expected[i]}");
+        return false;
+      }
+    }
+    return true;
+  }
+  if (actual is List || expected is List) return false;
+
+  if (actual is Map && expected is Map) {
+    if (actual.runtimeType != expected.runtimeType) return false;
+    if (actual.length != expected.length) return false;
+    for (dynamic key in actual.keys) {
+      if (!expected.containsKey(key)) return false;
+      if (actual[key] != expected[key]) return false;
+    }
+    return true;
+  }
+  if (actual is Map || expected is Map) return false;
+
+  if (actual is Set && expected is Set) {
+    if (actual.runtimeType != expected.runtimeType) return false;
+    if (actual.length != expected.length) return false;
+    for (dynamic value in actual) {
+      if (!expected.contains(value)) return false;
+    }
+    return true;
+  }
+  if (actual is Set || expected is Set) return false;
+
+  // More stuff?
+  return false;
+}
+
+impl.CommentString extractFirstComment(String test) {
+  Token firstToken = impl.scanRawBytes(utf8.encode(test) as Uint8List);
+  Token token = firstToken;
+  while (true) {
+    CommentToken comment = token.precedingComments;
+    if (comment != null) {
+      return impl.extractComments(comment, test);
+    }
+    if (token.isEof) break;
+    Token next = token.next;
+    if (next == null) break;
+    token = next;
+  }
+  return null;
+}
+
+List<impl.Test> extractTests(String test, [Uri uri]) {
+  return impl.extractTests(utf8.encode(test) as Uint8List,
+      uri ?? new Uri(scheme: "darttest", path: "/foo.dart"));
+}
diff --git a/pkg/front_end/test/dartdoctest_suite.dart b/pkg/front_end/test/dartdoctest_suite.dart
index 77266f1..b8fde00 100644
--- a/pkg/front_end/test/dartdoctest_suite.dart
+++ b/pkg/front_end/test/dartdoctest_suite.dart
@@ -35,7 +35,7 @@
 
   Stream<DartDocTestTestDescription> list(Chain suite) async* {
     await for (TestDescription entry in super.list(suite)) {
-      List<Test> tests = extractTestsFromUri(entry.uri);
+      List<Test> tests = await dartDocTest.extractTestsFromUri(entry.uri);
       if (tests.isEmpty) continue;
       yield new DartDocTestTestDescription(entry.shortName, entry.uri, tests);
     }
@@ -60,9 +60,12 @@
 
   Future<Result<DartDocTestTestDescription>> run(
       DartDocTestTestDescription description, Context context) async {
-    bool result = await context.dartDocTest
+    List<TestResult> result = await context.dartDocTest
         .compileAndRun(description.uri, description.tests);
-    if (result) {
+    bool boolResult = result
+        .map((e) => e.outcome == TestOutcome.Pass)
+        .fold(true, (previousValue, element) => previousValue && element);
+    if (boolResult) {
       return new Result<DartDocTestTestDescription>.pass(description);
     } else {
       return new Result<DartDocTestTestDescription>.fail(description);
diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt
index 2a53afb..2ff813f 100644
--- a/pkg/front_end/test/spell_checking_list_tests.txt
+++ b/pkg/front_end/test/spell_checking_list_tests.txt
@@ -195,10 +195,12 @@
 dart$doc$test$tester
 dart\$doc\$test\$tester
 dartanalyzer
+dartdocs
 dartdoctest
 dartdoctestrun
 dartfile
 dartfmt
+darttest
 dash
 dashes
 day
@@ -290,6 +292,7 @@
 exiting
 expanded
 expansive
+explainer
 extern
 fac
 faking
@@ -714,6 +717,7 @@
 sigint
 signalled
 sigwinch
+slashes
 slight
 sliver
 smoke
@@ -727,6 +731,7 @@
 spelled
 spelling
 spent
+spills
 splay
 splitting
 spurious
@@ -776,6 +781,7 @@
 theoretically
 thereof
 thread
+ticks
 timed
 timeout
 timer
diff --git a/pkg/front_end/tool/dart_doctest_impl.dart b/pkg/front_end/tool/dart_doctest_impl.dart
index 7939fec..5ca2e60 100644
--- a/pkg/front_end/tool/dart_doctest_impl.dart
+++ b/pkg/front_end/tool/dart_doctest_impl.dart
@@ -6,9 +6,18 @@
 import 'dart:convert';
 import 'dart:io';
 import 'dart:isolate';
-
 import 'dart:typed_data';
 
+import 'package:_fe_analyzer_shared/src/parser/parser.dart' show Parser;
+
+import 'package:_fe_analyzer_shared/src/messages/codes.dart' as codes;
+
+import 'package:_fe_analyzer_shared/src/parser/async_modifier.dart'
+    show AsyncModifier;
+
+import 'package:_fe_analyzer_shared/src/parser/forwarding_listener.dart'
+    show NullListener;
+
 import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
     show ScannerConfiguration;
 
@@ -18,11 +27,16 @@
     show Utf8BytesScanner;
 
 import 'package:_fe_analyzer_shared/src/scanner/token.dart' show Token;
+
 import 'package:front_end/src/api_prototype/compiler_options.dart';
 import 'package:front_end/src/api_prototype/experimental_flags.dart';
 import 'package:front_end/src/api_prototype/file_system.dart';
 import 'package:front_end/src/api_prototype/standard_file_system.dart';
 import 'package:front_end/src/base/processed_options.dart';
+
+import 'package:front_end/src/fasta/command_line_reporting.dart'
+    as command_line_reporting;
+
 import 'package:front_end/src/fasta/compiler_context.dart';
 import 'package:front_end/src/fasta/fasta_codes.dart';
 // ignore: import_of_legacy_library_into_null_safe
@@ -39,7 +53,6 @@
 const _portMessageTest = "test";
 const _portMessageGood = "good";
 const _portMessageBad = "bad";
-const _portMessageBadDetails = "badDetails";
 const _portMessageCrash = "crash";
 const _portMessageParseError = "parseError";
 const _portMessageDone = "done";
@@ -52,27 +65,50 @@
   late CompilerOptions options;
   late ProcessedOptions processedOpts;
   bool errors = false;
+  List<String> errorStrings = [];
+  final FileSystem? underlyingFileSystem;
+  final bool silent;
+
+  DartDocTest({this.underlyingFileSystem, this.silent: false});
+
+  FileSystem _getFileSystem() =>
+      underlyingFileSystem ?? StandardFileSystem.instance;
+
+  void _print(Object? object) {
+    if (!silent) print(object);
+  }
 
   /// All-in-one. Process a file and return if it was good.
-  Future<bool> process(Uri uri) async {
-    print("\n\nProcessing $uri");
+  Future<List<TestResult>> process(Uri uri) async {
+    _print("\n\nProcessing $uri");
     Stopwatch stopwatch = new Stopwatch()..start();
 
     // Extract test cases in file.
-    List<Test> tests = extractTestsFromUri(uri);
+    List<Test> tests = await extractTestsFromUri(uri);
 
     if (tests.isEmpty) {
-      print("No tests found in file in ${stopwatch.elapsedMilliseconds} ms.");
-      return true;
+      _print("No tests found in file in ${stopwatch.elapsedMilliseconds} ms.");
+      return [];
     }
-    print("Found ${tests.length} test(s) in file "
+    _print("Found ${tests.length} test(s) in file "
         "in ${stopwatch.elapsedMilliseconds} ms.");
 
     return await compileAndRun(uri, tests);
   }
 
-  Future<bool> compileAndRun(Uri uri, List<Test> tests) async {
+  Future<List<Test>> extractTestsFromUri(Uri uri) async {
+    // Extract test cases in file.
+    FileSystemEntity file = _getFileSystem().entityForUri(uri);
+    List<int> rawBytes = await file.readAsBytes();
+    return extractTests(
+        rawBytes is Uint8List ? rawBytes : new Uint8List.fromList(rawBytes),
+        uri);
+  }
+
+  Future<List<TestResult>> compileAndRun(Uri uri, List<Test> tests,
+      {bool silent: false}) async {
     errors = false;
+    errorStrings.clear();
 
     // Create code to amend the file with.
     StringBuffer sb = new StringBuffer();
@@ -81,13 +117,16 @@
           r"Future<void> $dart$doc$test$tester(dynamic dartDocTest) async {");
       for (Test test in tests) {
         if (test is TestParseError) {
-          sb.writeln("dartDocTest.parseError(\"${test.message}\");");
-        } else {
+          sb.writeln(
+              "dartDocTest.parseError(\"Parse error @ ${test.position}\");");
+        } else if (test is ExpectTest) {
           sb.writeln("try {");
           sb.writeln("  dartDocTest.test(${test.call}, ${test.result});");
           sb.writeln("} catch (e) {");
           sb.writeln("  dartDocTest.crash(e);");
           sb.writeln("}");
+        } else {
+          throw "Unknown test type: ${test.runtimeType}";
         }
       }
       sb.writeln("}");
@@ -99,8 +138,7 @@
 
     processedOpts.inputs.clear();
     processedOpts.inputs.add(uri);
-    AmendedFileSystem fileSystem =
-        new AmendedFileSystem(StandardFileSystem.instance);
+    AmendedFileSystem fileSystem = new AmendedFileSystem(_getFileSystem());
     fileSystem.amendFileUri = uri;
     fileSystem.amendWith = sb.toString();
     options.fileSystem = fileSystem;
@@ -114,10 +152,13 @@
     Component component =
         await incrementalCompiler!.computeDelta(entryPoints: [uri]);
     if (errors) {
-      print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
-      return false;
+      _print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
+      return [
+        new TestResult(null, TestOutcome.CompilationError)
+          ..message = errorStrings.join("\n")
+      ];
     }
-    print("Compiled (1) in ${stopwatch.elapsedMilliseconds} ms.");
+    _print("Compiled (1) in ${stopwatch.elapsedMilliseconds} ms.");
     stopwatch.reset();
 
     fileSystem.amendImportUri = component.uriToSource[uri]?.importUri;
@@ -125,16 +166,16 @@
     Component componentMain = await incrementalCompiler!.computeDelta(
         entryPoints: [AmendedFileSystem.mainUri], fullComponent: true);
     if (errors) {
-      print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
-      return false;
+      _print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
+      return [new TestResult(null, TestOutcome.CompilationError)];
     }
-    print("Compiled (2) in ${stopwatch.elapsedMilliseconds} ms.");
+    _print("Compiled (2) in ${stopwatch.elapsedMilliseconds} ms.");
     stopwatch.reset();
 
     Directory tmpDir = Directory.systemTemp.createTempSync();
     Uri dillOutUri = tmpDir.uri.resolve("dartdoctestrun.dill");
     await writeComponentToFile(componentMain, dillOutUri);
-    print("Wrote dill in ${stopwatch.elapsedMilliseconds} ms.");
+    _print("Wrote dill in ${stopwatch.elapsedMilliseconds} ms.");
     stopwatch.reset();
 
     // Spawn URI (dill uri) to run tests.
@@ -150,7 +191,7 @@
       completer.complete();
     });
     errorPort.listen((message) {
-      print("Isolate had an error: $message.");
+      _print("Isolate had an error: $message.");
       error = true;
       exitPort.close();
       errorPort.close();
@@ -163,23 +204,42 @@
     int crashCount = 0;
     int parseErrorCount = 0;
     bool done = false;
+    Test? currentTest;
+
+    List<TestResult> result = [];
+
     communicationPort.listen((message) {
       if (done) {
         throw "Didn't expect any more messages. Got '$message'";
       }
       if (message == _portMessageTest) {
+        currentTest = tests[testCount];
         testCount++;
       } else if (message == _portMessageGood) {
         goodCount++;
-      } else if (message == _portMessageBad) {
+        result.add(new TestResult(currentTest!, TestOutcome.Pass));
+      } else if (message.toString().startsWith("$_portMessageBad: ")) {
         badCount++;
-      } else if (message.toString().startsWith("$_portMessageBadDetails: ")) {
-        print(message.toString().substring("$_portMessageBadDetails: ".length));
-      } else if (message == _portMessageCrash) {
+        String strippedMessage =
+            message.toString().substring("$_portMessageBad: ".length);
+        result.add(new TestResult(currentTest!, TestOutcome.Failed)
+          ..message = strippedMessage);
+        _print(strippedMessage);
+      } else if (message.toString().startsWith("$_portMessageCrash: ")) {
+        String strippedMessage =
+            message.toString().substring("$_portMessageCrash: ".length);
+        result.add(new TestResult(currentTest!, TestOutcome.Crash)
+          ..message = strippedMessage);
         crashCount++;
+        _print(strippedMessage);
       } else if (message.toString().startsWith("$_portMessageParseError: ")) {
+        String strippedMessage =
+            message.toString().substring("$_portMessageParseError: ".length);
+        result.add(
+            new TestResult(currentTest!, TestOutcome.TestCompilationError)
+              ..message = strippedMessage);
         parseErrorCount++;
-        print(message.toString().substring("$_portMessageParseError: ".length));
+        _print(strippedMessage);
       } else if (message == _portMessageDone) {
         done = true;
         // don't complete completer here. Expect the exit port to close.
@@ -198,33 +258,34 @@
     );
     await completer.future;
     tmpDir.deleteSync(recursive: true);
+
     if (error) {
-      print("Completed with an error in ${stopwatch.elapsedMilliseconds} ms.");
-      return false;
+      _print("Completed with an error in ${stopwatch.elapsedMilliseconds} ms.");
+      return [new TestResult(null, TestOutcome.RuntimeError)];
     } else if (!done) {
-      print(
+      _print(
           "Didn't complete correctly in ${stopwatch.elapsedMilliseconds} ms.");
-      return false;
+      return [new TestResult(null, TestOutcome.FrameworkError)];
     } else if (testCount != tests.length) {
-      print("Didn't complete with error but ran "
+      _print("Didn't complete with error but ran "
           "${testCount} tests while expecting ${tests.length} "
           "in ${stopwatch.elapsedMilliseconds} ms.");
-      return false;
+      return [new TestResult(null, TestOutcome.FrameworkError)];
     } else {
-      print("Processed $testCount test(s) "
+      _print("Processed $testCount test(s) "
           "in ${stopwatch.elapsedMilliseconds} ms.");
       if (goodCount == testCount &&
           badCount == 0 &&
           crashCount == 0 &&
           parseErrorCount == 0) {
-        print("All tests passed.");
-        return true;
+        _print("All tests passed.");
+      } else {
+        _print("$goodCount OK; "
+            "$badCount bad; "
+            "$crashCount crashed; "
+            "$parseErrorCount parse errors.");
       }
-      print("$goodCount OK; "
-          "$badCount bad; "
-          "$crashCount crashed; "
-          "$parseErrorCount parse errors.");
-      return false;
+      return result;
     }
   }
 
@@ -237,9 +298,12 @@
     options.omitPlatform = true;
     options.onDiagnostic = (DiagnosticMessage message) {
       if (message.codeName == "InferredPackageUri") return;
-      print(message.plainTextFormatted.first);
+      _print(message.plainTextFormatted.first);
       if (message.severity == Severity.error) {
         errors = true;
+        for (String errorString in message.plainTextFormatted) {
+          errorStrings.add(errorString);
+        }
       }
     };
 
@@ -254,10 +318,28 @@
   }
 }
 
-List<Test> extractTestsFromUri(Uri uri) {
-  // Extract test cases in file.
-  File file = new File.fromUri(uri);
-  Uint8List rawBytes = file.readAsBytesSync();
+List<Test> extractTests(Uint8List rawBytes, Uri uriForReporting) {
+  String rawString = utf8.decode(rawBytes);
+  List<int> lineStarts = [];
+  Token firstToken = scanRawBytes(rawBytes, lineStarts: lineStarts);
+  Source source =
+      new Source(lineStarts, rawBytes, uriForReporting, uriForReporting);
+  Token token = firstToken;
+  List<Test> tests = [];
+  while (true) {
+    CommentToken? comment = token.precedingComments;
+    if (comment != null) {
+      tests.addAll(extractTestsFromComment(comment, rawString, source));
+    }
+    if (token.isEof) break;
+    Token? next = token.next;
+    if (next == null) break;
+    token = next;
+  }
+  return tests;
+}
+
+Token scanRawBytes(Uint8List rawBytes, {List<int>? lineStarts}) {
   Uint8List bytes = new Uint8List(rawBytes.length + 1);
   bytes.setRange(0, rawBytes.length, rawBytes);
 
@@ -275,120 +357,311 @@
       // configuration won't be reset.
     },
   );
-  Token firstToken = scanner.tokenize();
-  Token token = firstToken;
-  List<Test> tests = [];
-  while (!token.isEof) {
-    CommentToken? comment = token.precedingComments;
-    if (comment != null) {
-      tests.addAll(processComment(comment));
-      // TODO: Use parser to verify there's at least no syntax errors in the
-      // tests.
-    }
-    Token? next = token.next;
-    if (next == null) break;
-    token = next;
+  Token result = scanner.tokenize();
+  if (lineStarts != null) {
+    lineStarts.clear();
+    lineStarts.addAll(scanner.lineStarts);
   }
-  return tests;
+  return result;
 }
 
 const int $LF = 10;
-const int $OPEN_PAREN = 40;
-const int $CLOSE_PAREN = 41;
-const int $COMMA = 44;
+const int $SPACE = 32;
+const int $STAR = 42;
 
-class Test {
+class Test {}
+
+class ExpectTest implements Test {
   final String call;
   final String result;
 
-  Test(this.call, this.result);
+  ExpectTest(this.call, this.result);
+
+  bool operator ==(Object other) {
+    if (other is! ExpectTest) return false;
+    if (other.call != call) return false;
+    if (other.result != result) return false;
+    return true;
+  }
+
+  String toString() {
+    return "ExpectTest[$call, $result]";
+  }
 }
 
 class TestParseError implements Test {
   final String message;
+  final int position;
 
-  TestParseError(this.message);
+  TestParseError(this.message, this.position);
 
-  @override
-  String get call => throw UnimplementedError();
+  bool operator ==(Object other) {
+    if (other is! TestParseError) return false;
+    if (other.message != message) return false;
+    if (other.position != position) return false;
+    return true;
+  }
 
-  @override
-  String get result => throw UnimplementedError();
+  String toString() {
+    return "TestParseError[$position, $message]";
+  }
 }
 
-List<Test> processComment(CommentToken comment) {
+enum TestOutcome {
+  Pass,
+  Failed,
+  Crash,
+  TestCompilationError,
+  CompilationError,
+  RuntimeError,
+  FrameworkError
+}
+
+class TestResult {
+  final Test? test;
+  final TestOutcome outcome;
+  String? message;
+
+  TestResult(this.test, this.outcome);
+
+  bool operator ==(Object other) {
+    if (other is! TestResult) return false;
+    if (other.test != test) return false;
+    if (other.outcome != outcome) return false;
+    if (other.message != message) return false;
+    return true;
+  }
+
+  String toString() {
+    if (message != null) {
+      return "TestResult[$outcome, $test, $message]";
+    }
+    return "TestResult[$outcome, $test]";
+  }
+}
+
+List<Test> extractTestsFromComment(
+    CommentToken comment, String rawString, Source source) {
+  CommentString commentsData = extractComments(comment, rawString);
+  final String comments = commentsData.string;
+  List<Test> result = [];
+  int index = comments.indexOf("DartDocTest(");
+  if (index < 0) {
+    return result;
+  }
+
+  Test scanDartDoc(int scanOffset) {
+    final Token firstToken =
+        scanRawBytes(utf8.encode(comments.substring(scanOffset)) as Uint8List);
+    final ErrorListener listener = new ErrorListener();
+    final Parser parser = new Parser(listener);
+    parser.asyncState = AsyncModifier.Async;
+
+    final Token pastErrors = parser.skipErrorTokens(firstToken);
+    assert(pastErrors.isIdentifier);
+    assert(pastErrors.lexeme == "DartDocTest");
+
+    final Token startParen = pastErrors.next!;
+    assert(identical("(", startParen.stringValue));
+
+    // Advance index so we don't parse the same thing again (for error cases).
+    index = scanOffset + startParen.charEnd;
+
+    final Token beforeComma = parser.parseExpression(startParen);
+    final Token comma = beforeComma.next!;
+
+    if (listener.hasErrors) {
+      StringBuffer sb = new StringBuffer();
+      int firstPosition = _createParseErrorMessages(
+          listener, sb, commentsData, scanOffset, source);
+      return new TestParseError(sb.toString(), firstPosition);
+    } else if (!identical(",", comma.stringValue)) {
+      int position = commentsData.charOffset + scanOffset + comma.charOffset;
+      Message message = codes.templateExpectedButGot.withArguments(',');
+      return new TestParseError(
+        _createParseErrorMessage(source, position, comma, comma, message),
+        position,
+      );
+    }
+
+    Token beforeEndParen = parser.parseExpression(comma);
+    Token endParen = beforeEndParen.next!;
+
+    if (listener.hasErrors) {
+      StringBuffer sb = new StringBuffer();
+      int firstPosition = _createParseErrorMessages(
+          listener, sb, commentsData, scanOffset, source);
+      return new TestParseError(sb.toString(), firstPosition);
+    } else if (!identical(")", endParen.stringValue)) {
+      int position = commentsData.charOffset + scanOffset + endParen.charOffset;
+      Message message = codes.templateExpectedButGot.withArguments(')');
+      return new TestParseError(
+        _createParseErrorMessage(source, position, comma, comma, message),
+        position,
+      );
+    }
+
+    // Advance index so we don't parse the same thing again (success case).
+    index = scanOffset + endParen.charEnd;
+
+    int startPos = scanOffset + startParen.next!.charOffset;
+    int midEndPos = scanOffset + beforeComma.charEnd;
+    int midStartPos = scanOffset + comma.next!.charOffset;
+    int endPos = scanOffset + beforeEndParen.charEnd;
+    return new ExpectTest(
+      comments.substring(startPos, midEndPos),
+      comments.substring(midStartPos, endPos),
+    );
+  }
+
+  while (index >= 0) {
+    result.add(scanDartDoc(index));
+    index = comments.indexOf("DartDocTest(", index);
+  }
+  return result;
+}
+
+int _createParseErrorMessages(ErrorListener listener, StringBuffer sb,
+    CommentString commentsData, int scanOffset, Source source) {
+  assert(listener.recoverableErrors.isNotEmpty);
+  sb.writeln("Parse error(s):");
+  int? firstPosition;
+  for (RecoverableError recoverableError in listener.recoverableErrors) {
+    final int position = commentsData.charOffset +
+        scanOffset +
+        recoverableError.startToken.charOffset;
+    firstPosition ??= position;
+    sb.writeln("");
+    sb.write(_createParseErrorMessage(
+      source,
+      position,
+      recoverableError.startToken,
+      recoverableError.endToken,
+      recoverableError.message,
+    ));
+  }
+  return firstPosition!;
+}
+
+String _createParseErrorMessage(Source source, int position, Token startToken,
+    Token endToken, Message message) {
+  Location location = source.getLocation(source.importUri!, position);
+  return command_line_reporting.formatErrorMessage(
+      source.getTextLine(location.line),
+      location,
+      endToken.charEnd - startToken.charOffset,
+      source.importUri!.toString(),
+      message.message);
+}
+
+CommentString extractComments(CommentToken comment, String rawString) {
+  List<int> fileCodeUnits = rawString.codeUnits;
+  final int charOffset = comment.charOffset;
+  int expectedCharOffset = charOffset;
   StringBuffer sb = new StringBuffer();
   CommentToken? commentToken = comment;
   bool commentBlock = false;
+  bool commentBlockStar = false;
   while (commentToken != null) {
-    String data = commentToken.lexeme.trim();
+    if (expectedCharOffset != commentToken.charOffset) {
+      // Missing spaces/linebreaks.
+      assert(expectedCharOffset < commentToken.offset);
+      for (int i = expectedCharOffset; i < commentToken.offset; i++) {
+        if (fileCodeUnits[i] == $LF) {
+          sb.writeCharCode($LF);
+        } else {
+          sb.write(" ");
+        }
+      }
+    }
+    expectedCharOffset = commentToken.charEnd;
+    String data = commentToken.lexeme;
     if (!commentBlock) {
       if (data.startsWith("///")) {
-        data = data.substring(3).trim();
+        data = data.substring(3);
+        sb.write("   ");
       } else if (data.startsWith("//")) {
-        data = data.substring(2).trim();
-      } else if (data.startsWith("/*")) {
-        data = data.substring(2).trim();
+        data = data.substring(2);
+        sb.write("  ");
+      } else if (data.startsWith("/**")) {
+        data = data.substring(3);
         commentBlock = true;
+        commentBlockStar = true;
+        sb.write("   ");
+      } else if (data.startsWith("/*")) {
+        data = data.substring(2);
+        commentBlock = true;
+        sb.write("  ");
       }
     }
 
     if (commentBlock && data.endsWith("*/")) {
-      data = data.substring(0, data.length - 2).trim();
+      // Remove ending "*/"" as well as "starting" "*" if in a "/**" block.
+      List<int> codeUnits = data.codeUnits;
+      bool sawNewlineLast = false;
+      for (int i = 0; i < codeUnits.length - 2; i++) {
+        int codeUnit = codeUnits[i];
+        if (codeUnit == $LF) {
+          sb.writeCharCode($LF);
+          sawNewlineLast = true;
+        } else if (codeUnit <= $SPACE) {
+          sb.writeCharCode($SPACE);
+        } else if (commentBlockStar && sawNewlineLast && codeUnit == $STAR) {
+          sb.writeCharCode($SPACE);
+          sawNewlineLast = false;
+        } else {
+          sawNewlineLast = false;
+          sb.writeCharCode(codeUnit);
+        }
+      }
+      sb.write("  ");
       commentBlock = false;
-    }
-    if (data.isNotEmpty) {
+      commentBlockStar = false;
+    } else {
       sb.write(data);
     }
     Token? next = commentToken.next;
     commentToken = null;
     if (next is CommentToken) commentToken = next;
   }
-  String comments = sb.toString();
-  List<Test> result = [];
-  int index = comments.indexOf("DartDocTest(");
-  if (index < 0) {
-    return result;
-  }
-  List<int> codeUnits = comments.codeUnits;
+  return new CommentString(sb.toString(), charOffset);
+}
 
-  while (index >= 0) {
-    // DartDocTest starts at (the current) $index --- now find the end,
-    // parenthesis, comma etc.
-    // TODO: This doesn't work for string literals with e.g. '(' and ',' in it.
-    int parenDepth = 0;
-    int firstParen = -1;
-    int commaAt = -1;
-    while (index < comments.length) {
-      int codeUnit = codeUnits[index++];
-      if (codeUnit == $OPEN_PAREN) {
-        parenDepth++;
-        if (parenDepth == 1) {
-          firstParen = index;
-        }
-      } else if (codeUnit == $CLOSE_PAREN) {
-        parenDepth--;
-        if (parenDepth == 0) {
-          break;
-        }
-      } else if (parenDepth == 1 && commaAt < 0 && codeUnit == $COMMA) {
-        commaAt = index;
-      }
-    }
+class CommentString {
+  final String string;
+  final int charOffset;
 
-    int end = index;
-    if (parenDepth != 0 || firstParen < 0 || commaAt < 0) {
-      // TODO: Insert code-snippet that didn't parse...
-      result.add(new TestParseError("Parse error for test"));
-    } else {
-      result.add(new Test(
-        comments.substring(firstParen, commaAt - 1).trim(),
-        comments.substring(commaAt, end - 1).trim(),
-      ));
-    }
-    index = comments.indexOf("DartDocTest(", index);
+  CommentString(this.string, this.charOffset);
+
+  bool operator ==(Object other) {
+    if (other is! CommentString) return false;
+    if (other.string != string) return false;
+    if (other.charOffset != charOffset) return false;
+    return true;
   }
-  return result;
+
+  String toString() {
+    return "CommentString[$charOffset, $string]";
+  }
+}
+
+class ErrorListener extends NullListener {
+  List<RecoverableError> recoverableErrors = [];
+
+  @override
+  void handleRecoverableError(
+      Message message, Token startToken, Token endToken) {
+    super.handleRecoverableError(message, startToken, endToken);
+    recoverableErrors.add(new RecoverableError(message, startToken, endToken));
+  }
+}
+
+class RecoverableError {
+  final Message message;
+  final Token startToken;
+  final Token endToken;
+
+  RecoverableError(this.message, this.startToken, this.endToken);
 }
 
 class AmendedFileSystem implements FileSystem {
@@ -441,14 +714,13 @@
     if (_testImpl(actual, expected)) {
       port.send("$_portMessageGood");
     } else {
-      port.send("$_portMessageBad");
-      port.send("$_portMessageBadDetails: Expected '\$expected'; got '\$actual'.");
+      port.send("$_portMessageBad: Expected '\$expected'; got '\$actual'.");
     }
   }
 
   void crash(dynamic error) {
     port.send("$_portMessageTest");
-    port.send("$_portMessageCrash");
+    port.send("$_portMessageCrash: \$error");
   }
 
   void parseError(String message) {
diff --git a/pkg/test_runner/lib/src/test_suite.dart b/pkg/test_runner/lib/src/test_suite.dart
index 1f9253f..9a2dd15 100644
--- a/pkg/test_runner/lib/src/test_suite.dart
+++ b/pkg/test_runner/lib/src/test_suite.dart
@@ -22,6 +22,7 @@
 import 'multitest.dart';
 import 'path.dart';
 import 'repository.dart';
+import 'runtime_configuration.dart' show QemuConfig;
 import 'summary_report.dart';
 import 'test_case.dart';
 import 'test_file.dart';
@@ -251,6 +252,8 @@
 class VMTestSuite extends TestSuite {
   String targetRunnerPath;
   String hostRunnerPath;
+  List<String> initialHostArguments;
+  List<String> initialTargetArguments;
   final String dartDir;
 
   VMTestSuite(TestConfiguration configuration)
@@ -269,6 +272,18 @@
     } else {
       hostRunnerPath = targetRunnerPath;
     }
+
+    initialHostArguments = <String>[];
+    initialTargetArguments = <String>[];
+    if (configuration.useQemu) {
+      final config = QemuConfig.all[configuration.architecture];
+      initialHostArguments.insert(0, hostRunnerPath);
+      initialHostArguments.insertAll(0, config.arguments);
+      initialTargetArguments.insert(0, targetRunnerPath);
+      initialTargetArguments.insertAll(0, config.arguments);
+      hostRunnerPath = config.executable;
+      targetRunnerPath = config.executable;
+    }
   }
 
   void findTestCases(TestCaseEvent onTest, Map testCache) {
@@ -277,7 +292,7 @@
     var expectations = ExpectationSet.read(statusFiles, configuration);
 
     try {
-      for (var test in _listTests(hostRunnerPath)) {
+      for (var test in _listTests()) {
         _addTest(expectations, test, onTest);
       }
     } catch (error, s) {
@@ -315,7 +330,8 @@
         : '$buildDir/gen/kernel_service.dill';
     var dfePath = Path(filename).absolute.toNativePath();
     var args = [
-      // '--dfe' has to be the first argument for run_vm_test to pick it up.
+      ...initialTargetArguments,
+      // '--dfe' must be the first VM argument for run_vm_test to pick it up.
       '--dfe=$dfePath',
       if (expectations.contains(Expectation.crash)) '--suppress-core-dump',
       if (configuration.experiments.isNotEmpty)
@@ -330,10 +346,11 @@
     _addTestCase(testFile, fullName, [command], expectations, onTest);
   }
 
-  Iterable<VMUnitTest> _listTests(String runnerPath) {
-    var result = Process.runSync(runnerPath, ["--list"]);
+  Iterable<VMUnitTest> _listTests() {
+    var args = [...initialHostArguments, "--list"];
+    var result = Process.runSync(hostRunnerPath, args);
     if (result.exitCode != 0) {
-      throw "Failed to list tests: '$runnerPath --list'. "
+      throw "Failed to list tests: '$hostRunnerPath ${args.join(' ')}'. "
           "Process exited with ${result.exitCode}";
     }
 
diff --git a/tools/VERSION b/tools/VERSION
index 28ecc51..eeda70a 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 246
+PRERELEASE 247
 PRERELEASE_PATCH 0
\ No newline at end of file