Make padding consistent across all stack traces for Chain.toString().

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//963893002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42c6d8b..aaa409a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
 * Remove the line numbers and specific files in all terse folded frames, not
   just those from core libraries.
 
+* Make padding consistent across all stack traces for `Chain.toString()`.
+
 ## 1.2.1
 
 * Add `terse` to `LazyTrace.foldFrames()`.
diff --git a/README.md b/README.md
index 24da03c..fe85adb 100644
--- a/README.md
+++ b/README.md
@@ -169,20 +169,20 @@
     dart:io/timer_impl.dart 292                                   _handleTimeout
     dart:isolate-patch/isolate_patch.dart 115                     _RawReceivePortImpl._handleMessage
     ===== asynchronous gap ===========================
-    dart:async/zone.dart 476                   _ZoneDelegate.registerUnaryCallback
-    dart:async/zone.dart 666                   _CustomizedZone.registerUnaryCallback
-    dart:async/future_impl.dart 164            _Future._Future._then
-    dart:async/future_impl.dart 187            _Future.then
-    test.dart 13:12                            scheduleAsync
-    test.dart 7:18                             main.<fn>
-    dart:async/zone.dart 710                   _rootRun
-    dart:async/zone.dart 440                   _ZoneDelegate.run
-    dart:async/zone.dart 650                   _CustomizedZone.run
-    dart:async/zone.dart 944                   runZoned
-    package:stack_trace/src/chain.dart 93:20   Chain.capture
-    test.dart 6:16                             main
-    dart:isolate-patch/isolate_patch.dart 216  _startIsolate.isolateStartHandler
-    dart:isolate-patch/isolate_patch.dart 115  _RawReceivePortImpl._handleMessage
+    dart:async/zone.dart 476                                      _ZoneDelegate.registerUnaryCallback
+    dart:async/zone.dart 666                                      _CustomizedZone.registerUnaryCallback
+    dart:async/future_impl.dart 164                               _Future._Future._then
+    dart:async/future_impl.dart 187                               _Future.then
+    test.dart 13:12                                               scheduleAsync
+    test.dart 7:18                                                main.<fn>
+    dart:async/zone.dart 710                                      _rootRun
+    dart:async/zone.dart 440                                      _ZoneDelegate.run
+    dart:async/zone.dart 650                                      _CustomizedZone.run
+    dart:async/zone.dart 944                                      runZoned
+    package:stack_trace/src/chain.dart 93:20                      Chain.capture
+    test.dart 6:16                                                main
+    dart:isolate-patch/isolate_patch.dart 216                     _startIsolate.isolateStartHandler
+    dart:isolate-patch/isolate_patch.dart 115                     _RawReceivePortImpl._handleMessage
 
 That's a lot of text! If you look closely, though, you can see that `main` is
 listed in the first trace in the chain.
@@ -191,8 +191,8 @@
 the frames you don't care about. The terse version of the stack chain above is
 this:
 
-    test.dart 17:3   runAsync
-    test.dart 13:28  scheduleAsync.<fn>
+    test.dart 17:3       runAsync
+    test.dart 13:28      scheduleAsync.<fn>
     ===== asynchronous gap ===========================
     dart:async           _Future.then
     test.dart 13:12      scheduleAsync
diff --git a/lib/src/chain.dart b/lib/src/chain.dart
index 9555e76..03dbed6 100644
--- a/lib/src/chain.dart
+++ b/lib/src/chain.dart
@@ -6,6 +6,7 @@
 
 import 'dart:async';
 import 'dart:collection';
+import 'dart:math' as math;
 
 import 'frame.dart';
 import 'stack_zone_specification.dart';
@@ -15,6 +16,10 @@
 /// A function that handles errors in the zone wrapped by [Chain.capture].
 typedef void ChainHandler(error, Chain chain);
 
+/// The line used in the string representation of stack chains to represent
+/// the gap between traces.
+const _gap = '===== asynchronous gap ===========================\n';
+
 /// A chain of stack traces.
 ///
 /// A stack chain is a collection of one or more stack traces that collectively
@@ -36,9 +41,6 @@
 ///             "$stackChain");
 ///     });
 class Chain implements StackTrace {
-  /// The line used in the string representation of stack chains to represent
-  /// the gap between traces.
-  static const _GAP = '===== asynchronous gap ===========================\n';
 
   /// The stack traces that make up this chain.
   ///
@@ -180,5 +182,19 @@
   /// in the chain.
   Trace toTrace() => new Trace(flatten(traces.map((trace) => trace.frames)));
 
-  String toString() => traces.join(_GAP);
+  String toString() {
+    // Figure out the longest path so we know how much to pad.
+    var longest = traces.map((trace) {
+      return trace.frames.map((frame) => frame.location.length)
+          .fold(0, math.max);
+    }).fold(0, math.max);
+
+    // Don't call out to [Trace.toString] here because that doesn't ensure that
+    // padding is consistent across all traces.
+    return traces.map((trace) {
+      return trace.frames.map((frame) {
+        return '${padRight(frame.location, longest)}  ${frame.member}\n';
+      }).join();
+    }).join(_gap);
+  }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 24acfbc..1ac6caf 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@
 #
 # When the major version is upgraded, you *must* update that version constraint
 # in pub to stay in sync with this.
-version: 1.2.2-dev
+version: 1.2.2
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://github.com/dart-lang/stack_trace
 description: >
diff --git a/test/chain_test.dart b/test/chain_test.dart
index cf8a61f..bfe322c 100644
--- a/test/chain_test.dart
+++ b/test/chain_test.dart
@@ -479,6 +479,18 @@
     });
   });
 
+  test("toString() ensures that all traces are aligned", () {
+    var chain = new Chain([
+      new Trace.parse('short 10:11  Foo.bar\n'),
+      new Trace.parse('loooooooooooong 10:11  Zop.zoop')
+    ]);
+
+    expect(chain.toString(), equals(
+        'short 10:11            Foo.bar\n'
+        '===== asynchronous gap ===========================\n'
+        'loooooooooooong 10:11  Zop.zoop\n'));
+  });
+
   var userSlashCode = p.join('user', 'code.dart');
   group('Chain.terse', () {
     test('makes each trace terse', () {