Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1767)

Unified Diff: pkg/stack_trace/test/chain_test.dart

Issue 75863004: Add a stack chain class to the stack trace package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: code review Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/stack_trace/lib/stack_trace.dart ('k') | pkg/stack_trace/test/trace_test.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/stack_trace/test/chain_test.dart
diff --git a/pkg/stack_trace/test/chain_test.dart b/pkg/stack_trace/test/chain_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4a58a64d9792ce6e1fd0f4e9158a91bfd39bfa65
--- /dev/null
+++ b/pkg/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;
+}
« no previous file with comments | « pkg/stack_trace/lib/stack_trace.dart ('k') | pkg/stack_trace/test/trace_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698