From 2a8dba4af6d2b9e67367354744f74d4f061aa76b Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 15 Apr 2021 10:49:02 -0700 Subject: [PATCH] Treat some exceptions as unhandled when a debugger is attached (#78649) --- .../lib/src/animation/listener_helpers.dart | 2 + .../lib/src/foundation/assertions.dart | 30 +- .../lib/src/foundation/change_notifier.dart | 1 + .../flutter/lib/src/gestures/binding.dart | 1 + .../lib/src/gestures/pointer_router.dart | 1 + .../src/gestures/pointer_signal_resolver.dart | 2 +- .../flutter/lib/src/gestures/recognizer.dart | 1 + .../lib/src/painting/image_stream.dart | 2 + .../flutter/lib/src/rendering/object.dart | 2 + .../flutter/lib/src/scheduler/binding.dart | 6 + .../flutter/lib/src/services/binding.dart | 4 + packages/flutter/lib/src/widgets/actions.dart | 1 + packages/flutter/lib/src/widgets/binding.dart | 1 + .../lib/src/widgets/editable_text.dart | 3 + .../lib/src/widgets/focus_manager.dart | 1 + .../flutter/lib/src/widgets/framework.dart | 6 + .../lib/src/widgets/layout_builder.dart | 4 + packages/flutter/lib/src/widgets/router.dart | 3 +- packages/flutter/lib/src/widgets/sliver.dart | 1 + .../break_on_framework_exceptions_test.dart | 766 ++++++++++++++++++ 20 files changed, 833 insertions(+), 5 deletions(-) create mode 100644 packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart diff --git a/packages/flutter/lib/src/animation/listener_helpers.dart b/packages/flutter/lib/src/animation/listener_helpers.dart index 05b1ed17eec..85b488a95a8 100644 --- a/packages/flutter/lib/src/animation/listener_helpers.dart +++ b/packages/flutter/lib/src/animation/listener_helpers.dart @@ -135,6 +135,7 @@ mixin AnimationLocalListenersMixin { /// If listeners are added or removed during this function, the modifications /// will not change which listeners are called during this iteration. @protected + @pragma('vm:notify-debugger-on-exception') void notifyListeners() { final List localListeners = List.from(_listeners); for (final VoidCallback listener in localListeners) { @@ -223,6 +224,7 @@ mixin AnimationLocalStatusListenersMixin { /// If listeners are added or removed during this function, the modifications /// will not change which listeners are called during this iteration. @protected + @pragma('vm:notify-debugger-on-exception') void notifyStatusListeners(AnimationStatus status) { final List localListeners = List.from(_statusListeners); for (final AnimationStatusListener listener in localListeners) { diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart index 9dad6d3cd39..44af2b78b9f 100644 --- a/packages/flutter/lib/src/foundation/assertions.dart +++ b/packages/flutter/lib/src/foundation/assertions.dart @@ -15,6 +15,7 @@ import 'stack_frame.dart'; // late bool draconisAlive; // late bool draconisAmulet; // late Diagnosticable draconis; +// void methodThatMayThrow() { } /// Signature for [FlutterError.onError] handler. typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details); @@ -876,9 +877,7 @@ class FlutterError extends Error with DiagnosticableTreeMixin implements Asserti /// /// Do not call [onError] directly, instead, call [reportError], which /// forwards to [onError] if it is not null. - static FlutterExceptionHandler? onError = _defaultErrorHandler; - - static void _defaultErrorHandler(FlutterErrorDetails details) => presentError(details); + static FlutterExceptionHandler? onError = presentError; /// Called by the Flutter framework before attempting to parse a [StackTrace]. /// @@ -1101,6 +1100,31 @@ class FlutterError extends Error with DiagnosticableTreeMixin implements Asserti } /// Calls [onError] with the given details, unless it is null. + /// + /// {@tool snippet} + /// When calling this from a `catch` block consider annotating the method + /// containing the `catch` block with + /// `@pragma('vm:notify-debugger-on-exception')` to allow an attached debugger + /// to treat the exception as unhandled. This means instead of executing the + /// `catch` block, the debugger can break at the original source location from + /// which the exception was thrown. + /// + /// ```dart + /// @pragma('vm:notify-debugger-on-exception') + /// void doSomething() { + /// try { + /// methodThatMayThrow(); + /// } catch (exception, stack) { + /// FlutterError.reportError(FlutterErrorDetails( + /// exception: exception, + /// stack: stack, + /// library: 'example library', + /// context: ErrorDescription('while doing something'), + /// )); + /// } + /// } + /// ``` + /// {@end-tool} static void reportError(FlutterErrorDetails details) { assert(details != null); assert(details.exception != null); diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart index 4dd56c4f5b5..a895823e4fc 100644 --- a/packages/flutter/lib/src/foundation/change_notifier.dart +++ b/packages/flutter/lib/src/foundation/change_notifier.dart @@ -283,6 +283,7 @@ class ChangeNotifier implements Listenable { /// See the discussion at [removeListener]. @protected @visibleForTesting + @pragma('vm:notify-debugger-on-exception') void notifyListeners() { assert(_debugAssertNotDisposed()); if (_count == 0) diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 126cc15ff9c..1b161740045 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -390,6 +390,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H /// The `hitTestResult` argument may only be null for [PointerAddedEvent]s or /// [PointerRemovedEvent]s. @override // from HitTestDispatcher + @pragma('vm:notify-debugger-on-exception') void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(!locked); // No hit test information implies that this is a [PointerHoverEvent], diff --git a/packages/flutter/lib/src/gestures/pointer_router.dart b/packages/flutter/lib/src/gestures/pointer_router.dart index bab9b2d988c..0b8c17b7235 100644 --- a/packages/flutter/lib/src/gestures/pointer_router.dart +++ b/packages/flutter/lib/src/gestures/pointer_router.dart @@ -87,6 +87,7 @@ class PointerRouter { throw UnsupportedError('debugGlobalRouteCount is not supported in release builds'); } + @pragma('vm:notify-debugger-on-exception') void _dispatch(PointerEvent event, PointerRoute route, Matrix4? transform) { try { event = event.transformed(transform); diff --git a/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart b/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart index 7b7eadc9d2b..c3a29b8338a 100644 --- a/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart +++ b/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:flutter/foundation.dart'; import 'events.dart'; @@ -189,6 +188,7 @@ class PointerSignalResolver { /// /// This is called by the [GestureBinding] after the framework has finished /// dispatching the pointer signal event. + @pragma('vm:notify-debugger-on-exception') void resolve(PointerSignalEvent event) { if (_firstRegisteredCallback == null) { assert(_currentEvent == null); diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 92f4ab26d1a..4222a93204c 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -165,6 +165,7 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT /// callback that returns a string describing useful debugging information, /// e.g. the arguments passed to the callback. @protected + @pragma('vm:notify-debugger-on-exception') T? invokeCallback(String name, RecognizerCallback callback, { String Function()? debugReport }) { assert(callback != null); T? result; diff --git a/packages/flutter/lib/src/painting/image_stream.dart b/packages/flutter/lib/src/painting/image_stream.dart index ea714cc62cc..5d2ebf8cacd 100644 --- a/packages/flutter/lib/src/painting/image_stream.dart +++ b/packages/flutter/lib/src/painting/image_stream.dart @@ -614,6 +614,7 @@ abstract class ImageStreamCompleter with Diagnosticable { /// Calls all the registered listeners to notify them of a new image. @protected + @pragma('vm:notify-debugger-on-exception') void setImage(ImageInfo image) { _checkDisposed(); _currentImage?.dispose(); @@ -668,6 +669,7 @@ abstract class ImageStreamCompleter with Diagnosticable { /// /// See [FlutterErrorDetails] for further details on these values. @protected + @pragma('vm:notify-debugger-on-exception') void reportError({ DiagnosticsNode? context, required Object exception, diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 34f40fe8439..92eae374199 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1617,6 +1617,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im owner!._nodesNeedingLayout.add(this); } + @pragma('vm:notify-debugger-on-exception') void _layoutWithoutResize() { assert(_relayoutBoundary == this); RenderObject? debugPreviousActiveLayout; @@ -1671,6 +1672,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im /// children unconditionally. It is the [layout] method's responsibility (as /// implemented here) to return early if the child does not need to do any /// work to update its layout information. + @pragma('vm:notify-debugger-on-exception') void layout(Constraints constraints, { bool parentUsesSize = false }) { if (!kReleaseMode && debugProfileLayoutsEnabled) Timeline.startSync('$runtimeType', arguments: timelineArgumentsIndicatingLandmarkEvent); diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index 5ce5ba3e36d..ca99fc22eeb 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -280,6 +280,7 @@ mixin SchedulerBinding on BindingBase { } } + @pragma('vm:notify-debugger-on-exception') void _executeTimingsCallbacks(List timings) { final List clonedCallbacks = List.from(_timingsCallbacks); @@ -450,6 +451,10 @@ mixin SchedulerBinding on BindingBase { /// /// Also returns false if there are no tasks remaining. @visibleForTesting + // TODO(goderbauer): Add pragma (and enable test in + // break_on_framework_exceptions_test.dart) once debugger breaks on correct + // line, https://github.com/dart-lang/sdk/issues/45684 + // @pragma('vm:notify-debugger-on-exception') bool handleEventLoopCallback() { if (_taskQueue.isEmpty || locked) return false; @@ -1133,6 +1138,7 @@ mixin SchedulerBinding on BindingBase { // Wraps the callback in a try/catch and forwards any error to // [debugSchedulerExceptionHandler], if set. If not set, then simply prints // the error. + @pragma('vm:notify-debugger-on-exception') void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace? callbackStack ]) { assert(callback != null); assert(_FrameCallbackEntry.debugCurrentCallbackStack == null); diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 8992ba60e21..65aab975c3d 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -272,6 +272,10 @@ class _DefaultBinaryMessenger extends BinaryMessenger { } @override + // TODO(goderbauer): Add pragma (and enable test in + // break_on_framework_exceptions_test.dart) when it works on async methods, + // https://github.com/dart-lang/sdk/issues/45673 + // @pragma('vm:notify-debugger-on-exception') Future handlePlatformMessage( String channel, ByteData? data, diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart index d9d01b8782b..b1dc101b440 100644 --- a/packages/flutter/lib/src/widgets/actions.dart +++ b/packages/flutter/lib/src/widgets/actions.dart @@ -198,6 +198,7 @@ abstract class Action with Diagnosticable { /// See the discussion at [removeActionListener]. @protected @visibleForTesting + @pragma('vm:notify-debugger-on-exception') void notifyActionListeners() { if (_listeners.isEmpty) { return; diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 03bd0eaa95d..62af500ed1f 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -1187,6 +1187,7 @@ class RenderObjectToWidgetElement extends RootRenderObje assert(_newWidget == null); } + @pragma('vm:notify-debugger-on-exception') void _rebuild() { try { _child = updateChild(_child, widget.child, _rootChildSlot); diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 26cc41d629d..498550e2cf5 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1873,6 +1873,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } + @pragma('vm:notify-debugger-on-exception') void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) { // Take any actions necessary now that the user has completed editing. if (widget.onEditingComplete != null) { @@ -2126,6 +2127,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } + @pragma('vm:notify-debugger-on-exception') void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { // We return early if the selection is not valid. This can happen when the // text of [EditableText] is updated at the same time as the selection is @@ -2259,6 +2261,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien _lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom; } + @pragma('vm:notify-debugger-on-exception') void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) { // Only apply input formatters if the text has changed (including uncommited // text in the composing region), or when the user committed the composing diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index db196044d99..d789266b20a 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -1596,6 +1596,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { /// [FocusManager] notifies. void removeHighlightModeListener(ValueChanged listener) => _listeners.remove(listener); + @pragma('vm:notify-debugger-on-exception') void _notifyHighlightModeListeners() { if (_listeners.isEmpty) { return; diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index c3db9cc8c0b..3bde9eedfa5 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2505,6 +2505,7 @@ class BuildOwner { /// [debugPrintBuildScope] to true. This is useful when debugging problems /// involving widgets not getting marked dirty, or getting marked dirty too /// often. + @pragma('vm:notify-debugger-on-exception') void buildScope(Element context, [ VoidCallback? callback ]) { if (callback == null && _dirtyElements.isEmpty) return; @@ -2833,6 +2834,10 @@ class BuildOwner { /// /// After the current call stack unwinds, a microtask that notifies listeners /// about changes to global keys will run. + // TODO(goderbauer): Add pragma (and enable test in + // break_on_framework_exceptions_test.dart) once debugger breaks on correct + // line, https://github.com/dart-lang/sdk/issues/45684 + // @pragma('vm:notify-debugger-on-exception') void finalizeTree() { Timeline.startSync('Finalize tree', arguments: timelineArgumentsIndicatingLandmarkEvent); try { @@ -4579,6 +4584,7 @@ abstract class ComponentElement extends Element { /// Called automatically during [mount] to generate the first build, and by /// [rebuild] when the element needs updating. @override + @pragma('vm:notify-debugger-on-exception') void performRebuild() { if (!kReleaseMode && debugProfileBuildsEnabled) Timeline.startSync('${widget.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent); diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index 22a59f30f0d..17e2206bfbe 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -115,6 +115,10 @@ class _LayoutBuilderElement extends RenderOb } void _layout(ConstraintType constraints) { + // TODO(goderbauer): When https://github.com/dart-lang/sdk/issues/45710 is + // fixed: refactor the anonymous closure below into a named one, apply the + // @pragma('vm:notify-debugger-on-exception') to it and enable the + // corresponding test in break_on_framework_exceptions_test.dart. owner!.buildScope(this, () { Widget built; try { diff --git a/packages/flutter/lib/src/widgets/router.dart b/packages/flutter/lib/src/widgets/router.dart index e0aff335184..6db7f7d7977 100644 --- a/packages/flutter/lib/src/widgets/router.dart +++ b/packages/flutter/lib/src/widgets/router.dart @@ -760,6 +760,7 @@ class _CallbackHookProvider { /// Exceptions thrown by callbacks will be caught and reported using /// [FlutterError.reportError]. @protected + @pragma('vm:notify-debugger-on-exception') T invokeCallback(T defaultValue) { if (_callbacks.isEmpty) return defaultValue; @@ -773,7 +774,7 @@ class _CallbackHookProvider { context: ErrorDescription('while invoking the callback for $runtimeType'), informationCollector: () sync* { yield DiagnosticsProperty<_CallbackHookProvider>( - 'The $runtimeType that invoked the callback was:', + 'The $runtimeType that invoked the callback was', this, style: DiagnosticsTreeStyle.errorProperty, ); diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index a8bcb4e3755..97dc426d0a1 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -446,6 +446,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { } @override + @pragma('vm:notify-debugger-on-exception') Widget? build(BuildContext context, int index) { assert(builder != null); if (index < 0 || (childCount != null && index >= childCount!)) diff --git a/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart b/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart new file mode 100644 index 00000000000..6805993f8d9 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart @@ -0,0 +1,766 @@ +// 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. + +// @dart = 2.8 + +import 'package:file/file.dart'; + +import '../src/common.dart'; +import 'test_data/project.dart'; +import 'test_driver.dart'; +import 'test_utils.dart'; + +void main() { + Directory tempDir; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('break_on_framework_exceptions.'); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + testWithoutContext('breaks when AnimationController listener throws', () async { + final TestProject project = TestProject( + r''' + AnimationController(vsync: TestVSync(), duration: Duration.zero) + ..addListener(() { + throw 'AnimationController listener'; + }) + ..forward(); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'AnimationController listener';")); + }); + + testWithoutContext('breaks when AnimationController status listener throws', () async { + final TestProject project = TestProject( + r''' + AnimationController(vsync: TestVSync(), duration: Duration.zero) + ..addStatusListener((AnimationStatus _) { + throw 'AnimationController status listener'; + }) + ..forward(); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'AnimationController status listener';")); + }); + + testWithoutContext('breaks when ChangeNotifier listener throws', () async { + final TestProject project = TestProject( + r''' + ValueNotifier(0) + ..addListener(() { + throw 'ValueNotifier listener'; + }) + ..value = 1; + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'ValueNotifier listener';")); + }); + + testWithoutContext('breaks when handling a gesture throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget( + MaterialApp( + home: Center( + child: ElevatedButton( + child: const Text('foo'), + onPressed: () { + throw 'while handling a gesture'; + }, + ), + ), + ) + ); + await tester.tap(find.byType(ElevatedButton)); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'while handling a gesture';")); + }); + + testWithoutContext('breaks when platform message callback throws', () async { + final TestProject project = TestProject( + r''' + BasicMessageChannel('foo', const StringCodec()).setMessageHandler((_) { + throw 'platform message callback'; + }); + tester.binding.defaultBinaryMessenger.handlePlatformMessage('foo', const StringCodec().encodeMessage('Hello'), (_) {}); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'platform message callback';")); + }, skip: 'TODO(goderbauer): add pragma to _DefaultBinaryMessenger.handlePlatformMessage when async methods are supported (https://github.com/dart-lang/sdk/issues/45673) and enable this test'); + + testWithoutContext('breaks when SliverChildBuilderDelegate.builder throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(MaterialApp( + home: ListView.builder( + itemBuilder: (BuildContext context, int index) { + throw 'cannot build child'; + }, + ), + )); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'cannot build child';")); + }); + + testWithoutContext('breaks when EditableText.onChanged throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(MaterialApp( + home: Material( + child: TextField( + onChanged: (String t) { + throw 'onChanged'; + }, + ), + ), + )); + await tester.enterText(find.byType(TextField), 'foo'); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'onChanged';")); + }); + + testWithoutContext('breaks when EditableText.onEditingComplete throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(MaterialApp( + home: Material( + child: TextField( + onEditingComplete: () { + throw 'onEditingComplete'; + }, + ), + ), + )); + await tester.tap(find.byType(EditableText)); + await tester.pump(); + await tester.testTextInput.receiveAction(TextInputAction.done); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'onEditingComplete';")); + }); + + testWithoutContext('breaks when EditableText.onSelectionChanged throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(MaterialApp( + home: SelectableText('hello', + onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) { + throw 'onSelectionChanged'; + }, + ), + )); + await tester.tap(find.byType(SelectableText)); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'onSelectionChanged';")); + }); + + testWithoutContext('breaks when Action listener throws', () async { + final TestProject project = TestProject( + r''' + CallbackAction(onInvoke: (Intent _) { }) + ..addActionListener((_) { + throw 'action listener'; + }) + ..notifyActionListeners(); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'action listener';")); + }); + + testWithoutContext('breaks when pointer route throws', () async { + final TestProject project = TestProject( + r''' + PointerRouter() + ..addRoute(2, (PointerEvent event) { + throw 'pointer route'; + }) + ..route(TestPointer(2).down(Offset.zero)); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'pointer route';")); + }); + + testWithoutContext('breaks when PointerSignalResolver callback throws', () async { + final TestProject project = TestProject( + r''' + const PointerScrollEvent originalEvent = PointerScrollEvent(); + PointerSignalResolver() + ..register(originalEvent, (PointerSignalEvent event) { + throw 'PointerSignalResolver callback'; + }) + ..resolve(originalEvent); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'PointerSignalResolver callback';")); + }); + + testWithoutContext('breaks when PointerSignalResolver callback throws', () async { + final TestProject project = TestProject( + r''' + FocusManager.instance + ..addHighlightModeListener((_) { + throw 'highlight mode listener'; + }) + ..highlightStrategy = FocusHighlightStrategy.alwaysTouch + ..highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'highlight mode listener';")); + }); + + testWithoutContext('breaks when GestureBinding.dispatchEvent throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget( + MouseRegion( + onHover: (_) { + throw 'onHover'; + }, + ) + ); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(MouseRegion))); + await tester.pump(); + gesture.removePointer(); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'onHover';")); + }); + + testWithoutContext('breaks when ImageStreamListener.onImage throws', () async { + final TestProject project = TestProject( + r''' + final Completer completer = Completer(); + OneFrameImageStreamCompleter(completer.future) + ..addListener(ImageStreamListener((ImageInfo _, bool __) { + throw 'setImage'; + })); + completer.complete(ImageInfo(image: image)); + ''', + setup: r''' + late ui.Image image; + setUp(() async { + image = await createTestImage(); + }); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'setImage';")); + }); + + testWithoutContext('breaks when ImageStreamListener.onError throws', () async { + final TestProject project = TestProject( + r''' + final Completer completer = Completer(); + OneFrameImageStreamCompleter(completer.future) + ..addListener(ImageStreamListener( + (ImageInfo _, bool __) { }, + onError: (Object _, StackTrace? __) { + throw 'onError'; + }, + )); + completer.completeError('ERROR'); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'onError';")); + }); + + testWithoutContext('breaks when LayoutBuilder.builder throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(LayoutBuilder( + builder: (_, __) { + throw 'LayoutBuilder.builder'; + }, + )); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'LayoutBuilder.builder';")); + }, skip: 'TODO(goderbauer): Once https://github.com/dart-lang/sdk/issues/45710 is fixed, fix TODO in _LayoutBuilderElement._layout and enable this test'); + + testWithoutContext('breaks when _CallbackHookProvider callback throws', () async { + final TestProject project = TestProject( + r''' + RootBackButtonDispatcher() + ..addCallback(() { + throw '_CallbackHookProvider.callback'; + }) + ..invokeCallback(Future.value(false)); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw '_CallbackHookProvider.callback';")); + }); + + testWithoutContext('breaks when TimingsCallback throws', () async { + final TestProject project = TestProject( + r''' + SchedulerBinding.instance!.addTimingsCallback((List timings) { + throw 'TimingsCallback'; + }); + ui.window.onReportTimings!([]); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'TimingsCallback';")); + }); + + testWithoutContext('breaks when TimingsCallback throws', () async { + final TestProject project = TestProject( + r''' + SchedulerBinding.instance!.scheduleTask( + () { + throw 'scheduled task'; + }, + Priority.touch, + ); + await tester.pumpAndSettle(); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'scheduled task';")); + }, skip: 'TODO(goderbauer): add pragma to SchedulerBinding.handleEventLoopCallback when https://github.com/dart-lang/sdk/issues/45684 is fixed and enable this test'); + + testWithoutContext('breaks when FrameCallback throws', () async { + final TestProject project = TestProject( + r''' + SchedulerBinding.instance!.addPostFrameCallback((_) { + throw 'FrameCallback'; + }); + await tester.pump(); + ''' + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'FrameCallback';")); + }); + + testWithoutContext('breaks when attaching to render tree throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(TestWidget()); + ''', + classes: r''' + class TestWidget extends StatelessWidget { + @override + StatelessElement createElement() { + throw 'create element'; + } + + @override + Widget build(BuildContext context) => Container(); + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'create element';")); + }); + + testWithoutContext('breaks when RenderObject.performLayout throws', () async { + final TestProject project = TestProject( + r''' + TestRender().layout(BoxConstraints()); + ''', + classes: r''' + class TestRender extends RenderBox { + @override + void performLayout() { + throw 'performLayout'; + } + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'performLayout';")); + }); + + testWithoutContext('breaks when RenderObject.performResize throws', () async { + final TestProject project = TestProject( + r''' + TestRender().layout(BoxConstraints()); + ''', + classes: r''' + class TestRender extends RenderBox { + @override + bool get sizedByParent => true; + + @override + void performResize() { + throw 'performResize'; + } + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'performResize';")); + }); + + testWithoutContext('breaks when RenderObject.performLayout (without resize) throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(TestWidget()); + tester.renderObject(find.byType(TestWidget)).layoutThrows = true; + await tester.pump(); + ''', + classes: r''' + class TestWidget extends LeafRenderObjectWidget { + @override + RenderObject createRenderObject(BuildContext context) => TestRender(); + } + + class TestRender extends RenderBox { + bool get layoutThrows => _layoutThrows; + bool _layoutThrows = false; + set layoutThrows(bool value) { + if (value == _layoutThrows) { + return; + } + _layoutThrows = value; + markNeedsLayout(); + } + + @override + void performLayout() { + if (layoutThrows) { + throw 'performLayout without resize'; + } + size = constraints.biggest; + } + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'performLayout without resize';")); + }); + + testWithoutContext('breaks when StatelessWidget.build throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(TestWidget()); + ''', + classes: r''' + class TestWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + throw 'StatelessWidget.build'; + } + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'StatelessWidget.build';")); + }); + + testWithoutContext('breaks when StatefulWidget.build throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(TestWidget()); + ''', + classes: r''' + class TestWidget extends StatefulWidget { + @override + _TestWidgetState createState() => _TestWidgetState(); + } + + class _TestWidgetState extends State { + @override + Widget build(BuildContext context) { + throw 'StatefulWidget.build'; + } + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'StatefulWidget.build';")); + }); + + testWithoutContext('breaks when finalizing the tree throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(TestWidget()); + ''', + classes: r''' + class TestWidget extends StatefulWidget { + @override + _TestWidgetState createState() => _TestWidgetState(); + } + + class _TestWidgetState extends State { + @override + void dispose() { + super.dispose(); + throw 'dispose'; + } + + @override + Widget build(BuildContext context) => Container(); + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'dispose';")); + }, skip: 'TODO(goderbauer): add pragma to BuildOwner.finalizeTree when https://github.com/dart-lang/sdk/issues/45684 is fixed and enable this test'); + + testWithoutContext('breaks when rebuilding dirty elements throws', () async { + final TestProject project = TestProject( + r''' + await tester.pumpWidget(TestWidget()); + tester.element(find.byType(TestWidget)).throwOnRebuild = true; + await tester.pump(); + ''', + classes: r''' + class TestWidget extends StatelessWidget { + @override + StatelessElement createElement() => TestElement(this); + + @override + Widget build(BuildContext context) => Container(); + } + + class TestElement extends StatelessElement { + TestElement(StatelessWidget widget) : super(widget); + + bool get throwOnRebuild => _throwOnRebuild; + bool _throwOnRebuild = false; + set throwOnRebuild(bool value) { + if (value == _throwOnRebuild) { + return; + } + _throwOnRebuild = value; + markNeedsBuild(); + } + + @override + void rebuild() { + if (_throwOnRebuild) { + throw 'rebuild'; + } + super.rebuild(); + } + } + ''', + ); + await project.setUpIn(tempDir); + final FlutterTestTestDriver flutter = FlutterTestTestDriver(tempDir); + await flutter.test(withDebugger: true, pauseOnExceptions: true); + await flutter.waitForPause(); + + final int breakLine = (await flutter.getSourceLocation()).line; + expect(breakLine, project.lineContaining(project.test, "throw 'rebuild';")); + }); +} + +class TestProject extends Project { + TestProject(this.testBody, { this.setup, this.classes }); + + final String testBody; + final String setup; + final String classes; + + @override + final String pubspec = ''' + name: test + environment: + sdk: ">=2.12.0-0 <3.0.0" + + dependencies: + flutter: + sdk: flutter + dev_dependencies: + flutter_test: + sdk: flutter + '''; + + @override + final String main = ''; + + @override + String get test => _test.replaceFirst('// SETUP', setup ?? '').replaceFirst('// TEST_BODY', testBody).replaceFirst('// CLASSES', classes ?? ''); + + final String _test = r''' + import 'dart:async'; + import 'dart:ui' as ui; + + import 'package:flutter/animation.dart'; + import 'package:flutter/foundation.dart'; + import 'package:flutter/gestures.dart'; + import 'package:flutter/material.dart'; + import 'package:flutter/scheduler.dart'; + import 'package:flutter/services.dart'; + import 'package:flutter_test/flutter_test.dart'; + + void main() { + // SETUP + testWidgets('test', (WidgetTester tester) async { + // TEST_BODY + }); + } + // CLASSES + '''; +}