Add a stack chain class to the stack trace package. This class uses zones to track stack traces across asynchronous boundaries. It will aid considerably in debugging errors in heavily-asynchronous programs. R=floitsch@google.com, rnystrom@google.com BUG=7040 Review URL: https://codereview.chromium.org//75863004 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/stack_trace@30738 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart new file mode 100644 index 0000000..f6e2f3f --- /dev/null +++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -0,0 +1,173 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library stack_trace.chain; + +import 'dart:async'; +import 'dart:collection'; + +import 'stack_zone_specification.dart'; +import 'trace.dart'; +import 'utils.dart'; + +/// A function that handles errors in the zone wrapped by [Chain.capture]. +typedef void ChainHandler(error, Chain chain); + +/// A chain of stack traces. +/// +/// A stack chain is a collection of one or more stack traces that collectively +/// represent the path from [main] through nested function calls to a particular +/// code location, usually where an error was thrown. Multiple stack traces are +/// necessary when using asynchronous functions, since the program's stack is +/// reset before each asynchronous callback is run. +/// +/// Stack chains can be automatically tracked using [Chain.capture]. This sets +/// up a new [Zone] in which the current stack chain is tracked and can be +/// accessed using [new Chain.current]. Any errors that would be top-leveled in +/// the zone can be handled, along with their associated chains, with the +/// `onError` callback. +/// +/// For the most part [Chain.capture] will notice when an error is thrown and +/// associate the correct stack chain with it; the chain can be accessed using +/// [new Chain.forTrace]. However, there are some cases where exceptions won't +/// be automatically detected: any [Future] constructor, +/// [Completer.completeError], [Stream.addError], and libraries that use these. +/// 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. +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. + /// + /// Like the frames in a stack trace, the traces are ordered from most local + /// to least local. The first one is the trace where the actual exception was + /// raised, the second one is where that callback was scheduled, and so on. + final List<Trace> traces; + + /// The [StackZoneSpecification] for the current zone. + static StackZoneSpecification get _currentSpec => + Zone.current[#stack_trace.stack_zone.spec]; + + /// Runs [callback] in a [Zone] in which the current stack chain is tracked + /// and automatically associated with (most) errors. + /// + /// If [onError] is passed, any error in the zone that would otherwise go + /// unhandled is passed to it, along with the [Chain] associated with that + /// error. Note that if [callback] produces multiple unhandled errors, + /// [onError] may be called more than once. If [onError] isn't passed, the + /// parent Zone's `unhandledErrorHandler` will be called with the error and + /// its chain. + /// + /// For the most part an error thrown in the zone will have the correct stack + /// chain 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. 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. + /// + /// Note that even if [onError] isn't passed, this zone will still be an error + /// zone. This means that any errors that would cross the zone boundary are + /// considered unhandled. + /// + /// If [callback] returns a value, it will be returned by [capture] as well. + /// + /// Currently, capturing stack chains doesn't work when using dart2js due to + /// issues [15171] and [15105]. Stack chains reported on dart2js will contain + /// only one trace. + /// + /// [15171]: https://code.google.com/p/dart/issues/detail?id=15171 + /// [15105]: https://code.google.com/p/dart/issues/detail?id=15105 + static capture(callback(), {ChainHandler onError}) { + var spec = new StackZoneSpecification(onError); + return runZoned(callback, zoneSpecification: spec.toSpec(), zoneValues: { + #stack_trace.stack_zone.spec: spec + }); + } + + /// Ensures that any errors emitted by [futureOrStream] have the correct stack + /// chain information associated with them. + /// + /// 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); + } + } + + /// Returns the current stack chain. + /// + /// By default, the first frame of the first trace will be the line where + /// [Chain.current] is called. If [level] is passed, the first trace will + /// start that many frames up instead. + /// + /// If this is called outside of a [capture] zone, it just returns a + /// single-trace chain. + factory Chain.current([int level=0]) { + if (_currentSpec != null) return _currentSpec.currentChain(level + 1); + return new Chain([new Trace.current(level + 1)]); + } + + /// Returns the stack chain associated with [trace]. + /// + /// The first stack trace in the returned chain will always be [trace] + /// (converted to a [Trace] if necessary). If there is no chain associated + /// with [trace] or if this is called outside of a [capture] zone, this just + /// returns a single-trace chain containing [trace]. + /// + /// If [trace] is already a [Chain], it will be returned as-is. + factory Chain.forTrace(StackTrace trace) { + if (trace is Chain) return trace; + if (_currentSpec == null) return new Chain([new Trace.from(trace)]); + return _currentSpec.chainFor(trace); + } + + /// 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))); + + /// Returns a new [Chain] comprised of [traces]. + Chain(Iterable<Trace> traces) + : traces = new UnmodifiableListView<Trace>(traces.toList()); + + /// Returns a terser version of [this]. + /// + /// This calls [Trace.terse] on every trace in [traces], and discards any + /// trace that contain only internal frames. + Chain get terse { + return new Chain(traces.map((trace) => trace.terse).where((trace) { + // Ignore traces that contain only internal processing. + return trace.frames.length > 1; + })); + } + + /// Converts [this] to a [Trace]. + /// + /// The trace version of a chain is just the concatenation of all the traces + /// in the chain. + Trace toTrace() => new Trace(flatten(traces.map((trace) => trace.frames))); + + String toString() => traces.join(_GAP); +}
diff --git a/pkgs/stack_trace/lib/src/stack_zone_specification.dart b/pkgs/stack_trace/lib/src/stack_zone_specification.dart new file mode 100644 index 0000000..36e0717 --- /dev/null +++ b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
@@ -0,0 +1,205 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library stack_trace.stack_zone_specification; + +import 'dart:async'; + +import 'trace.dart'; +import 'chain.dart'; + +/// A class encapsulating the zone specification for a [Chain.capture] zone. +/// +/// Until they're materialized and exposed to the user, stack chains are tracked +/// as linked lists of [Trace]s using the [_Node] class. These nodes are stored +/// in three distinct ways: +/// +/// * When a callback is registered, a node is created and stored as a captured +/// local variable until the callback is run. +/// +/// * When a callback is run, its captured node is set as the [_currentNode] so +/// it can be available to [Chain.current] and to be linked into additional +/// chains when more callbacks are scheduled. +/// +/// * When a callback throws an error or a [Chain.track]ed Future or Stream +/// emits an error, the current node is associated with that error's stack +/// trace using the [_chains] expando. +/// +/// Since [ZoneSpecification] can't be extended or even implemented, in order to +/// get a real [ZoneSpecification] instance it's necessary to call [toSpec]. +class StackZoneSpecification { + /// The expando that associates stack chains with [StackTrace]s. + /// + /// The chains are associated with stack traces rather than errors themselves + /// because it's a common practice to throw strings as errors, which can't be + /// used with expandos. + /// + /// The chain associated with a given stack trace doesn't contain a node for + /// that stack trace. + final _chains = new Expando<_Node>("stack chains"); + + /// The error handler for the zone. + /// + /// If this is null, that indicates that any unhandled errors should be passed + /// to the parent zone. + final ChainHandler _onError; + + /// The most recent node of the current stack chain. + _Node _currentNode; + + StackZoneSpecification([this._onError]); + + /// Converts [this] to a real [ZoneSpecification]. + ZoneSpecification toSpec() { + return new ZoneSpecification( + handleUncaughtError: handleUncaughtError, + registerCallback: registerCallback, + registerUnaryCallback: registerUnaryCallback, + registerBinaryCallback: registerBinaryCallback); + } + + /// Returns the current stack chain. + /// + /// By default, the first frame of the first trace will be the line where + /// [currentChain] is called. If [level] is passed, the first trace will start + /// that many frames up instead. + Chain currentChain([int level=0]) => _createNode(level + 1).toChain(); + + /// Returns the stack chain associated with [trace], if one exists. + /// + /// The first stack trace in the returned chain will always be [trace] + /// (converted to a [Trace] if necessary). If there is no chain associated + /// with [trace], this just returns a single-trace chain containing [trace]. + Chain chainFor(StackTrace trace) { + if (trace is Chain) return trace; + var previous = trace == null ? null : _chains[trace]; + return new _Node(trace, previous).toChain(); + } + + /// Ensures that an error emitted by [future] has the correct stack + /// information associated with it. + /// + /// By default, the first frame of the first trace will be the line where + /// [trackFuture] is called. If [level] is passed, the first trace will start + /// that many frames up instead. + Future trackFuture(Future future, [int level=0]) { + var completer = new Completer.sync(); + 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; + completer.completeError(e, stackTrace); + }); + return completer.future; + } + + /// Ensures that any errors emitted by [stream] have the correct stack + /// information associated with them. + /// + /// By default, the first frame of the first trace will be the line where + /// [trackStream] is called. If [level] is passed, the first trace will start + /// that many frames up instead. + Stream trackStream(Stream stream, [int level=0]) { + var node = _createNode(level + 1); + return stream.transform(new StreamTransformer.fromHandlers( + handleError: (error, stackTrace, sink) { + if (stackTrace == null) stackTrace = new Trace.current(); + if (_chains[stackTrace] == null) _chains[stackTrace] = node; + sink.addError(error, stackTrace); + })); + } + + /// Tracks the current stack chain so it can be set to [_currentChain] when + /// [f] is run. + ZoneCallback registerCallback(Zone self, ZoneDelegate parent, Zone zone, + Function f) { + if (f == null) return parent.registerCallback(zone, null); + var node = _createNode(1); + return parent.registerCallback(zone, () => _run(f, node)); + } + + /// Tracks the current stack chain so it can be set to [_currentChain] when + /// [f] is run. + ZoneUnaryCallback registerUnaryCallback(Zone self, ZoneDelegate parent, + Zone zone, Function f) { + if (f == null) return parent.registerUnaryCallback(zone, null); + var node = _createNode(1); + return parent.registerUnaryCallback(zone, (arg) { + return _run(() => f(arg), node); + }); + } + + /// Tracks the current stack chain so it can be set to [_currentChain] when + /// [f] is run. + ZoneBinaryCallback registerBinaryCallback(Zone self, ZoneDelegate parent, + Zone zone, Function f) { + if (f == null) return parent.registerBinaryCallback(zone, null); + var node = _createNode(1); + return parent.registerBinaryCallback(zone, (arg1, arg2) { + return _run(() => f(arg1, arg2), node); + }); + } + + /// Looks up the chain associated with [stackTrace] and passes it either to + /// [_onError] or [parent]'s error handler. + handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone, error, + StackTrace stackTrace) { + if (_onError == null) { + return parent.handleUncaughtError(zone, error, chainFor(stackTrace)); + } else { + _onError(error, chainFor(stackTrace)); + } + } + + /// Creates a [_Node] with the current stack trace and linked to + /// [_currentNode]. + /// + /// By default, the first frame of the first trace will be the line where + /// [_createNode] is called. If [level] is passed, the first trace will start + /// that many frames up instead. + _Node _createNode([int level=0]) => + new _Node(new Trace.current(level + 1), _currentNode); + + // TODO(nweiz): use a more robust way of detecting and tracking errors when + // issue 15105 is fixed. + /// Runs [f] with [_currentNode] set to [node]. + /// + /// If [f] throws an error, this associates [node] with that error's stack + /// trace. + _run(Function f, _Node node) { + var previousNode = _currentNode; + _currentNode = node; + try { + return f(); + } catch (e, stackTrace) { + _chains[stackTrace] = node; + rethrow; + } finally { + _currentNode = previousNode; + } + } +} + +/// A linked list node representing a single entry in a stack chain. +class _Node { + /// The stack trace for this link of the chain. + final Trace trace; + + /// The previous node in the chain. + final _Node previous; + + _Node(StackTrace trace, [this.previous]) + : trace = trace == null ? new Trace.current() : new Trace.from(trace); + + /// Converts this to a [Chain]. + Chain toChain() { + var nodes = <Trace>[]; + var node = this; + while (node != null) { + nodes.add(node.trace); + node = node.previous; + } + return new Chain(nodes); + } +}
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart index c3db43e..88b9275 100644 --- a/pkgs/stack_trace/lib/src/trace.dart +++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -7,6 +7,7 @@ import 'dart:collection'; import 'dart:math' as math; +import 'chain.dart'; import 'frame.dart'; import 'lazy_trace.dart'; import 'utils.dart'; @@ -92,6 +93,7 @@ /// a [Trace], it will be returned as-is. factory Trace.from(StackTrace trace) { if (trace is Trace) return trace; + if (trace is Chain) return trace.toTrace(); return new LazyTrace(() => new Trace.parse(trace.toString())); } @@ -172,9 +174,14 @@ .where((line) => line != '[native code]') .map((line) => new Frame.parseFirefox(line))); - /// Parses this package's a string representation of a stack trace. + /// Parses this package's string representation of a stack trace. + /// + /// 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))); /// Returns a new [Trace] comprised of [frames]. @@ -191,10 +198,13 @@ /// Returns a terser version of [this]. /// /// This is accomplished by folding together multiple stack frames from the - /// core library, as in [foldFrames]. Remaining core library frames have their - /// libraries, "-patch" suffixes, and line numbers removed. + /// core library or from this package, as in [foldFrames]. Remaining core + /// library frames have their libraries, "-patch" suffixes, and line numbers + /// removed. Trace get terse { - return new Trace(foldFrames((frame) => frame.isCore).frames.map((frame) { + return new Trace(foldFrames((frame) { + return frame.isCore || frame.package == 'stack_trace'; + }).frames.map((frame) { if (!frame.isCore) return frame; var library = frame.library.replaceAll(_terseRegExp, ''); return new Frame(Uri.parse(library), null, null, frame.member);
diff --git a/pkgs/stack_trace/lib/src/utils.dart b/pkgs/stack_trace/lib/src/utils.dart index 08b3b96..62a2820 100644 --- a/pkgs/stack_trace/lib/src/utils.dart +++ b/pkgs/stack_trace/lib/src/utils.dart
@@ -18,3 +18,19 @@ return result.toString(); } +/// Flattens nested lists inside an iterable into a single list containing only +/// non-list elements. +List flatten(Iterable nested) { + var result = []; + helper(list) { + for (var element in list) { + if (element is List) { + helper(element); + } else { + result.add(element); + } + } + } + helper(nested); + return result; +}
diff --git a/pkgs/stack_trace/lib/stack_trace.dart b/pkgs/stack_trace/lib/stack_trace.dart index dba95e9..ac875a9 100644 --- a/pkgs/stack_trace/lib/stack_trace.dart +++ b/pkgs/stack_trace/lib/stack_trace.dart
@@ -25,3 +25,4 @@ export 'src/trace.dart'; export 'src/frame.dart'; +export 'src/chain.dart';
diff --git a/pkgs/stack_trace/test/chain_test.dart b/pkgs/stack_trace/test/chain_test.dart new file mode 100644 index 0000000..4a58a64 --- /dev/null +++ b/pkgs/stack_trace/test/chain_test.dart
@@ -0,0 +1,618 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library chain_test; + +import 'dart:async'; + +import 'package:stack_trace/stack_trace.dart'; +import 'package:unittest/unittest.dart'; + +import 'utils.dart'; + +void main() { + group('capture() with onError catches exceptions', () { + test('thrown in a microtask', () { + return captureFuture(() => inMicrotask(() => throw 'error')) + .then((chain) { + // Since there was only one asynchronous operation, there should be only + // two traces in the chain. + expect(chain.traces, hasLength(2)); + + // The first frame of the first trace should be the line on which the + // actual error was thrown. + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + + // The second trace should describe the stack when the error callback + // was scheduled. + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inMicrotask')))); + }); + }); + + test('thrown in a one-shot timer', () { + return captureFuture(() => inOneShotTimer(() => throw 'error')) + .then((chain) { + expect(chain.traces, hasLength(2)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inOneShotTimer')))); + }); + }); + + test('thrown in a periodic timer', () { + return captureFuture(() => inPeriodicTimer(() => throw 'error')) + .then((chain) { + expect(chain.traces, hasLength(2)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + }); + }); + + test('thrown in a nested series of asynchronous operations', () { + return captureFuture(() { + inPeriodicTimer(() { + inOneShotTimer(() => inMicrotask(() => throw 'error')); + }); + }).then((chain) { + expect(chain.traces, hasLength(4)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inMicrotask')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inOneShotTimer')))); + expect(chain.traces[3].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + }); + }); + + test('thrown in a long future chain', () { + return captureFuture(() => inFutureChain(() => throw 'error')) + .then((chain) { + // Despite many asynchronous operations, there's only one level of + // nested calls, so there should be only two traces in the chain. This + // is important; programmers expect stack trace memory consumption to be + // O(depth of program), not O(length of program). + expect(chain.traces, hasLength(2)); + + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inFutureChain')))); + }); + }); + + test('multiple times', () { + var completer = new Completer(); + var first = true; + + Chain.capture(() { + inMicrotask(() => throw 'first error'); + inPeriodicTimer(() => throw 'second error'); + }, onError: (error, chain) { + if (first) { + expect(error, equals('first error')); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inMicrotask')))); + first = false; + } else { + expect(error, equals('second error')); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + completer.complete(); + } + }); + + return completer.future; + }); + }); + + test('capture() without onError passes exceptions to parent zone', () { + var completer = new Completer(); + + runZoned(() { + Chain.capture(() => inMicrotask(() => throw 'error')); + }, onError: (error, chain) { + expect(error, equals('error')); + expect(chain, new isInstanceOf<Chain>()); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inMicrotask')))); + completer.complete(); + }); + + return completer.future; + }); + + group('current() within capture()', () { + test('called in a microtask', () { + var completer = new Completer(); + Chain.capture(() { + inMicrotask(() => completer.complete(new Chain.current())); + }); + + return completer.future.then((chain) { + expect(chain.traces, hasLength(2)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inMicrotask')))); + }); + }); + + test('called in a one-shot timer', () { + var completer = new Completer(); + Chain.capture(() { + inOneShotTimer(() => completer.complete(new Chain.current())); + }); + + return completer.future.then((chain) { + expect(chain.traces, hasLength(2)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inOneShotTimer')))); + }); + }); + + test('called in a periodic timer', () { + var completer = new Completer(); + Chain.capture(() { + inPeriodicTimer(() => completer.complete(new Chain.current())); + }); + + return completer.future.then((chain) { + expect(chain.traces, hasLength(2)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + }); + }); + + test('called in a nested series of asynchronous operations', () { + var completer = new Completer(); + Chain.capture(() { + inPeriodicTimer(() { + inOneShotTimer(() { + inMicrotask(() => completer.complete(new Chain.current())); + }); + }); + }); + + return completer.future.then((chain) { + expect(chain.traces, hasLength(4)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inMicrotask')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inOneShotTimer')))); + expect(chain.traces[3].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + }); + }); + + test('called in a long future chain', () { + var completer = new Completer(); + Chain.capture(() { + inFutureChain(() => completer.complete(new Chain.current())); + }); + + return completer.future.then((chain) { + expect(chain.traces, hasLength(2)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('inFutureChain')))); + }); + }); + }); + + test('current() outside of capture() returns a chain wrapping the current ' + 'trace', () { + var completer = new Completer(); + inMicrotask(() => completer.complete(new Chain.current())); + + return completer.future.then((chain) { + // Since the chain wasn't loaded within [Chain.capture], the full stack + // chain isn't available and it just returns the current stack when + // called. + expect(chain.traces, hasLength(1)); + expect(chain.traces.first.frames.first, frameMember(startsWith('main'))); + }); + }); + + group('forTrace() within capture()', () { + test('called for a stack trace from a microtask', () { + return Chain.capture(() { + return chainForTrace(inMicrotask, () => throw 'error'); + }).then((chain) { + // Because [chainForTrace] has to set up a future chain to capture the + // stack trace while still showing it to the zone specification, it adds + // an additional level of async nesting and so an additional trace. + expect(chain.traces, hasLength(3)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('chainForTrace')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inMicrotask')))); + }); + }); + + test('called for a stack trace from a one-shot timer', () { + return Chain.capture(() { + return chainForTrace(inOneShotTimer, () => 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('chainForTrace')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inOneShotTimer')))); + }); + }); + + test('called for a stack trace from a periodic timer', () { + return Chain.capture(() { + return chainForTrace(inPeriodicTimer, () => 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('chainForTrace')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + }); + }); + + test('called for a stack trace from a nested series of asynchronous ' + 'operations', () { + return Chain.capture(() { + return chainForTrace((callback) { + inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback))); + }, () => throw 'error'); + }).then((chain) { + expect(chain.traces, hasLength(5)); + expect(chain.traces[0].frames.first, frameMember(startsWith('main'))); + expect(chain.traces[1].frames, + contains(frameMember(startsWith('chainForTrace')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inMicrotask')))); + expect(chain.traces[3].frames, + contains(frameMember(startsWith('inOneShotTimer')))); + expect(chain.traces[4].frames, + contains(frameMember(startsWith('inPeriodicTimer')))); + }); + }); + + test('called for a stack trace from a long future chain', () { + return Chain.capture(() { + return chainForTrace(inFutureChain, () => 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('chainForTrace')))); + expect(chain.traces[2].frames, + contains(frameMember(startsWith('inFutureChain')))); + }); + }); + + test('called for an unregistered stack trace returns a chain wrapping that ' + 'trace', () { + var trace; + var chain = Chain.capture(() { + try { + throw 'error'; + } catch (_, stackTrace) { + trace = stackTrace; + return new Chain.forTrace(stackTrace); + } + }); + + expect(chain.traces, hasLength(1)); + expect(chain.traces.first.toString(), + equals(new Trace.from(trace).toString())); + }); + }); + + test('forTrace() outside of capture() returns a chain wrapping the given ' + 'trace', () { + var trace; + var chain = Chain.capture(() { + try { + throw 'error'; + } catch (_, stackTrace) { + trace = stackTrace; + return new Chain.forTrace(stackTrace); + } + }); + + expect(chain.traces, hasLength(1)); + expect(chain.traces.first.toString(), + 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.terse', () { + test('makes each trace terse', () { + var chain = new Chain([ + new Trace.parse( + 'dart:core 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz\n' + 'user/code.dart 10:11 Bang.qux\n' + 'dart:core 10:11 Zip.zap\n' + 'dart:core 10:11 Zop.zoop'), + new Trace.parse( + 'user/code.dart 10:11 Bang.qux\n' + 'dart:core 10:11 Foo.bar\n' + 'package:stack_trace/stack_trace.dart 10:11 Bar.baz\n' + 'dart:core 10:11 Zip.zap\n' + 'user/code.dart 10:11 Zop.zoop') + ]); + + expect(chain.terse.toString(), equals( + 'dart:core Bar.baz\n' + 'user/code.dart 10:11 Bang.qux\n' + 'dart:core Zop.zoop\n' + '===== asynchronous gap ===========================\n' + 'user/code.dart 10:11 Bang.qux\n' + 'dart:core Zip.zap\n' + 'user/code.dart 10:11 Zop.zoop\n')); + }); + + test('eliminates internal-only traces', () { + var chain = new Chain([ + new Trace.parse( + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz'), + new Trace.parse( + 'dart:core 10:11 Foo.bar\n' + 'package:stack_trace/stack_trace.dart 10:11 Bar.baz\n' + 'dart:core 10:11 Zip.zap'), + new Trace.parse( + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz') + ]); + + expect(chain.terse.toString(), equals( + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core Bar.baz\n' + '===== asynchronous gap ===========================\n' + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core Bar.baz\n')); + }); + }); + + test('Chain.toTrace eliminates asynchronous gaps', () { + var trace = new Chain([ + new Trace.parse( + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz'), + new Trace.parse( + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz') + ]).toTrace(); + + expect(trace.toString(), equals( + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz\n' + 'user/code.dart 10:11 Foo.bar\n' + 'dart:core 10:11 Bar.baz\n')); + }); + + 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')), + completion(equals('value'))); + + var trace = new Trace.current(); + expect(Chain.track(new Future.error('error', trace)) + .catchError((e, stackTrace) { + expect(e, equals('error')); + expect(stackTrace.toString(), equals(trace.toString())); + }), completes); + }); + }); + + test('forwards the future value outside of Chain.capture()', () { + expect(Chain.track(new Future.value('value')), + completion(equals('value'))); + + var trace = new Trace.current(); + expect(Chain.track(new Future.error('error', trace)) + .catchError((e, stackTrace) { + expect(e, equals('error')); + expect(stackTrace.toString(), equals(trace.toString())); + }), completes); + }); + }); + + 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() + ..add(1)..add(2)..add(3)..close(); + expect(Chain.track(controller.stream).toList(), + completion(equals([1, 2, 3]))); + + var trace = new Trace.current(); + controller = new StreamController()..addError('error', trace); + expect(Chain.track(controller.stream).toList() + .catchError((e, stackTrace) { + expect(e, equals('error')); + expect(stackTrace.toString(), equals(trace.toString())); + }), completes); + }); + }); + + test('forwards stream values outside of Chain.capture()', () { + Chain.capture(() { + var controller = new StreamController() + ..add(1)..add(2)..add(3)..close(); + expect(Chain.track(controller.stream).toList(), + completion(equals([1, 2, 3]))); + + var trace = new Trace.current(); + controller = new StreamController()..addError('error', trace); + expect(Chain.track(controller.stream).toList() + .catchError((e, stackTrace) { + expect(e, equals('error')); + expect(stackTrace.toString(), equals(trace.toString())); + }), completes); + }); + }); + }); +} + +/// Runs [callback] in a microtask callback. +void inMicrotask(callback()) => scheduleMicrotask(callback); + +/// Runs [callback] in a one-shot timer callback. +void inOneShotTimer(callback()) => Timer.run(callback); + +/// Runs [callback] once in a periodic timer callback. +void inPeriodicTimer(callback()) { + var count = 0; + new Timer.periodic(new Duration(milliseconds: 1), (timer) { + count++; + if (count != 5) return; + timer.cancel(); + callback(); + }); +} + +/// Runs [callback] within a long asynchronous Future chain. +void inFutureChain(callback()) { + new Future(() {}) + .then((_) => new Future(() {})) + .then((_) => new Future(() {})) + .then((_) => new Future(() {})) + .then((_) => new Future(() {})) + .then((_) => callback()) + .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); +} + +/// Returns a Stream that emits an error and is wrapped in [Chain.track]. +/// +/// If [trace] is passed, it's used as the stack trace for the error. +Stream trackedErrorStream([StackTrace trace]) { + var controller = new StreamController(); + controller.addError('error', trace); + return Chain.track(controller.stream); +} + +/// Runs [callback] within [asyncFn], then converts any errors raised into a +/// [Chain] with [Chain.forTrace]. +Future<Chain> chainForTrace(asyncFn(callback()), callback()) { + var completer = new Completer(); + asyncFn(() { + // We use `new Future.value().then(...)` here as opposed to [new Future] or + // [new Future.sync] because those methods don't pass the exception through + // the zone specification before propagating it, so there's no chance to + // attach a chain to its stack trace. See issue 15105. + new Future.value().then((_) => callback()) + .catchError(completer.completeError); + }); + return completer.future + .catchError((_, stackTrace) => new Chain.forTrace(stackTrace)); +} + +/// Runs [callback] in a [Chain.capture] zone and returns a Future that +/// completes to the stack chain for an error thrown by [callback]. +/// +/// [callback] is expected to throw the string `"error"`. +Future<Chain> captureFuture(callback()) { + var completer = new Completer<Chain>(); + Chain.capture(callback, onError: (error, chain) { + expect(error, equals('error')); + completer.complete(chain); + }); + return completer.future; +}
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart index ad916f1..4c3278f 100644 --- a/pkgs/stack_trace/test/trace_test.dart +++ b/pkgs/stack_trace/test/trace_test.dart
@@ -171,6 +171,24 @@ equals(Uri.parse("http://dartlang.org/foo/baz.dart"))); }); + test('parses a package:stack_trace stack chain correctly', () { + var trace = new Trace.parse( + 'http://dartlang.org/foo/bar.dart 10:11 Foo.<fn>.bar\n' + 'http://dartlang.org/foo/baz.dart Foo.<fn>.bar\n' + '===== asynchronous gap ===========================\n' + 'http://dartlang.org/foo/bang.dart 10:11 Foo.<fn>.bar\n' + 'http://dartlang.org/foo/quux.dart Foo.<fn>.bar'); + + expect(trace.frames[0].uri, + equals(Uri.parse("http://dartlang.org/foo/bar.dart"))); + expect(trace.frames[1].uri, + equals(Uri.parse("http://dartlang.org/foo/baz.dart"))); + expect(trace.frames[2].uri, + equals(Uri.parse("http://dartlang.org/foo/bang.dart"))); + expect(trace.frames[3].uri, + equals(Uri.parse("http://dartlang.org/foo/quux.dart"))); + }); + test('parses a real package:stack_trace stack trace correctly', () { var traceString = new Trace.current().toString(); expect(new Trace.parse(traceString).toString(), equals(traceString));
diff --git a/pkgs/stack_trace/test/utils.dart b/pkgs/stack_trace/test/utils.dart new file mode 100644 index 0000000..ed75631 --- /dev/null +++ b/pkgs/stack_trace/test/utils.dart
@@ -0,0 +1,37 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library stack_trace.test.utils; + +import 'package:unittest/unittest.dart'; + +/// Returns a matcher that runs [matcher] against a [Frame]'s `member` field. +Matcher frameMember(matcher) => + transform((frame) => frame.member, matcher, 'member'); + +/// Returns a matcher that runs [matcher] against a [Frame]'s `library` field. +Matcher frameLibrary(matcher) => + transform((frame) => frame.library, matcher, 'library'); + +/// Returns a matcher that runs [transformation] on its input, then matches +/// the output against [matcher]. +/// +/// [description] should be a noun phrase that describes the relation of the +/// output of [transformation] to its input. +Matcher transform(transformation(value), matcher, String description) => + new _TransformMatcher(transformation, wrapMatcher(matcher), description); + +class _TransformMatcher extends Matcher { + final Function _transformation; + final Matcher _matcher; + final String _description; + + _TransformMatcher(this._transformation, this._matcher, this._description); + + bool matches(item, Map matchState) => + _matcher.matches(_transformation(item), matchState); + + Description describe(Description description) => + description.add(_description).add(' ').addDescriptionOf(_matcher); +}