mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
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:
parent
3cd58547cd
commit
ee703da9de
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
));
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ class _MixedViewportElement extends RenderObjectElement {
|
|||
}
|
||||
owner.lockState(() {
|
||||
_doLayout(constraints);
|
||||
}, building: true);
|
||||
}, building: true, context: 'during $runtimeType layout');
|
||||
}
|
||||
|
||||
void postLayout() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue