Add an error callback to StackZoneSpecification.

This releases stack_trace 1.0.3.

R=rnystrom@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/stack_trace@40383 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5d594a..56c614a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.0.3
+
+* Use `Zone.errorCallback` to attach stack chains to all errors without the need
+  for `Chain.track`, which is now deprecated.
+
 ## 1.0.2
 
 * Remove a workaround for [issue 17083][].
diff --git a/README.md b/README.md
index 492a52a..38b768d 100644
--- a/README.md
+++ b/README.md
@@ -205,13 +205,4 @@
 
 That's a lot easier to understand!
 
-### `Chain.track`
-
-For the most part `Chain.capture` will notice when an error is thrown and
-associate the correct stack chain with it. However, there are some cases where
-exceptions won't be automatically detected: any `Future` constructor,
-`Completer.completeError`, `Stream.addError`, and libraries that use these such
-as `dart:io` and `dart:async`. For these, all you need to do is wrap the Future
-or Stream in a call to `Chain.track` and the errors will be tracked correctly.
-
 [Zone]: https://api.dartlang.org/apidocs/channels/stable/#dart-async.Zone
diff --git a/lib/src/chain.dart b/lib/src/chain.dart
index 78c6ae4..e4ac534 100644
--- a/lib/src/chain.dart
+++ b/lib/src/chain.dart
@@ -102,33 +102,13 @@
     });
   }
 
-  /// Ensures that any errors emitted by [futureOrStream] have the correct stack
-  /// chain information associated with them.
+  /// Returns [futureOrStream] unmodified.
   ///
-  /// For the most part an error thrown within a [capture] zone will have the
-  /// correct stack chain automatically associated with it. However, there are
-  /// some cases where exceptions won't be automatically detected: any [Future]
-  /// constructor, [Completer.completeError], [Stream.addError], and libraries
-  /// that use these.
-  ///
-  /// This returns a [Future] or [Stream] that will emit the same values and
-  /// errors as [futureOrStream]. The only exception is that if [futureOrStream]
-  /// emits an error without a stack trace, one will be added in the return
-  /// value.
-  ///
-  /// If this is called outside of a [capture] zone, it just returns
-  /// [futureOrStream] as-is.
-  ///
-  /// As the name suggests, [futureOrStream] may be either a [Future] or a
-  /// [Stream].
-  static track(futureOrStream) {
-    if (_currentSpec == null) return futureOrStream;
-    if (futureOrStream is Future) {
-      return _currentSpec.trackFuture(futureOrStream, 1);
-    } else {
-      return _currentSpec.trackStream(futureOrStream, 1);
-    }
-  }
+  /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
+  /// exceptions reported with [Completer.completeError] and
+  /// [StreamController.addError] were tracked correctly.
+  @Deprecated("Chain.track is not necessary in Dart 1.7+.")
+  static track(futureOrStream) => futureOrStream;
 
   /// Returns the current stack chain.
   ///
diff --git a/lib/src/stack_zone_specification.dart b/lib/src/stack_zone_specification.dart
index 9a4f7c0..292da65 100644
--- a/lib/src/stack_zone_specification.dart
+++ b/lib/src/stack_zone_specification.dart
@@ -56,7 +56,8 @@
         handleUncaughtError: handleUncaughtError,
         registerCallback: registerCallback,
         registerUnaryCallback: registerUnaryCallback,
-        registerBinaryCallback: registerBinaryCallback);
+        registerBinaryCallback: registerBinaryCallback,
+        errorCallback: errorCallback);
   }
 
   /// Returns the current stack chain.
@@ -88,7 +89,9 @@
     var node = _createNode(level + 1);
     future.then(completer.complete).catchError((e, stackTrace) {
       if (stackTrace == null) stackTrace = new Trace.current();
-      if (_chains[stackTrace] == null) _chains[stackTrace] = node;
+      if (stackTrace is! Chain && _chains[stackTrace] == null) {
+        _chains[stackTrace] = node;
+      }
       completer.completeError(e, stackTrace);
     });
     return completer.future;
@@ -105,7 +108,9 @@
     return stream.transform(new StreamTransformer.fromHandlers(
         handleError: (error, stackTrace, sink) {
       if (stackTrace == null) stackTrace = new Trace.current();
-      if (_chains[stackTrace] == null) _chains[stackTrace] = node;
+      if (stackTrace is! Chain && _chains[stackTrace] == null) {
+        _chains[stackTrace] = node;
+      }
       sink.addError(error, stackTrace);
     }));
   }
