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/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md index a5d594a..56c614a 100644 --- a/pkgs/stack_trace/CHANGELOG.md +++ b/pkgs/stack_trace/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/pkgs/stack_trace/README.md b/pkgs/stack_trace/README.md index 492a52a..38b768d 100644 --- a/pkgs/stack_trace/README.md +++ b/pkgs/stack_trace/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/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart index 78c6ae4..e4ac534 100644 --- a/pkgs/stack_trace/lib/src/chain.dart +++ b/pkgs/stack_trace/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/pkgs/stack_trace/lib/src/stack_zone_specification.dart b/pkgs/stack_trace/lib/src/stack_zone_specification.dart index 9a4f7c0..292da65 100644 --- a/pkgs/stack_trace/lib/src/stack_zone_specification.dart +++ b/pkgs/stack_trace/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/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml index 5226cf8..bf28770 100644 --- a/pkgs/stack_trace/pubspec.yaml +++ b/pkgs/stack_trace/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/pkgs/stack_trace/test/chain_test.dart b/pkgs/stack_trace/test/chain_test.dart index beb4721..6af3c6b 100644 --- a/pkgs/stack_trace/test/chain_test.dart +++ b/pkgs/stack_trace/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