Rationalise all our exception handling.
- Create a FlutterErrorDetails struct-like class that describes an
exception along with more details that aren't in the exception, like
where it was caught and what was going on when it was caught.
- Provide a FlutterError static API for handling these objects:
- FlutterError.onError which is called whenever Flutter catches an
error.
- FlutterError.reportError() which handles an error.
- FlutterError.dumpErrorToConsole() which is the default behavior
for onError.
- Removes all the existing exception handler callbacks.
- Replaces all the existing places that described exceptions using
debugPrint with calls to FlutterError.reportError().
- Extend lockState() to also catch exceptions, so that we catch
exceptions that happen during finalizers.
- Make the test framework catch errors and treat them as failures.
- Provide a mechanism to override this behavior in the test framework.
- Make the tests that used to depend on the exception handler
callbacks use this new mechanism.
- Make pump() also support the phase argument.
- Improve some tests using these new features.
Fixes #2356, #2988, #2985, #2220.diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart
index d71c401..b1d9ee56 100644
--- a/packages/flutter/lib/src/gestures/binding.dart
+++ b/packages/flutter/lib/src/gestures/binding.dart
@@ -16,8 +16,6 @@
import 'hit_test.dart';
import 'pointer_router.dart';
-typedef void GesturerExceptionHandler(PointerEvent event, HitTestTarget target, dynamic exception, StackTrace stack);
-
/// A binding for the gesture subsystem.
abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestable {
@@ -82,14 +80,6 @@
result.add(new HitTestEntry(this));
}
- /// This callback is invoked whenever an exception is caught by the Gesturer
- /// binding. The 'event' argument is the pointer event that was being routed.
- /// The 'target' argument is the class whose handleEvent function threw the
- /// exception. The 'exception' argument contains the object that was thrown,
- /// and the 'stack' argument contains the stack trace. If no handler is
- /// registered, then the information will be printed to the console instead.
- GesturerExceptionHandler debugGesturerExceptionHandler;
-
/// Dispatch the given event to the path of the given hit test result
void dispatchEvent(PointerEvent event, HitTestResult result) {
assert(result != null);
@@ -97,20 +87,20 @@
try {
entry.target.handleEvent(event, entry);
} catch (exception, stack) {
- if (debugGesturerExceptionHandler != null) {
- debugGesturerExceptionHandler(event, entry.target, exception, stack);
- } else {
- debugPrint('-- EXCEPTION CAUGHT BY GESTURE LIBRARY ---------------------------------');
- debugPrint('The following exception was raised while dispatching a pointer event:');
- debugPrint('$exception');
- debugPrint('Event:');
- debugPrint('$event');
- debugPrint('Target:');
- debugPrint('${entry.target}');
- debugPrint('Stack trace:');
- debugPrint('$stack');
- debugPrint('------------------------------------------------------------------------');
- }
+ FlutterError.reportError(new FlutterErrorDetailsForPointerEventDispatcher(
+ exception: exception,
+ stack: stack,
+ library: 'gesture library',
+ context: 'while dispatching a pointer event',
+ event: event,
+ hitTestEntry: entry,
+ informationCollector: (StringBuffer information) {
+ information.writeln('Event:');
+ information.writeln(' $event');
+ information.writeln('Target:');
+ information.write(' ${entry.target}');
+ }
+ ));
}
}
}
@@ -125,3 +115,44 @@
}
}
}
+
+/// Variant of [FlutterErrorDetails] with extra fields for the gesture
+/// library's binding's pointer event dispatcher ([Gesturer.dispatchEvent]).
+///
+/// See also [FlutterErrorDetailsForPointerRouter], which is also used by the
+/// gesture library.
+class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails {
+ /// Creates a [FlutterErrorDetailsForPointerEventDispatcher] object with the given
+ /// arguments setting the object's properties.
+ ///
+ /// The gesture library calls this constructor when catching an exception
+ /// that will subsequently be reported using [FlutterError.onError].
+ const FlutterErrorDetailsForPointerEventDispatcher({
+ dynamic exception,
+ StackTrace stack,
+ String library,
+ String context,
+ this.event,
+ this.hitTestEntry,
+ FlutterInformationCollector informationCollector,
+ bool silent
+ }) : super(
+ exception: exception,
+ stack: stack,
+ library: library,
+ context: context,
+ informationCollector: informationCollector,
+ silent: silent
+ );
+
+ /// The pointer event that was being routed when the exception was raised.
+ final PointerEvent event;
+
+ /// The hit test result entry for the object whose handleEvent method threw
+ /// the exception.
+ ///
+ /// The target object itself is given by the [HitTestEntry.target] property of
+ /// the hitTestEntry object.
+ final HitTestEntry hitTestEntry;
+}
+
diff --git a/packages/flutter/lib/src/gestures/pointer_router.dart b/packages/flutter/lib/src/gestures/pointer_router.dart
index 5985187..b791b55 100644
--- a/packages/flutter/lib/src/gestures/pointer_router.dart
+++ b/packages/flutter/lib/src/gestures/pointer_router.dart
@@ -11,8 +11,6 @@
/// A callback that receives a [PointerEvent]
typedef void PointerRoute(PointerEvent event);
-typedef void PointerExceptionHandler(PointerRouter source, PointerEvent event, PointerRoute route, dynamic exception, StackTrace stack);
-
/// A routing table for [PointerEvent] events.
class PointerRouter {
final Map<int, LinkedHashSet<PointerRoute>> _routeMap = new Map<int, LinkedHashSet<PointerRoute>>();
@@ -40,16 +38,6 @@
_routeMap.remove(pointer);
}
- /// This callback is invoked whenever an exception is caught by the pointer
- /// router. The 'source' argument is the [PointerRouter] object that caught
- /// the exception. The 'event' argument is the pointer event that was being
- /// routed. The 'route' argument is the callback that threw the exception. The
- /// 'exception' argument contains the object that was thrown, and the 'stack'
- /// argument contains the stack trace. If no handler is registered, then the
- /// human-readable parts of this information (the exception, event, and stack
- /// trace) will be printed to the console instead.
- PointerExceptionHandler debugPointerExceptionHandler;
-
/// Calls the routes registered for this pointer event.
///
/// Routes are called in the order in which they were added to the
@@ -64,19 +52,64 @@
try {
route(event);
} catch (exception, stack) {
- if (debugPointerExceptionHandler != null) {
- debugPointerExceptionHandler(this, event, route, exception, stack);
- } else {
- debugPrint('-- EXCEPTION CAUGHT BY GESTURE LIBRARY ---------------------------------');
- debugPrint('The following exception was raised while routing a pointer event:');
- debugPrint('$exception');
- debugPrint('Event:');
- debugPrint('$event');
- debugPrint('Stack trace:');
- debugPrint('$stack');
- debugPrint('------------------------------------------------------------------------');
- }
+ FlutterError.reportError(new FlutterErrorDetailsForPointerRouter(
+ exception: exception,
+ stack: stack,
+ library: 'gesture library',
+ context: 'while routing a pointer event',
+ router: this,
+ route: route,
+ event: event,
+ informationCollector: (StringBuffer information) {
+ information.writeln('Event:');
+ information.write(' $event');
+ }
+ ));
}
}
}
}
+
+/// Variant of [FlutterErrorDetails] with extra fields for the gestures
+/// library's pointer router ([PointerRouter]).
+///
+/// See also [FlutterErrorDetailsForPointerEventDispatcher], which is also used
+/// by the gestures library.
+class FlutterErrorDetailsForPointerRouter extends FlutterErrorDetails {
+ /// Creates a [FlutterErrorDetailsForPointerRouter] object with the given
+ /// arguments setting the object's properties.
+ ///
+ /// The gestures library calls this constructor when catching an exception
+ /// that will subsequently be reported using [FlutterError.onError].
+ const FlutterErrorDetailsForPointerRouter({
+ dynamic exception,
+ StackTrace stack,
+ String library,
+ String context,
+ this.router,
+ this.route,
+ this.event,
+ FlutterInformationCollector informationCollector,
+ bool silent
+ }) : super(
+ exception: exception,
+ stack: stack,
+ library: library,
+ context: context,
+ informationCollector: informationCollector,
+ silent: silent
+ );
+
+ /// The pointer router that caught the exception.
+ ///
+ /// In a typical application, this is the value of [Gesturer.pointerRouter] on
+ /// the binding ([Gesturer.instance]).
+ final PointerRouter router;
+
+ /// The callback that threw the exception.
+ final PointerRoute route;
+
+ /// The pointer event that was being routed when the exception was raised.
+ final PointerEvent event;
+}
+
diff --git a/packages/flutter/lib/src/http/mojo_client.dart b/packages/flutter/lib/src/http/mojo_client.dart
index af1d8f0..2fb4df9 100644
--- a/packages/flutter/lib/src/http/mojo_client.dart
+++ b/packages/flutter/lib/src/http/mojo_client.dart
@@ -149,14 +149,14 @@
ByteData data = await mojo.DataPipeDrainer.drainHandle(response.body);
Uint8List bodyBytes = new Uint8List.view(data.buffer);
return new Response(bodyBytes: bodyBytes, statusCode: response.statusCode);
- } catch (exception) {
- assert(() {
- debugPrint('-- EXCEPTION CAUGHT BY NETWORKING HTTP LIBRARY -------------------------');
- debugPrint('An exception was raised while sending bytes to the Mojo network library:');
- debugPrint('$exception');
- debugPrint('------------------------------------------------------------------------');
- return true;
- });
+ } catch (exception, stack) {
+ FlutterError.reportError(new FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'networking HTTP library',
+ context: 'while sending bytes to the Mojo network library',
+ silent: true
+ ));
return new Response(statusCode: 500);
} finally {
loader.close();
diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart
index e20837a..bc6c347 100644
--- a/packages/flutter/lib/src/rendering/box.dart
+++ b/packages/flutter/lib/src/rendering/box.dart
@@ -795,10 +795,13 @@
void performLayout() {
assert(() {
if (!sizedByParent) {
- debugPrint('$runtimeType needs to either override performLayout() to\n'
- 'set size and lay out children, or, set sizedByParent to true\n'
- 'so that performResize() sizes the render object.');
- assert(sizedByParent);
+ throw new FlutterError(
+ '$runtimeType did not implement performLayout().\n'
+ 'RenderBox subclasses need to either override performLayout() to '
+ 'set a size and lay out any children, or, set sizedByParent to true '
+ 'so that performResize() sizes the render object.'
+ );
+ return true;
}
return true;
});
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart
index 77dc0fe..f38fc66 100644
--- a/packages/flutter/lib/src/rendering/object.dart
+++ b/packages/flutter/lib/src/rendering/object.dart
@@ -394,16 +394,6 @@
typedef void LayoutCallback(Constraints constraints);
typedef double ExtentCallback(Constraints constraints);
-typedef void RenderingExceptionHandler(RenderObject source, String method, dynamic exception, StackTrace stack);
-/// This callback is invoked whenever an exception is caught by the rendering
-/// system. The 'source' argument is the [RenderObject] object that caught the
-/// exception. The 'method' argument is the method in which the exception
-/// occurred; it will be one of 'performResize', 'performLayout, or 'paint'. The
-/// 'exception' argument contains the object that was thrown, and the 'stack'
-/// argument contains the stack trace. If no handler is registered, then the
-/// information will be printed to the console instead.
-RenderingExceptionHandler debugRenderingExceptionHandler;
-
class _SemanticsGeometry {
_SemanticsGeometry() : transform = new Matrix4.identity();
_SemanticsGeometry.withClipFrom(_SemanticsGeometry other) {
@@ -889,49 +879,45 @@
void visitChildren(RenderObjectVisitor visitor) { }
dynamic debugCreator;
- static int _debugPrintedExceptionCount = 0;
void _debugReportException(String method, dynamic exception, StackTrace stack) {
- try {
- if (debugRenderingExceptionHandler != null) {
- debugRenderingExceptionHandler(this, method, exception, stack);
- } else {
- _debugPrintedExceptionCount += 1;
- if (_debugPrintedExceptionCount == 1) {
- debugPrint('-- EXCEPTION CAUGHT BY RENDERING LIBRARY -------------------------------');
- debugPrint('The following exception was raised during $method():');
- debugPrint('$exception');
- debugPrint('The following RenderObject was being processed when the exception was fired:\n${this}');
- if (debugCreator != null)
- debugPrint('This RenderObject had the following creator:\n$debugCreator');
- int depth = 0;
- List<String> descendants = <String>[];
- const int maxDepth = 5;
- void visitor(RenderObject child) {
- depth += 1;
+ FlutterError.reportError(new FlutterErrorDetailsForRendering(
+ exception: exception,
+ stack: stack,
+ library: 'rendering library',
+ context: 'during $method()',
+ renderObject: this,
+ informationCollector: (StringBuffer information) {
+ information.writeln('The following RenderObject was being processed when the exception was fired:\n${this}');
+ if (debugCreator != null)
+ information.writeln('This RenderObject had the following creator:\n$debugCreator');
+ List<String> descendants = <String>[];
+ const int maxDepth = 5;
+ int depth = 0;
+ const int maxLines = 30;
+ int lines = 0;
+ void visitor(RenderObject child) {
+ if (lines < maxLines) {
descendants.add('${" " * depth}$child');
+ depth += 1;
if (depth < maxDepth)
child.visitChildren(visitor);
depth -= 1;
+ } else if (lines == maxLines) {
+ descendants.add(' ...(descendants list truncated after $lines lines)');
}
- visitChildren(visitor);
- if (descendants.length > 1) {
- debugPrint('This RenderObject had the following descendants (showing up to depth $maxDepth):');
- } else if (descendants.length == 1) {
- debugPrint('This RenderObject had the following child:');
- } else {
- debugPrint('This RenderObject has no descendants.');
- }
- descendants.forEach(debugPrint);
- debugPrint('Stack trace:');
- debugPrint('$stack');
- debugPrint('------------------------------------------------------------------------');
- } else {
- debugPrint('Another exception was raised: ${exception.toString().split("\n")[0]}');
+ lines += 1;
}
+ visitChildren(visitor);
+ if (lines > 1) {
+ information.writeln('This RenderObject had the following descendants (showing up to depth $maxDepth):');
+ } else if (descendants.length == 1) {
+ information.writeln('This RenderObject had the following child:');
+ } else {
+ information.writeln('This RenderObject has no descendants.');
+ }
+ information.writeAll(descendants, '\n');
}
- } catch (exception) {
- debugPrint('(exception during exception handler: $exception)');
- }
+ ));
}
bool _debugDoingThisResize = false;
@@ -2153,3 +2139,32 @@
return result;
}
}
+
+/// Variant of [FlutterErrorDetails] with extra fields for the rendering
+/// library.
+class FlutterErrorDetailsForRendering extends FlutterErrorDetails {
+ /// Creates a [FlutterErrorDetailsForRendering] object with the given
+ /// arguments setting the object's properties.
+ ///
+ /// The rendering library calls this constructor when catching an exception
+ /// that will subsequently be reported using [FlutterError.onError].
+ const FlutterErrorDetailsForRendering({
+ dynamic exception,
+ StackTrace stack,
+ String library,
+ String context,
+ this.renderObject,
+ FlutterInformationCollector informationCollector,
+ bool silent
+ }) : super(
+ exception: exception,
+ stack: stack,
+ library: library,
+ context: context,
+ informationCollector: informationCollector,
+ silent: silent
+ );
+
+ /// The RenderObject that was being processed when the exception was caught.
+ final RenderObject renderObject;
+}
diff --git a/packages/flutter/lib/src/scheduler/scheduler.dart b/packages/flutter/lib/src/scheduler/scheduler.dart
index 998c4a0..361a2e9 100644
--- a/packages/flutter/lib/src/scheduler/scheduler.dart
+++ b/packages/flutter/lib/src/scheduler/scheduler.dart
@@ -24,16 +24,8 @@
/// common time base.
typedef void FrameCallback(Duration timeStamp);
-typedef void SchedulerExceptionHandler(dynamic exception, StackTrace stack);
-
typedef bool SchedulingStrategy({ int priority, Scheduler scheduler });
-/// This callback is invoked whenever an exception is caught by the scheduler.
-/// The 'exception' argument contains the object that was thrown, and the
-/// 'stack' argument contains the stack trace. If the callback is set, it is
-/// invoked instead of printing the information to the console.
-SchedulerExceptionHandler debugSchedulerExceptionHandler;
-
/// An entry in the scheduler's priority queue.
///
/// Combines the task and its priority.
@@ -277,16 +269,12 @@
try {
callback(timeStamp);
} catch (exception, stack) {
- if (debugSchedulerExceptionHandler != null) {
- debugSchedulerExceptionHandler(exception, stack);
- } else {
- debugPrint('-- EXCEPTION CAUGHT BY SCHEDULER LIBRARY -------------------------------');
- debugPrint('An exception was raised during a scheduler callback:');
- debugPrint('$exception');
- debugPrint('Stack trace:');
- debugPrint('$stack');
- debugPrint('------------------------------------------------------------------------');
- }
+ FlutterError.reportError(new FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'scheduler library',
+ context: 'during a scheduler callback'
+ ));
}
}
diff --git a/packages/flutter/lib/src/services/assertions.dart b/packages/flutter/lib/src/services/assertions.dart
index 080f356..d312ebb 100644
--- a/packages/flutter/lib/src/services/assertions.dart
+++ b/packages/flutter/lib/src/services/assertions.dart
@@ -2,6 +2,79 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'print.dart';
+
+/// Signature for [FlutterError.onException] handler.
+typedef void FlutterExceptionHandler(FlutterErrorDetails details);
+
+/// Signature for [FlutterErrorDetails.informationCollector] callback.
+///
+/// The text written to the information argument may contain newlines but should
+/// not end with a newline.
+typedef void FlutterInformationCollector(StringBuffer information);
+
+/// Class for information provided to [FlutterExceptionHandler] callbacks.
+///
+/// See [FlutterError.onError].
+class FlutterErrorDetails {
+ /// Creates a [FlutterErrorDetails] object with the given arguments setting
+ /// the object's properties.
+ ///
+ /// The framework calls this constructor when catching an exception that will
+ /// subsequently be reported using [FlutterError.onError].
+ const FlutterErrorDetails({
+ this.exception,
+ this.stack,
+ this.library: 'Flutter framework',
+ this.context,
+ this.informationCollector,
+ this.silent: false
+ });
+
+ /// The exception. Often this will be an [AssertionError], maybe specifically
+ /// a [FlutterError]. However, this could be any value at all.
+ final dynamic exception;
+
+ /// The stack trace from where the [exception] was thrown (as opposed to where
+ /// it was caught).
+ ///
+ /// StackTrace objects are opaque except for their [toString] function. A
+ /// stack trace is not expected to be machine-readable.
+ final StackTrace stack;
+
+ /// A human-readable brief name describing the library that caught the error
+ /// message. This is used by the default error handler in the header dumped to
+ /// the console.
+ final String library;
+
+ /// A human-readable description of where the error was caught (as opposed to
+ /// where it was thrown).
+ final String context;
+
+ /// A callback which, when invoked with a [StringBuffer] will write to that buffer
+ /// information that could help with debugging the problem.
+ ///
+ /// Information collector callbacks can be expensive, so the generated information
+ /// should be cached, rather than the callback being invoked multiple times.
+ final FlutterInformationCollector informationCollector;
+
+ /// Whether this error should be ignored by the default error reporting
+ /// behavior in release mode.
+ ///
+ /// If this is false, the default, then the default error handler will always
+ /// dump this error to the console.
+ ///
+ /// If this is true, then the default error handler would only dump this error
+ /// to the console in checked mode. In release mode, the error is ignored.
+ ///
+ /// This is used by certain exception handlers that catch errors that could be
+ /// triggered by environmental conditions (as opposed to logic errors). For
+ /// example, the HTTP library sets this flag so as to not report every 404
+ /// error to the console on end-user devices, while still allowing a custom
+ /// error handler to see the errors even in release builds.
+ final bool silent;
+}
+
/// Error class used to report Flutter-specific assertion failures and
/// contract violations.
class FlutterError extends AssertionError {
@@ -17,14 +90,16 @@
/// The message associated with this error.
///
- /// The message may have newlines in it. The first line should be a
- /// terse description of the error, e.g. "Incorrect GlobalKey usage"
- /// or "setState() or markNeedsBuild() called during build".
- /// Subsequent lines can then contain more information. In some
- /// cases, when a FlutterError is reported to the user, only the
- /// first line is included. For example, Flutter will typically only
- /// fully report the first exception at runtime, displaying only the
- /// first line of subsequent errors.
+ /// The message may have newlines in it. The first line should be a terse
+ /// description of the error, e.g. "Incorrect GlobalKey usage" or "setState()
+ /// or markNeedsBuild() called during build". Subsequent lines should contain
+ /// substantial additional information, ideally sufficient to develop a
+ /// correct solution to the problem.
+ ///
+ /// In some cases, when a FlutterError is reported to the user, only the first
+ /// line is included. For example, Flutter will typically only fully report
+ /// the first exception at runtime, displaying only the first line of
+ /// subsequent errors.
///
/// All sentences in the error should be correctly punctuated (i.e.,
/// do end the error message with a period).
@@ -32,4 +107,70 @@
@override
String toString() => message;
+
+ /// Called whenever the Flutter framework catches an error.
+ ///
+ /// The default behavior is to invoke [dumpErrorToConsole].
+ ///
+ /// You can set this to your own function to override this default behavior.
+ /// For example, you could report all errors to your server.
+ ///
+ /// If the error handler throws an exception, it will not be caught by the
+ /// Flutter framework.
+ ///
+ /// Set this to null to silently catch and ignore errors. This is not
+ /// recommended.
+ static FlutterExceptionHandler onError = dumpErrorToConsole;
+
+ static int _errorCount = 0;
+
+ /// Prints the given exception details to the console.
+ ///
+ /// The first time this is called, it dumps a very verbose message to the
+ /// console using [debugPrint].
+ ///
+ /// Subsequent calls only dump the first line of the exception.
+ ///
+ /// This is the default behavior for the [onError] handler.
+ static void dumpErrorToConsole(FlutterErrorDetails details) {
+ assert(details != null);
+ assert(details.exception != null);
+ bool reportError = !details.silent;
+ assert(() {
+ // In checked mode, we ignore the "silent" flag.
+ reportError = true;
+ return true;
+ });
+ if (!reportError)
+ return;
+ if (_errorCount == 0) {
+ final String header = '-- EXCEPTION CAUGHT BY ${details.library} '.toUpperCase();
+ const String footer = '------------------------------------------------------------------------';
+ debugPrint('$header${"-" * (footer.length - header.length)}');
+ debugPrint('The following exception was raised${ details.context != null ? " ${details.context}" : ""}:');
+ debugPrint('${details.exception}');
+ if (details.informationCollector != null) {
+ StringBuffer information = new StringBuffer();
+ details.informationCollector(information);
+ debugPrint(information.toString());
+ }
+ if (details.stack != null) {
+ debugPrint('Stack trace:');
+ debugPrint('${details.stack}$footer');
+ } else {
+ debugPrint(footer);
+ }
+ } else {
+ debugPrint('Another exception was raised: ${details.exception.toString().split("\n")[0]}');
+ }
+ _errorCount += 1;
+ }
+
+ /// Calls [onError] with the given details, unless it is null.
+ static void reportError(FlutterErrorDetails details) {
+ assert(details != null);
+ assert(details.exception != null);
+ if (onError != null)
+ onError(details);
+ }
}
diff --git a/packages/flutter/lib/src/services/fetch.dart b/packages/flutter/lib/src/services/fetch.dart
index bf2f7fc..b4d2a78 100644
--- a/packages/flutter/lib/src/services/fetch.dart
+++ b/packages/flutter/lib/src/services/fetch.dart
@@ -8,8 +8,8 @@
import 'package:mojo/mojo/url_response.mojom.dart' as mojom;
import 'package:mojo_services/mojo/url_loader.mojom.dart' as mojom;
+import 'assertions.dart';
import '../http/mojo_client.dart';
-import 'print.dart';
export 'package:mojo/mojo/url_response.mojom.dart' show UrlResponse;
@@ -27,14 +27,17 @@
message.writeln('Protocol error: ${response.statusCode} ${response.statusLine ?? "<no server message>"}');
if (response.url != request.url)
message.writeln('Final URL after redirects was: ${response.url}');
- throw message;
+ throw message; // this is not a FlutterError, because it's a real error, not an assertion
}
return response;
- } catch (exception) {
- debugPrint('-- EXCEPTION CAUGHT BY NETWORKING HTTP LIBRARY -------------------------');
- debugPrint('An exception was raised while sending bytes to the Mojo network library:');
- debugPrint('$exception');
- debugPrint('------------------------------------------------------------------------');
+ } catch (exception, stack) {
+ FlutterError.reportError(new FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'fetch service',
+ context: 'while sending bytes to the Mojo network library',
+ silent: true
+ ));
return null;
} finally {
loader.close();
diff --git a/packages/flutter/lib/src/services/image_resource.dart b/packages/flutter/lib/src/services/image_resource.dart
index cb11644..8537c8d 100644
--- a/packages/flutter/lib/src/services/image_resource.dart
+++ b/packages/flutter/lib/src/services/image_resource.dart
@@ -5,7 +5,7 @@
import 'dart:async';
import 'dart:ui' as ui show Image;
-import 'print.dart';
+import 'assertions.dart';
/// A [ui.Image] object with its corresponding scale.
///
@@ -61,7 +61,7 @@
_futureImage.then(
_handleImageLoaded,
onError: (dynamic exception, dynamic stack) {
- _handleImageError('Failed to load image:', exception, stack);
+ _handleImageError('while loading an image', exception, stack);
}
);
}
@@ -86,7 +86,7 @@
try {
listener(_image);
} catch (exception, stack) {
- _handleImageError('The following exception was thrown by a synchronously-invoked image listener:', exception, stack);
+ _handleImageError('by a synchronously-invoked image listener', exception, stack);
}
}
}
@@ -109,18 +109,18 @@
try {
listener(_image);
} catch (exception, stack) {
- _handleImageError('The following exception was thrown by an image listener:', exception, stack);
+ _handleImageError('by an image listener', exception, stack);
}
}
}
- void _handleImageError(String message, dynamic exception, dynamic stack) {
- debugPrint('-- EXCEPTION CAUGHT BY SERVICES LIBRARY --------------------------------');
- debugPrint(message);
- debugPrint('$exception');
- debugPrint('Stack trace:');
- debugPrint('$stack');
- debugPrint('------------------------------------------------------------------------');
+ void _handleImageError(String context, dynamic exception, dynamic stack) {
+ FlutterError.reportError(new FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'image resource service',
+ context: context
+ ));
}
@override
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index d07a3e4..a97ad99 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -170,7 +170,7 @@
} else {
element.update(this);
}
- }, building: true);
+ }, building: true, context: 'while attaching root widget to rendering tree');
return element;
}
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index 61ff0ac..d73507c 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -9,6 +9,7 @@
import 'debug.dart';
import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
export 'dart:ui' show hashValues, hashList;
export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugPrint;
@@ -695,17 +696,17 @@
bool _debugBuilding = false;
BuildableElement _debugCurrentBuildTarget;
- /// Establishes a scope in which widget build functions can run.
+ /// Establishes a scope in which calls to [State.setState] are forbidden.
///
- /// Inside a build scope, widget build functions are allowed to run, but
- /// State.setState() is forbidden. This mechanism prevents build functions
- /// from transitively requiring other build functions to run, potentially
- /// causing infinite loops.
+ /// This mechanism prevents build functions from transitively requiring other
+ /// build functions to run, potentially causing infinite loops.
///
- /// After unwinding the last build scope on the stack, the framework verifies
- /// that each global key is used at most once and notifies listeners about
- /// changes to global keys.
- void lockState(void callback(), { bool building: false }) {
+ /// If the building argument is true, then this is a build scope. Build scopes
+ /// cannot be nested.
+ ///
+ /// The context argument is used to describe the scope in case an exception is
+ /// caught while invoking the callback.
+ void lockState(void callback(), { bool building: false, String context }) {
assert(_debugStateLockLevel >= 0);
assert(() {
if (building) {
@@ -718,6 +719,8 @@
});
try {
callback();
+ } catch (e, stack) {
+ _debugReportException(context, e, stack);
} finally {
assert(() {
_debugStateLockLevel -= 1;
@@ -766,18 +769,25 @@
}
assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
_dirtyElements.clear();
- }, building: true);
+ }, building: true, context: 'while rebuilding dirty elements');
assert(_dirtyElements.isEmpty);
Timeline.finishSync();
}
/// Complete the element build pass by unmounting any elements that are no
/// longer active.
+ ///
/// This is called by beginFrame().
+ ///
+ /// In checked mode, this also verifies that each global key is used at most
+ /// once.
+ ///
+ /// After the current call stack unwinds, a microtask that notifies listeners
+ /// about changes to global keys will run.
void finalizeTree() {
lockState(() {
_inactiveElements._unmountAll();
- });
+ }, context: 'while finalizing the widget tree');
assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners);
}
@@ -2126,24 +2136,11 @@
}
}
-typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTrace stack);
-/// This callback is invoked whenever an exception is caught by the widget
-/// system. The 'context' argument is a description of what was happening when
-/// the exception occurred, and may include additional details such as
-/// descriptions of the objects involved. The 'exception' argument contains the
-/// object that was thrown, and the 'stack' argument contains the stack trace.
-/// If no callback is set, then a default behavior consisting of dumping the
-/// context, exception, and stack trace to the console is used instead.
-WidgetsExceptionHandler debugWidgetsExceptionHandler;
void _debugReportException(String context, dynamic exception, StackTrace stack) {
- if (debugWidgetsExceptionHandler != null) {
- debugWidgetsExceptionHandler(context, exception, stack);
- } else {
- debugPrint('-- EXCEPTION CAUGHT BY WIDGETS LIBRARY ---------------------------------');
- debugPrint('Exception caught while $context');
- debugPrint('$exception');
- debugPrint('Stack trace:');
- debugPrint('$stack');
- debugPrint('------------------------------------------------------------------------');
- }
+ FlutterError.reportError(new FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets library',
+ context: context
+ ));
}
diff --git a/packages/flutter/lib/src/widgets/mixed_viewport.dart b/packages/flutter/lib/src/widgets/mixed_viewport.dart
index a28449e..cef35fa 100644
--- a/packages/flutter/lib/src/widgets/mixed_viewport.dart
+++ b/packages/flutter/lib/src/widgets/mixed_viewport.dart
@@ -254,7 +254,7 @@
}
owner.lockState(() {
_doLayout(constraints);
- }, building: true);
+ }, building: true, context: 'during $runtimeType layout');
}
void postLayout() {
diff --git a/packages/flutter/lib/src/widgets/virtual_viewport.dart b/packages/flutter/lib/src/widgets/virtual_viewport.dart
index 204a9a2..eca2c0c 100644
--- a/packages/flutter/lib/src/widgets/virtual_viewport.dart
+++ b/packages/flutter/lib/src/widgets/virtual_viewport.dart
@@ -157,7 +157,7 @@
assert(startOffsetBase != null);
assert(startOffsetLimit != null);
_updatePaintOffset();
- owner.lockState(_materializeChildren, building: true);
+ owner.lockState(_materializeChildren, building: true, context: 'during $runtimeType layout');
}
void _materializeChildren() {
diff --git a/packages/flutter/test/widget/build_scope_test.dart b/packages/flutter/test/widget/build_scope_test.dart
index 12e35cb..2693fa5 100644
--- a/packages/flutter/test/widget/build_scope_test.dart
+++ b/packages/flutter/test/widget/build_scope_test.dart
@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
-import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
@@ -73,33 +72,12 @@
@override
void dispose() {
- setState(() {});
+ setState(() { /* This is invalid behavior. */ });
super.dispose();
}
}
void main() {
- dynamic cachedException;
-
- // ** WARNING **
- // THIS TEST OVERRIDES THE NORMAL EXCEPTION HANDLING
- // AND DOES NOT REPORT EXCEPTIONS FROM THE FRAMEWORK
-
- setUp(() {
- assert(cachedException == null);
- debugWidgetsExceptionHandler = (String context, dynamic exception, StackTrace stack) {
- cachedException = exception;
- };
- debugSchedulerExceptionHandler = (dynamic exception, StackTrace stack) { throw exception; };
- });
-
- tearDown(() {
- assert(cachedException == null);
- cachedException = null;
- debugWidgetsExceptionHandler = null;
- debugSchedulerExceptionHandler = null;
- });
-
test('Legal times for setState', () {
testWidgets((WidgetTester tester) {
GlobalKey flipKey = new GlobalKey();
@@ -129,21 +107,18 @@
test('Setting parent state during build is forbidden', () {
testWidgets((WidgetTester tester) {
- expect(cachedException, isNull);
tester.pumpWidget(new BadWidgetParent());
- expect(cachedException, isNotNull);
- cachedException = null;
+ expect(tester.takeException(), isNotNull);
tester.pumpWidget(new Container());
- expect(cachedException, isNull);
});
});
test('Setting state during dispose is forbidden', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new BadDisposeWidget());
- expect(() {
- tester.pumpWidget(new Container());
- }, throws);
+ expect(tester.takeException(), isNull);
+ tester.pumpWidget(new Container());
+ expect(tester.takeException(), isNotNull);
});
});
}
diff --git a/packages/flutter/test/widget/image_test.dart b/packages/flutter/test/widget/image_test.dart
index a3f0d80..176f14f 100644
--- a/packages/flutter/test/widget/image_test.dart
+++ b/packages/flutter/test/widget/image_test.dart
@@ -112,18 +112,16 @@
child: new AsyncImage(
provider: imageProvider1
)
- )
+ ),
+ null,
+ EnginePhase.layout
);
RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
- // An exception will be thrown when we try to draw the image. Catch it.
- RenderingExceptionHandler originalRenderingExceptionHandler = debugRenderingExceptionHandler;
- debugRenderingExceptionHandler = (_, __, ___, ____) => null;
imageProvider1.complete();
- tester.pump();
- tester.pump();
- debugRenderingExceptionHandler = originalRenderingExceptionHandler;
+ tester.async.flushMicrotasks(); // resolve the future from the image provider
+ tester.pump(null, EnginePhase.layout);
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull);
@@ -135,7 +133,9 @@
child: new AsyncImage(
provider: imageProvider2
)
- )
+ ),
+ null,
+ EnginePhase.layout
);
renderImage = key.currentContext.findRenderObject();
@@ -152,18 +152,16 @@
new AsyncImage(
key: key,
provider: imageProvider1
- )
+ ),
+ null,
+ EnginePhase.layout
);
RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
- // An exception will be thrown when we try to draw the image. Catch it.
- RenderingExceptionHandler originalRenderingExceptionHandler = debugRenderingExceptionHandler;
- debugRenderingExceptionHandler = (_, __, ___, ____) => null;
imageProvider1.complete();
- tester.pump();
- tester.pump();
- debugRenderingExceptionHandler = originalRenderingExceptionHandler;
+ tester.async.flushMicrotasks(); // resolve the future from the image provider
+ tester.pump(null, EnginePhase.layout);
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull);
@@ -173,7 +171,9 @@
new AsyncImage(
key: key,
provider: imageProvider2
- )
+ ),
+ null,
+ EnginePhase.layout
);
renderImage = key.currentContext.findRenderObject();
diff --git a/packages/flutter/test/widget/parent_data_test.dart b/packages/flutter/test/widget/parent_data_test.dart
index 491ad45..c12c5dd 100644
--- a/packages/flutter/test/widget/parent_data_test.dart
+++ b/packages/flutter/test/widget/parent_data_test.dart
@@ -48,20 +48,6 @@
final TestParentData kNonPositioned = new TestParentData();
void main() {
- dynamic cachedException;
-
- setUp(() {
- assert(cachedException == null);
- debugWidgetsExceptionHandler = (String context, dynamic exception, StackTrace stack) {
- cachedException = exception;
- };
- });
-
- tearDown(() {
- cachedException = null;
- debugWidgetsExceptionHandler = null;
- });
-
test('ParentDataWidget control test', () {
testWidgets((WidgetTester tester) {
@@ -259,8 +245,6 @@
test('ParentDataWidget conflicting data', () {
testWidgets((WidgetTester tester) {
- expect(cachedException, isNull);
-
tester.pumpWidget(
new Stack(
children: <Widget>[
@@ -276,14 +260,11 @@
]
)
);
-
- expect(cachedException, isNotNull);
- cachedException = null;
+ expect(tester.takeException(), isNotNull);
tester.pumpWidget(new Stack());
checkTree(tester, <TestParentData>[]);
- expect(cachedException, isNull);
tester.pumpWidget(
new Container(
@@ -298,9 +279,7 @@
)
)
);
-
- expect(cachedException, isNotNull);
- cachedException = null;
+ expect(tester.takeException(), isNotNull);
tester.pumpWidget(
new Stack()
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 60cce29..2b24355 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -4,11 +4,12 @@
import 'dart:ui' as ui show window;
-import 'package:quiver/testing/async.dart';
-import 'package:quiver/time.dart';
+import 'package:flutter/services.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
+import 'package:quiver/testing/async.dart';
+import 'package:quiver/time.dart';
import 'instrumentation.dart';
@@ -89,6 +90,21 @@
/// The supplied EnginePhase is the final phase reached during the pump pass;
/// if not supplied, the whole pass is executed.
void pumpWidget(Widget widget, [ Duration duration, EnginePhase phase ]) {
+ runApp(widget);
+ pump(duration, phase);
+ }
+
+ /// Triggers a frame sequence (build/layout/paint/etc),
+ /// then flushes microtasks.
+ ///
+ /// If duration is set, then advances the clock by that much first.
+ /// Doing this flushes microtasks.
+ ///
+ /// The supplied EnginePhase is the final phase reached during the pump pass;
+ /// if not supplied, the whole pass is executed.
+ void pump([ Duration duration, EnginePhase phase ]) {
+ if (duration != null)
+ async.elapse(duration);
if (binding is _SteppedWidgetFlutterBinding) {
// Some tests call WidgetFlutterBinding.ensureInitialized() manually, so
// we can't actually be sure we have a stepped binding.
@@ -98,8 +114,10 @@
// Can't step to a given phase in that case
assert(phase == null);
}
- runApp(widget);
- pump(duration);
+ binding.handleBeginFrame(new Duration(
+ milliseconds: clock.now().millisecondsSinceEpoch)
+ );
+ async.flushMicrotasks();
}
/// Artificially calls dispatchLocaleChanged on the Widget binding,
@@ -110,46 +128,72 @@
async.flushMicrotasks();
}
- /// Triggers a frame sequence (build/layout/paint/etc),
- /// then flushes microtasks.
- ///
- /// If duration is set, then advances the clock by that much first.
- /// Doing this flushes microtasks.
- void pump([ Duration duration ]) {
- if (duration != null)
- async.elapse(duration);
- binding.handleBeginFrame(new Duration(
- milliseconds: clock.now().millisecondsSinceEpoch)
- );
- async.flushMicrotasks();
- }
-
@override
void dispatchEvent(PointerEvent event, HitTestResult result) {
super.dispatchEvent(event, result);
async.flushMicrotasks();
}
+
+ /// Returns the exception most recently caught by the Flutter framework.
+ ///
+ /// Call this if you expect an exception during a test. If an exception is
+ /// thrown and this is not called, then the exception is rethrown when
+ /// the [testWidgets] call completes.
+ ///
+ /// If two exceptions are thrown in a row without the first one being
+ /// acknowledged with a call to this method, then when the second exception is
+ /// thrown, they are both dumped to the console and then the second is
+ /// rethrown from the exception handler. This will likely result in the
+ /// framework entering a highly unstable state and everything collapsing.
+ ///
+ /// It's safe to call this when there's no pending exception; it will return
+ /// null in that case.
+ dynamic takeException() {
+ dynamic result = _pendingException;
+ _pendingException = null;
+ return result;
+ }
+ dynamic _pendingException;
}
void testWidgets(callback(WidgetTester tester)) {
new FakeAsync().run((FakeAsync async) {
- WidgetTester tester = new WidgetTester._(async);
- runApp(new Container(key: new UniqueKey())); // Reset the tree to a known state.
- callback(tester);
- runApp(new Container(key: new UniqueKey())); // Unmount any remaining widgets.
- async.flushMicrotasks();
- assert(() {
- "An animation is still running even after the widget tree was disposed.";
- return Scheduler.instance.transientCallbackCount == 0;
- });
- assert(() {
- "A Timer is still running even after the widget tree was disposed.";
- return async.periodicTimerCount == 0;
- });
- assert(() {
- "A Timer is still running even after the widget tree was disposed.";
- return async.nonPeriodicTimerCount == 0;
- });
- assert(async.microtaskCount == 0); // Shouldn't be possible.
+ FlutterExceptionHandler oldHandler = FlutterError.onError;
+ try {
+ WidgetTester tester = new WidgetTester._(async);
+ FlutterError.onError = (FlutterErrorDetails details) {
+ if (tester._pendingException != null) {
+ FlutterError.dumpErrorToConsole(tester._pendingException);
+ FlutterError.dumpErrorToConsole(details.exception);
+ tester._pendingException = 'An uncaught exception was thrown.';
+ throw details.exception;
+ }
+ tester._pendingException = details;
+ };
+ runApp(new Container(key: new UniqueKey())); // Reset the tree to a known state.
+ callback(tester);
+ runApp(new Container(key: new UniqueKey())); // Unmount any remaining widgets.
+ async.flushMicrotasks();
+ assert(() {
+ "An animation is still running even after the widget tree was disposed.";
+ return Scheduler.instance.transientCallbackCount == 0;
+ });
+ assert(() {
+ "A Timer is still running even after the widget tree was disposed.";
+ return async.periodicTimerCount == 0;
+ });
+ assert(() {
+ "A Timer is still running even after the widget tree was disposed.";
+ return async.nonPeriodicTimerCount == 0;
+ });
+ assert(async.microtaskCount == 0); // Shouldn't be possible.
+ assert(() {
+ if (tester._pendingException != null)
+ FlutterError.dumpErrorToConsole(tester._pendingException);
+ return tester._pendingException == null;
+ });
+ } finally {
+ FlutterError.onError = oldHandler;
+ }
});
}