@@ -163,6 +168,26 @@
     }
   }
 
+  /// Attaches the current stack chain to [stackTrace], replacing it if
+  /// necessary.
+  AsyncError errorCallback(Zone self, ZoneDelegate parent, Zone zone,
+      Object error, StackTrace stackTrace) {
+    var asyncError = parent.errorCallback(zone, error, stackTrace);
+    if (asyncError != null) {
+      error = asyncError.error;
+      stackTrace = asyncError.stackTrace;
+    }
+
+    // Go up two levels to get through [_CustomZone.errorCallback].
+    if (stackTrace == null) {
+      stackTrace = _createNode(2).toChain();
+    } else {
+      if (_chains[stackTrace] == null) _chains[stackTrace] = _createNode(2);
+    }
+
+    return new AsyncError(error, stackTrace);
+  }
+
   /// Creates a [_Node] with the current stack trace and linked to
   /// [_currentNode].
   ///
diff --git a/pubspec.yaml b/pubspec.yaml
index 5226cf8..bf28770 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.0.2
+version: 1.0.3
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://www.dartlang.org
 description: >
@@ -19,4 +19,4 @@
 dev_dependencies:
   unittest: ">=0.9.0 <0.12.0"
 environment:
-  sdk: ">=1.0.0 <2.0.0"
+  sdk: ">=1.7.0-edge.40308 <2.0.0"
diff --git a/test/chain_test.dart b/test/chain_test.dart
index beb4721..6af3c6b 100644
--- a/test/chain_test.dart
+++ b/test/chain_test.dart
@@ -93,6 +93,37 @@
       });
     });
 
