Add tests for stack chains in the browser.
Now that dart-lang/sdk#15171 is fixed, this works!
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//1218903003.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36e9aa8..3fdf46e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.3.4
+
+* No longer say that stack chains aren't supported on dart2js now that
+ [sdk#15171][] is fixed. Note that this fix only applies to Dart 1.12.
+
+[sdk#15171]: https://github.com/dart-lang/sdk/issues/15171
+
## 1.3.3
* When a `null` stack trace is passed to a completer or stream controller in
diff --git a/lib/src/chain.dart b/lib/src/chain.dart
index 3e2100e..acd17e9 100644
--- a/lib/src/chain.dart
+++ b/lib/src/chain.dart
@@ -68,13 +68,6 @@
/// 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(() {
diff --git a/pubspec.yaml b/pubspec.yaml
index 578ecc9..93b0817 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.3.3
+version: 1.3.4
author: "Dart Team <misc@dartlang.org>"
homepage: http://github.com/dart-lang/stack_trace
description: >
diff --git a/test/chain/chain_test.dart b/test/chain/chain_test.dart
new file mode 100644
index 0000000..0eb3f00
--- /dev/null
+++ b/test/chain/chain_test.dart
@@ -0,0 +1,282 @@
+// Copyright (c) 2015, 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.
+
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('Chain.parse()', () {
+ test('parses a real Chain', () {
+ return captureFuture(() => inMicrotask(() => throw 'error'))
+ .then((chain) {
+ expect(new Chain.parse(chain.toString()).toString(),
+ equals(chain.toString()));
+ });
+ });
+
+ test('parses an empty string', () {
+ var chain = new Chain.parse('');
+ expect(chain.traces, isEmpty);
+ });
+
+ test('parses a chain containing empty traces', () {
+ var chain = new Chain.parse(
+ '===== asynchronous gap ===========================\n'
+ '===== asynchronous gap ===========================\n');
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames, isEmpty);
+ expect(chain.traces[1].frames, isEmpty);
+ expect(chain.traces[2].frames, isEmpty);
+ });
+ });
+
+ test("toString() ensures that all traces are aligned", () {
+ var chain = new Chain([
+ new Trace.parse('short 10:11 Foo.bar\n'),
+ new Trace.parse('loooooooooooong 10:11 Zop.zoop')
+ ]);
+
+ expect(chain.toString(), equals(
+ 'short 10:11 Foo.bar\n'
+ '===== asynchronous gap ===========================\n'
+ 'loooooooooooong 10:11 Zop.zoop\n'));
+ });
+
+ var userSlashCode = p.join('user', 'code.dart');
+ group('Chain.terse', () {
+ test('makes each trace terse', () {
+ 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'
+ '$userSlashCode 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ '$userSlashCode 10:11 Bang.qux\n'
+ 'dart:core Zip.zap\n'
+ '$userSlashCode 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(
+ '$userSlashCode 10:11 Foo.bar\n'
+ '===== asynchronous gap ===========================\n'
+ '$userSlashCode 10:11 Foo.bar\n'));
+ });
+
+ test("doesn't return an empty chain", () {
+ var chain = new Chain([
+ 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(
+ 'dart:core 10:11 A.b\n'
+ 'package:stack_trace/stack_trace.dart 10:11 C.d\n'
+ 'dart:core 10:11 E.f')
+ ]);
+
+ expect(chain.terse.toString(), equals('dart:core E.f\n'));
+ });
+ });
+
+ group('Chain.foldFrames', () {
+ test('folds each trace', () {
+ var chain = new Chain([
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bar.baz\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'a.dart 10:11 Zop.zoop'),
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bar.baz\n'
+ 'a.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+ expect(folded.toString(), equals(
+ 'a.dart 10:11 Bar.baz\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zop.zoop\n'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test('with terse: true, folds core frames as well', () {
+ var chain = new Chain([
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'dart:async-patch/future.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'dart:core 10:11 Bar.baz\n'
+ 'a.dart 10:11 Zop.zoop'),
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bar.baz\n'
+ 'a.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart',
+ terse: true);
+ expect(folded.toString(), equals(
+ 'dart:async Zip.zap\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'a.dart Zop.zoop\n'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test('eliminates completely-folded traces', () {
+ var chain = new Chain([
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'b.dart 10:11 Bang.qux'),
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bang.qux'),
+ new Trace.parse(
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+ expect(folded.toString(), equals(
+ 'a.dart 10:11 Foo.bar\n'
+ 'b.dart 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test("doesn't return an empty trace", () {
+ var chain = new Chain([
+ new Trace.parse(
+ 'a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bang.qux')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+ expect(folded.toString(), equals('a.dart 10:11 Bang.qux\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(
+ '$userSlashCode 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz\n'
+ '$userSlashCode 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz\n'));
+ });
+
+ group('Chain.track(Future)', () {
+ 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('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);
+ });
+ });
+ });
+}
\ No newline at end of file
diff --git a/test/chain/dart2js_test.dart b/test/chain/dart2js_test.dart
new file mode 100644
index 0000000..afb27fa
--- /dev/null
+++ b/test/chain/dart2js_test.dart
@@ -0,0 +1,338 @@
+// Copyright (c) 2015, 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.
+
+// dart2js chain tests are separated out because dart2js stack traces are
+// inconsistent due to inlining and browser differences. These tests don't
+// assert anything about the content of the traces, just the number of traces in
+// a chain.
+@TestOn('js')
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('capture() with onError catches exceptions', () {
+ test('thrown synchronously', () async {
+ var chain = await captureFuture(() => throw 'error');
+ expect(chain.traces, hasLength(1));
+ });
+
+ test('thrown in a microtask', () async {
+ var chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in a one-shot timer', () async {
+ var chain = await captureFuture(
+ () => inOneShotTimer(() => throw 'error'));
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in a periodic timer', () async {
+ var chain = await captureFuture(
+ () => inPeriodicTimer(() => throw 'error'));
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in a nested series of asynchronous operations', () async {
+ var chain = await captureFuture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() => inMicrotask(() => throw 'error'));
+ });
+ });
+
+ expect(chain.traces, hasLength(4));
+ });
+
+ test('thrown in a long future chain', () async {
+ var chain = await captureFuture(() => inFutureChain(() => throw 'error'));
+
+ // 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));
+ });
+
+ test('thrown in new Future()', () async {
+ var chain = await captureFuture(() => inNewFuture(() => throw 'error'));
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('thrown in new Future.sync()', () async {
+ var chain = await captureFuture(() {
+ inMicrotask(() => inSyncFuture(() => throw 'error'));
+ });
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('multiple times', () {
+ var completer = new Completer();
+ var first = true;
+
+ Chain.capture(() {
+ inMicrotask(() => throw 'first error');
+ inPeriodicTimer(() => throw 'second error');
+ }, onError: (error, chain) {
+ try {
+ if (first) {
+ expect(error, equals('first error'));
+ expect(chain.traces, hasLength(2));
+ first = false;
+ } else {
+ expect(error, equals('second error'));
+ expect(chain.traces, hasLength(2));
+ completer.complete();
+ }
+ } catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ test('passed to a completer', () async {
+ var trace = new Trace.current();
+ var chain = await captureFuture(() {
+ inMicrotask(() => completerErrorFuture(trace));
+ });
+
+ 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()));
+ });
+
+ test('passed to a completer with no stack trace', () async {
+ var chain = await captureFuture(() {
+ inMicrotask(() => completerErrorFuture());
+ });
+
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('passed to a stream controller', () async {
+ var trace = new Trace.current();
+ var chain = await captureFuture(() {
+ inMicrotask(() => controllerErrorStream(trace).listen(null));
+ });
+
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces.first.toString(), equals(trace.toString()));
+ });
+
+ test('passed to a stream controller with no stack trace', () async {
+ var chain = await captureFuture(() {
+ inMicrotask(() => controllerErrorStream().listen(null));
+ });
+
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('and relays them to the parent zone', () {
+ var completer = new Completer();
+
+ runZoned(() {
+ Chain.capture(() {
+ inMicrotask(() => throw 'error');
+ }, onError: (error, chain) {
+ expect(error, equals('error'));
+ expect(chain.traces, hasLength(2));
+ throw error;
+ });
+ }, onError: (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(chain, new isInstanceOf<Chain>());
+ expect(chain.traces, hasLength(2));
+ completer.complete();
+ } catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ 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) {
+ try {
+ expect(error, equals('error'));
+ expect(chain, new isInstanceOf<Chain>());
+ expect(chain.traces, hasLength(2));
+ completer.complete();
+ } catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ group('current() within capture()', () {
+ test('called in a microtask', () async {
+ var completer = new Completer();
+ Chain.capture(() {
+ inMicrotask(() => completer.complete(new Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a one-shot timer', () async {
+ var completer = new Completer();
+ Chain.capture(() {
+ inOneShotTimer(() => completer.complete(new Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a periodic timer', () async {
+ var completer = new Completer();
+ Chain.capture(() {
+ inPeriodicTimer(() => completer.complete(new Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a nested series of asynchronous operations', () async {
+ var completer = new Completer();
+ Chain.capture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() {
+ inMicrotask(() => completer.complete(new Chain.current()));
+ });
+ });
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(4));
+ });
+
+ test('called in a long future chain', () async {
+ var completer = new Completer();
+ Chain.capture(() {
+ inFutureChain(() => completer.complete(new Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+ });
+
+ test('current() outside of capture() returns a chain wrapping the current '
+ 'trace', () {
+ // The test runner runs all tests with chains enabled, so to test without we
+ // have to do some zone munging.
+ return runZoned(() async {
+ var completer = new Completer();
+ inMicrotask(() => completer.complete(new Chain.current()));
+
+ var chain = await completer.future;
+ // 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));
+ }, zoneValues: {#stack_trace.stack_zone.spec: null});
+ });
+
+ group('forTrace() within capture()', () {
+ test('called for a stack trace from a microtask', () async {
+ var chain = await Chain.capture(() {
+ return chainForTrace(inMicrotask, () => throw 'error');
+ });
+
+ // 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));
+ });
+
+ test('called for a stack trace from a one-shot timer', () async {
+ var chain = await Chain.capture(() {
+ return chainForTrace(inOneShotTimer, () => throw 'error');
+ });
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('called for a stack trace from a periodic timer', () async {
+ var chain = await Chain.capture(() {
+ return chainForTrace(inPeriodicTimer, () => throw 'error');
+ });
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('called for a stack trace from a nested series of asynchronous '
+ 'operations', () async {
+ var chain = await Chain.capture(() {
+ return chainForTrace((callback) {
+ inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+ }, () => throw 'error');
+ });
+
+ expect(chain.traces, hasLength(5));
+ });
+
+ test('called for a stack trace from a long future chain', () async {
+ var chain = await Chain.capture(() {
+ return chainForTrace(inFutureChain, () => throw 'error');
+ });
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ 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()));
+ });
+}
diff --git a/test/chain/utils.dart b/test/chain/utils.dart
new file mode 100644
index 0000000..1e19eeb
--- /dev/null
+++ b/test/chain/utils.dart
@@ -0,0 +1,93 @@
+// Copyright (c) 2015, 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.chain.utils;
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// 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(() {}));
+}
+
+void inNewFuture(callback()) {
+ new Future(callback);
+}
+
+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.
+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 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/test/chain/vm_test.dart b/test/chain/vm_test.dart
new file mode 100644
index 0000000..70635b7
--- /dev/null
+++ b/test/chain/vm_test.dart
@@ -0,0 +1,481 @@
+// Copyright (c) 2015, 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.
+
+// VM chain tests can rely on stronger guarantees about the contents of the
+// stack traces than dart2js.
+@TestOn('dart-vm')
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'utils.dart';
+
+void main() {
+ group('capture() with onError catches exceptions', () {
+ test('thrown synchronously', () {
+ return captureFuture(() => throw 'error')
+ .then((chain) {
+ expect(chain.traces, hasLength(1));
+ expect(chain.traces.single.frames.first,
+ frameMember(startsWith('main')));
+ });
+ });
+
+ 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('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;
+
+ Chain.capture(() {
+ inMicrotask(() => throw 'first error');
+ inPeriodicTimer(() => throw 'second error');
+ }, onError: (error, chain) {
+ try {
+ 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();
+ }
+ } catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ 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();
+
+ runZoned(() {
+ Chain.capture(() {
+ inMicrotask(() => throw 'error');
+ }, onError: (error, chain) {
+ expect(error, equals('error'));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ throw error;
+ });
+ }, onError: (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(chain, new isInstanceOf<Chain>());
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ completer.complete();
+ } catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ 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) {
+ try {
+ expect(error, equals('error'));
+ expect(chain, new isInstanceOf<Chain>());
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ completer.complete();
+ } catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ 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', () {
+ // The test runner runs all tests with chains enabled, so to test without we
+ // have to do some zone munging.
+ return runZoned(() {
+ 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')));
+ });
+ }, zoneValues: {#stack_trace.stack_zone.spec: null});
+ });
+
+ 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()));
+ });
+}
diff --git a/test/chain_test.dart b/test/chain_test.dart
deleted file mode 100644
index 65d6790..0000000
--- a/test/chain_test.dart
+++ /dev/null
@@ -1,832 +0,0 @@
-// 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.
-
-// dart2js currently doesn't support chain-capturing. See sdk#15171.
-@TestOn('vm')
-
-import 'dart:async';
-
-import 'package:path/path.dart' as p;
-import 'package:stack_trace/stack_trace.dart';
-import 'package:test/test.dart';
-
-import 'utils.dart';
-
-void main() {
- group('capture() with onError catches exceptions', () {
- test('thrown synchronously', () {
- return captureFuture(() => throw 'error')
- .then((chain) {
- expect(chain.traces, hasLength(1));
- expect(chain.traces.single.frames.first,
- frameMember(startsWith('main')));
- });
- });
-
- 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('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;
-
- Chain.capture(() {
- inMicrotask(() => throw 'first error');
- inPeriodicTimer(() => throw 'second error');
- }, onError: (error, chain) {
- try {
- 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();
- }
- } catch (error, stackTrace) {
- completer.completeError(error, stackTrace);
- }
- });
-
- 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();
-
- runZoned(() {
- Chain.capture(() {
- inMicrotask(() => throw 'error');
- }, onError: (error, chain) {
- expect(error, equals('error'));
- expect(chain.traces[1].frames,
- contains(frameMember(startsWith('inMicrotask'))));
- throw error;
- });
- }, onError: (error, chain) {
- try {
- expect(error, equals('error'));
- expect(chain, new isInstanceOf<Chain>());
- expect(chain.traces[1].frames,
- contains(frameMember(startsWith('inMicrotask'))));
- completer.complete();
- } catch (error, stackTrace) {
- completer.completeError(error, stackTrace);
- }
- });
-
- 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) {
- try {
- expect(error, equals('error'));
- expect(chain, new isInstanceOf<Chain>());
- expect(chain.traces[1].frames,
- contains(frameMember(startsWith('inMicrotask'))));
- completer.complete();
- } catch (error, stackTrace) {
- completer.completeError(error, stackTrace);
- }
- });
-
- 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', () {
- // The test runner runs all tests with chains enabled, so to test without we
- // have to do some zone munging.
- return runZoned(() {
- 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')));
- });
- }, zoneValues: {#stack_trace.stack_zone.spec: null});
- });
-
- 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()));
- });
-
- group('Chain.parse()', () {
- test('parses a real Chain', () {
- return captureFuture(() => inMicrotask(() => throw 'error'))
- .then((chain) {
- expect(new Chain.parse(chain.toString()).toString(),
- equals(chain.toString()));
- });
- });
-
- test('parses an empty string', () {
- var chain = new Chain.parse('');
- expect(chain.traces, isEmpty);
- });
-
- test('parses a chain containing empty traces', () {
- var chain = new Chain.parse(
- '===== asynchronous gap ===========================\n'
- '===== asynchronous gap ===========================\n');
- expect(chain.traces, hasLength(3));
- expect(chain.traces[0].frames, isEmpty);
- expect(chain.traces[1].frames, isEmpty);
- expect(chain.traces[2].frames, isEmpty);
- });
- });
-
- test("toString() ensures that all traces are aligned", () {
- var chain = new Chain([
- new Trace.parse('short 10:11 Foo.bar\n'),
- new Trace.parse('loooooooooooong 10:11 Zop.zoop')
- ]);
-
- expect(chain.toString(), equals(
- 'short 10:11 Foo.bar\n'
- '===== asynchronous gap ===========================\n'
- 'loooooooooooong 10:11 Zop.zoop\n'));
- });
-
- var userSlashCode = p.join('user', 'code.dart');
- group('Chain.terse', () {
- test('makes each trace terse', () {
- 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'
- '$userSlashCode 10:11 Bang.qux\n'
- '===== asynchronous gap ===========================\n'
- '$userSlashCode 10:11 Bang.qux\n'
- 'dart:core Zip.zap\n'
- '$userSlashCode 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(
- '$userSlashCode 10:11 Foo.bar\n'
- '===== asynchronous gap ===========================\n'
- '$userSlashCode 10:11 Foo.bar\n'));
- });
-
- test("doesn't return an empty chain", () {
- var chain = new Chain([
- 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(
- 'dart:core 10:11 A.b\n'
- 'package:stack_trace/stack_trace.dart 10:11 C.d\n'
- 'dart:core 10:11 E.f')
- ]);
-
- expect(chain.terse.toString(), equals('dart:core E.f\n'));
- });
- });
-
- group('Chain.foldFrames', () {
- test('folds each trace', () {
- var chain = new Chain([
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'a.dart 10:11 Bar.baz\n'
- 'b.dart 10:11 Bang.qux\n'
- 'a.dart 10:11 Zip.zap\n'
- 'a.dart 10:11 Zop.zoop'),
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'a.dart 10:11 Bar.baz\n'
- 'a.dart 10:11 Bang.qux\n'
- 'a.dart 10:11 Zip.zap\n'
- 'b.dart 10:11 Zop.zoop')
- ]);
-
- var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
- expect(folded.toString(), equals(
- 'a.dart 10:11 Bar.baz\n'
- 'b.dart 10:11 Bang.qux\n'
- 'a.dart 10:11 Zop.zoop\n'
- '===== asynchronous gap ===========================\n'
- 'a.dart 10:11 Zip.zap\n'
- 'b.dart 10:11 Zop.zoop\n'));
- });
-
- test('with terse: true, folds core frames as well', () {
- var chain = new Chain([
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'dart:async-patch/future.dart 10:11 Zip.zap\n'
- 'b.dart 10:11 Bang.qux\n'
- 'dart:core 10:11 Bar.baz\n'
- 'a.dart 10:11 Zop.zoop'),
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'a.dart 10:11 Bar.baz\n'
- 'a.dart 10:11 Bang.qux\n'
- 'a.dart 10:11 Zip.zap\n'
- 'b.dart 10:11 Zop.zoop')
- ]);
-
- var folded = chain.foldFrames((frame) => frame.library == 'a.dart',
- terse: true);
- expect(folded.toString(), equals(
- 'dart:async Zip.zap\n'
- 'b.dart 10:11 Bang.qux\n'
- 'a.dart Zop.zoop\n'
- '===== asynchronous gap ===========================\n'
- 'a.dart Zip.zap\n'
- 'b.dart 10:11 Zop.zoop\n'));
- });
-
- test('eliminates completely-folded traces', () {
- var chain = new Chain([
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'b.dart 10:11 Bang.qux'),
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'a.dart 10:11 Bang.qux'),
- new Trace.parse(
- 'a.dart 10:11 Zip.zap\n'
- 'b.dart 10:11 Zop.zoop')
- ]);
-
- var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
- expect(folded.toString(), equals(
- 'a.dart 10:11 Foo.bar\n'
- 'b.dart 10:11 Bang.qux\n'
- '===== asynchronous gap ===========================\n'
- 'a.dart 10:11 Zip.zap\n'
- 'b.dart 10:11 Zop.zoop\n'));
- });
-
- test("doesn't return an empty trace", () {
- var chain = new Chain([
- new Trace.parse(
- 'a.dart 10:11 Foo.bar\n'
- 'a.dart 10:11 Bang.qux')
- ]);
-
- var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
- expect(folded.toString(), equals('a.dart 10:11 Bang.qux\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(
- '$userSlashCode 10:11 Foo.bar\n'
- 'dart:core 10:11 Bar.baz\n'
- '$userSlashCode 10:11 Foo.bar\n'
- 'dart:core 10:11 Bar.baz\n'));
- });
-
- group('Chain.track(Future)', () {
- 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('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(() {}));
-}
-
-void inNewFuture(callback()) {
- new Future(callback);
-}
-
-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.
-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 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;
-}