Revert "Revert "Fix remaining holes in stack trace demangling (#60478)" (#60916)" (#60996)
This reverts commit 2c7e5dd93548d84dad61bf2de171cdb89fa3efc6.
diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart
index 393a69b..e296f3b 100644
--- a/packages/flutter/lib/src/foundation/assertions.dart
+++ b/packages/flutter/lib/src/foundation/assertions.dart
@@ -28,6 +28,14 @@
/// and other callbacks that collect information describing an error.
typedef InformationCollector = Iterable<DiagnosticsNode> Function();
+/// Signature for a function that demangles [StackTrace] objects into a format
+/// that can be parsed by [StackFrame].
+///
+/// See also:
+///
+/// * [FlutterError.demangleStackTrace], which shows an example implementation.
+typedef StackTraceDemangler = StackTrace Function(StackTrace details);
+
/// Partial information from a stack frame for stack filtering purposes.
///
/// See also:
@@ -651,7 +659,7 @@
// If not: Error is in user code (user violated assertion in framework).
// If so: Error is in Framework. We either need an assertion higher up
// in the stack, or we've violated our own assertions.
- final List<StackFrame> stackFrames = StackFrame.fromStackTrace(stack)
+ final List<StackFrame> stackFrames = StackFrame.fromStackTrace(FlutterError.demangleStackTrace(stack))
.skipWhile((StackFrame frame) => frame.packageScheme == 'dart')
.toList();
final bool ourFault = stackFrames.length >= 2
@@ -863,6 +871,31 @@
/// recommended.
static FlutterExceptionHandler onError = (FlutterErrorDetails details) => presentError(details);
+ /// Called by the Flutter framework before attempting to parse a [StackTrace].
+ ///
+ /// Some [StackTrace] implementations have a different toString format from
+ /// what the framework expects, like ones from package:stack_trace. To make
+ /// sure we can still parse and filter mangled [StackTrace]s, the framework
+ /// first calls this function to demangle them.
+ ///
+ /// This should be set in any environment that could propagate a non-standard
+ /// stack trace to the framework. Otherwise, the default behavior is to assume
+ /// all stack traces are in a standard format.
+ ///
+ /// The following example demangles package:stack_trace traces by converting
+ /// them into vm traces, which the framework is able to parse:
+ ///
+ /// ```dart
+ /// FlutterError.demangleStackTrace = (StackTrace stackTrace) {
+ /// if (stack is stack_trace.Trace)
+ // return stack.vmTrace;
+ // if (stack is stack_trace.Chain)
+ // return stack.toTrace().vmTrace;
+ // return stack;
+ /// };
+ /// ```
+ static StackTraceDemangler demangleStackTrace = (StackTrace stackTrace) => stackTrace;
+
/// Called whenever the Flutter framework wants to present an error to the
/// users.
///
@@ -1069,7 +1102,11 @@
void debugPrintStack({StackTrace stackTrace, String label, int maxFrames}) {
if (label != null)
debugPrint(label);
- stackTrace ??= StackTrace.current;
+ if (stackTrace == null) {
+ stackTrace = StackTrace.current;
+ } else {
+ stackTrace = FlutterError.demangleStackTrace(stackTrace);
+ }
Iterable<String> lines = stackTrace.toString().trimRight().split('\n');
if (kIsWeb && lines.isNotEmpty) {
// Remove extra call to StackTrace.current for web platform.
@@ -1105,11 +1142,7 @@
}) : super(
name: name,
value: stack,
- properties: stack == null
- ? <DiagnosticsNode>[]
- : (stackFilter ?? FlutterError.defaultStackFilter)(stack.toString().trimRight().split('\n'))
- .map<DiagnosticsNode>(_createStackFrame)
- .toList(),
+ properties: _applyStackFilter(stack, stackFilter),
style: DiagnosticsTreeStyle.flat,
showSeparator: showSeparator,
allowTruncate: true,
@@ -1127,6 +1160,17 @@
showSeparator: showSeparator,
);
+ static List<DiagnosticsNode> _applyStackFilter(
+ StackTrace stack,
+ IterableFilter<String> stackFilter,
+ ) {
+ if (stack == null)
+ return <DiagnosticsNode>[];
+ final IterableFilter<String> filter = stackFilter ?? FlutterError.defaultStackFilter;
+ final Iterable<String> frames = filter('${FlutterError.demangleStackTrace(stack)}'.trimRight().split('\n'));
+ return frames.map<DiagnosticsNode>(_createStackFrame).toList();
+ }
+
static DiagnosticsNode _createStackFrame(String frame) {
return DiagnosticsNode.message(frame, allowWrap: false);
}
diff --git a/packages/flutter/lib/src/foundation/stack_frame.dart b/packages/flutter/lib/src/foundation/stack_frame.dart
index ee039e0..0521c5b 100644
--- a/packages/flutter/lib/src/foundation/stack_frame.dart
+++ b/packages/flutter/lib/src/foundation/stack_frame.dart
@@ -190,6 +190,13 @@
return stackOverFlowElision;
}
+ assert(
+ line != '===== asynchronous gap ===========================',
+ 'Got a stack frame from package:stack_trace, where a vm or web frame was expected. '
+ 'This can happen if FlutterError.demangleStackTrace was not set in an environment '
+ 'that propagates non-standard stack traces to the framework, such as during tests.'
+ );
+
// Web frames.
if (!line.startsWith('#')) {
return _parseWebFrame(line);
diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart
index 9477eb6..cbcbbdc 100644
--- a/packages/flutter/lib/src/scheduler/binding.dart
+++ b/packages/flutter/lib/src/scheduler/binding.dart
@@ -621,7 +621,9 @@
debugPrint('When the current transient callback was registered, this was the stack:');
debugPrint(
FlutterError.defaultStackFilter(
- _FrameCallbackEntry.debugCurrentCallbackStack.toString().trimRight().split('\n')
+ FlutterError.demangleStackTrace(
+ _FrameCallbackEntry.debugCurrentCallbackStack,
+ ).toString().trimRight().split('\n')
).join('\n')
);
} else {
diff --git a/packages/flutter/test/foundation/stack_frame_test.dart b/packages/flutter/test/foundation/stack_frame_test.dart
index 4f53495..6cfb62f 100644
--- a/packages/flutter/test/foundation/stack_frame_test.dart
+++ b/packages/flutter/test/foundation/stack_frame_test.dart
@@ -73,6 +73,16 @@
}, skip: isBrowser); // The VM test harness can handle a stack overflow, but
// the browser cannot - running this test in a browser will cause it to become
// unresponsive.
+
+ test('Traces from package:stack_trace throw assertion', () {
+ try {
+ StackFrame.fromStackString(mangledStackString);
+ assert(false, 'StackFrame.fromStackString did not throw on a mangled stack trace');
+ } catch (e) {
+ expect(e, isA<AssertionError>());
+ expect('$e', contains('Got a stack frame from package:stack_trace'));
+ }
+ });
}
const String stackString = '''
@@ -152,6 +162,21 @@
#37 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:307:19)
#38 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)''';
+const String mangledStackString = '''
+dart:async/future_impl.dart 23:44 _Completer.completeError
+test\\bindings_async_gap_test.dart 42:17 main.<fn>.<fn>
+package:flutter_test/src/binding.dart 744:19 TestWidgetsFlutterBinding._runTestBody
+===== asynchronous gap ===========================
+dart:async/zone.dart 1121:19 _CustomZone.registerUnaryCallback
+dart:async-patch/async_patch.dart 83:23 _asyncThenWrapperHelper
+dart:async/zone.dart 1222:13 _rootRunBinary
+dart:async/zone.dart 1107:19 _CustomZone.runBinary
+package:flutter_test/src/binding.dart 724:14 TestWidgetsFlutterBinding._runTest
+package:flutter_test/src/binding.dart 1124:24 AutomatedTestWidgetsFlutterBinding.runTest.<fn>
+package:fake_async/fake_async.dart 177:54 FakeAsync.run.<fn>.<fn>
+dart:async/zone.dart 1190:13 _rootRun
+''';
+
const List<StackFrame> asyncStackFrames = <StackFrame>[
StackFrame(number: 0, className: '', method: 'getSampleStack', packageScheme: 'file', package: '<unknown>', packagePath: '/path/to/flutter/packages/flutter/test/foundation/error_reporting_test.dart', line: 40, column: 57, source: '#0 getSampleStack.<anonymous closure> (file:///path/to/flutter/packages/flutter/test/foundation/error_reporting_test.dart:40:57)'),
StackFrame(number: 1, className: 'Future', method: 'sync', packageScheme: 'dart', package: 'async', packagePath: 'future.dart', line: 224, column: 31, isConstructor: true, source: '#1 new Future.sync (dart:async/future.dart:224:31)'),
diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart
index f26b7b6..d39123a 100644
--- a/packages/flutter_test/lib/src/binding.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -516,6 +516,7 @@
return result;
}
FlutterExceptionHandler _oldExceptionHandler;
+ StackTraceDemangler _oldStackTraceDemangler;
FlutterErrorDetails _pendingExceptionDetails;
static const TextStyle _messageStyle = TextStyle(
@@ -579,10 +580,6 @@
// our main future completing.
assert(Zone.current == _parentZone);
if (_pendingExceptionDetails != null) {
- assert(
- _unmangle(_pendingExceptionDetails.stack) == _pendingExceptionDetails.stack,
- 'The test binding presented an unmangled stack trace to the framework.',
- );
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the error!
reportTestException(_pendingExceptionDetails, testDescription);
_pendingExceptionDetails = null;
@@ -613,9 +610,9 @@
assert(description != null);
assert(inTest);
_oldExceptionHandler = FlutterError.onError;
+ _oldStackTraceDemangler = FlutterError.demangleStackTrace;
int _exceptionCount = 0; // number of un-taken exceptions
FlutterError.onError = (FlutterErrorDetails details) {
- details = details.copyWith(stack: _unmangle(details.stack));
if (_pendingExceptionDetails != null) {
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the errors!
if (_exceptionCount == 0) {
@@ -634,6 +631,17 @@
_pendingExceptionDetails = details;
}
};
+ FlutterError.demangleStackTrace = (StackTrace stack) {
+ // package:stack_trace uses ZoneSpecification.errorCallback to add useful
+ // information to stack traces, in this case the Trace and Chain classes
+ // can be present. Because these StackTrace implementations do not follow
+ // the format the framework expects, we covert them to a vm trace here.
+ if (stack is stack_trace.Trace)
+ return stack.vmTrace;
+ if (stack is stack_trace.Chain)
+ return stack.toTrace().vmTrace;
+ return stack;
+ };
final Completer<void> testCompleter = Completer<void>();
final VoidCallback testCompletionHandler = _createTestCompletionHandler(description, testCompleter);
void handleUncaughtError(dynamic exception, StackTrace stack) {
@@ -647,7 +655,7 @@
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the error!
FlutterError.dumpErrorToConsole(FlutterErrorDetails(
exception: exception,
- stack: _unmangle(stack),
+ stack: stack,
context: ErrorDescription('running a test (but after the test had completed)'),
library: 'Flutter test framework',
), forceReport: true);
@@ -694,7 +702,7 @@
final int stackLinesToOmit = reportExpectCall(stack, omittedFrames);
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
- stack: _unmangle(stack),
+ stack: stack,
context: ErrorDescription('running a test'),
library: 'Flutter test framework',
stackFilter: (Iterable<String> frames) {
@@ -842,6 +850,7 @@
void postTest() {
assert(inTest);
FlutterError.onError = _oldExceptionHandler;
+ FlutterError.demangleStackTrace = _oldStackTraceDemangler;
_pendingExceptionDetails = null;
_parentZone = null;
buildOwner.focusManager = FocusManager();
@@ -1713,11 +1722,3 @@
_label?.paint(context.canvas, offset - const Offset(0.0, 10.0));
}
}
-
-StackTrace _unmangle(StackTrace stack) {
- if (stack is stack_trace.Trace)
- return stack.vmTrace;
- if (stack is stack_trace.Chain)
- return stack.toTrace().vmTrace;
- return stack;
-}
diff --git a/packages/flutter_test/test/bindings_async_gap_test.dart b/packages/flutter_test/test/bindings_async_gap_test.dart
index 27b7cb1..aced2bf 100644
--- a/packages/flutter_test/test/bindings_async_gap_test.dart
+++ b/packages/flutter_test/test/bindings_async_gap_test.dart
@@ -49,4 +49,4 @@
class CustomException implements Exception {
const CustomException();
-}
+}
\ No newline at end of file
diff --git a/packages/flutter_test/test/demangle_test.dart b/packages/flutter_test/test/demangle_test.dart
new file mode 100644
index 0000000..85a9d0c
--- /dev/null
+++ b/packages/flutter_test/test/demangle_test.dart
@@ -0,0 +1,73 @@
+// Copyright 2014 The Flutter Authors. 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:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stack_trace/stack_trace.dart' as stack_trace;
+
+Future<void> main() async {
+ // We use AutomatedTestWidgetsFlutterBinding to allow the test binding to set
+ // FlutterError.demangleStackTrace and FlutterError.onError without testWidgets.
+ final AutomatedTestWidgetsFlutterBinding binding = AutomatedTestWidgetsFlutterBinding();
+
+ test('FlutterErrorDetails demangles', () async {
+ await binding.runTest(() async {
+ // When we call toString on a FlutterErrorDetails, it attempts to parse and
+ // filter the stack trace, which fails if demangleStackTrace returns a
+ // mangled stack trace.
+ FlutterErrorDetails(
+ exception: const CustomException(),
+ stack: await getMangledStack(),
+ ).toString();
+
+ // Additional logic is used to parse assertion stack traces.
+ FlutterErrorDetails(
+ exception: AssertionError('Some assertion'),
+ stack: await getMangledStack(),
+ ).toString();
+ }, () {});
+ binding.postTest();
+ });
+
+ test('debugPrintStack demangles', () async {
+ await binding.runTest(() async {
+ final DebugPrintCallback oldDebugPrint = debugPrint;
+ try {
+ debugPrint = (String message, {int wrapWidth}) {};
+ debugPrintStack(
+ stackTrace: await getMangledStack(),
+ );
+ } finally {
+ debugPrint = oldDebugPrint;
+ }
+ }, () {});
+ binding.postTest();
+ });
+}
+
+Future<StackTrace> getMangledStack() {
+ // package:test uses package:stack_trace to wrap tests in a Zone that overrides
+ // errorCallback, the error callback transforms any StackTrace propagated
+ // to futures into a Chain, which has a format different from the vm.
+ final Completer<StackTrace> stackCompleter = Completer<StackTrace>();
+ final Completer<void> completer = Completer<void>();
+ completer.future.then(
+ (void value) {
+ assert(false);
+ },
+ onError: (Object error, StackTrace stack) {
+ expect(error, isA<CustomException>());
+ expect(stack, isA<stack_trace.Chain>());
+ stackCompleter.complete(stack);
+ },
+ );
+ completer.completeError(const CustomException());
+ return stackCompleter.future;
+}
+
+class CustomException implements Exception {
+ const CustomException();
+}