+    test('thrown in new Future()', () {
+      return captureFuture(() => inNewFuture(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+        // The second trace is the one captured by
+        // [StackZoneSpecification.errorCallback]. Because that runs
+        // asynchronously within [new Future], it doesn't actually refer to the
+        // source file at all.
+        expect(chain.traces[1].frames,
+            everyElement(frameLibrary(isNot(contains('chain_test')))));
+
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inNewFuture'))));
+      });
+    });
+
+    test('thrown in new Future.sync()', () {
+      return captureFuture(() {
+        inMicrotask(() => inSyncFuture(() => throw 'error'));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inSyncFuture'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
     test('multiple times', () {
       var completer = new Completer();
       var first = true;
@@ -121,6 +152,73 @@
       return completer.future;
     });
 
+    test('passed to a completer', () {
+      var trace = new Trace.current();
+      return captureFuture(() {
+        inMicrotask(() => completerErrorFuture(trace));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+
+        // The first trace is the trace that was manually reported for the
+        // error.
+        expect(chain.traces.first.toString(), equals(trace.toString()));
+
+        // The second trace is the trace that was captured when
+        // [Completer.addError] was called.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('completerErrorFuture'))));
+
+        // The third trace is the automatically-captured trace from when the
+        // microtask was scheduled.
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a completer with no stack trace', () {
+      return captureFuture(() {
+        inMicrotask(() => completerErrorFuture());
+      }).then((chain) {
+        expect(chain.traces, hasLength(2));
+
+        // The first trace is the one captured when [Completer.addError] was
+        // called.
+        expect(chain.traces[0].frames,
+            contains(frameMember(startsWith('completerErrorFuture'))));
+
+        // The second trace is the automatically-captured trace from when the
+        // microtask was scheduled.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a stream controller', () {
+      var trace = new Trace.current();
+      return captureFuture(() {
+        inMicrotask(() => controllerErrorStream(trace).listen(null));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces.first.toString(), equals(trace.toString()));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('controllerErrorStream'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a stream controller with no stack trace', () {
+      return captureFuture(() {
+        inMicrotask(() => controllerErrorStream().listen(null));
+      }).then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames,
+            contains(frameMember(startsWith('controllerErrorStream'))));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
     test('and relays them to the parent zone', () {
       var completer = new Completer();
 
@@ -526,50 +624,6 @@
   });
 
   group('Chain.track(Future)', () {
-    test('associates the current chain with a manually-reported exception with '
-        'a stack trace', () {
-      var trace = new Trace.current();
-      return captureFuture(() {
-        inMicrotask(() => trackedErrorFuture(trace));
-      }).then((chain) {
-        expect(chain.traces, hasLength(3));
-
-        // The first trace is the trace that was manually reported for the
-        // error.
-        expect(chain.traces.first.toString(), equals(trace.toString()));
-
-        // The second trace is the trace that was captured when [Chain.track]
-        // was called.
-        expect(chain.traces[1].frames.first,
-            frameMember(startsWith('trackedErrorFuture')));
-
-        // The third trace is the automatically-captured trace from when the
-        // microtask was scheduled.
-        expect(chain.traces[2].frames,
-            contains(frameMember(startsWith('inMicrotask'))));
-      });
-    });
-
-    test('associates the current chain with a manually-reported exception with '
-        'no stack trace', () {
-      return captureFuture(() {
-        inMicrotask(() => trackedErrorFuture());
-      }).then((chain) {
-        expect(chain.traces, hasLength(3));
-
-        // The first trace is the one captured by
-        // [StackZoneSpecification.trackFuture], which should contain only
-        // stack_trace and dart: frames.
-        expect(chain.traces.first.frames,
-            everyElement(frameLibrary(isNot(contains('chain_test')))));
-
-        expect(chain.traces[1].frames.first,
-            frameMember(startsWith('trackedErrorFuture')));
-        expect(chain.traces[2].frames,
-            contains(frameMember(startsWith('inMicrotask'))));
-      });
-    });
-
     test('forwards the future value within Chain.capture()', () {
       Chain.capture(() {
         expect(Chain.track(new Future.value('value')),
@@ -598,36 +652,6 @@
   });
 
   group('Chain.track(Stream)', () {
-    test('associates the current chain with a manually-reported exception with '
-        'a stack trace', () {
-      var trace = new Trace.current();
-      return captureFuture(() {
-        inMicrotask(() => trackedErrorStream(trace).listen(null));
-      }).then((chain) {
-        expect(chain.traces, hasLength(3));
-        expect(chain.traces.first.toString(), equals(trace.toString()));
-        expect(chain.traces[1].frames.first,
-            frameMember(startsWith('trackedErrorStream')));
-        expect(chain.traces[2].frames,
-            contains(frameMember(startsWith('inMicrotask'))));
-      });
-    });
-
-    test('associates the current chain with a manually-reported exception with '
-        'no stack trace', () {
-      return captureFuture(() {
-        inMicrotask(() => trackedErrorStream().listen(null));
-      }).then((chain) {
-        expect(chain.traces, hasLength(3));
-        expect(chain.traces.first.frames,
-            everyElement(frameLibrary(isNot(contains('chain_test')))));
-        expect(chain.traces[1].frames.first,
-            frameMember(startsWith('trackedErrorStream')));
-        expect(chain.traces[2].frames,
-            contains(frameMember(startsWith('inMicrotask'))));
-      });
-    });
-
     test('forwards stream values within Chain.capture()', () {
       Chain.capture(() {
         var controller = new StreamController()
@@ -692,22 +716,30 @@
       .then((_) => new Future(() {}));
 }
 
-/// Returns a Future that completes to an error and is wrapped in [Chain.track].
-///
-/// If [trace] is passed, it's used as the stack trace for the error.
-Future trackedErrorFuture([StackTrace trace]) {
-  var completer = new Completer();
-  completer.completeError('error', trace);
-  return Chain.track(completer.future);
+void inNewFuture(callback()) {
+  new Future(callback);
 }
 
-/// Returns a Stream that emits an error and is wrapped in [Chain.track].
+void inSyncFuture(callback()) {
+  new Future.sync(callback);
+}
+
+/// Returns a Future that completes to an error using a completer.
 ///
 /// If [trace] is passed, it's used as the stack trace for the error.
-Stream trackedErrorStream([StackTrace trace]) {
+Future completerErrorFuture([StackTrace trace]) {
+  var completer = new Completer();
+  completer.completeError('error', trace);
+  return completer.future;
+}
+
+/// Returns a Stream that emits an error using a controller.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Stream controllerErrorStream([StackTrace trace]) {
   var controller = new StreamController();
   controller.addError('error', trace);
-  return Chain.track(controller.stream);
+  return controller.stream;
 }
 
 /// Runs [callback] within [asyncFn], then converts any errors raised into a