Support parsing empty stack chains.

Closes dart-lang/stack_trace#4

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1171873002.
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
index d4696ec..8615187 100644
--- a/pkgs/stack_trace/CHANGELOG.md
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -4,6 +4,8 @@
   nested `Chain.capture()` blocks, substitute the inner block's chain rather
   than the outer block's.
 
+* Add support for empty chains and chains of empty traces to `Chain.parse()`.
+
 ## 1.3.2
 
 * Don't crash when running `Trace.terse` on empty stack traces.
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
index 87cc6be..3e2100e 100644
--- a/pkgs/stack_trace/lib/src/chain.dart
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -127,8 +127,11 @@
   /// Parses a string representation of a stack chain.
   ///
   /// Specifically, this parses the output of [Chain.toString].
-  factory Chain.parse(String chain) =>
-    new Chain(chain.split(_gap).map((trace) => new Trace.parseFriendly(trace)));
+  factory Chain.parse(String chain) {
+    if (chain.isEmpty) return new Chain([]);
+    return new Chain(
+        chain.split(_gap).map((trace) => new Trace.parseFriendly(trace)));
+  }
 
   /// Returns a new [Chain] comprised of [traces].
   Chain(Iterable<Trace> traces)
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
index 84a19ef..418c4ab 100644
--- a/pkgs/stack_trace/lib/src/trace.dart
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -188,10 +188,12 @@
   /// This also parses string representations of [Chain]s. They parse to the
   /// same trace that [Chain.toTrace] would return.
   Trace.parseFriendly(String trace)
-      : this(trace.trim().split("\n")
-          // Filter out asynchronous gaps from [Chain]s.
-          .where((line) => !line.startsWith('====='))
-          .map((line) => new Frame.parseFriendly(line)));
+      : this(trace.isEmpty
+            ? []
+            : trace.trim().split("\n")
+                // Filter out asynchronous gaps from [Chain]s.
+                .where((line) => !line.startsWith('====='))
+                .map((line) => new Frame.parseFriendly(line)));
 
   /// Returns a new [Trace] comprised of [frames].
   Trace(Iterable<Frame> frames)
diff --git a/pkgs/stack_trace/test/chain_test.dart b/pkgs/stack_trace/test/chain_test.dart
index bfe322c..0b6f36e 100644
--- a/pkgs/stack_trace/test/chain_test.dart
+++ b/pkgs/stack_trace/test/chain_test.dart
@@ -472,10 +472,28 @@
         equals(new Trace.from(trace).toString()));
   });
 
-  test('Chain.parse() parses a real Chain', () {
-    return captureFuture(() => inMicrotask(() => throw 'error')).then((chain) {
-      expect(new Chain.parse(chain.toString()).toString(),
-          equals(chain.toString()));
+  group('Chain.parse()', () {
+    test('parses a real Chain', () {
+      return captureFuture(() => inMicrotask(() => throw 'error'))
+          .then((chain) {
+        expect(new Chain.parse(chain.toString()).toString(),
+            equals(chain.toString()));
+      });
+    });
+
+    test('parses an empty string', () {
+      var chain = new Chain.parse('');
+      expect(chain.traces, isEmpty);
+    });
+
+    test('parses a chain containing empty traces', () {
+      var chain = new Chain.parse(
+          '===== asynchronous gap ===========================\n'
+          '===== asynchronous gap ===========================\n');
+      expect(chain.traces, hasLength(3));
+      expect(chain.traces[0].frames, isEmpty);
+      expect(chain.traces[1].frames, isEmpty);
+      expect(chain.traces[2].frames, isEmpty);
     });
   });