diff --git a/.github/ISSUE_TEMPLATE/stack_trace.md b/.github/ISSUE_TEMPLATE/stack_trace.md new file mode 100644 index 000000000..417362b3f --- /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 3ab79c051..25efb2a1f 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 000000000..7435967a8 --- /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 0201aa284..563f90dea 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ don't naturally belong to other topic monorepos (like | [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [![package issues](https://img.shields.io/badge/package:source_maps-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps) | | [source_span](pkgs/source_span/) | Provides a standard representation for source code locations and spans. | [![package issues](https://img.shields.io/badge/package:source_span-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_span) | [![pub package](https://img.shields.io/pub/v/source_span.svg)](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. | [![package issues](https://img.shields.io/badge/package:sse-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asse) | [![pub package](https://img.shields.io/pub/v/sse.svg)](https://pub.dev/packages/sse) | +| [stack_trace](pkgs/stack_trace/) | A package for manipulating stack traces and printing them readably. | [![package issues](https://img.shields.io/badge/package:stack_trace-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astack_trace) | [![pub package](https://img.shields.io/pub/v/stack_trace.svg)](https://pub.dev/packages/stack_trace) | | [stream_transform](pkgs/stream_transform/) | A collection of utilities to transform and manipulate streams. | [![package issues](https://img.shields.io/badge/package:stream_transform-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_transform) | [![pub package](https://img.shields.io/pub/v/stream_transform.svg)](https://pub.dev/packages/stream_transform) | | [term_glyph](pkgs/term_glyph/) | Useful Unicode glyphs and ASCII substitutes. | [![package issues](https://img.shields.io/badge/package:term_glyph-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aterm_glyph) | [![pub package](https://img.shields.io/pub/v/term_glyph.svg)](https://pub.dev/packages/term_glyph) | | [test_reflective_loader](pkgs/test_reflective_loader/) | Support for discovering tests and test suites using reflection. | [![package issues](https://img.shields.io/badge/package:test_reflective_loader-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader) | [![pub package](https://img.shields.io/pub/v/test_reflective_loader.svg)](https://pub.dev/packages/test_reflective_loader) | diff --git a/pkgs/stack_trace/.gitignore b/pkgs/stack_trace/.gitignore new file mode 100644 index 000000000..f0230158a --- /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 000000000..e92cf9cea --- /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 + 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 `<_async_body>` in VM frames' members with the + terser ``. + +## 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 "". diff --git a/pkgs/stack_trace/LICENSE b/pkgs/stack_trace/LICENSE new file mode 100644 index 000000000..162572a44 --- /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 000000000..b10a55638 --- /dev/null +++ b/pkgs/stack_trace/README.md @@ -0,0 +1,169 @@ +[![Build Status](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml) +[![pub package](https://img.shields.io/pub/v/stack_trace.svg)](https://pub.dev/packages/stack_trace) +[![package publisher](https://img.shields.io/pub/publisher/stack_trace.svg)](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. (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. (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. (dart:async:349:28) + #12 Timer.run. (dart:async:2402:21) + #13 Timer.Timer. (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. + 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. + 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. + dart:async 2402:21 Timer.run. + dart:async-patch 15:15 Timer.Timer. + +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. + 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. + +## 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. (file:///Users/kevmoo/github/stack_trace/example/example.dart:8:52) + + +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. + package:stack_trace/src/stack_zone_specification.dart 126:26 StackZoneSpecification._registerUnaryCallback.. + package:stack_trace/src/stack_zone_specification.dart 208:15 StackZoneSpecification._run + package:stack_trace/src/stack_zone_specification.dart 126:14 StackZoneSpecification._registerUnaryCallback. + 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. + 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. + 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. + ===== asynchronous gap =========================== + dart:async _Future.then + test.dart 13:12 scheduleAsync + test.dart 7:18 main. + 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 000000000..4eb82ceca --- /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 000000000..d601ca441 --- /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.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 000000000..6a815c6bc --- /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 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 Function() callback, + {void Function(Object error, Chain)? onError, + bool when = true, + bool errorZone = true, + Map? 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 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 traces) : traces = List.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 000000000..d4043b780 --- /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 (?:(?.+) )?' + r'(?:\(?(?:(?\S+):wasm-function\[(?\d+)\]' + r'\:0x(?[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'^(?.*?)@(?:(?\S+).*?:wasm-function' + r'\[(?\d+)\]:0x(?[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\[(?.*)\]@\[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'<(|[^>]+)_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 `` 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, '') + .replaceAll('', ''); + var uri = match[2]!.startsWith(' 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 "", while + // IE10 lists them as "Anonymous function". + return parseJsLocation( + match[2]!, + match[1]! + .replaceAll('', '') + .replaceAll('Anonymous function', '') + .replaceAll('(anonymous function)', '')); + } else { + // The second form looks like " at LOCATION", and is used for + // anonymous functions. + return parseJsLocation(match[3]!, ''); + } + } + + 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 = ''; + } + 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, '.').join(); + if (member == '') member = ''; + + // Some Firefox members have initial dots. We remove them for + // consistency with other platforms. + member = member.replaceFirst(_initialDot, ''); + } else { + member = ''; + } + + 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 000000000..063ed59db --- /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 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 000000000..3ecaa2df0 --- /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 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 000000000..901a5ee8b --- /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 _registerCallback( + 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 _registerUnaryCallback( + 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 _registerBinaryCallback( + 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 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 = []; + _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 000000000..b8c62f5cf --- /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 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([]); + 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 _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 frames, {String? original}) + : frames = List.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('')) return false; + return frame.line == null; + }; + } + + var newFrames = []; + 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 000000000..27e97f6e0 --- /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 000000000..bd971fe56 --- /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'^\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 000000000..005b7afa3 --- /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 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'[^.]+\.'), + (match) => '${match[1]}.<${match[1]}_async_body>') + .replaceAll('', ''); + 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 000000000..fad30ce26 --- /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 000000000..4f387b1c4 --- /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 000000000..d5426dda9 --- /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' + '\n' + '#2 main (file:///my_app.dart:9:3)\n' + '\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.error('oh no'); + }, onError: expectAsync2((error, chain) { + expect(error, equals('oh no')); + expect(chain, isA()); + })).then(expectAsync1((_) {}, count: 0), + onError: expectAsync2((_, __) {}, count: 0)); + }); + + test('with no onError blocks errors', () { + runZonedGuarded(() { + Chain.capture(() => Future.error('oh no')).then( + expectAsync1((_) {}, count: 0), + onError: expectAsync2((_, __) {}, count: 0)); + }, expectAsync2((error, chain) { + expect(error, equals('oh no')); + expect(chain, isA()); + })); + }); + + test("with errorZone: false doesn't block errors", () { + expect(Chain.capture(() => Future.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.error('oh no'), when: false), + throwsA('oh no')); + }); + + test('with onError blocks errors', () { + Chain.capture(() { + return Future.error('oh no'); + }, onError: expectAsync2((error, chain) { + expect(error, equals('oh no')); + expect(chain, isA()); + }), when: false); + }); + + test("doesn't enable chain-tracking", () { + return Chain.disable(() { + return Chain.capture(() { + var completer = Completer(); + 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(); + 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(); + 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(); + 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 000000000..abb842dfc --- /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(); + 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(); + + 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().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(); + + runZonedGuarded(() { + Chain.capture(() => inMicrotask(() => throw 'error')); + }, (error, chain) { + try { + expect(error, equals('error')); + expect(chain, + isA().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.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.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.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.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.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(); + 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 000000000..27fb0e684 --- /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 completerErrorFuture([StackTrace? trace]) { + var completer = Completer(); + completer.completeError('error', trace); + return completer.future; +} + +/// Returns a Stream that emits an error using a controller. +/// +/// If [trace] is passed, it's used as the stack trace for the error. +Stream controllerErrorStream([StackTrace? trace]) { + var controller = StreamController(); + controller.addError('error', trace); + return controller.stream; +} + +/// Runs [callback] within [asyncFn], then converts any errors raised into a +/// [Chain] with [Chain.forTrace]. +Future chainForTrace( + void Function(void Function()) asyncFn, void Function() callback) { + var completer = Completer(); + asyncFn(() { + // We use `new Future.value().then(...)` here as opposed to [new Future] or + // [new Future.sync] because those methods don't pass the exception through + // the zone specification before propagating it, so there's no chance to + // attach a chain to its stack trace. See issue 15105. + Future.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 captureFuture(void Function() callback) { + var completer = Completer(); + 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 000000000..5c6c0b7d9 --- /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(); + 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(); + + 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().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(); + + runZonedGuarded(() { + Chain.capture(() => inMicrotask(() => throw 'error')); + }, (error, chain) { + try { + expect(error, equals('error')); + expect( + chain, + isA().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.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.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.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.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.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(); + 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 000000000..a5dfc2064 --- /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 "" to ""', () { + String? parsedMember(String member) => + Frame.parseVM('#0 $member (foo:0:0)').member; + + expect(parsedMember('Foo.'), equals('Foo.')); + expect(parsedMember('..bar'), + equals('..bar')); + }); + + test('converts "<_async_body>" to ""', () { + var frame = + Frame.parseVM('#0 Foo.<_async_body> (foo:0:0)'); + expect(frame.member, equals('Foo.')); + }); + + test('converts "" to ""', () { + var frame = Frame.parseVM('#0 Foo. (foo:0:0)'); + expect(frame.member, equals('Foo.')); + }); + + 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('')); + }); + + 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 ' + '(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 ' + '(https://example.com/stuff.dart.js:560:28), :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 ' + '(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 "" to ""', () { + String? parsedMember(String member) => + Frame.parseV8(' at $member (foo:0:0)').member; + + expect(parsedMember('Foo.'), equals('Foo.')); + expect( + parsedMember('..bar'), equals('..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('')); + 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('')); + 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('')); + }); + + 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('')); + }); + + 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.')); + + 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.')); + }); + + 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.')); + + 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.')); + }); + + 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.')); + }); + + test( + 'parses a deeply-nested anonymous stack frame with parameters ' + 'correctly', () { + var frame = Frame.parseFirefox('.convertDartClosureToJS/\$function.')); + }); + + 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('')); + }); + + 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..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..bar')); + }); + + test('parses a stack frame with no line or column correctly', () { + var frame = + Frame.parseFriendly('https://dart.dev/foo/bar.dart Foo..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..bar')); + }); + + test('parses a stack frame with no column correctly', () { + var frame = + Frame.parseFriendly('https://dart.dev/foo/bar.dart 10 Foo..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..bar')); + }); + + test('parses a stack frame with a relative path correctly', () { + var frame = Frame.parseFriendly('foo/bar.dart 10:11 Foo..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..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 (: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 "" to ""', () { + expect( + Frame.parseVM('#0 Foo. ' + '(dart:core/uri.dart:5:10)') + .toString(), + equals('dart:core/uri.dart 5:10 in Foo.')); + }); + + 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()); + 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 000000000..e09de9555 --- /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), :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..zap (dart:async/future.dart:0:2)\n' + '#2 zip..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..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..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..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('')); + expect( + trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js'))); + expect(trace.frames[2].member, equals('.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..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..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('')); + 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..bar\n' + 'https://dart.dev/foo/baz.dart Foo..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..bar\n' + 'https://dart.dev/foo/baz.dart Foo..bar\n' + '===== asynchronous gap ===========================\n' + 'https://dart.dev/foo/bang.dart 10:11 Foo..bar\n' + 'https://dart.dev/foo/quux.dart Foo..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..bar\n' + 'https://dart.dev/foo/baz.dart Foo..bar\n' + 'https://dart.dev/foo/bang.dart 10:11 Foo..bar\n' + 'https://dart.dev/foo/quux.dart Foo..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' + '\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' + '\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, ''); + }); + + 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, ''); + }); + }); + + test('.toString() nicely formats the stack trace', () { + var trace = Trace.parse(''' +#0 Foo._bar (foo/bar.dart:42:21) +#1 zip..zap (dart:async/future.dart:0:2) +#2 zip..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..zap +https://pub.dev/thing.dart 1:100 zip..zap +''')); + }); + + test('.vmTrace returns a native-style trace', () { + var uri = path.toUri(path.absolute('foo')); + var trace = Trace([ + Frame(uri, 10, 20, 'Foo.'), + 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. ($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.<_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 000000000..98cb5ede0 --- /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().having((p0) => p0.member, 'member', matcher); + +/// Returns a matcher that runs [matcher] against a [Frame]'s `library` field. +Matcher frameLibrary(Object? matcher) => + isA().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 000000000..70ac0143a --- /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...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...')); + }); + + test('at level 0 returns a trace starting at the current frame', () { + var trace = Trace.current(); + expect(trace.frames.first.member, equals('main...')); + }); + + test('at level 1 returns a trace starting at the parent frame', () { + var trace = getCurrentTrace(1); + expect(trace.frames.first.member, equals('main...')); + }); + + test('at level 2 returns a trace starting at the grandparent frame', () { + var trace = nestedGetCurrentTrace(2); + expect(trace.frames.first.member, equals('main...')); + }); + + 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..')); + }); + + 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..')); + }); + + test('at level 2 returns the grandparent frame', () { + expect(nestedGetCaller(2).member, equals('main..')); + }); + + test('throws an ArgumentError for negative levels', () { + expect(() => Frame.caller(-1), throwsArgumentError); + }); + }); +}