Async gap fixes (#96)

* Fix async gap handling. (backpublish)

Fix an issue where an async gap at the end of a stack trace would not
get parsed correctly due to the trailing newline being `trim()`'d.

Add tests to cover this case.

* Fixes async gap handling in Trace.parse and Chain.parse (backpublish)

Co-authored-by: Clement Skau <cskau-g@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f189e6..f3cbc36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.9.6 (backpublish)
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces. (Also fixed separately in 1.10.0-nullsafety.3)
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces, when parsing with `Trace.parse` and `Chain.parse`. (Also fixed
+  separately in 1.10.0-nullsafety.6)
+
 ## 1.9.5
 
 * Parse the format for `data:` URIs that the Dart VM has used since `2.2.0`.
diff --git a/lib/src/chain.dart b/lib/src/chain.dart
index 6b2eec5..ecab35a 100644
--- a/lib/src/chain.dart
+++ b/lib/src/chain.dart
@@ -173,8 +173,10 @@
   factory Chain.parse(String chain) {
     if (chain.isEmpty) return Chain([]);
     if (chain.contains(vmChainGap)) {
-      return Chain(
-          chain.split(vmChainGap).map((trace) => Trace.parseVM(trace)));
+      return Chain(chain
+          .split(vmChainGap)
+          .where((line) => line.isNotEmpty)
+          .map((trace) => Trace.parseVM(trace)));
     }
     if (!chain.contains(chainGap)) return Chain([Trace.parse(chain)]);
 
diff --git a/lib/src/trace.dart b/lib/src/trace.dart
index 96902a1..d66c0db 100644
--- a/lib/src/trace.dart
+++ b/lib/src/trace.dart
@@ -153,7 +153,16 @@
   static List<Frame> _parseVM(String trace) {
     // Ignore [vmChainGap]. This matches the behavior of
     // `Chain.parse().toTrace()`.
-    var lines = trace.trim().replaceAll(vmChainGap, '').split('\n');
+    var lines = trace
+        .trim()
+        .replaceAll(vmChainGap, '')
+        .split('\n')
+        .where((line) => line.isNotEmpty);
+
+    if (lines.isEmpty) {
+      return [];
+    }
+
     var frames = lines
         .take(lines.length - 1)
         .map((line) => Frame.parseVM(line))
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 838a093..0dd1755 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -8,7 +8,7 @@
 
 /// The line used in the string representation of VM stack chains to represent
 /// the gap between traces.
-const vmChainGap = '<asynchronous suspension>\n';
+final vmChainGap = RegExp(r'^<asynchronous suspension>\n?$', multiLine: true);
 
 // TODO(nweiz): When cross-platform imports work, use them to set this.
 /// Whether we're running in a JS context.
diff --git a/pubspec.yaml b/pubspec.yaml
index 147f7cc..e72ee38 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,5 @@
 name: stack_trace
-version: 1.9.5
-
+version: 1.9.6
 description: A package for manipulating stack traces and printing them readably.
 homepage: https://github.com/dart-lang/stack_trace
 
diff --git a/test/chain/chain_test.dart b/test/chain/chain_test.dart
index b6ad33e..d71b274 100644
--- a/test/chain/chain_test.dart
+++ b/test/chain/chain_test.dart
@@ -36,6 +36,21 @@
       expect(chain.traces[1].frames, isEmpty);
       expect(chain.traces[2].frames, isEmpty);
     });
+
+    test('parses a chain with VM gaps', () {
+      final chain =
+          Chain.parse('#1      MyClass.run (package:my_lib.dart:134:5)\n'
+              '<asynchronous suspension>\n'
+              '#2      main (file:///my_app.dart:9:3)\n'
+              '<asynchronous suspension>\n');
+      expect(chain.traces, hasLength(2));
+      expect(chain.traces[0].frames, hasLength(1));
+      expect(chain.traces[0].frames[0].toString(),
+          equals('package:my_lib.dart 134:5 in MyClass.run'));
+      expect(chain.traces[1].frames, hasLength(1));
+      expect(chain.traces[1].frames[0].toString(),
+          equals('/my_app.dart 9:3 in main'));
+    });
   });
 
   group('Chain.capture()', () {
diff --git a/test/trace_test.dart b/test/trace_test.dart
index ec7b074..4df6e9a 100644
--- a/test/trace_test.dart
+++ b/test/trace_test.dart
@@ -249,6 +249,25 @@
           equals(Uri.parse('https://dart.dev/foo/quux.dart')));
     });
 
+    test('parses a package:stack_trace stack chain with end gap correctly', () {
+      var trace =
+          Trace.parse('https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/baz.dart        Foo.<fn>.bar\n'
+              'https://dart.dev/foo/bang.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/quux.dart        Foo.<fn>.bar'
+              '===== asynchronous gap ===========================\n');
+
+      expect(trace.frames.length, 4);
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+      expect(trace.frames[3].uri,
+          equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+    });
+
     test('parses a real package:stack_trace stack trace correctly', () {
       var traceString = Trace.current().toString();
       expect(Trace.parse(traceString).toString(), equals(traceString));
@@ -259,6 +278,28 @@
       expect(trace.frames, isEmpty);
       expect(trace.toString(), equals(''));
     });
+
+    test('parses trace with async gap correctly', () {
+      var trace = Trace.parse('#0      bop (file:///pull.dart:42:23)\n'
+          '<asynchronous suspension>\n'
+          '#1      twist (dart:the/future.dart:0:2)\n'
+          '#2      main (dart:my/file.dart:4:6)\n');
+
+      expect(trace.frames.length, 3);
+      expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+      expect(trace.frames[2].uri, equals(Uri.parse('dart:my/file.dart')));
+    });
+
+    test('parses trace with async gap at end correctly', () {
+      var trace = Trace.parse('#0      bop (file:///pull.dart:42:23)\n'
+          '#1      twist (dart:the/future.dart:0:2)\n'
+          '<asynchronous suspension>\n');
+
+      expect(trace.frames.length, 2);
+      expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+    });
   });
 
   test('.toString() nicely formats the stack trace', () {