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.
This commit is contained in:
Ian Hickson 2016-04-02 10:24:12 -07:00
parent 3cd58547cd
commit ee703da9de
17 changed files with 496 additions and 287 deletions

View file

@ -16,8 +16,6 @@ import 'events.dart';
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 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl
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 @@ abstract class Gesturer extends BindingBase implements HitTestTarget, HitTestabl
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 @@ 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;
}

View file

@ -11,8 +11,6 @@ import 'events.dart';
/// 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 @@ class PointerRouter {
_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 @@ class PointerRouter {
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;
}

View file

@ -149,14 +149,14 @@ class MojoClient {
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();

View file

@ -795,10 +795,13 @@ abstract class RenderBox extends RenderObject {
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;
});

View file

@ -394,16 +394,6 @@ typedef void RenderObjectVisitor(RenderObject child);
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 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
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 @@ abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, Parent
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;
}

View file

@ -24,16 +24,8 @@ double timeDilation = 1.0;
/// 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 @@ abstract class Scheduler extends BindingBase {
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'
));
}
}

View file

@ -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 @@ class FlutterError extends AssertionError {
/// 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 @@ class FlutterError extends AssertionError {
@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);
}
}

View file

@ -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_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 @@ Future<mojom.UrlResponse> fetch(mojom.UrlRequest request, { bool require200: fal
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();

View file

@ -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 @@ class ImageResource {
_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 @@ class ImageResource {
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 @@ class ImageResource {
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

View file

@ -170,7 +170,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
} else {
element.update(this);
}
}, building: true);
}, building: true, context: 'while attaching root widget to rendering tree');
return element;
}

View file

@ -9,6 +9,7 @@ import 'dart:developer';
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 @@ class BuildOwner {
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 @@ class BuildOwner {
});
try {
callback();
} catch (e, stack) {
_debugReportException(context, e, stack);
} finally {
assert(() {
_debugStateLockLevel -= 1;
@ -766,18 +769,25 @@ class BuildOwner {
}
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 @@ 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) {
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
));
}

View file

@ -254,7 +254,7 @@ class _MixedViewportElement extends RenderObjectElement {
}
owner.lockState(() {
_doLayout(constraints);
}, building: true);
}, building: true, context: 'during $runtimeType layout');
}
void postLayout() {

View file

@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement {
assert(startOffsetBase != null);
assert(startOffsetLimit != null);
_updatePaintOffset();
owner.lockState(_materializeChildren, building: true);
owner.lockState(_materializeChildren, building: true, context: 'during $runtimeType layout');
}
void _materializeChildren() {

View file

@ -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 @@ class BadDisposeWidgetState extends State<BadDisposeWidget> {
@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 @@ void main() {
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);
});
});
}

View file

@ -112,18 +112,16 @@ void main() {
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 @@ void main() {
child: new AsyncImage(
provider: imageProvider2
)
)
),
null,
EnginePhase.layout
);
renderImage = key.currentContext.findRenderObject();
@ -152,18 +152,16 @@ void main() {
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 @@ void main() {
new AsyncImage(
key: key,
provider: imageProvider2
)
),
null,
EnginePhase.layout
);
renderImage = key.currentContext.findRenderObject();

View file

@ -48,20 +48,6 @@ void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) {
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 @@ void main() {
test('ParentDataWidget conflicting data', () {
testWidgets((WidgetTester tester) {
expect(cachedException, isNull);
tester.pumpWidget(
new Stack(
children: <Widget>[
@ -276,14 +260,11 @@ void main() {
]
)
);
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 @@ void main() {
)
)
);
expect(cachedException, isNotNull);
cachedException = null;
expect(tester.takeException(), isNotNull);
tester.pumpWidget(
new Stack()

View file

@ -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 @@ class WidgetTester extends Instrumentation {
/// 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 @@ class WidgetTester extends Instrumentation {
// 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 @@ class WidgetTester extends Instrumentation {
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;
}
});
}