Add tests for stack chains in the browser. Now that dart-lang/sdkdart-lang/stack_trace#15171 is fixed, this works! R=rnystrom@google.com Review URL: https://codereview.chromium.org//1218903003.
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md index 36e9aa8..3fdf46e 100644 --- a/pkgs/stack_trace/CHANGELOG.md +++ b/pkgs/stack_trace/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/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart index 3e2100e..acd17e9 100644 --- a/pkgs/stack_trace/lib/src/chain.dart +++ b/pkgs/stack_trace/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/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml index 578ecc9..93b0817 100644 --- a/pkgs/stack_trace/pubspec.yaml +++ b/pkgs/stack_trace/pubspec.yaml
@@ -7,7 +7,7 @@ # # When the major version is upgraded, you *must* update that version constraint # in pub to stay in sync with this. -version: 1.3.3 +version: 1.3.4 author: "Dart Team <misc@dartlang.org>" homepage: http://github.com/dart-lang/stack_trace description: >
diff --git a/pkgs/stack_trace/test/chain/chain_test.dart b/pkgs/stack_trace/test/chain/chain_test.dart new file mode 100644 index 0000000..0eb3f00 --- /dev/null +++ b/pkgs/stack_trace/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/pkgs/stack_trace/test/chain/dart2js_test.dart b/pkgs/stack_trace/test/chain/dart2js_test.dart new file mode 100644 index 0000000..afb27fa --- /dev/null +++ b/pkgs/stack_trace/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/pkgs/stack_trace/test/chain/utils.dart b/pkgs/stack_trace/test/chain/utils.dart new file mode 100644 index 0000000..1e19eeb --- /dev/null +++ b/pkgs/stack_trace/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/pkgs/stack_trace/test/chain/vm_test.dart b/pkgs/stack_trace/test/chain/vm_test.dart new file mode 100644 index 0000000..70635b7 --- /dev/null +++ b/pkgs/stack_trace/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/pkgs/stack_trace/test/chain_test.dart b/pkgs/stack_trace/test/chain_test.dart deleted file mode 100644 index 65d6790..0000000 --- a/pkgs/stack_trace/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; -}