Add Chain.foldFrames to pkg/stack_trace.

R=rnystrom@google.com
BUG=17082

Review URL: https://codereview.chromium.org//180223005

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/stack_trace@33041 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
index 4055bc8..2500e5e 100644
--- a/pkgs/stack_trace/lib/src/chain.dart
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -7,6 +7,7 @@
 import 'dart:async';
 import 'dart:collection';
 
+import 'frame.dart';
 import 'stack_zone_specification.dart';
 import 'trace.dart';
 import 'utils.dart';
@@ -172,6 +173,32 @@
     return new Chain(nonEmptyTraces);
   }
 
+  /// Returns a new [Chain] based on [this] where multiple stack frames matching
+  /// [predicate] are folded together.
+  ///
+  /// This means that whenever there are multiple frames in a row that match
+  /// [predicate], only the last one is kept. In addition, traces that are
+  /// composed entirely of frames matching [predicate] are omitted.
+  ///
+  /// This is useful for limiting the amount of library code that appears in a
+  /// stack trace by only showing user code and code that's called by user code.
+  Chain foldFrames(bool predicate(Frame frame)) {
+    var foldedTraces = traces.map((trace) => trace.foldFrames(predicate));
+    var nonEmptyTraces = foldedTraces.where((trace) {
+      // Ignore traces that contain only folded frames. These traces will be
+      // folded into a single frame each.
+      return trace.frames.length > 1;
+    });
+
+    // If all the traces contain only internal processing, preserve the last
+    // (top-most) one so that the chain isn't empty.
+    if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
+      return new Chain([foldedTraces.last]);
+    }
+
+    return new Chain(nonEmptyTraces);
+  }
+
   /// Converts [this] to a [Trace].
   ///
   /// The trace version of a chain is just the concatenation of all the traces
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
index c1d751f..c540d78 100644
--- a/pkgs/stack_trace/lib/src/trace.dart
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -229,7 +229,7 @@
   ///
   /// This is useful for limiting the amount of library code that appears in a
   /// stack trace by only showing user code and code that's called by user code.
-  Trace foldFrames(bool predicate(frame)) {
+  Trace foldFrames(bool predicate(Frame frame)) {
     var newFrames = <Frame>[];
     for (var frame in frames.reversed) {
       if (!predicate(frame)) {
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
index 1e62ca6..1bd4776 100644
--- a/pkgs/stack_trace/pubspec.yaml
+++ b/pkgs/stack_trace/pubspec.yaml
@@ -1,5 +1,5 @@
 name: stack_trace
-version: 0.9.2
+version: 0.9.2-dev
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://www.dartlang.org
 description: >
diff --git a/pkgs/stack_trace/test/chain_test.dart b/pkgs/stack_trace/test/chain_test.dart
index 9c16690..d9915eb 100644
--- a/pkgs/stack_trace/test/chain_test.dart
+++ b/pkgs/stack_trace/test/chain_test.dart
@@ -387,7 +387,7 @@
           'dart:core             Bar.baz\n'));
     });
 
-    test("doesn't return in an empty chain", () {
+    test("doesn't return an empty chain", () {
       var chain = new Chain([
         new Trace.parse(
             'dart:core 10:11                             Foo.bar\n'
@@ -403,6 +403,67 @@
     });
   });
 
+  group('Chain.foldFrames', () {
+    test('folds each trace', () {
+      var chain = new Chain([
+        new Trace.parse(
+            'a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'b.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'a.dart 10:11  Zop.zoop'),
+        new Trace.parse(
+            'a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'a.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(folded.toString(), equals(
+          'a.dart 10:11  Bar.baz\n'
+          'b.dart 10:11  Bang.qux\n'
+          'a.dart 10:11  Zop.zoop\n'
+          '===== asynchronous gap ===========================\n'
+          'a.dart 10:11  Zip.zap\n'
+          'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test('eliminates completely-folded traces', () {
+      var chain = new Chain([
+        new Trace.parse(
+            'a.dart 10:11  Foo.bar\n'
+            'b.dart 10:11  Bang.qux'),
+        new Trace.parse(
+            'a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bang.qux'),
+        new Trace.parse(
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(folded.toString(), equals(
+          'a.dart 10:11  Foo.bar\n'
+          'b.dart 10:11  Bang.qux\n'
+          '===== asynchronous gap ===========================\n'
+          'a.dart 10:11  Zip.zap\n'
+          'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test("doesn't return an empty trace", () {
+      var chain = new Chain([
+        new Trace.parse(
+            'a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bang.qux')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(folded.toString(), equals('a.dart 10:11  Bang.qux\n'));
+    });
+  });
+
   test('Chain.toTrace eliminates asynchronous gaps', () {
     var trace = new Chain([
       new Trace.parse(