Commit ee703da9 authored by Ian Hickson's avatar Ian Hickson

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.
parent 3cd58547
...@@ -16,8 +16,6 @@ import 'events.dart'; ...@@ -16,8 +16,6 @@ import 'events.dart';
import 'hit_test.dart'; import 'hit_test.dart';
import 'pointer_router.dart'; import 'pointer_router.dart';
typedef void GesturerExceptionHandler(PointerEvent event, HitTestTarget target, dynamic exception, StackTrace stack);
/// A binding for the gesture subsystem. /// A binding for the gesture subsystem.
abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestable { abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestable {
...@@ -82,14 +80,6 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl ...@@ -82,14 +80,6 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl
result.add(new HitTestEntry(this)); 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 /// Dispatch the given event to the path of the given hit test result
void dispatchEvent(PointerEvent event, HitTestResult result) { void dispatchEvent(PointerEvent event, HitTestResult result) {
assert(result != null); assert(result != null);
...@@ -97,20 +87,20 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl ...@@ -97,20 +87,20 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl
try { try {
entry.target.handleEvent(event, entry); entry.target.handleEvent(event, entry);
} catch (exception, stack) { } catch (exception, stack) {
if (debugGesturerExceptionHandler != null) { FlutterError.reportError(new FlutterErrorDetailsForPointerEventDispatcher(
debugGesturerExceptionHandler(event, entry.target, exception, stack); exception: exception,
} else { stack: stack,
debugPrint('-- EXCEPTION CAUGHT BY GESTURE LIBRARY ---------------------------------'); library: 'gesture library',
debugPrint('The following exception was raised while dispatching a pointer event:'); context: 'while dispatching a pointer event',
debugPrint('$exception'); event: event,
debugPrint('Event:'); hitTestEntry: entry,
debugPrint('$event'); informationCollector: (StringBuffer information) {
debugPrint('Target:'); information.writeln('Event:');
debugPrint('${entry.target}'); information.writeln(' $event');
debugPrint('Stack trace:'); information.writeln('Target:');
debugPrint('$stack'); information.write(' ${entry.target}');
debugPrint('------------------------------------------------------------------------'); }
} ));
} }
} }
} }
...@@ -125,3 +115,44 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl ...@@ -125,3 +115,44 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl
} }
} }
} }
/// 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;
}
...@@ -11,8 +11,6 @@ import 'events.dart'; ...@@ -11,8 +11,6 @@ import 'events.dart';
/// A callback that receives a [PointerEvent] /// A callback that receives a [PointerEvent]
typedef void PointerRoute(PointerEvent event); typedef void PointerRoute(PointerEvent event);
typedef void PointerExceptionHandler(PointerRouter source, PointerEvent event, PointerRoute route, dynamic exception, StackTrace stack);
/// A routing table for [PointerEvent] events. /// A routing table for [PointerEvent] events.
class PointerRouter { class PointerRouter {
final Map<int, LinkedHashSet<PointerRoute>> _routeMap = new Map<int, LinkedHashSet<PointerRoute>>(); final Map<int, LinkedHashSet<PointerRoute>> _routeMap = new Map<int, LinkedHashSet<PointerRoute>>();
...@@ -40,16 +38,6 @@ class PointerRouter { ...@@ -40,16 +38,6 @@ class PointerRouter {
_routeMap.remove(pointer); _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. /// Calls the routes registered for this pointer event.
/// ///
/// Routes are called in the order in which they were added to the /// Routes are called in the order in which they were added to the
...@@ -64,19 +52,64 @@ class PointerRouter { ...@@ -64,19 +52,64 @@ class PointerRouter {
try { try {
route(event); route(event);
} catch (exception, stack) { } catch (exception, stack) {
if (debugPointerExceptionHandler != null) { FlutterError.reportError(new FlutterErrorDetailsForPointerRouter(
debugPointerExceptionHandler(this, event, route, exception, stack); exception: exception,
} else { stack: stack,
debugPrint('-- EXCEPTION CAUGHT BY GESTURE LIBRARY ---------------------------------'); library: 'gesture library',
debugPrint('The following exception was raised while routing a pointer event:'); context: 'while routing a pointer event',
debugPrint('$exception'); router: this,
debugPrint('Event:'); route: route,
debugPrint('$event'); event: event,
debugPrint('Stack trace:'); informationCollector: (StringBuffer information) {
debugPrint('$stack'); information.writeln('Event:');
debugPrint('------------------------------------------------------------------------'); 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;
}
...@@ -149,14 +149,14 @@ class MojoClient { ...@@ -149,14 +149,14 @@ class MojoClient {
ByteData data = await mojo.DataPipeDrainer.drainHandle(response.body); ByteData data = await mojo.DataPipeDrainer.drainHandle(response.body);
Uint8List bodyBytes = new Uint8List.view(data.buffer); Uint8List bodyBytes = new Uint8List.view(data.buffer);
return new Response(bodyBytes: bodyBytes, statusCode: response.statusCode); return new Response(bodyBytes: bodyBytes, statusCode: response.statusCode);
} catch (exception) { } catch (exception, stack) {
assert(() { FlutterError.reportError(new FlutterErrorDetails(
debugPrint('-- EXCEPTION CAUGHT BY NETWORKING HTTP LIBRARY -------------------------'); exception: exception,
debugPrint('An exception was raised while sending bytes to the Mojo network library:'); stack: stack,
debugPrint('$exception'); library: 'networking HTTP library',
debugPrint('------------------------------------------------------------------------'); context: 'while sending bytes to the Mojo network library',
return true; silent: true
}); ));
return new Response(statusCode: 500); return new Response(statusCode: 500);
} finally { } finally {
loader.close(); loader.close();
......
...@@ -795,10 +795,13 @@ abstract class RenderBox extends RenderObject { ...@@ -795,10 +795,13 @@ abstract class RenderBox extends RenderObject {
void performLayout() { void performLayout() {
assert(() { assert(() {
if (!sizedByParent) { if (!sizedByParent) {
debugPrint('$runtimeType needs to either override performLayout() to\n' throw new FlutterError(
'set size and lay out children, or, set sizedByParent to true\n' '$runtimeType did not implement performLayout().\n'
'so that performResize() sizes the render object.'); 'RenderBox subclasses need to either override performLayout() to '
assert(sizedByParent); 'set a size and lay out any children, or, set sizedByParent to true '
'so that performResize() sizes the render object.'
);
return true;
} }
return true; return true;
}); });
......
...@@ -394,16 +394,6 @@ typedef void RenderObjectVisitor(RenderObject child); ...@@ -394,16 +394,6 @@ typedef void RenderObjectVisitor(RenderObject child);
typedef void LayoutCallback(Constraints constraints); typedef void LayoutCallback(Constraints constraints);
typedef double ExtentCallback(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 { class _SemanticsGeometry {
_SemanticsGeometry() : transform = new Matrix4.identity(); _SemanticsGeometry() : transform = new Matrix4.identity();
_SemanticsGeometry.withClipFrom(_SemanticsGeometry other) { _SemanticsGeometry.withClipFrom(_SemanticsGeometry other) {
...@@ -889,49 +879,45 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -889,49 +879,45 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
void visitChildren(RenderObjectVisitor visitor) { } void visitChildren(RenderObjectVisitor visitor) { }
dynamic debugCreator; dynamic debugCreator;
static int _debugPrintedExceptionCount = 0;
void _debugReportException(String method, dynamic exception, StackTrace stack) { void _debugReportException(String method, dynamic exception, StackTrace stack) {
try { FlutterError.reportError(new FlutterErrorDetailsForRendering(
if (debugRenderingExceptionHandler != null) { exception: exception,
debugRenderingExceptionHandler(this, method, exception, stack); stack: stack,
} else { library: 'rendering library',
_debugPrintedExceptionCount += 1; context: 'during $method()',
if (_debugPrintedExceptionCount == 1) { renderObject: this,
debugPrint('-- EXCEPTION CAUGHT BY RENDERING LIBRARY -------------------------------'); informationCollector: (StringBuffer information) {
debugPrint('The following exception was raised during $method():'); information.writeln('The following RenderObject was being processed when the exception was fired:\n${this}');
debugPrint('$exception'); if (debugCreator != null)
debugPrint('The following RenderObject was being processed when the exception was fired:\n${this}'); information.writeln('This RenderObject had the following creator:\n$debugCreator');
if (debugCreator != null) List<String> descendants = <String>[];
debugPrint('This RenderObject had the following creator:\n$debugCreator'); const int maxDepth = 5;
int depth = 0; int depth = 0;
List<String> descendants = <String>[]; const int maxLines = 30;
const int maxDepth = 5; int lines = 0;
void visitor(RenderObject child) { void visitor(RenderObject child) {
depth += 1; if (lines < maxLines) {
descendants.add('${" " * depth}$child'); descendants.add('${" " * depth}$child');
depth += 1;
if (depth < maxDepth) if (depth < maxDepth)
child.visitChildren(visitor); child.visitChildren(visitor);
depth -= 1; depth -= 1;
} else if (lines == maxLines) {
descendants.add(' ...(descendants list truncated after $lines lines)');
} }
visitChildren(visitor); lines += 1;
if (descendants.length > 1) { }
debugPrint('This RenderObject had the following descendants (showing up to depth $maxDepth):'); visitChildren(visitor);
} else if (descendants.length == 1) { if (lines > 1) {
debugPrint('This RenderObject had the following child:'); information.writeln('This RenderObject had the following descendants (showing up to depth $maxDepth):');
} else { } else if (descendants.length == 1) {
debugPrint('This RenderObject has no descendants.'); information.writeln('This RenderObject had the following child:');
}
descendants.forEach(debugPrint);
debugPrint('Stack trace:');
debugPrint('$stack');
debugPrint('------------------------------------------------------------------------');
} else { } else {
debugPrint('Another exception was raised: ${exception.toString().split("\n")[0]}'); information.writeln('This RenderObject has no descendants.');
} }
information.writeAll(descendants, '\n');
} }
} catch (exception) { ));
debugPrint('(exception during exception handler: $exception)');
}
} }
bool _debugDoingThisResize = false; bool _debugDoingThisResize = false;
...@@ -2153,3 +2139,32 @@ abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, Parent ...@@ -2153,3 +2139,32 @@ abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, Parent
return result; 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;
}
...@@ -24,16 +24,8 @@ double timeDilation = 1.0; ...@@ -24,16 +24,8 @@ double timeDilation = 1.0;
/// common time base. /// common time base.
typedef void FrameCallback(Duration timeStamp); typedef void FrameCallback(Duration timeStamp);
typedef void SchedulerExceptionHandler(dynamic exception, StackTrace stack);
typedef bool SchedulingStrategy({ int priority, Scheduler scheduler }); 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. /// An entry in the scheduler's priority queue.
/// ///
/// Combines the task and its priority. /// Combines the task and its priority.
...@@ -277,16 +269,12 @@ abstract class Scheduler extends BindingBase { ...@@ -277,16 +269,12 @@ abstract class Scheduler extends BindingBase {
try { try {
callback(timeStamp); callback(timeStamp);
} catch (exception, stack) { } catch (exception, stack) {
if (debugSchedulerExceptionHandler != null) { FlutterError.reportError(new FlutterErrorDetails(
debugSchedulerExceptionHandler(exception, stack); exception: exception,
} else { stack: stack,
debugPrint('-- EXCEPTION CAUGHT BY SCHEDULER LIBRARY -------------------------------'); library: 'scheduler library',
debugPrint('An exception was raised during a scheduler callback:'); context: 'during a scheduler callback'
debugPrint('$exception'); ));
debugPrint('Stack trace:');
debugPrint('$stack');
debugPrint('------------------------------------------------------------------------');
}
} }
} }
......
...@@ -2,6 +2,79 @@ ...@@ -2,6 +2,79 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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 /// Error class used to report Flutter-specific assertion failures and
/// contract violations. /// contract violations.
class FlutterError extends AssertionError { class FlutterError extends AssertionError {
...@@ -17,14 +90,16 @@ class FlutterError extends AssertionError { ...@@ -17,14 +90,16 @@ class FlutterError extends AssertionError {
/// The message associated with this error. /// The message associated with this error.
/// ///
/// The message may have newlines in it. The first line should be a /// The message may have newlines in it. The first line should be a terse
/// terse description of the error, e.g. "Incorrect GlobalKey usage" /// description of the error, e.g. "Incorrect GlobalKey usage" or "setState()
/// or "setState() or markNeedsBuild() called during build". /// or markNeedsBuild() called during build". Subsequent lines should contain
/// Subsequent lines can then contain more information. In some /// substantial additional information, ideally sufficient to develop a
/// cases, when a FlutterError is reported to the user, only the /// correct solution to the problem.
/// first line is included. For example, Flutter will typically only ///
/// fully report the first exception at runtime, displaying only the /// In some cases, when a FlutterError is reported to the user, only the first
/// first line of subsequent errors. /// 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., /// All sentences in the error should be correctly punctuated (i.e.,
/// do end the error message with a period). /// do end the error message with a period).
...@@ -32,4 +107,70 @@ class FlutterError extends AssertionError { ...@@ -32,4 +107,70 @@ class FlutterError extends AssertionError {
@override @override
String toString() => message; 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);
}
} }
...@@ -8,8 +8,8 @@ import 'package:mojo/mojo/url_request.mojom.dart' as mojom; ...@@ -8,8 +8,8 @@ import 'package:mojo/mojo/url_request.mojom.dart' as mojom;
import 'package:mojo/mojo/url_response.mojom.dart' as mojom; import 'package:mojo/mojo/url_response.mojom.dart' as mojom;
import 'package:mojo_services/mojo/url_loader.mojom.dart' as mojom; import 'package:mojo_services/mojo/url_loader.mojom.dart' as mojom;
import 'assertions.dart';
import '../http/mojo_client.dart'; import '../http/mojo_client.dart';
import 'print.dart';
export 'package:mojo/mojo/url_response.mojom.dart' show UrlResponse; export 'package:mojo/mojo/url_response.mojom.dart' show UrlResponse;
...@@ -27,14 +27,17 @@ Future<mojom.UrlResponse> fetch(mojom.UrlRequest request, { bool require200: fal ...@@ -27,14 +27,17 @@ Future<mojom.UrlResponse> fetch(mojom.UrlRequest request, { bool require200: fal
message.writeln('Protocol error: ${response.statusCode} ${response.statusLine ?? "<no server message>"}'); message.writeln('Protocol error: ${response.statusCode} ${response.statusLine ?? "<no server message>"}');
if (response.url != request.url) if (response.url != request.url)
message.writeln('Final URL after redirects was: ${response.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; return response;
} catch (exception) { } catch (exception, stack) {
debugPrint('-- EXCEPTION CAUGHT BY NETWORKING HTTP LIBRARY -------------------------'); FlutterError.reportError(new FlutterErrorDetails(
debugPrint('An exception was raised while sending bytes to the Mojo network library:'); exception: exception,
debugPrint('$exception'); stack: stack,
debugPrint('------------------------------------------------------------------------'); library: 'fetch service',
context: 'while sending bytes to the Mojo network library',
silent: true
));
return null; return null;
} finally { } finally {
loader.close(); loader.close();
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'print.dart'; import 'assertions.dart';
/// A [ui.Image] object with its corresponding scale. /// A [ui.Image] object with its corresponding scale.
/// ///
...@@ -61,7 +61,7 @@ class ImageResource { ...@@ -61,7 +61,7 @@ class ImageResource {
_futureImage.then( _futureImage.then(
_handleImageLoaded, _handleImageLoaded,
onError: (dynamic exception, dynamic stack) { onError: (dynamic exception, dynamic stack) {
_handleImageError('Failed to load image:', exception, stack); _handleImageError('while loading an image', exception, stack);
} }
); );
} }
...@@ -86,7 +86,7 @@ class ImageResource { ...@@ -86,7 +86,7 @@ class ImageResource {
try { try {
listener(_image); listener(_image);
} catch (exception, stack) { } 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 @@ class ImageResource { ...@@ -109,18 +109,18 @@ class ImageResource {
try { try {
listener(_image); listener(_image);
} catch (exception, stack) { } 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) { void _handleImageError(String context, dynamic exception, dynamic stack) {
debugPrint('-- EXCEPTION CAUGHT BY SERVICES LIBRARY --------------------------------'); FlutterError.reportError(new FlutterErrorDetails(
debugPrint(message); exception: exception,
debugPrint('$exception'); stack: stack,
debugPrint('Stack trace:'); library: 'image resource service',
debugPrint('$stack'); context: context
debugPrint('------------------------------------------------------------------------'); ));
} }
@override @override
......
...@@ -170,7 +170,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -170,7 +170,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
} else { } else {
element.update(this); element.update(this);
} }
}, building: true); }, building: true, context: 'while attaching root widget to rendering tree');
return element; return element;
} }
......
...@@ -9,6 +9,7 @@ import 'dart:developer'; ...@@ -9,6 +9,7 @@ import 'dart:developer';
import 'debug.dart'; import 'debug.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
export 'dart:ui' show hashValues, hashList; export 'dart:ui' show hashValues, hashList;
export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugPrint; export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugPrint;
...@@ -695,17 +696,17 @@ class BuildOwner { ...@@ -695,17 +696,17 @@ class BuildOwner {
bool _debugBuilding = false; bool _debugBuilding = false;
BuildableElement _debugCurrentBuildTarget; 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 /// This mechanism prevents build functions from transitively requiring other
/// State.setState() is forbidden. This mechanism prevents build functions /// build functions to run, potentially causing infinite loops.
/// from transitively requiring other build functions to run, potentially
/// causing infinite loops.
/// ///
/// After unwinding the last build scope on the stack, the framework verifies /// If the building argument is true, then this is a build scope. Build scopes
/// that each global key is used at most once and notifies listeners about /// cannot be nested.
/// changes to global keys. ///
void lockState(void callback(), { bool building: false }) { /// 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(_debugStateLockLevel >= 0);
assert(() { assert(() {
if (building) { if (building) {
...@@ -718,6 +719,8 @@ class BuildOwner { ...@@ -718,6 +719,8 @@ class BuildOwner {
}); });
try { try {
callback(); callback();
} catch (e, stack) {
_debugReportException(context, e, stack);
} finally { } finally {
assert(() { assert(() {
_debugStateLockLevel -= 1; _debugStateLockLevel -= 1;
...@@ -766,18 +769,25 @@ class BuildOwner { ...@@ -766,18 +769,25 @@ class BuildOwner {
} }
assert(!_dirtyElements.any((BuildableElement element) => element.dirty)); assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
_dirtyElements.clear(); _dirtyElements.clear();
}, building: true); }, building: true, context: 'while rebuilding dirty elements');
assert(_dirtyElements.isEmpty); assert(_dirtyElements.isEmpty);
Timeline.finishSync(); Timeline.finishSync();
} }
/// Complete the element build pass by unmounting any elements that are no /// Complete the element build pass by unmounting any elements that are no
/// longer active. /// longer active.
///
/// This is called by beginFrame(). /// 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() { void finalizeTree() {
lockState(() { lockState(() {
_inactiveElements._unmountAll(); _inactiveElements._unmountAll();
}); }, context: 'while finalizing the widget tree');
assert(GlobalKey._debugCheckForDuplicates); assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners); scheduleMicrotask(GlobalKey._notifyListeners);
} }
...@@ -2126,24 +2136,11 @@ class MultiChildRenderObjectElement extends RenderObjectElement { ...@@ -2126,24 +2136,11 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
} }
} }
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) { void _debugReportException(String context, dynamic exception, StackTrace stack) {
if (debugWidgetsExceptionHandler != null) { FlutterError.reportError(new FlutterErrorDetails(
debugWidgetsExceptionHandler(context, exception, stack); exception: exception,
} else { stack: stack,
debugPrint('-- EXCEPTION CAUGHT BY WIDGETS LIBRARY ---------------------------------'); library: 'widgets library',
debugPrint('Exception caught while $context'); context: context
debugPrint('$exception'); ));
debugPrint('Stack trace:');
debugPrint('$stack');
debugPrint('------------------------------------------------------------------------');
}
} }
...@@ -254,7 +254,7 @@ class _MixedViewportElement extends RenderObjectElement { ...@@ -254,7 +254,7 @@ class _MixedViewportElement extends RenderObjectElement {
} }
owner.lockState(() { owner.lockState(() {
_doLayout(constraints); _doLayout(constraints);
}, building: true); }, building: true, context: 'during $runtimeType layout');
} }
void postLayout() { void postLayout() {
......
...@@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement { ...@@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement {
assert(startOffsetBase != null); assert(startOffsetBase != null);
assert(startOffsetLimit != null); assert(startOffsetLimit != null);
_updatePaintOffset(); _updatePaintOffset();
owner.lockState(_materializeChildren, building: true); owner.lockState(_materializeChildren, building: true, context: 'during $runtimeType layout');
} }
void _materializeChildren() { void _materializeChildren() {
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -73,33 +72,12 @@ class BadDisposeWidgetState extends State<BadDisposeWidget> { ...@@ -73,33 +72,12 @@ class BadDisposeWidgetState extends State<BadDisposeWidget> {
@override @override
void dispose() { void dispose() {
setState(() {}); setState(() { /* This is invalid behavior. */ });
super.dispose(); super.dispose();
} }
} }
void main() { 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', () { test('Legal times for setState', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
GlobalKey flipKey = new GlobalKey(); GlobalKey flipKey = new GlobalKey();
...@@ -129,21 +107,18 @@ void main() { ...@@ -129,21 +107,18 @@ void main() {
test('Setting parent state during build is forbidden', () { test('Setting parent state during build is forbidden', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
expect(cachedException, isNull);
tester.pumpWidget(new BadWidgetParent()); tester.pumpWidget(new BadWidgetParent());
expect(cachedException, isNotNull); expect(tester.takeException(), isNotNull);
cachedException = null;
tester.pumpWidget(new Container()); tester.pumpWidget(new Container());
expect(cachedException, isNull);
}); });
}); });
test('Setting state during dispose is forbidden', () { test('Setting state during dispose is forbidden', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
tester.pumpWidget(new BadDisposeWidget()); tester.pumpWidget(new BadDisposeWidget());
expect(() { expect(tester.takeException(), isNull);
tester.pumpWidget(new Container()); tester.pumpWidget(new Container());
}, throws); expect(tester.takeException(), isNotNull);
}); });
}); });
} }
...@@ -112,18 +112,16 @@ void main() { ...@@ -112,18 +112,16 @@ void main() {
child: new AsyncImage( child: new AsyncImage(
provider: imageProvider1 provider: imageProvider1
) )
) ),
null,
EnginePhase.layout
); );
RenderImage renderImage = key.currentContext.findRenderObject(); RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull); 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(); imageProvider1.complete();
tester.pump(); tester.async.flushMicrotasks(); // resolve the future from the image provider
tester.pump(); tester.pump(null, EnginePhase.layout);
debugRenderingExceptionHandler = originalRenderingExceptionHandler;
renderImage = key.currentContext.findRenderObject(); renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull); expect(renderImage.image, isNotNull);
...@@ -135,7 +133,9 @@ void main() { ...@@ -135,7 +133,9 @@ void main() {
child: new AsyncImage( child: new AsyncImage(
provider: imageProvider2 provider: imageProvider2
) )
) ),
null,
EnginePhase.layout
); );
renderImage = key.currentContext.findRenderObject(); renderImage = key.currentContext.findRenderObject();
...@@ -152,18 +152,16 @@ void main() { ...@@ -152,18 +152,16 @@ void main() {
new AsyncImage( new AsyncImage(
key: key, key: key,
provider: imageProvider1 provider: imageProvider1
) ),
null,
EnginePhase.layout
); );
RenderImage renderImage = key.currentContext.findRenderObject(); RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull); 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(); imageProvider1.complete();
tester.pump(); tester.async.flushMicrotasks(); // resolve the future from the image provider
tester.pump(); tester.pump(null, EnginePhase.layout);
debugRenderingExceptionHandler = originalRenderingExceptionHandler;
renderImage = key.currentContext.findRenderObject(); renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull); expect(renderImage.image, isNotNull);
...@@ -173,7 +171,9 @@ void main() { ...@@ -173,7 +171,9 @@ void main() {
new AsyncImage( new AsyncImage(
key: key, key: key,
provider: imageProvider2 provider: imageProvider2
) ),
null,
EnginePhase.layout
); );
renderImage = key.currentContext.findRenderObject(); renderImage = key.currentContext.findRenderObject();
......
...@@ -48,20 +48,6 @@ void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) { ...@@ -48,20 +48,6 @@ void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) {
final TestParentData kNonPositioned = new TestParentData(); final TestParentData kNonPositioned = new TestParentData();
void main() { 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', () { test('ParentDataWidget control test', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
...@@ -259,8 +245,6 @@ void main() { ...@@ -259,8 +245,6 @@ void main() {
test('ParentDataWidget conflicting data', () { test('ParentDataWidget conflicting data', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
expect(cachedException, isNull);
tester.pumpWidget( tester.pumpWidget(
new Stack( new Stack(
children: <Widget>[ children: <Widget>[
...@@ -276,14 +260,11 @@ void main() { ...@@ -276,14 +260,11 @@ void main() {
] ]
) )
); );
expect(tester.takeException(), isNotNull);
expect(cachedException, isNotNull);
cachedException = null;
tester.pumpWidget(new Stack()); tester.pumpWidget(new Stack());
checkTree(tester, <TestParentData>[]); checkTree(tester, <TestParentData>[]);
expect(cachedException, isNull);
tester.pumpWidget( tester.pumpWidget(
new Container( new Container(
...@@ -298,9 +279,7 @@ void main() { ...@@ -298,9 +279,7 @@ void main() {
) )
) )
); );
expect(tester.takeException(), isNotNull);
expect(cachedException, isNotNull);
cachedException = null;
tester.pumpWidget( tester.pumpWidget(
new Stack() new Stack()
......
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
import 'dart:ui' as ui show window; import 'dart:ui' as ui show window;
import 'package:quiver/testing/async.dart'; import 'package:flutter/services.dart';
import 'package:quiver/time.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:quiver/testing/async.dart';
import 'package:quiver/time.dart';
import 'instrumentation.dart'; import 'instrumentation.dart';
...@@ -89,6 +90,21 @@ class WidgetTester extends Instrumentation { ...@@ -89,6 +90,21 @@ class WidgetTester extends Instrumentation {
/// The supplied EnginePhase is the final phase reached during the pump pass; /// The supplied EnginePhase is the final phase reached during the pump pass;
/// if not supplied, the whole pass is executed. /// if not supplied, the whole pass is executed.
void pumpWidget(Widget widget, [ Duration duration, EnginePhase phase ]) { 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) { if (binding is _SteppedWidgetFlutterBinding) {
// Some tests call WidgetFlutterBinding.ensureInitialized() manually, so // Some tests call WidgetFlutterBinding.ensureInitialized() manually, so
// we can't actually be sure we have a stepped binding. // we can't actually be sure we have a stepped binding.
...@@ -98,8 +114,10 @@ class WidgetTester extends Instrumentation { ...@@ -98,8 +114,10 @@ class WidgetTester extends Instrumentation {
// Can't step to a given phase in that case // Can't step to a given phase in that case
assert(phase == null); assert(phase == null);
} }
runApp(widget); binding.handleBeginFrame(new Duration(
pump(duration); milliseconds: clock.now().millisecondsSinceEpoch)
);
async.flushMicrotasks();
} }
/// Artificially calls dispatchLocaleChanged on the Widget binding, /// Artificially calls dispatchLocaleChanged on the Widget binding,
...@@ -110,46 +128,72 @@ class WidgetTester extends Instrumentation { ...@@ -110,46 +128,72 @@ class WidgetTester extends Instrumentation {
async.flushMicrotasks(); 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 @override
void dispatchEvent(PointerEvent event, HitTestResult result) { void dispatchEvent(PointerEvent event, HitTestResult result) {
super.dispatchEvent(event, result); super.dispatchEvent(event, result);
async.flushMicrotasks(); 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)) { void testWidgets(callback(WidgetTester tester)) {
new FakeAsync().run((FakeAsync async) { new FakeAsync().run((FakeAsync async) {
WidgetTester tester = new WidgetTester._(async); FlutterExceptionHandler oldHandler = FlutterError.onError;
runApp(new Container(key: new UniqueKey())); // Reset the tree to a known state. try {
callback(tester); WidgetTester tester = new WidgetTester._(async);
runApp(new Container(key: new UniqueKey())); // Unmount any remaining widgets. FlutterError.onError = (FlutterErrorDetails details) {
async.flushMicrotasks(); if (tester._pendingException != null) {
assert(() { FlutterError.dumpErrorToConsole(tester._pendingException);
"An animation is still running even after the widget tree was disposed."; FlutterError.dumpErrorToConsole(details.exception);
return Scheduler.instance.transientCallbackCount == 0; tester._pendingException = 'An uncaught exception was thrown.';
}); throw details.exception;
assert(() { }
"A Timer is still running even after the widget tree was disposed."; tester._pendingException = details;
return async.periodicTimerCount == 0; };
}); runApp(new Container(key: new UniqueKey())); // Reset the tree to a known state.
assert(() { callback(tester);
"A Timer is still running even after the widget tree was disposed."; runApp(new Container(key: new UniqueKey())); // Unmount any remaining widgets.
return async.nonPeriodicTimerCount == 0; async.flushMicrotasks();
}); assert(() {
assert(async.microtaskCount == 0); // Shouldn't be possible. "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;
}
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment