Merge branch 'main' into merge-stack_trace-package
diff --git a/.github/ISSUE_TEMPLATE/stack_trace.md b/.github/ISSUE_TEMPLATE/stack_trace.md
new file mode 100644
index 0000000..417362b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stack_trace.md
@@ -0,0 +1,5 @@
+---
+name: "package:stack_trace"
+about: "Create a bug or file a feature request against package:stack_trace."
+labels: "package:stack_trace"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 3ab79c0..25efb2a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -112,6 +112,10 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/sse/**'
+'package:stack_trace':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/stack_trace/**'
+
'package:stream_transform':
- changed-files:
- any-glob-to-any-file: 'pkgs/stream_transform/**'
diff --git a/.github/workflows/stack_trace.yaml b/.github/workflows/stack_trace.yaml
new file mode 100644
index 0000000..7435967
--- /dev/null
+++ b/.github/workflows/stack_trace.yaml
@@ -0,0 +1,75 @@
+name: package:stack_trace
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stack_trace.yaml'
+ - 'pkgs/stack_trace/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stack_trace.yaml'
+ - 'pkgs/stack_trace/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/stack_trace/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run browser tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 0201aa2..563f90d 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@
| [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [](https://pub.dev/packages/source_maps) |
| [source_span](pkgs/source_span/) | Provides a standard representation for source code locations and spans. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_span) | [](https://pub.dev/packages/source_span) |
| [sse](pkgs/sse/) | Provides client and server functionality for setting up bi-directional communication through Server Sent Events (SSE) and corresponding POST requests. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asse) | [](https://pub.dev/packages/sse) |
+| [stack_trace](pkgs/stack_trace/) | A package for manipulating stack traces and printing them readably. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astack_trace) | [](https://pub.dev/packages/stack_trace) |
| [stream_transform](pkgs/stream_transform/) | A collection of utilities to transform and manipulate streams. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_transform) | [](https://pub.dev/packages/stream_transform) |
| [term_glyph](pkgs/term_glyph/) | Useful Unicode glyphs and ASCII substitutes. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aterm_glyph) | [](https://pub.dev/packages/term_glyph) |
| [test_reflective_loader](pkgs/test_reflective_loader/) | Support for discovering tests and test suites using reflection. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader) | [](https://pub.dev/packages/test_reflective_loader) |
diff --git a/pkgs/stack_trace/.gitignore b/pkgs/stack_trace/.gitignore
new file mode 100644
index 0000000..f023015
--- /dev/null
+++ b/pkgs/stack_trace/.gitignore
@@ -0,0 +1,6 @@
+# See https://dart.dev/guides/libraries/private-files
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
new file mode 100644
index 0000000..e92cf9c
--- /dev/null
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -0,0 +1,363 @@
+## 1.12.1
+
+* Move to `dart-lang/tools` monorepo.
+
+## 1.12.0
+
+* Added support for parsing Wasm frames of Chrome (V8), Firefox, Safari.
+* Require Dart 3.4 or greater
+
+## 1.11.1
+
+* Make use of `@pragma('vm:awaiter-link')` to make package work better with
+ Dart VM's builtin awaiter stack unwinding. No other changes.
+
+## 1.11.0
+
+* Added the parameter `zoneValues` to `Chain.capture` to be able to use custom
+ zone values with the `runZoned` internal calls.
+* Populate the pubspec `repository` field.
+* Require Dart 2.18 or greater
+
+## 1.10.0
+
+* Stable release for null safety.
+* Fix broken test, `test/chain/vm_test.dart`, which incorrectly handles
+ asynchronous suspension gap markers at the end of stack traces.
+
+## 1.10.0-nullsafety.6
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces, when parsing with `Trace.parse` and `Chain.parse`.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+ guidelines.
+
+## 1.10.0-nullsafety.5
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.10.0-nullsafety.4
+
+* Allow the `2.10.0` stable and dev SDKs.
+
+## 1.10.0-nullsafety.3
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces.
+
+## 1.10.0-nullsafety.2
+
+* Forward fix for a change in SDK type promotion behavior.
+
+## 1.10.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.10.0-nullsafety
+
+* Opt in to null safety.
+
+## 1.9.6 (backpublish)
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces. (Also fixed separately in 1.10.0-nullsafety.3)
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces, when parsing with `Trace.parse` and `Chain.parse`. (Also fixed
+ separately in 1.10.0-nullsafety.6)
+
+## 1.9.5
+
+* Parse the format for `data:` URIs that the Dart VM has used since `2.2.0`.
+
+## 1.9.4
+
+* Add support for firefox anonymous stack traces.
+* Add support for chrome eval stack traces without a column.
+* Change the argument type to `Chain.capture` from `Function(dynamic, Chain)` to
+ `Function(Object, Chain)`. Existing functions which take `dynamic` are still
+ fine, but new uses can have a safer type.
+
+## 1.9.3
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.9.2
+
+* Fix Dart 2.0 runtime cast failure in test.
+
+## 1.9.1
+
+* Preserve the original chain for a trace to handle cases where an
+ error is rethrown.
+
+## 1.9.0
+
+* Add an `errorZone` parameter to `Chain.capture()` that makes it avoid creating
+ an error zone.
+
+## 1.8.3
+
+* `Chain.forTrace()` now returns a full stack chain for *all* `StackTrace`s
+ within `Chain.capture()`, even those that haven't been processed by
+ `dart:async` yet.
+
+* `Chain.forTrace()` now uses the Dart VM's stack chain information when called
+ synchronously within `Chain.capture()`. This matches the existing behavior
+ outside `Chain.capture()`.
+
+* `Chain.forTrace()` now trims the VM's stack chains for the innermost stack
+ trace within `Chain.capture()` (unless it's called synchronously, as above).
+ This avoids duplicated frames and makes the format of the innermost traces
+ consistent with the other traces in the chain.
+
+## 1.8.2
+
+* Update to use strong-mode clean Zone API.
+
+## 1.8.1
+
+* Use official generic function syntax.
+
+* Updated minimum SDK to 1.23.0.
+
+## 1.8.0
+
+* Add a `Trace.original` field to provide access to the original `StackTrace`s
+ from which the `Trace` was created, and a matching constructor parameter to
+ `new Trace()`.
+
+## 1.7.4
+
+* Always run `onError` callbacks for `Chain.capture()` in the parent zone.
+
+## 1.7.3
+
+* Fix broken links in the README.
+
+## 1.7.2
+
+* `Trace.foldFrames()` and `Chain.foldFrames()` now remove the outermost folded
+ frame. This matches the behavior of `.terse` with core frames.
+
+* Fix bug parsing a friendly frame with spaces in the member name.
+
+* Fix bug parsing a friendly frame where the location is a data url.
+
+## 1.7.1
+
+* Make `Trace.parse()`, `Chain.parse()`, treat the VM's new causal asynchronous
+ stack traces as chains. Outside of a `Chain.capture()` block, `new
+ Chain.current()` will return a stack chain constructed from the asynchronous
+ stack traces.
+
+## 1.7.0
+
+* Add a `Chain.disable()` function that disables stack-chain tracking.
+
+* Fix a bug where `Chain.capture(..., when: false)` would throw if an error was
+ emitted without a stack trace.
+
+## 1.6.8
+
+* Add a note to the documentation of `Chain.terse` and `Trace.terse`.
+
+## 1.6.7
+
+* Fix a bug where `new Frame.caller()` returned the wrong depth of frame on
+ Dartium.
+
+## 1.6.6
+
+* `new Trace.current()` and `new Chain.current()` now skip an extra frame when
+ run in a JS context. This makes their return values match the VM context.
+
+## 1.6.5
+
+* Really fix strong mode warnings.
+
+## 1.6.4
+
+* Fix a syntax error introduced in 1.6.3.
+
+## 1.6.3
+
+* Make `Chain.capture()` generic. Its signature is now `T Chain.capture<T>(T
+ callback(), ...)`.
+
+## 1.6.2
+
+* Fix all strong mode warnings.
+
+## 1.6.1
+
+* Use `StackTrace.current` in Dart SDK 1.14 to get the current stack trace.
+
+## 1.6.0
+
+* Add a `when` parameter to `Chain.capture()`. This allows capturing to be
+ easily enabled and disabled based on whether the application is running in
+ debug/development mode or not.
+
+* Deprecate the `ChainHandler` typedef. This didn't provide any value over
+ directly annotating the function argument, and it made the documentation less
+ clear.
+
+## 1.5.1
+
+* Fix a crash in `Chain.foldFrames()` and `Chain.terse` when one of the chain's
+ traces has no frames.
+
+## 1.5.0
+
+* `new Chain.parse()` now parses all the stack trace formats supported by `new
+ Trace.parse()`. Formats other than that emitted by `Chain.toString()` will
+ produce single-element chains.
+
+* `new Trace.parse()` now parses the output of `Chain.toString()`. It produces
+ the same result as `Chain.parse().toTrace()`.
+
+## 1.4.2
+
+* Improve the display of `data:` URIs in stack traces.
+
+## 1.4.1
+
+* Fix a crashing bug in `UnparsedFrame.toString()`.
+
+## 1.4.0
+
+* `new Trace.parse()` and related constructors will no longer throw an exception
+ if they encounter an unparseable stack frame. Instead, they will generate an
+ `UnparsedFrame`, which exposes no metadata but preserves the frame's original
+ text.
+
+* Properly parse native-code V8 frames.
+
+## 1.3.5
+
+* Properly shorten library names for pathnames of folded frames on Windows.
+
+## 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
+ nested `Chain.capture()` blocks, substitute the inner block's chain rather
+ than the outer block's.
+
+* Add support for empty chains and chains of empty traces to `Chain.parse()`.
+
+* Don't crash when parsing stack traces from Dart VM stack overflows.
+
+## 1.3.2
+
+* Don't crash when running `Trace.terse` on empty stack traces.
+
+## 1.3.1
+
+* Support more types of JavaScriptCore stack frames.
+
+## 1.3.0
+
+* Support stack traces generated by JavaScriptCore. They can be explicitly
+ parsed via `new Trace.parseJSCore` and `new Frame.parseJSCore`.
+
+## 1.2.4
+
+* Fix a type annotation in `LazyTrace`.
+
+## 1.2.3
+
+* Fix a crash in `Chain.parse`.
+
+## 1.2.2
+
+* Don't print the first folded frame of terse stack traces. This frame
+ is always just an internal isolate message handler anyway. This
+ improves the readability of stack traces, especially in stack chains.
+
+* Remove the line numbers and specific files in all terse folded frames, not
+ just those from core libraries.
+
+* Make padding consistent across all stack traces for `Chain.toString()`.
+
+## 1.2.1
+
+* Add `terse` to `LazyTrace.foldFrames()`.
+
+* Further improve stack chains when using the VM's async/await implementation.
+
+## 1.2.0
+
+* Add a `terse` argument to `Trace.foldFrames()` and `Chain.foldFrames()`. This
+ allows them to inherit the behavior of `Trace.terse` and `Chain.terse` without
+ having to duplicate the logic.
+
+## 1.1.3
+
+* Produce nicer-looking stack chains when using the VM's async/await
+ implementation.
+
+## 1.1.2
+
+* Support VM frames without line *or* column numbers, which async/await programs
+ occasionally generate.
+
+* Replace `<<anonymous closure>_async_body>` in VM frames' members with the
+ terser `<async>`.
+
+## 1.1.1
+
+* Widen the SDK constraint to include 1.7.0-dev.4.0.
+
+## 1.1.0
+
+* Unify the parsing of Safari and Firefox stack traces. This fixes an error in
+ Firefox trace parsing.
+
+* Deprecate `Trace.parseSafari6_0`, `Trace.parseSafari6_1`,
+ `Frame.parseSafari6_0`, and `Frame.parseSafari6_1`.
+
+* Add `Frame.parseSafari`.
+
+## 1.0.3
+
+* Use `Zone.errorCallback` to attach stack chains to all errors without the need
+ for `Chain.track`, which is now deprecated.
+
+## 1.0.2
+
+* Remove a workaround for [issue 17083][].
+
+[issue 17083]: https://github.com/dart-lang/sdk/issues/17083
+
+## 1.0.1
+
+* Synchronous errors in the [Chain.capture] callback are now handled correctly.
+
+## 1.0.0
+
+* No API changes, just declared stable.
+
+## 0.9.3+2
+
+* Update the dependency on path.
+
+* Improve the formatting of library URIs in stack traces.
+
+## 0.9.3+1
+
+* If an error is thrown in `Chain.capture`'s `onError` handler, that error is
+ handled by the parent zone. This matches the behavior of `runZoned` in
+ `dart:async`.
+
+## 0.9.3
+
+* Add a `Chain.foldFrames` method that parallels `Trace.foldFrames`.
+
+* Record anonymous method frames in IE10 as "<fn>".
diff --git a/pkgs/stack_trace/LICENSE b/pkgs/stack_trace/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/stack_trace/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/stack_trace/README.md b/pkgs/stack_trace/README.md
new file mode 100644
index 0000000..b10a556
--- /dev/null
+++ b/pkgs/stack_trace/README.md
@@ -0,0 +1,169 @@
+[](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml)
+[](https://pub.dev/packages/stack_trace)
+[](https://pub.dev/packages/stack_trace/publisher)
+
+This library provides the ability to parse, inspect, and manipulate stack traces
+produced by the underlying Dart implementation. It also provides functions to
+produce string representations of stack traces in a more readable format than
+the native [StackTrace] implementation.
+
+`Trace`s can be parsed from native [StackTrace]s using `Trace.from`, or captured
+using `Trace.current`. Native [StackTrace]s can also be directly converted to
+human-readable strings using `Trace.format`.
+
+[StackTrace]: https://api.dart.dev/stable/dart-core/StackTrace-class.html
+
+Here's an example native stack trace from debugging this library:
+
+ #0 Object.noSuchMethod (dart:core-patch:1884:25)
+ #1 Trace.terse.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:47:21)
+ #2 IterableMixinWorkaround.reduce (dart:collection:29:29)
+ #3 List.reduce (dart:core-patch:1247:42)
+ #4 Trace.terse (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:40:35)
+ #5 format (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/stack_trace.dart:24:28)
+ #6 main.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/test.dart:21:29)
+ #7 _CatchErrorFuture._sendError (dart:async:525:24)
+ #8 _FutureImpl._setErrorWithoutAsyncTrace (dart:async:393:26)
+ #9 _FutureImpl._setError (dart:async:378:31)
+ #10 _ThenFuture._sendValue (dart:async:490:16)
+ #11 _FutureImpl._handleValue.<anonymous closure> (dart:async:349:28)
+ #12 Timer.run.<anonymous closure> (dart:async:2402:21)
+ #13 Timer.Timer.<anonymous closure> (dart:async-patch:15:15)
+
+and its human-readable representation:
+
+ dart:core-patch 1884:25 Object.noSuchMethod
+ pkg/stack_trace/lib/src/trace.dart 47:21 Trace.terse.<fn>
+ dart:collection 29:29 IterableMixinWorkaround.reduce
+ dart:core-patch 1247:42 List.reduce
+ pkg/stack_trace/lib/src/trace.dart 40:35 Trace.terse
+ pkg/stack_trace/lib/stack_trace.dart 24:28 format
+ test.dart 21:29 main.<fn>
+ dart:async 525:24 _CatchErrorFuture._sendError
+ dart:async 393:26 _FutureImpl._setErrorWithoutAsyncTrace
+ dart:async 378:31 _FutureImpl._setError
+ dart:async 490:16 _ThenFuture._sendValue
+ dart:async 349:28 _FutureImpl._handleValue.<fn>
+ dart:async 2402:21 Timer.run.<fn>
+ dart:async-patch 15:15 Timer.Timer.<fn>
+
+You can further clean up the stack trace using `Trace.terse`. This folds
+together multiple stack frames from the Dart core libraries, so that only the
+core library method that was directly called from user code is visible. For
+example:
+
+ dart:core Object.noSuchMethod
+ pkg/stack_trace/lib/src/trace.dart 47:21 Trace.terse.<fn>
+ dart:core List.reduce
+ pkg/stack_trace/lib/src/trace.dart 40:35 Trace.terse
+ pkg/stack_trace/lib/stack_trace.dart 24:28 format
+ test.dart 21:29 main.<fn>
+
+## Stack Chains
+
+This library also provides the ability to capture "stack chains" with the
+`Chain` class. When writing asynchronous code, a single stack trace isn't very
+useful, since the call stack is unwound every time something async happens. A
+stack chain tracks stack traces through asynchronous calls, so that you can see
+the full path from `main` down to the error.
+
+To use stack chains, just wrap the code that you want to track in
+`Chain.capture`. This will create a new [Zone][] in which stack traces are
+recorded and woven into chains every time an asynchronous call occurs. Zones are
+sticky, too, so any asynchronous operations started in the `Chain.capture`
+callback will have their chains tracked, as will asynchronous operations they
+start and so on.
+
+Here's an example of some code that doesn't capture its stack chains:
+
+```dart
+import 'dart:async';
+
+void main() {
+ _scheduleAsync();
+}
+
+void _scheduleAsync() {
+ Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+ throw 'oh no!';
+}
+```
+
+If we run this, it prints the following:
+
+ Unhandled exception:
+ oh no!
+ #0 _runAsync (file:///Users/kevmoo/github/stack_trace/example/example.dart:12:3)
+ #1 _scheduleAsync.<anonymous closure> (file:///Users/kevmoo/github/stack_trace/example/example.dart:8:52)
+ <asynchronous suspension>
+
+Notice how there's no mention of `main` in that stack trace. All we know is that
+the error was in `runAsync`; we don't know why `runAsync` was called.
+
+Now let's look at the same code with stack chains captured:
+
+```dart
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+ Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+ Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+ throw 'oh no!';
+}
+```
+
+Now if we run it, it prints this:
+
+ Unhandled exception:
+ oh no!
+ example/example.dart 14:3 _runAsync
+ example/example.dart 10:52 _scheduleAsync.<fn>
+ package:stack_trace/src/stack_zone_specification.dart 126:26 StackZoneSpecification._registerUnaryCallback.<fn>.<fn>
+ package:stack_trace/src/stack_zone_specification.dart 208:15 StackZoneSpecification._run
+ package:stack_trace/src/stack_zone_specification.dart 126:14 StackZoneSpecification._registerUnaryCallback.<fn>
+ dart:async/zone.dart 1406:47 _rootRunUnary
+ dart:async/zone.dart 1307:19 _CustomZone.runUnary
+ ===== asynchronous gap ===========================
+ dart:async/zone.dart 1328:19 _CustomZone.registerUnaryCallback
+ dart:async/future_impl.dart 315:23 Future.then
+ example/example.dart 10:40 _scheduleAsync
+ package:stack_trace/src/chain.dart 97:24 Chain.capture.<fn>
+ dart:async/zone.dart 1398:13 _rootRun
+ dart:async/zone.dart 1300:19 _CustomZone.run
+ dart:async/zone.dart 1803:10 _runZoned
+ dart:async/zone.dart 1746:10 runZoned
+ package:stack_trace/src/chain.dart 95:12 Chain.capture
+ example/example.dart 6:9 main
+ dart:isolate-patch/isolate_patch.dart 297:19 _delayEntrypointInvocation.<fn>
+ dart:isolate-patch/isolate_patch.dart 192:12 _RawReceivePortImpl._handleMessage
+
+That's a lot of text! If you look closely, though, you can see that `main` is
+listed in the first trace in the chain.
+
+Thankfully, you can call `Chain.terse` just like `Trace.terse` to get rid of all
+the frames you don't care about. The terse version of the stack chain above is
+this:
+
+ test.dart 17:3 runAsync
+ test.dart 13:28 scheduleAsync.<fn>
+ ===== asynchronous gap ===========================
+ dart:async _Future.then
+ test.dart 13:12 scheduleAsync
+ test.dart 7:18 main.<fn>
+ package:stack_trace Chain.capture
+ test.dart 6:16 main
+
+That's a lot easier to understand!
+
+[Zone]: https://api.dart.dev/stable/dart-async/Zone-class.html
diff --git a/pkgs/stack_trace/analysis_options.yaml b/pkgs/stack_trace/analysis_options.yaml
new file mode 100644
index 0000000..4eb82ce
--- /dev/null
+++ b/pkgs/stack_trace/analysis_options.yaml
@@ -0,0 +1,22 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - unnecessary_await_in_return
+ - use_string_buffers
diff --git a/pkgs/stack_trace/example/example.dart b/pkgs/stack_trace/example/example.dart
new file mode 100644
index 0000000..d601ca4
--- /dev/null
+++ b/pkgs/stack_trace/example/example.dart
@@ -0,0 +1,15 @@
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+ Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+ Future<void>.delayed(const Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+ throw StateError('oh no!');
+}
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
new file mode 100644
index 0000000..6a815c6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -0,0 +1,264 @@
+// 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.
+
+import 'dart:async';
+import 'dart:math' as math;
+
+import 'frame.dart';
+import 'lazy_chain.dart';
+import 'stack_zone_specification.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A function that handles errors in the zone wrapped by [Chain.capture].
+@Deprecated('Will be removed in stack_trace 2.0.0.')
+typedef ChainHandler = void Function(dynamic error, Chain chain);
+
+/// An opaque key used to track the current [StackZoneSpecification].
+final _specKey = Object();
+
+/// A chain of stack traces.
+///
+/// A stack chain is a collection of one or more stack traces that collectively
+/// represent the path from `main` through nested function calls to a particular
+/// code location, usually where an error was thrown. Multiple stack traces are
+/// necessary when using asynchronous functions, since the program's stack is
+/// reset before each asynchronous callback is run.
+///
+/// Stack chains can be automatically tracked using [Chain.capture]. This sets
+/// up a new [Zone] in which the current stack chain is tracked and can be
+/// accessed using [Chain.current]. Any errors that would be top-leveled in
+/// the zone can be handled, along with their associated chains, with the
+/// `onError` callback. For example:
+///
+/// Chain.capture(() {
+/// // ...
+/// }, onError: (error, stackChain) {
+/// print("Caught error $error\n"
+/// "$stackChain");
+/// });
+class Chain implements StackTrace {
+ /// The stack traces that make up this chain.
+ ///
+ /// Like the frames in a stack trace, the traces are ordered from most local
+ /// to least local. The first one is the trace where the actual exception was
+ /// raised, the second one is where that callback was scheduled, and so on.
+ final List<Trace> traces;
+
+ /// The [StackZoneSpecification] for the current zone.
+ static StackZoneSpecification? get _currentSpec =>
+ Zone.current[_specKey] as StackZoneSpecification?;
+
+ /// If [when] is `true`, runs [callback] in a [Zone] in which the current
+ /// stack chain is tracked and automatically associated with (most) errors.
+ ///
+ /// If [when] is `false`, this does not track stack chains. Instead, it's
+ /// identical to [runZoned], except that it wraps any errors in
+ /// [Chain.forTrace]—which will only wrap the trace unless there's a different
+ /// [Chain.capture] active. This makes it easy for the caller to only capture
+ /// stack chains in debug mode or during development.
+ ///
+ /// If [onError] is passed, any error in the zone that would otherwise go
+ /// unhandled is passed to it, along with the [Chain] associated with that
+ /// error. Note that if [callback] produces multiple unhandled errors,
+ /// [onError] may be called more than once. If [onError] isn't passed, the
+ /// parent Zone's `unhandledErrorHandler` will be called with the error and
+ /// its chain.
+ ///
+ /// The zone this creates will be an error zone if either [onError] is
+ /// not `null` and [when] is false,
+ /// or if both [when] and [errorZone] are `true`.
+ /// If [errorZone] is `false`, [onError] must be `null`.
+ ///
+ /// If [callback] returns a value, it will be returned by [capture] as well.
+ ///
+ /// [zoneValues] is added to the [runZoned] calls.
+ static T capture<T>(T Function() callback,
+ {void Function(Object error, Chain)? onError,
+ bool when = true,
+ bool errorZone = true,
+ Map<Object?, Object?>? zoneValues}) {
+ if (!errorZone && onError != null) {
+ throw ArgumentError.value(
+ onError, 'onError', 'must be null if errorZone is false');
+ }
+
+ if (!when) {
+ if (onError == null) return runZoned(callback, zoneValues: zoneValues);
+ return runZonedGuarded(callback, (error, stackTrace) {
+ onError(error, Chain.forTrace(stackTrace));
+ }, zoneValues: zoneValues) as T;
+ }
+
+ var spec = StackZoneSpecification(onError, errorZone: errorZone);
+ return runZoned(() {
+ try {
+ return callback();
+ } on Object catch (error, stackTrace) {
+ // Forward synchronous errors through the async error path to match the
+ // behavior of `runZonedGuarded`.
+ Zone.current.handleUncaughtError(error, stackTrace);
+
+ // If the expected return type of capture() is not nullable, this will
+ // throw a cast exception. But the only other alternative is to throw
+ // some other exception. Casting null to T at least lets existing uses
+ // where T is a nullable type continue to work.
+ return null as T;
+ }
+ }, zoneSpecification: spec.toSpec(), zoneValues: {
+ ...?zoneValues,
+ _specKey: spec,
+ StackZoneSpecification.disableKey: false
+ });
+ }
+
+ /// If [when] is `true` and this is called within a [Chain.capture] zone, runs
+ /// [callback] in a [Zone] in which chain capturing is disabled.
+ ///
+ /// If [callback] returns a value, it will be returned by [disable] as well.
+ static T disable<T>(T Function() callback, {bool when = true}) {
+ var zoneValues =
+ when ? {_specKey: null, StackZoneSpecification.disableKey: true} : null;
+
+ return runZoned(callback, zoneValues: zoneValues);
+ }
+
+ /// Returns [futureOrStream] unmodified.
+ ///
+ /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
+ /// exceptions reported with [Completer.completeError] and
+ /// [StreamController.addError] were tracked correctly.
+ @Deprecated('Chain.track is not necessary in Dart 1.7+.')
+ static dynamic track(Object? futureOrStream) => futureOrStream;
+
+ /// Returns the current stack chain.
+ ///
+ /// By default, the first frame of the first trace will be the line where
+ /// [Chain.current] is called. If [level] is passed, the first trace will
+ /// start that many frames up instead.
+ ///
+ /// If this is called outside of a [capture] zone, it just returns a
+ /// single-trace chain.
+ factory Chain.current([int level = 0]) {
+ if (_currentSpec != null) return _currentSpec!.currentChain(level + 1);
+
+ var chain = Chain.forTrace(StackTrace.current);
+ return LazyChain(() {
+ // JS includes a frame for the call to StackTrace.current, but the VM
+ // doesn't, so we skip an extra frame in a JS context.
+ var first = Trace(chain.traces.first.frames.skip(level + (inJS ? 2 : 1)),
+ original: chain.traces.first.original.toString());
+ return Chain([first, ...chain.traces.skip(1)]);
+ });
+ }
+
+ /// Returns the stack chain associated with [trace].
+ ///
+ /// The first stack trace in the returned chain will always be [trace]
+ /// (converted to a [Trace] if necessary). If there is no chain associated
+ /// with [trace] or if this is called outside of a [capture] zone, this just
+ /// returns a single-trace chain containing [trace].
+ ///
+ /// If [trace] is already a [Chain], it will be returned as-is.
+ factory Chain.forTrace(StackTrace trace) {
+ if (trace is Chain) return trace;
+ if (_currentSpec != null) return _currentSpec!.chainFor(trace);
+ if (trace is Trace) return Chain([trace]);
+ return LazyChain(() => Chain.parse(trace.toString()));
+ }
+
+ /// Parses a string representation of a stack chain.
+ ///
+ /// If [chain] is the output of a call to [Chain.toString], it will be parsed
+ /// as a full stack chain. Otherwise, it will be parsed as in [Trace.parse]
+ /// and returned as a single-trace chain.
+ factory Chain.parse(String chain) {
+ if (chain.isEmpty) return Chain([]);
+ if (chain.contains(vmChainGap)) {
+ return Chain(chain
+ .split(vmChainGap)
+ .where((line) => line.isNotEmpty)
+ .map(Trace.parseVM));
+ }
+ if (!chain.contains(chainGap)) return Chain([Trace.parse(chain)]);
+
+ return Chain(chain.split(chainGap).map(Trace.parseFriendly));
+ }
+
+ /// Returns a new [Chain] comprised of [traces].
+ Chain(Iterable<Trace> traces) : traces = List<Trace>.unmodifiable(traces);
+
+ /// Returns a terser version of this chain.
+ ///
+ /// This calls [Trace.terse] on every trace in [traces], and discards any
+ /// trace that contain only internal frames.
+ ///
+ /// This won't do anything with a raw JavaScript trace, since there's no way
+ /// to determine which frames come from which Dart libraries. However, the
+ /// [`source_map_stack_trace`](https://pub.dev/packages/source_map_stack_trace)
+ /// package can be used to convert JavaScript traces into Dart-style traces.
+ Chain get terse => foldFrames((_) => false, terse: true);
+
+ /// Returns a new [Chain] based on this chain where multiple stack frames
+ /// matching [predicate] are folded together.
+ ///
+ /// This means that whenever there are multiple frames in a row that match
+ /// [predicate], only the last one is kept. In addition, traces that are
+ /// composed entirely of frames matching [predicate] are omitted.
+ ///
+ /// This is useful for limiting the amount of library code that appears in a
+ /// stack trace by only showing user code and code that's called by user code.
+ ///
+ /// If [terse] is true, this will also fold together frames from the core
+ /// library or from this package, and simplify core library frames as in
+ /// [Trace.terse].
+ Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+ var foldedTraces =
+ traces.map((trace) => trace.foldFrames(predicate, terse: terse));
+ var nonEmptyTraces = foldedTraces.where((trace) {
+ // Ignore traces that contain only folded frames.
+ if (trace.frames.length > 1) return true;
+ if (trace.frames.isEmpty) return false;
+
+ // In terse mode, the trace may have removed an outer folded frame,
+ // leaving a single non-folded frame. We can detect a folded frame because
+ // it has no line information.
+ if (!terse) return false;
+ return trace.frames.single.line != null;
+ });
+
+ // If all the traces contain only internal processing, preserve the last
+ // (top-most) one so that the chain isn't empty.
+ if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
+ return Chain([foldedTraces.last]);
+ }
+
+ return Chain(nonEmptyTraces);
+ }
+
+ /// Converts this chain to a [Trace].
+ ///
+ /// The trace version of a chain is just the concatenation of all the traces
+ /// in the chain.
+ Trace toTrace() => Trace(traces.expand((trace) => trace.frames));
+
+ @override
+ String toString() {
+ // Figure out the longest path so we know how much to pad.
+ var longest = traces
+ .map((trace) => trace.frames
+ .map((frame) => frame.location.length)
+ .fold(0, math.max))
+ .fold(0, math.max);
+
+ // Don't call out to [Trace.toString] here because that doesn't ensure that
+ // padding is consistent across all traces.
+ return traces
+ .map((trace) => trace.frames
+ .map((frame) =>
+ '${frame.location.padRight(longest)} ${frame.member}\n')
+ .join())
+ .join(chainGap);
+ }
+}
diff --git a/pkgs/stack_trace/lib/src/frame.dart b/pkgs/stack_trace/lib/src/frame.dart
new file mode 100644
index 0000000..d4043b7
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/frame.dart
@@ -0,0 +1,458 @@
+// 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.
+
+import 'package:path/path.dart' as path;
+
+import 'trace.dart';
+import 'unparsed_frame.dart';
+
+// #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
+// #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42)
+// #1 Foo._bar (file:///home/nweiz/code/stuff.dart)
+final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
+
+// at Object.stringify (native)
+// at VW.call$0 (https://example.com/stuff.dart.js:560:28)
+// at VW.call$0 (eval as fn
+// (https://example.com/stuff.dart.js:560:28), efn:3:28)
+// at https://example.com/stuff.dart.js:560:28
+final _v8JsFrame =
+ RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$');
+
+// https://example.com/stuff.dart.js:560:28
+// https://example.com/stuff.dart.js:560
+//
+// Group 1: URI, required
+// Group 2: line number, required
+// Group 3: column number, optional
+final _v8JsUrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$');
+
+// With names:
+//
+// at Error.f (wasm://wasm/0006d966:wasm-function[119]:0xbb13)
+// at g (wasm://wasm/0006d966:wasm-function[796]:0x143b4)
+//
+// Without names:
+//
+// at wasm://wasm/0005168a:wasm-function[119]:0xbb13
+// at wasm://wasm/0005168a:wasm-function[796]:0x143b4
+//
+// Matches named groups:
+//
+// - "member": optional, `Error.f` in the first example, NA in the second.
+// - "uri": `wasm://wasm/0006d966`.
+// - "index": `119`.
+// - "offset": (hex number) `bb13`.
+//
+// To avoid having multiple groups for the same part of the frame, this regex
+// matches unmatched parentheses after the member name.
+final _v8WasmFrame = RegExp(r'^\s*at (?:(?<member>.+) )?'
+ r'(?:\(?(?:(?<uri>\S+):wasm-function\[(?<index>\d+)\]'
+ r'\:0x(?<offset>[0-9a-fA-F]+))\)?)$');
+
+// eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28
+// eval as function (https://example.com/stuff.dart.js:560:28)
+// eval as function (eval as otherFunction
+// (https://example.com/stuff.dart.js:560:28))
+final _v8EvalLocation =
+ RegExp(r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\d+:\d+)?$');
+
+// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+final _firefoxEvalLocation =
+ RegExp(r'(\S+)@(\S+) line (\d+) >.* (Function|eval):\d+:\d+');
+
+// .VW.call$0@https://example.com/stuff.dart.js:560
+// .VW.call$0("arg")@https://example.com/stuff.dart.js:560
+// .VW.call$0/name<@https://example.com/stuff.dart.js:560
+// .VW.call$0@https://example.com/stuff.dart.js:560:36
+// https://example.com/stuff.dart.js:560
+final _firefoxSafariJSFrame = RegExp(r'^'
+ r'(?:' // Member description. Not present in some Safari frames.
+ r'([^@(/]*)' // The actual name of the member.
+ r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
+ r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
+ r'(?:\(.*\))?' // Arguments to the closure.
+ r'@'
+ r')?'
+ r'(.*?)' // The frame's URL.
+ r':'
+ r'(\d*)' // The line number. Empty in Safari if it's unknown.
+ r'(?::(\d*))?' // The column number. Not present in older browsers and
+ // empty in Safari if it's unknown.
+ r'$');
+
+// With names:
+//
+// g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// f@http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// main@http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// Without names:
+//
+// @http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// @http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// @http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// JSShell in the command line uses a different format, which this regex also
+// parses.
+//
+// With names:
+//
+// main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Without names:
+//
+// @/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Matches named groups:
+//
+// - "member": Function name, may be empty: `g`.
+// - "uri": `http://localhost:8080/test.wasm`.
+// - "index": `796`.
+// - "offset": (in hex) `143b4`.
+final _firefoxWasmFrame =
+ RegExp(r'^(?<member>.*?)@(?:(?<uri>\S+).*?:wasm-function'
+ r'\[(?<index>\d+)\]:0x(?<offset>[0-9a-fA-F]+))$');
+
+// With names:
+//
+// (Note: Lines below are literal text, e.g. <?> is not a placeholder, it's a
+// part of the stack frame.)
+//
+// <?>.wasm-function[g]@[wasm code]
+// <?>.wasm-function[f]@[wasm code]
+// <?>.wasm-function[main]@[wasm code]
+//
+// Without names:
+//
+// <?>.wasm-function[796]@[wasm code]
+// <?>.wasm-function[795]@[wasm code]
+// <?>.wasm-function[792]@[wasm code]
+//
+// Matches named group "member": `g` or `796`.
+final _safariWasmFrame =
+ RegExp(r'^.*?wasm-function\[(?<member>.*)\]@\[wasm code\]$');
+
+// foo/bar.dart 10:11 Foo._bar
+// foo/bar.dart 10:11 (anonymous function).dart.fn
+// https://dart.dev/foo/bar.dart Foo._bar
+// data:... 10:11 Foo._bar
+final _friendlyFrame = RegExp(r'^(\S+)(?: (\d+)(?::(\d+))?)?\s+([^\d].*)$');
+
+/// A regular expression that matches asynchronous member names generated by the
+/// VM.
+final _asyncBody = RegExp(r'<(<anonymous closure>|[^>]+)_async_body>');
+
+final _initialDot = RegExp(r'^\.');
+
+/// A single stack frame. Each frame points to a precise location in Dart code.
+class Frame {
+ /// The URI of the file in which the code is located.
+ ///
+ /// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
+ final Uri uri;
+
+ /// The line number on which the code location is located.
+ ///
+ /// This can be null, indicating that the line number is unknown or
+ /// unimportant.
+ final int? line;
+
+ /// The column number of the code location.
+ ///
+ /// This can be null, indicating that the column number is unknown or
+ /// unimportant.
+ final int? column;
+
+ /// The name of the member in which the code location occurs.
+ ///
+ /// Anonymous closures are represented as `<fn>` in this member string.
+ final String? member;
+
+ /// Whether this stack frame comes from the Dart core libraries.
+ bool get isCore => uri.scheme == 'dart';
+
+ /// Returns a human-friendly description of the library that this stack frame
+ /// comes from.
+ ///
+ /// This will usually be the string form of [uri], but a relative URI will be
+ /// used if possible. Data URIs will be truncated.
+ String get library {
+ if (uri.scheme == 'data') return 'data:...';
+ return path.prettyUri(uri);
+ }
+
+ /// Returns the name of the package this stack frame comes from, or `null` if
+ /// this stack frame doesn't come from a `package:` URL.
+ String? get package {
+ if (uri.scheme != 'package') return null;
+ return uri.path.split('/').first;
+ }
+
+ /// A human-friendly description of the code location.
+ String get location {
+ if (line == null) return library;
+ if (column == null) return '$library $line';
+ return '$library $line:$column';
+ }
+
+ /// Returns a single frame of the current stack.
+ ///
+ /// By default, this will return the frame above the current method. If
+ /// [level] is `0`, it will return the current method's frame; if [level] is
+ /// higher than `1`, it will return higher frames.
+ factory Frame.caller([int level = 1]) {
+ if (level < 0) {
+ throw ArgumentError('Argument [level] must be greater than or equal '
+ 'to 0.');
+ }
+
+ return Trace.current(level + 1).frames.first;
+ }
+
+ /// Parses a string representation of a Dart VM stack frame.
+ factory Frame.parseVM(String frame) => _catchFormatException(frame, () {
+ // The VM sometimes folds multiple stack frames together and replaces
+ // them with "...".
+ if (frame == '...') {
+ return Frame(Uri(), null, null, '...');
+ }
+
+ var match = _vmFrame.firstMatch(frame);
+ if (match == null) return UnparsedFrame(frame);
+
+ // Get the pieces out of the regexp match. Function, URI and line should
+ // always be found. The column is optional.
+ var member = match[1]!
+ .replaceAll(_asyncBody, '<async>')
+ .replaceAll('<anonymous closure>', '<fn>');
+ var uri = match[2]!.startsWith('<data:')
+ ? Uri.dataFromString('')
+ : Uri.parse(match[2]!);
+
+ var lineAndColumn = match[3]!.split(':');
+ var line =
+ lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
+ var column =
+ lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
+ return Frame(uri, line, column, member);
+ });
+
+ /// Parses a string representation of a Chrome/V8 stack frame.
+ factory Frame.parseV8(String frame) => _catchFormatException(frame, () {
+ // Try to match a Wasm frame first: the Wasm frame regex won't match a
+ // JS frame but the JS frame regex may match a Wasm frame.
+ var match = _v8WasmFrame.firstMatch(frame);
+ if (match != null) {
+ final member = match.namedGroup('member');
+ final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+ final functionIndex = match.namedGroup('index')!;
+ final functionOffset =
+ int.parse(match.namedGroup('offset')!, radix: 16);
+ return Frame(uri, 1, functionOffset + 1, member ?? functionIndex);
+ }
+
+ match = _v8JsFrame.firstMatch(frame);
+ if (match != null) {
+ // v8 location strings can be arbitrarily-nested, since it adds a
+ // layer of nesting for each eval performed on that line.
+ Frame parseJsLocation(String location, String member) {
+ var evalMatch = _v8EvalLocation.firstMatch(location);
+ while (evalMatch != null) {
+ location = evalMatch[1]!;
+ evalMatch = _v8EvalLocation.firstMatch(location);
+ }
+
+ if (location == 'native') {
+ return Frame(Uri.parse('native'), null, null, member);
+ }
+
+ var urlMatch = _v8JsUrlLocation.firstMatch(location);
+ if (urlMatch == null) return UnparsedFrame(frame);
+
+ final uri = _uriOrPathToUri(urlMatch[1]!);
+ final line = int.parse(urlMatch[2]!);
+ final columnMatch = urlMatch[3];
+ final column = columnMatch != null ? int.parse(columnMatch) : null;
+ return Frame(uri, line, column, member);
+ }
+
+ // V8 stack frames can be in two forms.
+ if (match[2] != null) {
+ // The first form looks like " at FUNCTION (LOCATION)". V8 proper
+ // lists anonymous functions within eval as "<anonymous>", while
+ // IE10 lists them as "Anonymous function".
+ return parseJsLocation(
+ match[2]!,
+ match[1]!
+ .replaceAll('<anonymous>', '<fn>')
+ .replaceAll('Anonymous function', '<fn>')
+ .replaceAll('(anonymous function)', '<fn>'));
+ } else {
+ // The second form looks like " at LOCATION", and is used for
+ // anonymous functions.
+ return parseJsLocation(match[3]!, '<fn>');
+ }
+ }
+
+ return UnparsedFrame(frame);
+ });
+
+ /// Parses a string representation of a JavaScriptCore stack trace.
+ factory Frame.parseJSCore(String frame) => Frame.parseV8(frame);
+
+ /// Parses a string representation of an IE stack frame.
+ ///
+ /// IE10+ frames look just like V8 frames. Prior to IE10, stack traces can't
+ /// be retrieved.
+ factory Frame.parseIE(String frame) => Frame.parseV8(frame);
+
+ /// Parses a Firefox 'eval' or 'function' stack frame.
+ ///
+ /// For example:
+ ///
+ /// ```
+ /// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+ /// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+ /// ```
+ factory Frame._parseFirefoxEval(String frame) =>
+ _catchFormatException(frame, () {
+ final match = _firefoxEvalLocation.firstMatch(frame);
+ if (match == null) return UnparsedFrame(frame);
+ var member = match[1]!.replaceAll('/<', '');
+ final uri = _uriOrPathToUri(match[2]!);
+ final line = int.parse(match[3]!);
+ if (member.isEmpty || member == 'anonymous') {
+ member = '<fn>';
+ }
+ return Frame(uri, line, null, member);
+ });
+
+ /// Parses a string representation of a Firefox or Safari stack frame.
+ factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () {
+ var match = _firefoxSafariJSFrame.firstMatch(frame);
+ if (match != null) {
+ if (match[3]!.contains(' line ')) {
+ return Frame._parseFirefoxEval(frame);
+ }
+
+ // Normally this is a URI, but in a jsshell trace it can be a path.
+ var uri = _uriOrPathToUri(match[3]!);
+
+ var member = match[1];
+ if (member != null) {
+ member +=
+ List.filled('/'.allMatches(match[2]!).length, '.<fn>').join();
+ if (member == '') member = '<fn>';
+
+ // Some Firefox members have initial dots. We remove them for
+ // consistency with other platforms.
+ member = member.replaceFirst(_initialDot, '');
+ } else {
+ member = '<fn>';
+ }
+
+ var line = match[4] == '' ? null : int.parse(match[4]!);
+ var column =
+ match[5] == null || match[5] == '' ? null : int.parse(match[5]!);
+ return Frame(uri, line, column, member);
+ }
+
+ match = _firefoxWasmFrame.firstMatch(frame);
+ if (match != null) {
+ final member = match.namedGroup('member')!;
+ final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+ final functionIndex = match.namedGroup('index')!;
+ final functionOffset =
+ int.parse(match.namedGroup('offset')!, radix: 16);
+ return Frame(uri, 1, functionOffset + 1,
+ member.isNotEmpty ? member : functionIndex);
+ }
+
+ match = _safariWasmFrame.firstMatch(frame);
+ if (match != null) {
+ final member = match.namedGroup('member')!;
+ return Frame(Uri(path: 'wasm code'), null, null, member);
+ }
+
+ return UnparsedFrame(frame);
+ });
+
+ /// Parses a string representation of a Safari 6.0 stack frame.
+ @Deprecated('Use Frame.parseSafari instead.')
+ factory Frame.parseSafari6_0(String frame) => Frame.parseFirefox(frame);
+
+ /// Parses a string representation of a Safari 6.1+ stack frame.
+ @Deprecated('Use Frame.parseSafari instead.')
+ factory Frame.parseSafari6_1(String frame) => Frame.parseFirefox(frame);
+
+ /// Parses a string representation of a Safari stack frame.
+ factory Frame.parseSafari(String frame) => Frame.parseFirefox(frame);
+
+ /// Parses this package's string representation of a stack frame.
+ factory Frame.parseFriendly(String frame) => _catchFormatException(frame, () {
+ var match = _friendlyFrame.firstMatch(frame);
+ if (match == null) {
+ throw FormatException(
+ "Couldn't parse package:stack_trace stack trace line '$frame'.");
+ }
+ // Fake truncated data urls generated by the friendly stack trace format
+ // cause Uri.parse to throw an exception so we have to special case
+ // them.
+ var uri = match[1] == 'data:...'
+ ? Uri.dataFromString('')
+ : Uri.parse(match[1]!);
+ // If there's no scheme, this is a relative URI. We should interpret it
+ // as relative to the current working directory.
+ if (uri.scheme == '') {
+ uri = path.toUri(path.absolute(path.fromUri(uri)));
+ }
+
+ var line = match[2] == null ? null : int.parse(match[2]!);
+ var column = match[3] == null ? null : int.parse(match[3]!);
+ return Frame(uri, line, column, match[4]);
+ });
+
+ /// A regular expression matching an absolute URI.
+ static final _uriRegExp = RegExp(r'^[a-zA-Z][-+.a-zA-Z\d]*://');
+
+ /// A regular expression matching a Windows path.
+ static final _windowsRegExp = RegExp(r'^([a-zA-Z]:[\\/]|\\\\)');
+
+ /// Converts [uriOrPath], which can be a URI, a Windows path, or a Posix path,
+ /// to a URI (absolute if possible).
+ static Uri _uriOrPathToUri(String uriOrPath) {
+ if (uriOrPath.contains(_uriRegExp)) {
+ return Uri.parse(uriOrPath);
+ } else if (uriOrPath.contains(_windowsRegExp)) {
+ return Uri.file(uriOrPath, windows: true);
+ } else if (uriOrPath.startsWith('/')) {
+ return Uri.file(uriOrPath, windows: false);
+ }
+
+ // As far as I've seen, Firefox and V8 both always report absolute paths in
+ // their stack frames. However, if we do get a relative path, we should
+ // handle it gracefully.
+ if (uriOrPath.contains('\\')) return path.windows.toUri(uriOrPath);
+ return Uri.parse(uriOrPath);
+ }
+
+ /// Runs [body] and returns its result.
+ ///
+ /// If [body] throws a [FormatException], returns an [UnparsedFrame] with
+ /// [text] instead.
+ static Frame _catchFormatException(String text, Frame Function() body) {
+ try {
+ return body();
+ } on FormatException catch (_) {
+ return UnparsedFrame(text);
+ }
+ }
+
+ Frame(this.uri, this.line, this.column, this.member);
+
+ @override
+ String toString() => '$location in $member';
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_chain.dart b/pkgs/stack_trace/lib/src/lazy_chain.dart
new file mode 100644
index 0000000..063ed59
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_chain.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Chain].
+typedef ChainThunk = Chain Function();
+
+/// A wrapper around a [ChainThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyChain implements Chain {
+ final ChainThunk _thunk;
+ late final Chain _chain = _thunk();
+
+ LazyChain(this._thunk);
+
+ @override
+ List<Trace> get traces => _chain.traces;
+ @override
+ Chain get terse => _chain.terse;
+ @override
+ Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+ LazyChain(() => _chain.foldFrames(predicate, terse: terse));
+ @override
+ Trace toTrace() => LazyTrace(_chain.toTrace);
+ @override
+ String toString() => _chain.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_trace.dart b/pkgs/stack_trace/lib/src/lazy_trace.dart
new file mode 100644
index 0000000..3ecaa2d
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_trace.dart
@@ -0,0 +1,33 @@
+// 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.
+
+import 'frame.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Trace].
+typedef TraceThunk = Trace Function();
+
+/// A wrapper around a [TraceThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyTrace implements Trace {
+ final TraceThunk _thunk;
+ late final Trace _trace = _thunk();
+
+ LazyTrace(this._thunk);
+
+ @override
+ List<Frame> get frames => _trace.frames;
+ @override
+ StackTrace get original => _trace.original;
+ @override
+ StackTrace get vmTrace => _trace.vmTrace;
+ @override
+ Trace get terse => LazyTrace(() => _trace.terse);
+ @override
+ Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+ LazyTrace(() => _trace.foldFrames(predicate, terse: terse));
+ @override
+ String toString() => _trace.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/stack_zone_specification.dart b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
new file mode 100644
index 0000000..901a5ee
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
@@ -0,0 +1,262 @@
+// 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.
+
+import 'dart:async';
+
+import 'chain.dart';
+import 'lazy_chain.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A class encapsulating the zone specification for a [Chain.capture] zone.
+///
+/// Until they're materialized and exposed to the user, stack chains are tracked
+/// as linked lists of [Trace]s using the [_Node] class. These nodes are stored
+/// in three distinct ways:
+///
+/// * When a callback is registered, a node is created and stored as a captured
+/// local variable until the callback is run.
+///
+/// * When a callback is run, its captured node is set as the [_currentNode] so
+/// it can be available to [Chain.current] and to be linked into additional
+/// chains when more callbacks are scheduled.
+///
+/// * When a callback throws an error or a Future or Stream emits an error, the
+/// current node is associated with that error's stack trace using the
+/// [_chains] expando.
+///
+/// Since [ZoneSpecification] can't be extended or even implemented, in order to
+/// get a real [ZoneSpecification] instance it's necessary to call [toSpec].
+class StackZoneSpecification {
+ /// An opaque object used as a zone value to disable chain tracking in a given
+ /// zone.
+ ///
+ /// If `Zone.current[disableKey]` is `true`, no stack chains will be tracked.
+ static final disableKey = Object();
+
+ /// Whether chain-tracking is disabled in the current zone.
+ bool get _disabled => Zone.current[disableKey] == true;
+
+ /// The expando that associates stack chains with [StackTrace]s.
+ ///
+ /// The chains are associated with stack traces rather than errors themselves
+ /// because it's a common practice to throw strings as errors, which can't be
+ /// used with expandos.
+ ///
+ /// The chain associated with a given stack trace doesn't contain a node for
+ /// that stack trace.
+ final _chains = Expando<_Node>('stack chains');
+
+ /// The error handler for the zone.
+ ///
+ /// If this is null, that indicates that any unhandled errors should be passed
+ /// to the parent zone.
+ final void Function(Object error, Chain)? _onError;
+
+ /// The most recent node of the current stack chain.
+ _Node? _currentNode;
+
+ /// Whether this is an error zone.
+ final bool _errorZone;
+
+ StackZoneSpecification(this._onError, {bool errorZone = true})
+ : _errorZone = errorZone;
+
+ /// Converts this specification to a real [ZoneSpecification].
+ ZoneSpecification toSpec() => ZoneSpecification(
+ handleUncaughtError: _errorZone ? _handleUncaughtError : null,
+ registerCallback: _registerCallback,
+ registerUnaryCallback: _registerUnaryCallback,
+ registerBinaryCallback: _registerBinaryCallback,
+ errorCallback: _errorCallback);
+
+ /// Returns the current stack chain.
+ ///
+ /// By default, the first frame of the first trace will be the line where
+ /// [currentChain] is called. If [level] is passed, the first trace will start
+ /// that many frames up instead.
+ Chain currentChain([int level = 0]) => _createNode(level + 1).toChain();
+
+ /// Returns the stack chain associated with [trace], if one exists.
+ ///
+ /// The first stack trace in the returned chain will always be [trace]
+ /// (converted to a [Trace] if necessary). If there is no chain associated
+ /// with [trace], this just returns a single-trace chain containing [trace].
+ Chain chainFor(StackTrace? trace) {
+ if (trace is Chain) return trace;
+ trace ??= StackTrace.current;
+
+ var previous = _chains[trace] ?? _currentNode;
+ if (previous == null) {
+ // If there's no [_currentNode], we're running synchronously beneath
+ // [Chain.capture] and we should fall back to the VM's stack chaining. We
+ // can't use [Chain.from] here because it'll just call [chainFor] again.
+ if (trace is Trace) return Chain([trace]);
+ return LazyChain(() => Chain.parse(trace!.toString()));
+ } else {
+ if (trace is! Trace) {
+ var original = trace;
+ trace = LazyTrace(() => Trace.parse(_trimVMChain(original)));
+ }
+
+ return _Node(trace, previous).toChain();
+ }
+ }
+
+ /// Tracks the current stack chain so it can be set to [_currentNode] when
+ /// [f] is run.
+ ZoneCallback<R> _registerCallback<R>(
+ Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+ if (_disabled) return parent.registerCallback(zone, f);
+ var node = _createNode(1);
+ return parent.registerCallback(zone, () => _run(f, node));
+ }
+
+ /// Tracks the current stack chain so it can be set to [_currentNode] when
+ /// [f] is run.
+ ZoneUnaryCallback<R, T> _registerUnaryCallback<R, T>(
+ Zone self,
+ ZoneDelegate parent,
+ Zone zone,
+ @pragma('vm:awaiter-link') R Function(T) f) {
+ if (_disabled) return parent.registerUnaryCallback(zone, f);
+ var node = _createNode(1);
+ return parent.registerUnaryCallback(
+ zone, (arg) => _run(() => f(arg), node));
+ }
+
+ /// Tracks the current stack chain so it can be set to [_currentNode] when
+ /// [f] is run.
+ ZoneBinaryCallback<R, T1, T2> _registerBinaryCallback<R, T1, T2>(
+ Zone self, ZoneDelegate parent, Zone zone, R Function(T1, T2) f) {
+ if (_disabled) return parent.registerBinaryCallback(zone, f);
+
+ var node = _createNode(1);
+ return parent.registerBinaryCallback(
+ zone, (arg1, arg2) => _run(() => f(arg1, arg2), node));
+ }
+
+ /// Looks up the chain associated with [stackTrace] and passes it either to
+ /// [_onError] or [parent]'s error handler.
+ void _handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone,
+ Object error, StackTrace stackTrace) {
+ if (_disabled) {
+ parent.handleUncaughtError(zone, error, stackTrace);
+ return;
+ }
+
+ var stackChain = chainFor(stackTrace);
+ if (_onError == null) {
+ parent.handleUncaughtError(zone, error, stackChain);
+ return;
+ }
+
+ // TODO(nweiz): Currently this copies a lot of logic from [runZoned]. Just
+ // allow [runBinary] to throw instead once issue 18134 is fixed.
+ try {
+ // TODO(rnystrom): Is the null-assertion correct here? It is nullable in
+ // Zone. Should we check for that here?
+ self.parent!.runBinary(_onError, error, stackChain);
+ } on Object catch (newError, newStackTrace) {
+ if (identical(newError, error)) {
+ parent.handleUncaughtError(zone, error, stackChain);
+ } else {
+ parent.handleUncaughtError(zone, newError, newStackTrace);
+ }
+ }
+ }
+
+ /// Attaches the current stack chain to [stackTrace], replacing it if
+ /// necessary.
+ AsyncError? _errorCallback(Zone self, ZoneDelegate parent, Zone zone,
+ Object error, StackTrace? stackTrace) {
+ if (_disabled) return parent.errorCallback(zone, error, stackTrace);
+
+ // Go up two levels to get through [_CustomZone.errorCallback].
+ if (stackTrace == null) {
+ stackTrace = _createNode(2).toChain();
+ } else {
+ if (_chains[stackTrace] == null) _chains[stackTrace] = _createNode(2);
+ }
+
+ var asyncError = parent.errorCallback(zone, error, stackTrace);
+ return asyncError ?? AsyncError(error, stackTrace);
+ }
+
+ /// Creates a [_Node] with the current stack trace and linked to
+ /// [_currentNode].
+ ///
+ /// By default, the first frame of the first trace will be the line where
+ /// [_createNode] is called. If [level] is passed, the first trace will start
+ /// that many frames up instead.
+ _Node _createNode([int level = 0]) =>
+ _Node(_currentTrace(level + 1), _currentNode);
+
+ // TODO(nweiz): use a more robust way of detecting and tracking errors when
+ // issue 15105 is fixed.
+ /// Runs [f] with [_currentNode] set to [node].
+ ///
+ /// If [f] throws an error, this associates [node] with that error's stack
+ /// trace.
+ T _run<T>(T Function() f, _Node node) {
+ var previousNode = _currentNode;
+ _currentNode = node;
+ try {
+ return f();
+ } catch (e, stackTrace) {
+ // We can see the same stack trace multiple times if it's rethrown through
+ // guarded callbacks. The innermost chain will have the most
+ // information so it should take precedence.
+ _chains[stackTrace] ??= node;
+ rethrow;
+ } finally {
+ _currentNode = previousNode;
+ }
+ }
+
+ /// Like [Trace.current], but if the current stack trace has VM chaining
+ /// enabled, this only returns the innermost sub-trace.
+ Trace _currentTrace([int? level]) {
+ var stackTrace = StackTrace.current;
+ return LazyTrace(() {
+ var text = _trimVMChain(stackTrace);
+ var trace = Trace.parse(text);
+ // JS includes a frame for the call to StackTrace.current, but the VM
+ // doesn't, so we skip an extra frame in a JS context.
+ return Trace(trace.frames.skip((level ?? 0) + (inJS ? 2 : 1)),
+ original: text);
+ });
+ }
+
+ /// Removes the VM's stack chains from the native [trace], since we're
+ /// generating our own and we don't want duplicate frames.
+ String _trimVMChain(StackTrace trace) {
+ var text = trace.toString();
+ var index = text.indexOf(vmChainGap);
+ return index == -1 ? text : text.substring(0, index);
+ }
+}
+
+/// A linked list node representing a single entry in a stack chain.
+class _Node {
+ /// The stack trace for this link of the chain.
+ final Trace trace;
+
+ /// The previous node in the chain.
+ final _Node? previous;
+
+ _Node(StackTrace trace, [this.previous]) : trace = Trace.from(trace);
+
+ /// Converts this to a [Chain].
+ Chain toChain() {
+ var nodes = <Trace>[];
+ _Node? node = this;
+ while (node != null) {
+ nodes.add(node.trace);
+ node = node.previous;
+ }
+ return Chain(nodes);
+ }
+}
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
new file mode 100644
index 0000000..b8c62f5
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -0,0 +1,341 @@
+// 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.
+
+import 'dart:math' as math;
+
+import 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'unparsed_frame.dart';
+import 'utils.dart';
+import 'vm_trace.dart';
+
+final _terseRegExp = RegExp(r'(-patch)?([/\\].*)?$');
+
+/// A RegExp to match V8's stack traces.
+///
+/// V8's traces start with a line that's either just "Error" or else is a
+/// description of the exception that occurred. That description can be multiple
+/// lines, so we just look for any line other than the first that begins with
+/// three or four spaces and "at".
+final _v8Trace = RegExp(r'\n ?at ');
+
+/// A RegExp to match indidual lines of V8's stack traces.
+///
+/// This is intended to filter out the leading exception details of the trace
+/// though it is possible for the message to match this as well.
+final _v8TraceLine = RegExp(r' ?at ');
+
+/// A RegExp to match Firefox's eval and Function stack traces.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack
+///
+/// These stack traces look like:
+///
+/// ````
+/// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+/// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+/// ````
+final _firefoxEvalTrace = RegExp(r'@\S+ line \d+ >.* (Function|eval):\d+:\d+');
+
+/// A RegExp to match Firefox and Safari's stack traces.
+///
+/// Firefox and Safari have very similar stack trace formats, so we use the same
+/// logic for parsing them.
+///
+/// Firefox's trace frames start with the name of the function in which the
+/// error occurred, possibly including its parameters inside `()`. For example,
+/// `.VW.call$0("arg")@https://example.com/stuff.dart.js:560`.
+///
+/// Safari traces occasionally don't include the initial method name followed by
+/// "@", and they always have both the line and column number (or just a
+/// trailing colon if no column number is available). They can also contain
+/// empty lines or lines consisting only of `[native code]`.
+final _firefoxSafariTrace = RegExp(
+ r'^'
+ r'(' // Member description. Not present in some Safari frames.
+ r'([.0-9A-Za-z_$/<]|\(.*\))*' // Member name and arguments.
+ r'@'
+ r')?'
+ r'[^\s]*' // Frame URL.
+ r':\d*' // Line or column number. Some older frames only have a line number.
+ r'$',
+ multiLine: true);
+
+/// A RegExp to match this package's stack traces.
+final _friendlyTrace =
+ RegExp(r'^[^\s<][^\s]*( \d+(:\d+)?)?[ \t]+[^\s]+$', multiLine: true);
+
+/// A stack trace, comprised of a list of stack frames.
+class Trace implements StackTrace {
+ /// The stack frames that comprise this stack trace.
+ final List<Frame> frames;
+
+ /// The original stack trace from which this trace was parsed.
+ final StackTrace original;
+
+ /// Returns a human-readable representation of [stackTrace]. If [terse] is
+ /// set, this folds together multiple stack frames from the Dart core
+ /// libraries, so that only the core library method directly called from user
+ /// code is visible (see [Trace.terse]).
+ static String format(StackTrace stackTrace, {bool terse = true}) {
+ var trace = Trace.from(stackTrace);
+ if (terse) trace = trace.terse;
+ return trace.toString();
+ }
+
+ /// Returns the current stack trace.
+ ///
+ /// By default, the first frame of this trace will be the line where
+ /// [Trace.current] is called. If [level] is passed, the trace will start that
+ /// many frames up instead.
+ factory Trace.current([int level = 0]) {
+ if (level < 0) {
+ throw ArgumentError('Argument [level] must be greater than or equal '
+ 'to 0.');
+ }
+
+ var trace = Trace.from(StackTrace.current);
+ return LazyTrace(
+ () =>
+ // JS includes a frame for the call to StackTrace.current, but the VM
+ // doesn't, so we skip an extra frame in a JS context.
+ Trace(trace.frames.skip(level + (inJS ? 2 : 1)),
+ original: trace.original.toString()),
+ );
+ }
+
+ /// Returns a new stack trace containing the same data as [trace].
+ ///
+ /// If [trace] is a native [StackTrace], its data will be parsed out; if it's
+ /// a [Trace], it will be returned as-is.
+ factory Trace.from(StackTrace trace) {
+ if (trace is Trace) return trace;
+ if (trace is Chain) return trace.toTrace();
+ return LazyTrace(() => Trace.parse(trace.toString()));
+ }
+
+ /// Parses a string representation of a stack trace.
+ ///
+ /// [trace] should be formatted in the same way as a Dart VM or browser stack
+ /// trace. If it's formatted as a stack chain, this will return the equivalent
+ /// of [Chain.toTrace].
+ factory Trace.parse(String trace) {
+ try {
+ if (trace.isEmpty) return Trace(<Frame>[]);
+ if (trace.contains(_v8Trace)) return Trace.parseV8(trace);
+ if (trace.contains('\tat ')) return Trace.parseJSCore(trace);
+ if (trace.contains(_firefoxSafariTrace) ||
+ trace.contains(_firefoxEvalTrace)) {
+ return Trace.parseFirefox(trace);
+ }
+ if (trace.contains(chainGap)) return Chain.parse(trace).toTrace();
+ if (trace.contains(_friendlyTrace)) {
+ return Trace.parseFriendly(trace);
+ }
+
+ // Default to parsing the stack trace as a VM trace. This is also hit on
+ // IE and Safari, where the stack trace is just an empty string (issue
+ // 11257).
+ return Trace.parseVM(trace);
+ } on FormatException catch (error) {
+ throw FormatException('${error.message}\nStack trace:\n$trace');
+ }
+ }
+
+ /// Parses a string representation of a Dart VM stack trace.
+ Trace.parseVM(String trace) : this(_parseVM(trace), original: trace);
+
+ static List<Frame> _parseVM(String trace) {
+ // Ignore [vmChainGap]. This matches the behavior of
+ // `Chain.parse().toTrace()`.
+ var lines = trace
+ .trim()
+ .replaceAll(vmChainGap, '')
+ .split('\n')
+ .where((line) => line.isNotEmpty);
+
+ if (lines.isEmpty) {
+ return [];
+ }
+
+ var frames = lines.take(lines.length - 1).map(Frame.parseVM).toList();
+
+ // TODO(nweiz): Remove this when issue 23614 is fixed.
+ if (!lines.last.endsWith('.da')) {
+ frames.add(Frame.parseVM(lines.last));
+ }
+
+ return frames;
+ }
+
+ /// Parses a string representation of a Chrome/V8 stack trace.
+ Trace.parseV8(String trace)
+ : this(
+ trace
+ .split('\n')
+ .skip(1)
+ // It's possible that an Exception's description contains a line
+ // that looks like a V8 trace line, which will screw this up.
+ // Unfortunately, that's impossible to detect.
+ .skipWhile((line) => !line.startsWith(_v8TraceLine))
+ .map(Frame.parseV8),
+ original: trace);
+
+ /// Parses a string representation of a JavaScriptCore stack trace.
+ Trace.parseJSCore(String trace)
+ : this(
+ trace
+ .split('\n')
+ .where((line) => line != '\tat ')
+ .map(Frame.parseV8),
+ original: trace);
+
+ /// Parses a string representation of an Internet Explorer stack trace.
+ ///
+ /// IE10+ traces look just like V8 traces. Prior to IE10, stack traces can't
+ /// be retrieved.
+ Trace.parseIE(String trace) : this.parseV8(trace);
+
+ /// Parses a string representation of a Firefox stack trace.
+ Trace.parseFirefox(String trace)
+ : this(
+ trace
+ .trim()
+ .split('\n')
+ .where((line) => line.isNotEmpty && line != '[native code]')
+ .map(Frame.parseFirefox),
+ original: trace);
+
+ /// Parses a string representation of a Safari stack trace.
+ Trace.parseSafari(String trace) : this.parseFirefox(trace);
+
+ /// Parses a string representation of a Safari 6.1+ stack trace.
+ @Deprecated('Use Trace.parseSafari instead.')
+ Trace.parseSafari6_1(String trace) : this.parseSafari(trace);
+
+ /// Parses a string representation of a Safari 6.0 stack trace.
+ @Deprecated('Use Trace.parseSafari instead.')
+ Trace.parseSafari6_0(String trace)
+ : this(
+ trace
+ .trim()
+ .split('\n')
+ .where((line) => line != '[native code]')
+ .map(Frame.parseFirefox),
+ original: trace);
+
+ /// Parses this package's string representation of a stack trace.
+ ///
+ /// This also parses string representations of [Chain]s. They parse to the
+ /// same trace that [Chain.toTrace] would return.
+ Trace.parseFriendly(String trace)
+ : this(
+ trace.isEmpty
+ ? []
+ : trace
+ .trim()
+ .split('\n')
+ // Filter out asynchronous gaps from [Chain]s.
+ .where((line) => !line.startsWith('====='))
+ .map(Frame.parseFriendly),
+ original: trace);
+
+ /// Returns a new [Trace] comprised of [frames].
+ Trace(Iterable<Frame> frames, {String? original})
+ : frames = List<Frame>.unmodifiable(frames),
+ original = StackTrace.fromString(original ?? '');
+
+ /// Returns a VM-style [StackTrace] object.
+ ///
+ /// The return value's [toString] method will always return a string
+ /// representation in the Dart VM's stack trace format, regardless of what
+ /// platform is being used.
+ StackTrace get vmTrace => VMTrace(frames);
+
+ /// Returns a terser version of this trace.
+ ///
+ /// This is accomplished by folding together multiple stack frames from the
+ /// core library or from this package, as in [foldFrames]. Remaining core
+ /// library frames have their libraries, "-patch" suffixes, and line numbers
+ /// removed. If the outermost frame of the stack trace is a core library
+ /// frame, it's removed entirely.
+ ///
+ /// This won't do anything with a raw JavaScript trace, since there's no way
+ /// to determine which frames come from which Dart libraries. However, the
+ /// [`source_map_stack_trace`][https://pub.dev/packages/source_map_stack_trace]
+ /// package can be used to convert JavaScript traces into Dart-style traces.
+ ///
+ /// For custom folding, see [foldFrames].
+ Trace get terse => foldFrames((_) => false, terse: true);
+
+ /// Returns a new [Trace] based on `this` where multiple stack frames matching
+ /// [predicate] are folded together.
+ ///
+ /// This means that whenever there are multiple frames in a row that match
+ /// [predicate], only the last one is kept. This is useful for limiting the
+ /// amount of library code that appears in a stack trace by only showing user
+ /// code and code that's called by user code.
+ ///
+ /// If [terse] is true, this will also fold together frames from the core
+ /// library or from this package, simplify core library frames, and
+ /// potentially remove the outermost frame as in [Trace.terse].
+ Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+ if (terse) {
+ var oldPredicate = predicate;
+ predicate = (frame) {
+ if (oldPredicate(frame)) return true;
+
+ if (frame.isCore) return true;
+ if (frame.package == 'stack_trace') return true;
+
+ // Ignore async stack frames without any line or column information.
+ // These come from the VM's async/await implementation and represent
+ // internal frames. They only ever show up in stack chains and are
+ // always surrounded by other traces that are actually useful, so we can
+ // just get rid of them.
+ // TODO(nweiz): Get rid of this logic some time after issue 22009 is
+ // fixed.
+ if (!frame.member!.contains('<async>')) return false;
+ return frame.line == null;
+ };
+ }
+
+ var newFrames = <Frame>[];
+ for (var frame in frames.reversed) {
+ if (frame is UnparsedFrame || !predicate(frame)) {
+ newFrames.add(frame);
+ } else if (newFrames.isEmpty || !predicate(newFrames.last)) {
+ newFrames.add(Frame(frame.uri, frame.line, frame.column, frame.member));
+ }
+ }
+
+ if (terse) {
+ newFrames = newFrames.map((frame) {
+ if (frame is UnparsedFrame || !predicate(frame)) return frame;
+ var library = frame.library.replaceAll(_terseRegExp, '');
+ return Frame(Uri.parse(library), null, null, frame.member);
+ }).toList();
+
+ if (newFrames.length > 1 && predicate(newFrames.first)) {
+ newFrames.removeAt(0);
+ }
+ }
+
+ return Trace(newFrames.reversed, original: original.toString());
+ }
+
+ @override
+ String toString() {
+ // Figure out the longest path so we know how much to pad.
+ var longest =
+ frames.map((frame) => frame.location.length).fold(0, math.max);
+
+ // Print out the stack trace nicely formatted.
+ return frames.map((frame) {
+ if (frame is UnparsedFrame) return '$frame\n';
+ return '${frame.location.padRight(longest)} ${frame.member}\n';
+ }).join();
+ }
+}
diff --git a/pkgs/stack_trace/lib/src/unparsed_frame.dart b/pkgs/stack_trace/lib/src/unparsed_frame.dart
new file mode 100644
index 0000000..27e97f6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/unparsed_frame.dart
@@ -0,0 +1,33 @@
+// 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 'frame.dart';
+
+/// A frame that failed to parse.
+///
+/// The [member] property contains the original frame's contents.
+class UnparsedFrame implements Frame {
+ @override
+ final Uri uri = Uri(path: 'unparsed');
+ @override
+ final int? line = null;
+ @override
+ final int? column = null;
+ @override
+ final bool isCore = false;
+ @override
+ final String library = 'unparsed';
+ @override
+ final String? package = null;
+ @override
+ final String location = 'unparsed';
+
+ @override
+ final String member;
+
+ UnparsedFrame(this.member);
+
+ @override
+ String toString() => member;
+}
diff --git a/pkgs/stack_trace/lib/src/utils.dart b/pkgs/stack_trace/lib/src/utils.dart
new file mode 100644
index 0000000..bd971fe
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/utils.dart
@@ -0,0 +1,15 @@
+// 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.
+
+/// The line used in the string representation of stack chains to represent
+/// the gap between traces.
+const chainGap = '===== asynchronous gap ===========================\n';
+
+/// The line used in the string representation of VM stack chains to represent
+/// the gap between traces.
+final vmChainGap = RegExp(r'^<asynchronous suspension>\n?$', multiLine: true);
+
+// TODO(nweiz): When cross-platform imports work, use them to set this.
+/// Whether we're running in a JS context.
+const bool inJS = 0.0 is int;
diff --git a/pkgs/stack_trace/lib/src/vm_trace.dart b/pkgs/stack_trace/lib/src/vm_trace.dart
new file mode 100644
index 0000000..005b7af
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/vm_trace.dart
@@ -0,0 +1,32 @@
+// 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.
+
+import 'frame.dart';
+
+/// An implementation of [StackTrace] that emulates the behavior of the VM's
+/// implementation.
+///
+/// In particular, when [toString] is called, this returns a string in the VM's
+/// stack trace format.
+class VMTrace implements StackTrace {
+ /// The stack frames that comprise this stack trace.
+ final List<Frame> frames;
+
+ VMTrace(this.frames);
+
+ @override
+ String toString() {
+ var i = 1;
+ return frames.map((frame) {
+ var number = '#${i++}'.padRight(8);
+ var member = frame.member!
+ .replaceAllMapped(RegExp(r'[^.]+\.<async>'),
+ (match) => '${match[1]}.<${match[1]}_async_body>')
+ .replaceAll('<fn>', '<anonymous closure>');
+ var line = frame.line ?? 0;
+ var column = frame.column ?? 0;
+ return '$number$member (${frame.uri}:$line:$column)\n';
+ }).join();
+ }
+}
diff --git a/pkgs/stack_trace/lib/stack_trace.dart b/pkgs/stack_trace/lib/stack_trace.dart
new file mode 100644
index 0000000..fad30ce
--- /dev/null
+++ b/pkgs/stack_trace/lib/stack_trace.dart
@@ -0,0 +1,8 @@
+// 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.
+
+export 'src/chain.dart';
+export 'src/frame.dart';
+export 'src/trace.dart';
+export 'src/unparsed_frame.dart';
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
new file mode 100644
index 0000000..4f387b1
--- /dev/null
+++ b/pkgs/stack_trace/pubspec.yaml
@@ -0,0 +1,14 @@
+name: stack_trace
+version: 1.12.1
+description: A package for manipulating stack traces and printing them readably.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stack_trace
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ path: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.6
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..d5426dd
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/chain_test.dart
@@ -0,0 +1,375 @@
+// 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', () async {
+ // ignore: only_throw_errors
+ final chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+
+ expect(
+ Chain.parse(chain.toString()).toString(),
+ equals(chain.toString()),
+ );
+ });
+
+ test('parses an empty string', () {
+ var chain = Chain.parse('');
+ expect(chain.traces, isEmpty);
+ });
+
+ test('parses a chain containing empty traces', () {
+ var chain =
+ 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('parses a chain with VM gaps', () {
+ final chain =
+ Chain.parse('#1 MyClass.run (package:my_lib.dart:134:5)\n'
+ '<asynchronous suspension>\n'
+ '#2 main (file:///my_app.dart:9:3)\n'
+ '<asynchronous suspension>\n');
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames, hasLength(1));
+ expect(chain.traces[0].frames[0].toString(),
+ equals('package:my_lib.dart 134:5 in MyClass.run'));
+ expect(chain.traces[1].frames, hasLength(1));
+ expect(
+ chain.traces[1].frames[0].toString(),
+ anyOf(
+ equals('/my_app.dart 9:3 in main'), // VM
+ equals('file:///my_app.dart 9:3 in main'), // Browser
+ ),
+ );
+ });
+ });
+
+ group('Chain.capture()', () {
+ test('with onError blocks errors', () {
+ Chain.capture(() {
+ return Future<void>.error('oh no');
+ }, onError: expectAsync2((error, chain) {
+ expect(error, equals('oh no'));
+ expect(chain, isA<Chain>());
+ })).then(expectAsync1((_) {}, count: 0),
+ onError: expectAsync2((_, __) {}, count: 0));
+ });
+
+ test('with no onError blocks errors', () {
+ runZonedGuarded(() {
+ Chain.capture(() => Future<void>.error('oh no')).then(
+ expectAsync1((_) {}, count: 0),
+ onError: expectAsync2((_, __) {}, count: 0));
+ }, expectAsync2((error, chain) {
+ expect(error, equals('oh no'));
+ expect(chain, isA<Chain>());
+ }));
+ });
+
+ test("with errorZone: false doesn't block errors", () {
+ expect(Chain.capture(() => Future<void>.error('oh no'), errorZone: false),
+ throwsA('oh no'));
+ });
+
+ test("doesn't allow onError and errorZone: false", () {
+ expect(() => Chain.capture(() {}, onError: (_, __) {}, errorZone: false),
+ throwsArgumentError);
+ });
+
+ group('with when: false', () {
+ test("with no onError doesn't block errors", () {
+ expect(Chain.capture(() => Future<void>.error('oh no'), when: false),
+ throwsA('oh no'));
+ });
+
+ test('with onError blocks errors', () {
+ Chain.capture(() {
+ return Future<void>.error('oh no');
+ }, onError: expectAsync2((error, chain) {
+ expect(error, equals('oh no'));
+ expect(chain, isA<Chain>());
+ }), when: false);
+ });
+
+ test("doesn't enable chain-tracking", () {
+ return Chain.disable(() {
+ return Chain.capture(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() {
+ completer.complete(Chain.current());
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(1));
+ });
+ }, when: false);
+ });
+ });
+ });
+ });
+
+ test('Chain.capture() with custom zoneValues', () {
+ return Chain.capture(() {
+ expect(Zone.current[#enabled], true);
+ }, zoneValues: {#enabled: true});
+ });
+
+ group('Chain.disable()', () {
+ test('disables chain-tracking', () {
+ return Chain.disable(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(1));
+ });
+ });
+ });
+
+ test('Chain.capture() re-enables chain-tracking', () {
+ return Chain.disable(() {
+ return Chain.capture(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ });
+ });
+ });
+ });
+
+ test('preserves parent zones of the capture zone', () {
+ // The outer disable call turns off the test package's chain-tracking.
+ return Chain.disable(() {
+ return runZoned(() {
+ return Chain.capture(() {
+ expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+ });
+ }, zoneValues: {#enabled: true});
+ });
+ });
+
+ test('preserves child zones of the capture zone', () {
+ // The outer disable call turns off the test package's chain-tracking.
+ return Chain.disable(() {
+ return Chain.capture(() {
+ return runZoned(() {
+ expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+ }, zoneValues: {#enabled: true});
+ });
+ });
+ });
+
+ test("with when: false doesn't disable", () {
+ return Chain.capture(() {
+ return Chain.disable(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ });
+ }, when: false);
+ });
+ });
+ });
+
+ test('toString() ensures that all traces are aligned', () {
+ var chain = Chain([
+ Trace.parse('short 10:11 Foo.bar\n'),
+ 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 = Chain([
+ 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'),
+ 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 = Chain([
+ Trace.parse('user/code.dart 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz'),
+ 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'),
+ 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 = Chain([
+ 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'),
+ 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'));
+ });
+
+ // Regression test for #9
+ test("doesn't crash on empty traces", () {
+ var chain = Chain([
+ Trace.parse('user/code.dart 10:11 Bang.qux'),
+ Trace([]),
+ Trace.parse('user/code.dart 10:11 Bang.qux')
+ ]);
+
+ expect(
+ chain.terse.toString(),
+ equals('$userSlashCode 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ '$userSlashCode 10:11 Bang.qux\n'));
+ });
+ });
+
+ group('Chain.foldFrames', () {
+ test('folds each trace', () {
+ var chain = Chain([
+ 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'),
+ 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 = Chain([
+ 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'),
+ 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'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test('eliminates completely-folded traces', () {
+ var chain = Chain([
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'b.dart 10:11 Bang.qux'),
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bang.qux'),
+ 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 = Chain([
+ 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 = Chain([
+ Trace.parse('user/code.dart 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz'),
+ 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'));
+ });
+}
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..abb842d
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/dart2js_test.dart
@@ -0,0 +1,337 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+// 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')
+library;
+
+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 = Completer<void>();
+ 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();
+ }
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ test('passed to a completer', () async {
+ var trace = 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 = 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 = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() {
+ inMicrotask(() => throw 'error');
+ }, onError: (error, chain) {
+ expect(error, equals('error'));
+ expect(chain.traces, hasLength(2));
+ throw error;
+ });
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(chain,
+ isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+ });
+
+ test('capture() without onError passes exceptions to parent zone', () {
+ var completer = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() => inMicrotask(() => throw 'error'));
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(chain,
+ isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ group('current() within capture()', () {
+ test('called in a microtask', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inMicrotask(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a one-shot timer', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inOneShotTimer(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a periodic timer', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a nested series of asynchronous operations', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() {
+ inMicrotask(() => completer.complete(Chain.current()));
+ });
+ });
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(4));
+ });
+
+ test('called in a long future chain', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inFutureChain(() => completer.complete(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.
+ Chain.disable(() async {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(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));
+ }),
+ );
+
+ group('forTrace() within capture()', () {
+ test('called for a stack trace from a microtask', () async {
+ var chain = await Chain.capture(
+ () => 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(
+ () => 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(
+ () => 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(() => 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(
+ () => chainForTrace(inFutureChain, () => throw 'error'));
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test(
+ 'called for an unregistered stack trace returns a chain wrapping that '
+ 'trace', () {
+ late StackTrace trace;
+ var chain = Chain.capture(() {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ return Chain.forTrace(stackTrace);
+ }
+ });
+
+ expect(chain.traces, hasLength(1));
+ expect(
+ chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+ });
+ });
+
+ test(
+ 'forTrace() outside of capture() returns a chain wrapping the given '
+ 'trace', () {
+ late StackTrace trace;
+ var chain = Chain.capture(() {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ return Chain.forTrace(stackTrace);
+ }
+ });
+
+ expect(chain.traces, hasLength(1));
+ expect(chain.traces.first.toString(), equals(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..27fb0e6
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/utils.dart
@@ -0,0 +1,94 @@
+// 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:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Runs [callback] in a microtask callback.
+void inMicrotask(void Function() callback) => scheduleMicrotask(callback);
+
+/// Runs [callback] in a one-shot timer callback.
+void inOneShotTimer(void Function() callback) => Timer.run(callback);
+
+/// Runs [callback] once in a periodic timer callback.
+void inPeriodicTimer(void Function() callback) {
+ var count = 0;
+ Timer.periodic(const Duration(milliseconds: 1), (timer) {
+ count++;
+ if (count != 5) return;
+ timer.cancel();
+ callback();
+ });
+}
+
+/// Runs [callback] within a long asynchronous Future chain.
+void inFutureChain(void Function() callback) {
+ Future(() {})
+ .then((_) => Future(() {}))
+ .then((_) => Future(() {}))
+ .then((_) => Future(() {}))
+ .then((_) => Future(() {}))
+ .then((_) => callback())
+ .then((_) => Future(() {}));
+}
+
+void inNewFuture(void Function() callback) {
+ Future(callback);
+}
+
+void inSyncFuture(void Function() callback) {
+ 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<void> completerErrorFuture([StackTrace? trace]) {
+ var completer = Completer<void>();
+ 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<void> controllerErrorStream([StackTrace? trace]) {
+ var controller = StreamController<void>();
+ 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(
+ void Function(void Function()) asyncFn, void Function() callback) {
+ var completer = Completer<Chain>();
+ 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.
+ Future<void>.value()
+ .then((_) => callback())
+ .catchError(completer.completeError);
+ });
+
+ return completer.future
+ .catchError((_, StackTrace stackTrace) => 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(void Function() callback) {
+ var completer = 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..5c6c0b7
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/vm_test.dart
@@ -0,0 +1,508 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+// VM chain tests can rely on stronger guarantees about the contents of the
+// stack traces than dart2js.
+@TestOn('dart-vm')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/src/utils.dart';
+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', () async {
+ late StackTrace vmTrace;
+ var chain = await captureFuture(() {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ vmTrace = stackTrace;
+ rethrow;
+ }
+ });
+
+ // Because there's no chain context for a synchronous error, we fall back
+ // on the VM's stack chain tracking.
+ expect(
+ chain.toString(), equals(Chain.parse(vmTrace.toString()).toString()));
+ });
+
+ 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 = Completer<void>();
+ 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();
+ }
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ test('passed to a completer', () {
+ var trace = 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 = 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 = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() {
+ inMicrotask(() => throw 'error');
+ }, onError: (error, chain) {
+ expect(error, equals('error'));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ throw error;
+ });
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(
+ chain,
+ isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+ contains(frameMember(startsWith('inMicrotask')))));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+ });
+
+ test('capture() without onError passes exceptions to parent zone', () {
+ var completer = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() => inMicrotask(() => throw 'error'));
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(
+ chain,
+ isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+ contains(frameMember(startsWith('inMicrotask')))));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ group('current() within capture()', () {
+ test('called in a microtask', () {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inMicrotask(() => completer.complete(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 = Completer<Chain>();
+ Chain.capture(() {
+ inOneShotTimer(() => completer.complete(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 = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() => completer.complete(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 = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() {
+ inMicrotask(() => completer.complete(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 = Completer<Chain>();
+ Chain.capture(() {
+ inFutureChain(() => completer.complete(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.
+ return Chain.disable(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ // Since the chain wasn't loaded within [Chain.capture], the full stack
+ // chain isn't available and it just returns the current stack when
+ // called.
+ expect(chain.traces, hasLength(1));
+ expect(
+ chain.traces.first.frames.first, frameMember(startsWith('main')));
+ });
+ });
+ });
+
+ group('forTrace() within capture()', () {
+ test('called for a stack trace from a microtask', () {
+ return Chain.capture(() {
+ return chainForTrace(inMicrotask, () => throw 'error');
+ }).then((chain) {
+ // Because [chainForTrace] has to set up a future chain to capture the
+ // stack trace while still showing it to the zone specification, it adds
+ // an additional level of async nesting and so an additional trace.
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('called for a stack trace from a one-shot timer', () {
+ return Chain.capture(() {
+ return chainForTrace(inOneShotTimer, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ });
+ });
+
+ test('called for a stack trace from a periodic timer', () {
+ return Chain.capture(() {
+ return chainForTrace(inPeriodicTimer, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test(
+ 'called for a stack trace from a nested series of asynchronous '
+ 'operations', () {
+ return Chain.capture(() {
+ return chainForTrace((callback) {
+ inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+ }, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(5));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ expect(chain.traces[3].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ expect(chain.traces[4].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test('called for a stack trace from a long future chain', () {
+ return Chain.capture(() {
+ return chainForTrace(inFutureChain, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inFutureChain'))));
+ });
+ });
+
+ test('called for an unregistered stack trace uses the current chain',
+ () async {
+ late StackTrace trace;
+ var chain = await Chain.capture(() async {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ return Chain.forTrace(stackTrace);
+ }
+ });
+
+ expect(chain.traces, hasLength(greaterThan(1)));
+
+ // Assert that we've trimmed the VM's stack chains here to avoid
+ // duplication.
+ expect(chain.traces.first.toString(),
+ equals(Chain.parse(trace.toString()).traces.first.toString()));
+ });
+ });
+
+ test(
+ 'forTrace() outside of capture() returns a chain describing the VM stack '
+ 'chain', () {
+ // Disable the test package's chain-tracking.
+ return Chain.disable(() async {
+ late StackTrace trace;
+ await Chain.capture(() async {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ }
+ });
+
+ final chain = Chain.forTrace(trace);
+ final traceStr = trace.toString();
+ final gaps = vmChainGap.allMatches(traceStr);
+ // If the trace ends on a gap, there's no sub-trace following the gap.
+ final expectedLength =
+ (gaps.last.end == traceStr.length) ? gaps.length : gaps.length + 1;
+ expect(chain.traces, hasLength(expectedLength));
+ expect(
+ chain.traces.first.frames, contains(frameMember(startsWith('main'))));
+ });
+ });
+}
diff --git a/pkgs/stack_trace/test/frame_test.dart b/pkgs/stack_trace/test/frame_test.dart
new file mode 100644
index 0000000..a5dfc20
--- /dev/null
+++ b/pkgs/stack_trace/test/frame_test.dart
@@ -0,0 +1,729 @@
+// 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.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('.parseVM', () {
+ test('parses a stack frame with column correctly', () {
+ var frame = Frame.parseVM('#1 Foo._bar '
+ '(file:///home/nweiz/code/stuff.dart:42:21)');
+ expect(
+ frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(frame.line, equals(42));
+ expect(frame.column, equals(21));
+ expect(frame.member, equals('Foo._bar'));
+ });
+
+ test('parses a stack frame without column correctly', () {
+ var frame = Frame.parseVM('#1 Foo._bar '
+ '(file:///home/nweiz/code/stuff.dart:24)');
+ expect(
+ frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(frame.line, equals(24));
+ expect(frame.column, null);
+ expect(frame.member, equals('Foo._bar'));
+ });
+
+ // This can happen with async stack traces. See issue 22009.
+ test('parses a stack frame without line or column correctly', () {
+ var frame = Frame.parseVM('#1 Foo._bar '
+ '(file:///home/nweiz/code/stuff.dart)');
+ expect(
+ frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Foo._bar'));
+ });
+
+ test('converts "<anonymous closure>" to "<fn>"', () {
+ String? parsedMember(String member) =>
+ Frame.parseVM('#0 $member (foo:0:0)').member;
+
+ expect(parsedMember('Foo.<anonymous closure>'), equals('Foo.<fn>'));
+ expect(parsedMember('<anonymous closure>.<anonymous closure>.bar'),
+ equals('<fn>.<fn>.bar'));
+ });
+
+ test('converts "<<anonymous closure>_async_body>" to "<async>"', () {
+ var frame =
+ Frame.parseVM('#0 Foo.<<anonymous closure>_async_body> (foo:0:0)');
+ expect(frame.member, equals('Foo.<async>'));
+ });
+
+ test('converts "<function_name_async_body>" to "<async>"', () {
+ var frame = Frame.parseVM('#0 Foo.<function_name_async_body> (foo:0:0)');
+ expect(frame.member, equals('Foo.<async>'));
+ });
+
+ test('parses a folded frame correctly', () {
+ var frame = Frame.parseVM('...');
+
+ expect(frame.member, equals('...'));
+ expect(frame.uri, equals(Uri()));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ });
+ });
+
+ group('.parseV8', () {
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseV8, '');
+ expectIsUnparsed(Frame.parseV8, '#1');
+ expectIsUnparsed(Frame.parseV8, '#1 Foo');
+ expectIsUnparsed(Frame.parseV8, '#1 (dart:async/future.dart:10:15)');
+ expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+ });
+
+ test('parses a stack frame correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(https://example.com/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a : in the authority', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(http://localhost:8080/stuff.dart.js:560:28)');
+ expect(
+ frame.uri, equals(Uri.parse('http://localhost:8080/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute POSIX path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(/path/to/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute Windows path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ r'(C:\path\to\stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a Windows UNC path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ r'(\\mount\path\to\stuff.dart.js:560:28)');
+ expect(
+ frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative POSIX path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(path/to/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative Windows path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ r'(path\to\stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses an anonymous stack frame correctly', () {
+ var frame =
+ Frame.parseV8(' at https://example.com/stuff.dart.js:560:28');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('<fn>'));
+ });
+
+ test('parses a native stack frame correctly', () {
+ var frame = Frame.parseV8(' at Object.stringify (native)');
+ expect(frame.uri, Uri.parse('native'));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Object.stringify'));
+ });
+
+ test('parses a stack frame with [as ...] correctly', () {
+ // Ignore "[as ...]", since other stack trace formats don't support a
+ // similar construct.
+ var frame = Frame.parseV8(' at VW.call\$0 [as call\$4] '
+ '(https://example.com/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a basic eval stack frame correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at <anonymous> '
+ '(https://example.com/stuff.dart.js:560:28))');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('parses an IE10 eval stack frame correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at Anonymous function '
+ '(https://example.com/stuff.dart.js:560:28))');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('parses an eval stack frame with inner position info correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at <anonymous> '
+ '(https://example.com/stuff.dart.js:560:28), <anonymous>:3:28)');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('parses a nested eval stack frame correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at <anonymous> '
+ '(eval at sub (https://example.com/stuff.dart.js:560:28)))');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('converts "<anonymous>" to "<fn>"', () {
+ String? parsedMember(String member) =>
+ Frame.parseV8(' at $member (foo:0:0)').member;
+
+ expect(parsedMember('Foo.<anonymous>'), equals('Foo.<fn>'));
+ expect(
+ parsedMember('<anonymous>.<anonymous>.bar'), equals('<fn>.<fn>.bar'));
+ });
+
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseV8, '');
+ expectIsUnparsed(Frame.parseV8, ' at');
+ expectIsUnparsed(Frame.parseV8, ' at Foo');
+ expectIsUnparsed(Frame.parseV8, ' at Foo (dart:async/future.dart)');
+ expectIsUnparsed(Frame.parseV8, ' at (dart:async/future.dart:10:15)');
+ expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+ expectIsUnparsed(Frame.parseV8, ' at dart:async/future.dart');
+ expectIsUnparsed(Frame.parseV8, 'dart:async/future.dart:10:15');
+ });
+ });
+
+ group('.parseFirefox/.parseSafari', () {
+ test('parses a Firefox stack trace with anonymous function', () {
+ var trace = Trace.parse('''
+Foo._bar@https://example.com/stuff.js:18056:12
+anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+baz@https://pub.dev/buz.js:56355:55
+ ''');
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].line, equals(18056));
+ expect(trace.frames[0].column, equals(12));
+ expect(trace.frames[0].member, equals('Foo._bar'));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].line, equals(693));
+ expect(trace.frames[1].column, isNull);
+ expect(trace.frames[1].member, equals('<fn>'));
+ expect(trace.frames[2].uri, equals(Uri.parse('https://pub.dev/buz.js')));
+ expect(trace.frames[2].line, equals(56355));
+ expect(trace.frames[2].column, equals(55));
+ expect(trace.frames[2].member, equals('baz'));
+ });
+
+ test('parses a Firefox stack trace with nested evals in anonymous function',
+ () {
+ var trace = Trace.parse('''
+ Foo._bar@https://example.com/stuff.js:18056:12
+ anonymous@file:///C:/example.html line 7 > eval line 1 > eval:1:1
+ anonymous@file:///C:/example.html line 45 > Function:1:1
+ ''');
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].line, equals(18056));
+ expect(trace.frames[0].column, equals(12));
+ expect(trace.frames[0].member, equals('Foo._bar'));
+ expect(trace.frames[1].uri, equals(Uri.parse('file:///C:/example.html')));
+ expect(trace.frames[1].line, equals(7));
+ expect(trace.frames[1].column, isNull);
+ expect(trace.frames[1].member, equals('<fn>'));
+ expect(trace.frames[2].uri, equals(Uri.parse('file:///C:/example.html')));
+ expect(trace.frames[2].line, equals(45));
+ expect(trace.frames[2].column, isNull);
+ expect(trace.frames[2].member, equals('<fn>'));
+ });
+
+ test('parses a simple stack frame correctly', () {
+ var frame = Frame.parseFirefox(
+ '.VW.call\$0@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute POSIX path correctly', () {
+ var frame = Frame.parseFirefox('.VW.call\$0@/path/to/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute Windows path correctly', () {
+ var frame =
+ Frame.parseFirefox(r'.VW.call$0@C:\path\to\stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a Windows UNC path correctly', () {
+ var frame =
+ Frame.parseFirefox(r'.VW.call$0@\\mount\path\to\stuff.dart.js:560');
+ expect(
+ frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative POSIX path correctly', () {
+ var frame = Frame.parseFirefox('.VW.call\$0@path/to/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative Windows path correctly', () {
+ var frame = Frame.parseFirefox(r'.VW.call$0@path\to\stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a simple anonymous stack frame correctly', () {
+ var frame = Frame.parseFirefox('@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('<fn>'));
+ });
+
+ test('parses a nested anonymous stack frame correctly', () {
+ var frame =
+ Frame.parseFirefox('.foo/<@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+
+ frame = Frame.parseFirefox('.foo/@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+ });
+
+ test('parses a named nested anonymous stack frame correctly', () {
+ var frame = Frame.parseFirefox(
+ '.foo/.name<@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+
+ frame = Frame.parseFirefox(
+ '.foo/.name@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+ });
+
+ test('parses a stack frame with parameters correctly', () {
+ var frame = Frame.parseFirefox(
+ '.foo(12, "@)()/<")@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo'));
+ });
+
+ test('parses a nested anonymous stack frame with parameters correctly', () {
+ var frame = Frame.parseFirefox(
+ '.foo(12, "@)()/<")/.fn<@https://example.com/stuff.dart.js:560',
+ );
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+ });
+
+ test(
+ 'parses a deeply-nested anonymous stack frame with parameters '
+ 'correctly', () {
+ var frame = Frame.parseFirefox('.convertDartClosureToJS/\$function</<@'
+ 'https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('convertDartClosureToJS.<fn>.<fn>'));
+ });
+
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseFirefox, '');
+ expectIsUnparsed(Frame.parseFirefox, '.foo');
+ expectIsUnparsed(Frame.parseFirefox, '.foo@dart:async/future.dart');
+ expectIsUnparsed(Frame.parseFirefox, '.foo(@dart:async/future.dart:10');
+ expectIsUnparsed(Frame.parseFirefox, '@dart:async/future.dart');
+ });
+
+ test('parses a simple stack frame correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('foo\$bar'));
+ });
+
+ test('parses an anonymous stack frame correctly', () {
+ var frame = Frame.parseFirefox('https://dart.dev/foo/bar.dart:10:11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('<fn>'));
+ });
+
+ test('parses a stack frame with no line correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart::11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('foo\$bar'));
+ });
+
+ test('parses a stack frame with no column correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo\$bar'));
+ });
+
+ test('parses a stack frame with no line or column correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('foo\$bar'));
+ });
+ });
+
+ group('.parseFriendly', () {
+ test('parses a simple stack frame correctly', () {
+ var frame = Frame.parseFriendly(
+ 'https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('parses a stack frame with no line or column correctly', () {
+ var frame =
+ Frame.parseFriendly('https://dart.dev/foo/bar.dart Foo.<fn>.bar');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('parses a stack frame with no column correctly', () {
+ var frame =
+ Frame.parseFriendly('https://dart.dev/foo/bar.dart 10 Foo.<fn>.bar');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('parses a stack frame with a relative path correctly', () {
+ var frame = Frame.parseFriendly('foo/bar.dart 10:11 Foo.<fn>.bar');
+ expect(frame.uri,
+ equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseFriendly, '');
+ expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart');
+ expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart 10:11');
+ });
+
+ test('parses a data url stack frame with no line or column correctly', () {
+ var frame = Frame.parseFriendly('data:... main');
+ expect(frame.uri.scheme, equals('data'));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('main'));
+ });
+
+ test('parses a data url stack frame correctly', () {
+ var frame = Frame.parseFriendly('data:... 10:11 main');
+ expect(frame.uri.scheme, equals('data'));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('main'));
+ });
+
+ test('parses a stack frame with spaces in the member name correctly', () {
+ var frame = Frame.parseFriendly(
+ 'foo/bar.dart 10:11 (anonymous function).dart.fn');
+ expect(frame.uri,
+ equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('(anonymous function).dart.fn'));
+ });
+
+ test(
+ 'parses a stack frame with spaces in the member name and no line or '
+ 'column correctly', () {
+ var frame = Frame.parseFriendly(
+ 'https://dart.dev/foo/bar.dart (anonymous function).dart.fn');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('(anonymous function).dart.fn'));
+ });
+ });
+
+ test('only considers dart URIs to be core', () {
+ bool isCore(String library) =>
+ Frame.parseVM('#0 Foo ($library:0:0)').isCore;
+
+ expect(isCore('dart:core'), isTrue);
+ expect(isCore('dart:async'), isTrue);
+ expect(isCore('dart:core/uri.dart'), isTrue);
+ expect(isCore('dart:async/future.dart'), isTrue);
+ expect(isCore('bart:core'), isFalse);
+ expect(isCore('sdart:core'), isFalse);
+ expect(isCore('darty:core'), isFalse);
+ expect(isCore('bart:core/uri.dart'), isFalse);
+ });
+
+ group('.library', () {
+ test('returns the URI string for non-file URIs', () {
+ expect(Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').library,
+ equals('dart:async/future.dart'));
+ expect(
+ Frame.parseVM('#0 Foo '
+ '(https://dart.dev/stuff/thing.dart:0:0)')
+ .library,
+ equals('https://dart.dev/stuff/thing.dart'));
+ });
+
+ test('returns the relative path for file URIs', () {
+ expect(Frame.parseVM('#0 Foo (foo/bar.dart:0:0)').library,
+ equals(path.join('foo', 'bar.dart')));
+ });
+
+ test('truncates legacy data: URIs', () {
+ var frame = Frame.parseVM(
+ '#0 Foo (data:application/dart;charset=utf-8,blah:0:0)');
+ expect(frame.library, equals('data:...'));
+ });
+
+ test('truncates data: URIs', () {
+ var frame = Frame.parseVM(
+ '#0 main (<data:application/dart;charset=utf-8>:1:15)');
+ expect(frame.library, equals('data:...'));
+ });
+ });
+
+ group('.location', () {
+ test(
+ 'returns the library and line/column numbers for non-core '
+ 'libraries', () {
+ expect(
+ Frame.parseVM('#0 Foo '
+ '(https://dart.dev/thing.dart:5:10)')
+ .location,
+ equals('https://dart.dev/thing.dart 5:10'));
+ expect(Frame.parseVM('#0 Foo (foo/bar.dart:1:2)').location,
+ equals('${path.join('foo', 'bar.dart')} 1:2'));
+ });
+ });
+
+ group('.package', () {
+ test('returns null for non-package URIs', () {
+ expect(
+ Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').package, isNull);
+ expect(
+ Frame.parseVM('#0 Foo '
+ '(https://dart.dev/stuff/thing.dart:0:0)')
+ .package,
+ isNull);
+ });
+
+ test('returns the package name for package: URIs', () {
+ expect(Frame.parseVM('#0 Foo (package:foo/foo.dart:0:0)').package,
+ equals('foo'));
+ expect(Frame.parseVM('#0 Foo (package:foo/zap/bar.dart:0:0)').package,
+ equals('foo'));
+ });
+ });
+
+ group('.toString()', () {
+ test(
+ 'returns the library and line/column numbers for non-core '
+ 'libraries', () {
+ expect(
+ Frame.parseVM('#0 Foo (https://dart.dev/thing.dart:5:10)').toString(),
+ equals('https://dart.dev/thing.dart 5:10 in Foo'));
+ });
+
+ test('converts "<anonymous closure>" to "<fn>"', () {
+ expect(
+ Frame.parseVM('#0 Foo.<anonymous closure> '
+ '(dart:core/uri.dart:5:10)')
+ .toString(),
+ equals('dart:core/uri.dart 5:10 in Foo.<fn>'));
+ });
+
+ test('prints a frame without a column correctly', () {
+ expect(Frame.parseVM('#0 Foo (dart:core/uri.dart:5)').toString(),
+ equals('dart:core/uri.dart 5 in Foo'));
+ });
+
+ test('prints relative paths as relative', () {
+ var relative = path.normalize('relative/path/to/foo.dart');
+ expect(Frame.parseFriendly('$relative 5:10 Foo').toString(),
+ equals('$relative 5:10 in Foo'));
+ });
+ });
+
+ test('parses a V8 Wasm frame with a name', () {
+ var frame = Frame.parseV8(' at Error._throwWithCurrentStackTrace '
+ '(wasm://wasm/0006d966:wasm-function[119]:0xbb13)');
+ expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+ expect(frame.line, 1);
+ expect(frame.column, 0xbb13 + 1);
+ expect(frame.member, 'Error._throwWithCurrentStackTrace');
+ });
+
+ test('parses a V8 Wasm frame with a name with spaces', () {
+ var frame = Frame.parseV8(' at main tear-off trampoline '
+ '(wasm://wasm/0017fbea:wasm-function[863]:0x23cc8)');
+ expect(frame.uri, Uri.parse('wasm://wasm/0017fbea'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x23cc8 + 1);
+ expect(frame.member, 'main tear-off trampoline');
+ });
+
+ test('parses a V8 Wasm frame with a name with colons and parens', () {
+ var frame = Frame.parseV8(' at a::b::c() '
+ '(https://a.b.com/x/y/z.wasm:wasm-function[66334]:0x12c28ad)');
+ expect(frame.uri, Uri.parse('https://a.b.com/x/y/z.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x12c28ad + 1);
+ expect(frame.member, 'a::b::c()');
+ });
+
+ test('parses a V8 Wasm frame without a name', () {
+ var frame =
+ Frame.parseV8(' at wasm://wasm/0006d966:wasm-function[119]:0xbb13');
+ expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+ expect(frame.line, 1);
+ expect(frame.column, 0xbb13 + 1);
+ expect(frame.member, '119');
+ });
+
+ test('parses a Firefox Wasm frame with a name', () {
+ var frame = Frame.parseFirefox(
+ 'g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+ expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x143b4 + 1);
+ expect(frame.member, 'g');
+ });
+
+ test('parses a Firefox Wasm frame with a name with spaces', () {
+ var frame = Frame.parseFirefox(
+ 'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387');
+ expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x14387 + 1);
+ expect(frame.member, 'main tear-off trampoline');
+ });
+
+ test('parses a Firefox Wasm frame without a name', () {
+ var frame = Frame.parseFirefox(
+ '@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+ expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x143b4 + 1);
+ expect(frame.member, '796');
+ });
+
+ test('parses a Safari Wasm frame with a name', () {
+ var frame = Frame.parseSafari('<?>.wasm-function[g]@[wasm code]');
+ expect(frame.uri, Uri.parse('wasm code'));
+ expect(frame.line, null);
+ expect(frame.column, null);
+ expect(frame.member, 'g');
+ });
+
+ test('parses a Safari Wasm frame with a name', () {
+ var frame = Frame.parseSafari(
+ '<?>.wasm-function[main tear-off trampoline]@[wasm code]');
+ expect(frame.uri, Uri.parse('wasm code'));
+ expect(frame.line, null);
+ expect(frame.column, null);
+ expect(frame.member, 'main tear-off trampoline');
+ });
+
+ test('parses a Safari Wasm frame without a name', () {
+ var frame = Frame.parseSafari('<?>.wasm-function[796]@[wasm code]');
+ expect(frame.uri, Uri.parse('wasm code'));
+ expect(frame.line, null);
+ expect(frame.column, null);
+ expect(frame.member, '796');
+ });
+}
+
+void expectIsUnparsed(Frame Function(String) constructor, String text) {
+ var frame = constructor(text);
+ expect(frame, isA<UnparsedFrame>());
+ expect(frame.toString(), equals(text));
+}
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
new file mode 100644
index 0000000..e09de95
--- /dev/null
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -0,0 +1,615 @@
+// 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.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+ // This just shouldn't crash.
+ test('a native stack trace is parseable', Trace.current);
+
+ group('.parse', () {
+ test('.parse parses a V8 stack trace with eval statment correctly', () {
+ var trace = Trace.parse(r'''Error
+ at Object.eval (eval at Foo (main.dart.js:588), <anonymous>:3:47)''');
+ expect(trace.frames[0].uri, Uri.parse('main.dart.js'));
+ expect(trace.frames[0].member, equals('Object.eval'));
+ expect(trace.frames[0].line, equals(588));
+ expect(trace.frames[0].column, isNull);
+ });
+
+ test('.parse parses a VM stack trace correctly', () {
+ var trace = Trace.parse(
+ '#0 Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)\n'
+ '#1 zip.<anonymous closure>.zap (dart:async/future.dart:0:2)\n'
+ '#2 zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)',
+ );
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(trace.frames[1].uri, equals(Uri.parse('dart:async/future.dart')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.dart')));
+ });
+
+ test('parses a V8 stack trace correctly', () {
+ var trace = Trace.parse('Error\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('Exception: foo\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('Exception: foo\n'
+ ' bar\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('Exception: foo\n'
+ ' bar\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at (anonymous function).zip.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].member, equals('<fn>'));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ expect(trace.frames[2].member, equals('<fn>.zip.zap'));
+ });
+
+ // JavaScriptCore traces are just like V8, except that it doesn't have a
+ // header and it starts with a tab rather than spaces.
+ test('parses a JavaScriptCore stack trace correctly', () {
+ var trace =
+ Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+ '\tat https://example.com/stuff.js:0:2\n'
+ '\tat zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+ '\tat \n'
+ '\tat zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[1].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace correctly', () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('zip/<@https://example.com/stuff.js:0\n'
+ 'Foo._bar@https://example.com/stuff.js:42\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'Foo._bar@https://example.com/stuff.js:42');
+
+ expect(
+ trace.frames[0].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[2].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace containing native code correctly',
+ () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+ '[native code]');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ expect(trace.frames.length, equals(3));
+ });
+
+ test('parses a Firefox/Safari stack trace without a method name correctly',
+ () {
+ var trace = Trace.parse('https://example.com/stuff.js:42\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].member, equals('<fn>'));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace with an empty line correctly',
+ () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+ '\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace with a column number correctly',
+ () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42:2\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].line, equals(42));
+ expect(trace.frames[0].column, equals(2));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a package:stack_trace stack trace correctly', () {
+ var trace =
+ Trace.parse('https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/baz.dart Foo.<fn>.bar');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+ });
+
+ test('parses a package:stack_trace stack chain correctly', () {
+ var trace =
+ Trace.parse('https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/baz.dart Foo.<fn>.bar\n'
+ '===== asynchronous gap ===========================\n'
+ 'https://dart.dev/foo/bang.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/quux.dart Foo.<fn>.bar');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+ expect(trace.frames[2].uri,
+ equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+ expect(trace.frames[3].uri,
+ equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+ });
+
+ test('parses a package:stack_trace stack chain with end gap correctly', () {
+ var trace = Trace.parse(
+ 'https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/baz.dart Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/bang.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/quux.dart Foo.<fn>.bar===== asynchronous gap ===========================\n',
+ );
+
+ expect(trace.frames.length, 4);
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+ expect(trace.frames[2].uri,
+ equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+ expect(trace.frames[3].uri,
+ equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+ });
+
+ test('parses a real package:stack_trace stack trace correctly', () {
+ var traceString = Trace.current().toString();
+ expect(Trace.parse(traceString).toString(), equals(traceString));
+ });
+
+ test('parses an empty string correctly', () {
+ var trace = Trace.parse('');
+ expect(trace.frames, isEmpty);
+ expect(trace.toString(), equals(''));
+ });
+
+ test('parses trace with async gap correctly', () {
+ var trace = Trace.parse('#0 bop (file:///pull.dart:42:23)\n'
+ '<asynchronous suspension>\n'
+ '#1 twist (dart:the/future.dart:0:2)\n'
+ '#2 main (dart:my/file.dart:4:6)\n');
+
+ expect(trace.frames.length, 3);
+ expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+ expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+ expect(trace.frames[2].uri, equals(Uri.parse('dart:my/file.dart')));
+ });
+
+ test('parses trace with async gap at end correctly', () {
+ var trace = Trace.parse('#0 bop (file:///pull.dart:42:23)\n'
+ '#1 twist (dart:the/future.dart:0:2)\n'
+ '<asynchronous suspension>\n');
+
+ expect(trace.frames.length, 2);
+ expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+ expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+ });
+
+ test('parses a V8 stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ '\tat Error._throwWithCurrentStackTrace (wasm://wasm/0006d892:wasm-function[119]:0xbaf8)\n'
+ '\tat main (wasm://wasm/0006d892:wasm-function[792]:0x14378)\n'
+ '\tat main tear-off trampoline (wasm://wasm/0006d892:wasm-function[794]:0x14387)\n'
+ '\tat _invokeMain (wasm://wasm/0006d892:wasm-function[70]:0xa56c)\n'
+ '\tat InstantiatedApp.invokeMain (/home/user/test.mjs:361:37)\n'
+ '\tat main (/home/user/run_wasm.js:416:21)\n'
+ '\tat async action (/home/user/run_wasm.js:353:38)\n'
+ '\tat async eventLoop (/home/user/run_wasm.js:329:9)');
+
+ expect(trace.frames.length, 8);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('wasm://wasm/0006d892'));
+ expect(trace.frames[0].line, 1);
+ expect(trace.frames[0].column, 0xbaf8 + 1);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[4].line, 361);
+ expect(trace.frames[4].column, 37);
+ expect(trace.frames[4].member, 'InstantiatedApp.invokeMain');
+
+ expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+ expect(trace.frames[5].line, 416);
+ expect(trace.frames[5].column, 21);
+ expect(trace.frames[5].member, 'main');
+ });
+
+ test('parses Firefox stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ 'Error._throwWithCurrentStackTrace@http://localhost:8080/test.wasm:wasm-function[119]:0xbaf8\n'
+ 'main@http://localhost:8080/test.wasm:wasm-function[792]:0x14378\n'
+ 'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387\n'
+ '_invokeMain@http://localhost:8080/test.wasm:wasm-function[70]:0xa56c\n'
+ 'invoke@http://localhost:8080/test.mjs:48:26');
+
+ expect(trace.frames.length, 5);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(trace.frames[0].line, 1);
+ expect(trace.frames[0].column, 0xbaf8 + 1);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('http://localhost:8080/test.mjs'));
+ expect(trace.frames[4].line, 48);
+ expect(trace.frames[4].column, 26);
+ expect(trace.frames[4].member, 'invoke');
+ });
+
+ test('parses JSShell stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ 'Error._throwWithCurrentStackTrace@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[119]:0xbaf8\n'
+ 'main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378\n'
+ 'main tear-off trampoline@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[794]:0x14387\n'
+ '_invokeMain@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[70]:0xa56c\n'
+ 'invokeMain@/home/user/test.mjs:361:37\n'
+ 'main@/home/user/run_wasm.js:416:21\n'
+ 'async*action@/home/user/run_wasm.js:353:44\n'
+ 'eventLoop@/home/user/run_wasm.js:329:15\n'
+ 'self.dartMainRunner@/home/user/run_wasm.js:354:14\n'
+ '@/home/user/run_wasm.js:419:15');
+
+ expect(trace.frames.length, 10);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[0].line, 1);
+ expect(trace.frames[0].column, 0xbaf8 + 1);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[4].line, 361);
+ expect(trace.frames[4].column, 37);
+ expect(trace.frames[4].member, 'invokeMain');
+
+ expect(trace.frames[9].uri, Uri.parse('file:///home/user/run_wasm.js'));
+ expect(trace.frames[9].line, 419);
+ expect(trace.frames[9].column, 15);
+ expect(trace.frames[9].member, '<fn>');
+ });
+
+ test('parses Safari stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ '<?>.wasm-function[Error._throwWithCurrentStackTrace]@[wasm code]\n'
+ '<?>.wasm-function[main]@[wasm code]\n'
+ '<?>.wasm-function[main tear-off trampoline]@[wasm code]\n'
+ '<?>.wasm-function[_invokeMain]@[wasm code]\n'
+ 'invokeMain@/home/user/test.mjs:361:48\n'
+ '@/home/user/run_wasm.js:416:31');
+
+ expect(trace.frames.length, 6);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('wasm code'));
+ expect(trace.frames[0].line, null);
+ expect(trace.frames[0].column, null);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[4].line, 361);
+ expect(trace.frames[4].column, 48);
+ expect(trace.frames[4].member, 'invokeMain');
+
+ expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+ expect(trace.frames[5].line, 416);
+ expect(trace.frames[5].column, 31);
+ expect(trace.frames[5].member, '<fn>');
+ });
+ });
+
+ test('.toString() nicely formats the stack trace', () {
+ var trace = Trace.parse('''
+#0 Foo._bar (foo/bar.dart:42:21)
+#1 zip.<anonymous closure>.zap (dart:async/future.dart:0:2)
+#2 zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)
+''');
+
+ expect(trace.toString(), equals('''
+${path.join('foo', 'bar.dart')} 42:21 Foo._bar
+dart:async/future.dart 0:2 zip.<fn>.zap
+https://pub.dev/thing.dart 1:100 zip.<fn>.zap
+'''));
+ });
+
+ test('.vmTrace returns a native-style trace', () {
+ var uri = path.toUri(path.absolute('foo'));
+ var trace = Trace([
+ Frame(uri, 10, 20, 'Foo.<fn>'),
+ Frame(Uri.parse('https://dart.dev/foo.dart'), null, null, 'bar'),
+ Frame(Uri.parse('dart:async'), 15, null, 'baz'),
+ ]);
+
+ expect(
+ trace.vmTrace.toString(),
+ equals('#1 Foo.<anonymous closure> ($uri:10:20)\n'
+ '#2 bar (https://dart.dev/foo.dart:0:0)\n'
+ '#3 baz (dart:async:15:0)\n'));
+ });
+
+ group('folding', () {
+ group('.terse', () {
+ test('folds core frames together bottom-up', () {
+ var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#0 notCore (foo.dart:42:21)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+#5 alsoNotCore (bar.dart:10:20)
+''');
+
+ expect(trace.terse.toString(), equals('''
+dart:core bottom
+foo.dart 42:21 notCore
+dart:async bottom
+bar.dart 10:20 alsoNotCore
+'''));
+ });
+
+ test('folds empty async frames', () {
+ var trace = Trace.parse('''
+#0 top (dart:async/future.dart:0:2)
+#1 empty.<<anonymous closure>_async_body> (bar.dart)
+#2 bottom (dart:async-patch/future.dart:9:11)
+#3 notCore (foo.dart:42:21)
+''');
+
+ expect(trace.terse.toString(), equals('''
+dart:async bottom
+foo.dart 42:21 notCore
+'''));
+ });
+
+ test('removes the bottom-most async frame', () {
+ var trace = Trace.parse('''
+#0 notCore (foo.dart:42:21)
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+''');
+
+ expect(trace.terse.toString(), equals('''
+foo.dart 42:21 notCore
+'''));
+ });
+
+ test("won't make a trace empty", () {
+ var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+''');
+
+ expect(trace.terse.toString(), equals('''
+dart:core bottom
+'''));
+ });
+
+ test("won't panic on an empty trace", () {
+ expect(Trace.parse('').terse.toString(), equals(''));
+ });
+ });
+
+ group('.foldFrames', () {
+ test('folds frames together bottom-up', () {
+ var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (foo.dart:1:100)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (dart:io/socket.dart:5:10)
+#5 fooBottom (dart:async-patch/future.dart:9:11)
+''');
+
+ var folded =
+ trace.foldFrames((frame) => frame.member!.startsWith('foo'));
+ expect(folded.toString(), equals('''
+foo.dart 42:21 notFoo
+foo.dart 1:100 fooBottom
+bar.dart 10:20 alsoNotFoo
+dart:async-patch/future.dart 9:11 fooBottom
+'''));
+ });
+
+ test('will never fold unparsed frames', () {
+ var trace = Trace.parse(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+''');
+
+ expect(trace.foldFrames((frame) => true).toString(), equals(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+'''));
+ });
+
+ group('with terse: true', () {
+ test('folds core frames as well', () {
+ var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 coreBottom (dart:async/future.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 coreBottom (dart:async-patch/future.dart:9:11)
+''');
+
+ var folded = trace.foldFrames(
+ (frame) => frame.member!.startsWith('foo'),
+ terse: true);
+ expect(folded.toString(), equals('''
+foo.dart 42:21 notFoo
+dart:async coreBottom
+bar.dart 10:20 alsoNotFoo
+'''));
+ });
+
+ test('shortens folded frames', () {
+ var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (package:foo/bar.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 fooBottom (foo/bar.dart:9:11)
+#6 againNotFoo (bar.dart:20:20)
+''');
+
+ var folded = trace.foldFrames(
+ (frame) => frame.member!.startsWith('foo'),
+ terse: true);
+ expect(folded.toString(), equals('''
+foo.dart 42:21 notFoo
+package:foo fooBottom
+bar.dart 10:20 alsoNotFoo
+foo fooBottom
+bar.dart 20:20 againNotFoo
+'''));
+ });
+
+ test('removes the bottom-most folded frame', () {
+ var trace = Trace.parse('''
+#2 fooTop (package:foo/bar.dart:0:2)
+#3 notFoo (bar.dart:10:20)
+#5 fooBottom (foo/bar.dart:9:11)
+''');
+
+ var folded = trace.foldFrames(
+ (frame) => frame.member!.startsWith('foo'),
+ terse: true);
+ expect(folded.toString(), equals('''
+package:foo fooTop
+bar.dart 10:20 notFoo
+'''));
+ });
+ });
+ });
+ });
+}
diff --git a/pkgs/stack_trace/test/utils.dart b/pkgs/stack_trace/test/utils.dart
new file mode 100644
index 0000000..98cb5ed
--- /dev/null
+++ b/pkgs/stack_trace/test/utils.dart
@@ -0,0 +1,14 @@
+// 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.
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `member` field.
+Matcher frameMember(Object? matcher) =>
+ isA<Frame>().having((p0) => p0.member, 'member', matcher);
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `library` field.
+Matcher frameLibrary(Object? matcher) =>
+ isA<Frame>().having((p0) => p0.library, 'library', matcher);
diff --git a/pkgs/stack_trace/test/vm_test.dart b/pkgs/stack_trace/test/vm_test.dart
new file mode 100644
index 0000000..70ac014
--- /dev/null
+++ b/pkgs/stack_trace/test/vm_test.dart
@@ -0,0 +1,112 @@
+// 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.
+
+/// This file tests stack_trace's ability to parse live stack traces. It's a
+/// dual of dartium_test.dart, since method names can differ somewhat from
+/// platform to platform. No similar file exists for dart2js since the specific
+/// method names there are implementation details.
+@TestOn('vm')
+library;
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+// The name of this (trivial) function is verified as part of the test
+String getStackTraceString() => StackTrace.current.toString();
+
+// The name of this (trivial) function is verified as part of the test
+StackTrace getStackTraceObject() => StackTrace.current;
+
+Frame getCaller([int? level]) {
+ if (level == null) return Frame.caller();
+ return Frame.caller(level);
+}
+
+Frame nestedGetCaller(int level) => getCaller(level);
+
+Trace getCurrentTrace([int level = 0]) => Trace.current(level);
+
+Trace nestedGetCurrentTrace(int level) => getCurrentTrace(level);
+
+void main() {
+ group('Trace', () {
+ test('.parse parses a real stack trace correctly', () {
+ var string = getStackTraceString();
+ var trace = Trace.parse(string);
+ expect(path.url.basename(trace.frames.first.uri.path),
+ equals('vm_test.dart'));
+ expect(trace.frames.first.member, equals('getStackTraceString'));
+ });
+
+ test('converts from a native stack trace correctly', () {
+ var trace = Trace.from(getStackTraceObject());
+ expect(path.url.basename(trace.frames.first.uri.path),
+ equals('vm_test.dart'));
+ expect(trace.frames.first.member, equals('getStackTraceObject'));
+ });
+
+ test('.from handles a stack overflow trace correctly', () {
+ void overflow() => overflow();
+
+ late Trace? trace;
+ try {
+ overflow();
+ } catch (_, stackTrace) {
+ trace = Trace.from(stackTrace);
+ }
+
+ expect(trace!.frames.first.member, equals('main.<fn>.<fn>.overflow'));
+ });
+
+ group('.current()', () {
+ test('with no argument returns a trace starting at the current frame',
+ () {
+ var trace = Trace.current();
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('at level 0 returns a trace starting at the current frame', () {
+ var trace = Trace.current();
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('at level 1 returns a trace starting at the parent frame', () {
+ var trace = getCurrentTrace(1);
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('at level 2 returns a trace starting at the grandparent frame', () {
+ var trace = nestedGetCurrentTrace(2);
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('throws an ArgumentError for negative levels', () {
+ expect(() => Trace.current(-1), throwsArgumentError);
+ });
+ });
+ });
+
+ group('Frame.caller()', () {
+ test('with no argument returns the parent frame', () {
+ expect(getCaller().member, equals('main.<fn>.<fn>'));
+ });
+
+ test('at level 0 returns the current frame', () {
+ expect(getCaller(0).member, equals('getCaller'));
+ });
+
+ test('at level 1 returns the current frame', () {
+ expect(getCaller(1).member, equals('main.<fn>.<fn>'));
+ });
+
+ test('at level 2 returns the grandparent frame', () {
+ expect(nestedGetCaller(2).member, equals('main.<fn>.<fn>'));
+ });
+
+ test('throws an ArgumentError for negative levels', () {
+ expect(() => Frame.caller(-1), throwsArgumentError);
+ });
+ });
+}