[CFE] DartDoc tests prints errors on crash better

Before:
```
Bang or other message!
Processed 4 test(s) in 205 ms.
3 OK; 0 bad; 1 crashed; 0 parse errors.
```

Now:
```
Failure:
Test from file://path/to/pkg/front_end/lib/src/kernel/body_builder.dart:10086:7 crashed with this message:
Bang or other message!

Stacktrace:
#0      debugName (package:front_end/src/kernel/body_builder.dart:10102:23)

Processed 4 test(s) in 90 ms.
3 OK; 0 bad; 1 crashed; 0 parse errors.
```

Change-Id: I49e8543dea5dc5285091fb57e8b92c45684fb5d8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/379060
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
diff --git a/pkg/front_end/test/dartdoc_test_test.dart b/pkg/front_end/test/dartdoc_test_test.dart
index 4588e5f..65e561d 100644
--- a/pkg/front_end/test/dartdoc_test_test.dart
+++ b/pkg/front_end/test/dartdoc_test_test.dart
@@ -150,7 +150,9 @@
   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'",
+      ..message = "type 'int' is not a subtype of type 'String' of 'other'\n"
+          "\n"
+          "Stacktrace:",
     new impl.TestResult(tests[1], impl.TestOutcome.Pass),
   ];
   memoryFileSystem.entityForUri(test6).writeAsStringSync(test);
@@ -209,6 +211,32 @@
   ];
   memoryFileSystem.entityForUri(test9).writeAsStringSync(test);
   expect(await dartDocTest.process(test9), expected);
+
+  // Test crashes with stacktrace.
+  Uri test10 = new Uri(scheme: "darttest", path: "/test10.dart");
+  test = """
+// DartDocTest(await _internal(), 42)
+Future<int> _internal() async {
+  await Future.delayed(new Duration(milliseconds: 1));
+  if (1+1==2) throw "I threw!";
+  return 42;
+}
+""";
+  tests = extractTests(test, test10);
+  expect(tests.length, 1);
+  expected = [
+    new impl.TestResult(tests[0], impl.TestOutcome.Crash)
+      ..message = "I threw!"
+          "\n"
+          "\nStacktrace:"
+          "\n#0      _internal (darttest:/test10.dart:4:15)"
+          "\n<asynchronous suspension>",
+  ];
+  memoryFileSystem.entityForUri(test10).writeAsStringSync(test);
+  expect(await dartDocTest.process(test10), expected);
+
+  // TODO(jensj): Run in non-silent mode, but capturing the stdout, to verify
+  // the actually written text on stdout.
 }
 
 void testTestExtraction() {
diff --git a/pkg/front_end/tool/dart_doctest_impl.dart b/pkg/front_end/tool/dart_doctest_impl.dart
index cbccbda..10e22e8 100644
--- a/pkg/front_end/tool/dart_doctest_impl.dart
+++ b/pkg/front_end/tool/dart_doctest_impl.dart
@@ -122,15 +122,15 @@
           case ExpectTest():
             sb.writeln("try {");
             sb.writeln("  dartDocTest.test(${test.call}, ${test.result});");
-            sb.writeln("} catch (e) {");
-            sb.writeln("  dartDocTest.crash(e);");
+            sb.writeln("} catch (e, st) {");
+            sb.writeln("  dartDocTest.crash(e, st);");
             sb.writeln("}");
           case ThrowsTest():
             sb.writeln("try {");
             sb.writeln(
                 "  await dartDocTest.throws(() async { ${test.call}; });");
-            sb.writeln("} catch (e) {");
-            sb.writeln("  dartDocTest.crash(e);");
+            sb.writeln("} catch (e, st) {");
+            sb.writeln("  dartDocTest.crash(e, st);");
             sb.writeln("}");
         }
       }
@@ -247,12 +247,26 @@
             "Test from ${currentTest!.location} failed with this message:\n"
             "$strippedMessage\n");
       } else if (message.toString().startsWith("$_portMessageCrash: ")) {
+        List<String> strippedMessageLines = message
+            .toString()
+            .substring("$_portMessageCrash: ".length)
+            .split("\n");
+        int end = 1;
+        for (int i = 0; i < strippedMessageLines.length; i++) {
+          if (strippedMessageLines[i].contains(r"$dart$doc$test$tester ()")) {
+            end = i;
+            break;
+          }
+        }
         String strippedMessage =
-            message.toString().substring("$_portMessageCrash: ".length);
+            strippedMessageLines.sublist(0, end).join("\n");
+
         result.add(new TestResult(currentTest!, TestOutcome.Crash)
           ..message = strippedMessage);
         crashCount++;
-        _print(strippedMessage);
+        _print("Failure:\n"
+            "Test from ${currentTest!.location} crashed with this message:\n"
+            "$strippedMessage\n");
       } else if (message.toString().startsWith("$_portMessageParseError: ")) {
         String strippedMessage =
             message.toString().substring("$_portMessageParseError: ".length);
@@ -260,7 +274,9 @@
             new TestResult(currentTest!, TestOutcome.TestCompilationError)
               ..message = strippedMessage);
         parseErrorCount++;
-        _print(strippedMessage);
+        _print("Failure:\n"
+            "Test from ${currentTest!.location} has a parse error:\n"
+            "$strippedMessage\n");
       } else if (message == _portMessageDone) {
         done = true;
         // don't complete completer here. Expect the exit port to close.
@@ -373,9 +389,9 @@
     }
   }
 
-  void crash(dynamic error) {
+  void crash(dynamic error, dynamic st) {
     port.send("$_portMessageTest");
-    port.send("$_portMessageCrash: \$error");
+    port.send("$_portMessageCrash: \$error\\n\\nStacktrace:\\n\$st");
   }
 
   void parseError(String message) {