Unverified Commit 63aa5b36 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Refactor core uses of FlutterError. (#30983)

Make FlutterError objects more structured so they can be displayed better in debugging tools such as Dart DevTools.
parent f8dfa367
<<skip until matching line>> <<skip until matching line>>
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test: The following assertion was thrown running a test:
Guarded function conflict\. You must use "await" with all Future-returning test APIs\. Guarded function conflict\.
The guarded "guardedHelper" function was called from .*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart on line [0-9]+\. You must use "await" with all Future-returning test APIs\.
Then, the "expect" function was called from .*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart on line [0-9]+\. The guarded "guardedHelper" function was called from
The first function \(guardedHelper\) had not yet finished executing at the time that the second function \(expect\) was called\. Since both are guarded, and the second was not a nested call inside the first, the first must complete its execution before the second can be called\. Typically, this is achieved by putting an "await" statement in front of the call to the first\. .*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart[ \n]on[ \n]line[ \n][0-9]+\.
If you are confident that all test APIs are being called using "await", and this expect\(\) call is not being called at the top level but is itself being called from some sort of callback registered before the guardedHelper method was called, then consider using expectSync\(\) instead\. Then, the "expect" function was called from
.*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart[ \n]on[ \n]line[ \n][0-9]+\.
The first function \(guardedHelper\) had not yet finished executing at the time that the second
function \(expect\) was called\. Since both are guarded, and the second was not a nested call inside
the first, the first must complete its execution before the second can be called\. Typically, this is
achieved by putting an "await" statement in front of the call to the first\.
If you are confident that all test APIs are being called using "await", and this expect\(\) call is
not being called at the top level but is itself being called from some sort of callback registered
before the guardedHelper method was called, then consider using expectSync\(\) instead\.
When the first function \(guardedHelper\) was called, this was the stack: When the first function \(guardedHelper\) was called, this was the stack:
<<skip until matching line>> <<skip until matching line>>
......
...@@ -2,10 +2,16 @@ ...@@ -2,10 +2,16 @@
<<skip until matching line>> <<skip until matching line>>
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test: The following assertion was thrown running a test:
Guarded function conflict\. You must use "await" with all Future-returning test APIs\. Guarded function conflict\.
The guarded method "pump" from class WidgetTester was called from .*dev/automated_tests/flutter_test/test_async_utils_unguarded_test.dart on line [0-9]+\. You must use "await" with all Future-returning test APIs\.
Then, it was called again from .*dev/automated_tests/flutter_test/test_async_utils_unguarded_test.dart on line [0-9]+\. The guarded method "pump" from class WidgetTester was called from
The first method had not yet finished executing at the time that the second method was called\. Since both are guarded, and the second was not a nested call inside the first, the first must complete its execution before the second can be called\. Typically, this is achieved by putting an "await" statement in front of the call to the first\. .*dev/automated_tests/flutter_test/test_async_utils_unguarded_test.dart[ \n]on[ \n]line[ \n][0-9]+\.
Then, it was called again from
.*dev/automated_tests/flutter_test/test_async_utils_unguarded_test.dart[ \n]on[ \n]line[ \n][0-9]+\.
The first method had not yet finished executing at the time that the second method was called\. Since
both are guarded, and the second was not a nested call inside the first, the first must complete its
execution before the second can be called\. Typically, this is achieved by putting an "await"
statement in front of the call to the first\.
When the first method was called, this was the stack: When the first method was called, this was the stack:
<<skip until matching line>> <<skip until matching line>>
......
...@@ -16,7 +16,7 @@ void main() { ...@@ -16,7 +16,7 @@ void main() {
handleUncaughtError:(Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) { handleUncaughtError:(Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: error, exception: error,
context: 'In the Zone handleUncaughtError handler', context: ErrorDescription('In the Zone handleUncaughtError handler'),
silent: false, silent: false,
)); ));
}); });
......
...@@ -129,10 +129,13 @@ mixin AnimationLocalListenersMixin { ...@@ -129,10 +129,13 @@ mixin AnimationLocalListenersMixin {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'animation library', library: 'animation library',
context: 'while notifying listeners for $runtimeType', context: ErrorDescription('while notifying listeners for $runtimeType'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('The $runtimeType notifying listeners was:'); yield DiagnosticsProperty<AnimationLocalListenersMixin>(
information.write(' $this'); 'The $runtimeType notifying listeners was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
}, },
)); ));
} }
...@@ -195,10 +198,13 @@ mixin AnimationLocalStatusListenersMixin { ...@@ -195,10 +198,13 @@ mixin AnimationLocalStatusListenersMixin {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'animation library', library: 'animation library',
context: 'while notifying status listeners for $runtimeType', context: ErrorDescription('while notifying status listeners for $runtimeType'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('The $runtimeType notifying status listeners was:'); yield DiagnosticsProperty<AnimationLocalStatusListenersMixin>(
information.write(' $this'); 'The $runtimeType notifying status listeners was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
}, },
)); ));
} }
......
...@@ -522,7 +522,7 @@ abstract class BindingBase { ...@@ -522,7 +522,7 @@ abstract class BindingBase {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: caughtException, exception: caughtException,
stack: caughtStack, stack: caughtStack,
context: 'during a service extension callback for "$method"', context: ErrorDescription('during a service extension callback for "$method"'),
)); ));
return developer.ServiceExtensionResponse.error( return developer.ServiceExtensionResponse.error(
developer.ServiceExtensionResponse.extensionError, developer.ServiceExtensionResponse.extensionError,
......
...@@ -209,10 +209,13 @@ class ChangeNotifier implements Listenable { ...@@ -209,10 +209,13 @@ class ChangeNotifier implements Listenable {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'foundation library', library: 'foundation library',
context: 'while dispatching notifications for $runtimeType', context: ErrorDescription('while dispatching notifications for $runtimeType'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('The $runtimeType sending notification was:'); yield DiagnosticsProperty<ChangeNotifier>(
information.write(' $this'); 'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
}, },
)); ));
} }
......
...@@ -183,12 +183,11 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -183,12 +183,11 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'gesture library', library: 'gesture library',
context: 'while dispatching a non-hit-tested pointer event', context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
event: event, event: event,
hitTestEntry: null, hitTestEntry: null,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Event:'); yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
information.writeln(' $event');
}, },
)); ));
} }
...@@ -202,14 +201,12 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -202,14 +201,12 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'gesture library', library: 'gesture library',
context: 'while dispatching a pointer event', context: ErrorDescription('while dispatching a pointer event'),
event: event, event: event,
hitTestEntry: entry, hitTestEntry: entry,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Event:'); yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
information.writeln(' $event'); yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
information.writeln('Target:');
information.write(' ${entry.target}');
}, },
)); ));
} }
...@@ -244,7 +241,7 @@ class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails { ...@@ -244,7 +241,7 @@ class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails {
dynamic exception, dynamic exception,
StackTrace stack, StackTrace stack,
String library, String library,
String context, DiagnosticsNode context,
this.event, this.event,
this.hitTestEntry, this.hitTestEntry,
InformationCollector informationCollector, InformationCollector informationCollector,
......
...@@ -76,13 +76,12 @@ class PointerRouter { ...@@ -76,13 +76,12 @@ class PointerRouter {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'gesture library', library: 'gesture library',
context: 'while routing a pointer event', context: ErrorDescription('while routing a pointer event'),
router: this, router: this,
route: route, route: route,
event: event, event: event,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Event:'); yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
information.write(' $event');
}, },
)); ));
} }
...@@ -123,7 +122,7 @@ class FlutterErrorDetailsForPointerRouter extends FlutterErrorDetails { ...@@ -123,7 +122,7 @@ class FlutterErrorDetailsForPointerRouter extends FlutterErrorDetails {
dynamic exception, dynamic exception,
StackTrace stack, StackTrace stack,
String library, String library,
String context, DiagnosticsNode context,
this.router, this.router,
this.route, this.route,
this.event, this.event,
......
...@@ -55,10 +55,9 @@ class PointerSignalResolver { ...@@ -55,10 +55,9 @@ class PointerSignalResolver {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'gesture library', library: 'gesture library',
context: 'while resolving a PointerSignalEvent', context: ErrorDescription('while resolving a PointerSignalEvent'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Event:'); yield DiagnosticsProperty<PointerSignalEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
information.write(' $event');
}, },
)); ));
} }
......
...@@ -169,12 +169,11 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -169,12 +169,11 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'gesture', library: 'gesture',
context: 'while handling a gesture', context: ErrorDescription('while handling a gesture'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Handler: $name'); yield StringProperty('Handler', name);
information.writeln('Recognizer:'); yield DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty);
information.writeln(' $this'); }
},
)); ));
} }
return result; return result;
......
...@@ -362,7 +362,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -362,7 +362,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
'The onRefresh callback returned null.\n' 'The onRefresh callback returned null.\n'
'The RefreshIndicator onRefresh callback must return a Future.' 'The RefreshIndicator onRefresh callback must return a Future.'
), ),
context: 'when calling onRefresh', context: ErrorDescription('when calling onRefresh'),
library: 'material library', library: 'material library',
)); ));
return true; return true;
......
...@@ -274,14 +274,12 @@ abstract class ImageProvider<T> { ...@@ -274,14 +274,12 @@ abstract class ImageProvider<T> {
imageCompleter.setError( imageCompleter.setError(
exception: exception, exception: exception,
stack: stack, stack: stack,
context: 'while resolving an image', context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Image provider: $this'); yield DiagnosticsProperty<ImageProvider>('Image provider', this);
information.writeln('Image configuration: $configuration'); yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
if (obtainedKey != null) { yield DiagnosticsProperty<T>('Image key', obtainedKey, defaultValue: null);
information.writeln('Image key: $obtainedKey');
}
}, },
); );
} }
...@@ -448,9 +446,9 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe ...@@ -448,9 +446,9 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key),
scale: key.scale, scale: key.scale,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Image provider: $this'); yield DiagnosticsProperty<ImageProvider>('Image provider', this);
information.write('Image key: $key'); yield DiagnosticsProperty<AssetBundleImageKey>('Image key', key);
}, },
); );
} }
...@@ -505,9 +503,9 @@ class NetworkImage extends ImageProvider<NetworkImage> { ...@@ -505,9 +503,9 @@ class NetworkImage extends ImageProvider<NetworkImage> {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key),
scale: key.scale, scale: key.scale,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Image provider: $this'); yield DiagnosticsProperty<ImageProvider>('Image provider', this);
information.write('Image key: $key'); yield DiagnosticsProperty<NetworkImage>('Image key', key);
}, },
); );
} }
...@@ -579,8 +577,8 @@ class FileImage extends ImageProvider<FileImage> { ...@@ -579,8 +577,8 @@ class FileImage extends ImageProvider<FileImage> {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key),
scale: key.scale, scale: key.scale,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('Path: ${file?.path}'); yield ErrorDescription('Path: ${file?.path}');
}, },
); );
} }
...@@ -815,7 +813,7 @@ class _ErrorImageCompleter extends ImageStreamCompleter { ...@@ -815,7 +813,7 @@ class _ErrorImageCompleter extends ImageStreamCompleter {
_ErrorImageCompleter(); _ErrorImageCompleter();
void setError({ void setError({
String context, DiagnosticsNode context,
dynamic exception, dynamic exception,
StackTrace stack, StackTrace stack,
InformationCollector informationCollector, InformationCollector informationCollector,
......
...@@ -284,7 +284,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -284,7 +284,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
listener(_currentImage, true); listener(_currentImage, true);
} catch (exception, stack) { } catch (exception, stack) {
reportError( reportError(
context: 'by a synchronously-called image listener', context: ErrorDescription('by a synchronously-called image listener'),
exception: exception, exception: exception,
stack: stack, stack: stack,
); );
...@@ -298,7 +298,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -298,7 +298,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
FlutterErrorDetails( FlutterErrorDetails(
exception: exception, exception: exception,
library: 'image resource service', library: 'image resource service',
context: 'by a synchronously-called image error listener', context: ErrorDescription('by a synchronously-called image error listener'),
stack: stack, stack: stack,
), ),
); );
...@@ -336,7 +336,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -336,7 +336,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
listener(image, false); listener(image, false);
} catch (exception, stack) { } catch (exception, stack) {
reportError( reportError(
context: 'by an image listener', context: ErrorDescription('by an image listener'),
exception: exception, exception: exception,
stack: stack, stack: stack,
); );
...@@ -374,7 +374,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -374,7 +374,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// See [FlutterErrorDetails] for further details on these values. /// See [FlutterErrorDetails] for further details on these values.
@protected @protected
void reportError({ void reportError({
String context, DiagnosticsNode context,
dynamic exception, dynamic exception,
StackTrace stack, StackTrace stack,
InformationCollector informationCollector, InformationCollector informationCollector,
...@@ -405,7 +405,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -405,7 +405,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError( FlutterError.reportError(
FlutterErrorDetails( FlutterErrorDetails(
context: 'when reporting an error to an image listener', context: ErrorDescription('when reporting an error to an image listener'),
library: 'image resource service', library: 'image resource service',
exception: exception, exception: exception,
stack: stack, stack: stack,
...@@ -451,7 +451,7 @@ class OneFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -451,7 +451,7 @@ class OneFrameImageStreamCompleter extends ImageStreamCompleter {
: assert(image != null) { : assert(image != null) {
image.then<void>(setImage, onError: (dynamic error, StackTrace stack) { image.then<void>(setImage, onError: (dynamic error, StackTrace stack) {
reportError( reportError(
context: 'resolving a single-frame image stream', context: ErrorDescription('resolving a single-frame image stream'),
exception: error, exception: error,
stack: stack, stack: stack,
informationCollector: informationCollector, informationCollector: informationCollector,
...@@ -511,7 +511,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -511,7 +511,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_scale = scale { _scale = scale {
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) { codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError( reportError(
context: 'resolving an image codec', context: ErrorDescription('resolving an image codec'),
exception: error, exception: error,
stack: stack, stack: stack,
informationCollector: informationCollector, informationCollector: informationCollector,
...@@ -579,7 +579,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -579,7 +579,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_nextFrame = await _codec.getNextFrame(); _nextFrame = await _codec.getNextFrame();
} catch (exception, stack) { } catch (exception, stack) {
reportError( reportError(
context: 'resolving an image frame', context: ErrorDescription('resolving an image frame'),
exception: exception, exception: exception,
stack: stack, stack: stack,
informationCollector: _informationCollector, informationCollector: _informationCollector,
......
This diff is collapsed.
...@@ -198,16 +198,23 @@ mixin DebugOverflowIndicatorMixin on RenderObject { ...@@ -198,16 +198,23 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
return regions; return regions;
} }
void _reportOverflow(RelativeRect overflow, String overflowHints) { void _reportOverflow(RelativeRect overflow, List<DiagnosticsNode> overflowHints) {
overflowHints ??= 'The edge of the $runtimeType that is ' overflowHints ??= <DiagnosticsNode>[];
'overflowing has been marked in the rendering with a yellow and black ' if (overflowHints.isEmpty) {
'striped pattern. This is usually caused by the contents being too big ' overflowHints.add(ErrorDescription(
'for the $runtimeType.\n' 'The edge of the $runtimeType that is '
'This is considered an error condition because it indicates that there ' 'overflowing has been marked in the rendering with a yellow and black '
'is content that cannot be seen. If the content is legitimately bigger ' 'striped pattern. This is usually caused by the contents being too big '
'than the available space, consider clipping it with a ClipRect widget ' 'for the $runtimeType.'
'before putting it in the $runtimeType, or using a scrollable ' ));
'container, like a ListView.'; overflowHints.add(ErrorHint(
'This is considered an error condition because it indicates that there '
'is content that cannot be seen. If the content is legitimately bigger '
'than the available space, consider clipping it with a ClipRect widget '
'before putting it in the $runtimeType, or using a scrollable '
'container, like a ListView.'
));
}
final List<String> overflows = <String>[]; final List<String> overflows = <String>[];
if (overflow.left > 0.0) if (overflow.left > 0.0)
...@@ -232,18 +239,22 @@ mixin DebugOverflowIndicatorMixin on RenderObject { ...@@ -232,18 +239,22 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
overflows[overflows.length - 1] = 'and ${overflows[overflows.length - 1]}'; overflows[overflows.length - 1] = 'and ${overflows[overflows.length - 1]}';
overflowText = overflows.join(', '); overflowText = overflows.join(', ');
} }
// TODO(jacobr): add the overflows in pixels as structured data so they can
// be visualized in debugging tools.
FlutterError.reportError( FlutterError.reportError(
FlutterErrorDetailsForRendering( FlutterErrorDetailsForRendering(
exception: 'A $runtimeType overflowed by $overflowText.', exception: FlutterError('A $runtimeType overflowed by $overflowText.'),
library: 'rendering library', library: 'rendering library',
context: 'during layout', context: ErrorDescription('during layout'),
renderObject: this, renderObject: this,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln(overflowHints); yield* overflowHints;
information.writeln('The specific $runtimeType in question is:'); yield describeForError('The specific $runtimeType in question is');
information.writeln(' ${toStringShallow(joiner: '\n ')}'); // TODO(jacobr): this line is ascii art that it would be nice to
information.writeln('◢◤' * (FlutterError.wrapWidth ~/ 2)); // handle a little more generically in GUI debugging clients in the
}, // future.
yield DiagnosticsNode.message('◢◤' * (FlutterError.wrapWidth ~/ 2), allowWrap: false);
}
), ),
); );
} }
...@@ -259,7 +270,7 @@ mixin DebugOverflowIndicatorMixin on RenderObject { ...@@ -259,7 +270,7 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
Offset offset, Offset offset,
Rect containerRect, Rect containerRect,
Rect childRect, { Rect childRect, {
String overflowHints, List<DiagnosticsNode> overflowHints,
}) { }) {
final RelativeRect overflow = RelativeRect.fromRect(containerRect, childRect); final RelativeRect overflow = RelativeRect.fromRect(containerRect, childRect);
......
...@@ -653,15 +653,16 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl ...@@ -653,15 +653,16 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
final String identity = _direction == Axis.horizontal ? 'row' : 'column'; final String identity = _direction == Axis.horizontal ? 'row' : 'column';
final String axis = _direction == Axis.horizontal ? 'horizontal' : 'vertical'; final String axis = _direction == Axis.horizontal ? 'horizontal' : 'vertical';
final String dimension = _direction == Axis.horizontal ? 'width' : 'height'; final String dimension = _direction == Axis.horizontal ? 'width' : 'height';
String error, message; DiagnosticsNode error, message;
String addendum = ''; final List<DiagnosticsNode> addendum = <DiagnosticsNode>[];
if (!canFlex && (mainAxisSize == MainAxisSize.max || _getFit(child) == FlexFit.tight)) { if (!canFlex && (mainAxisSize == MainAxisSize.max || _getFit(child) == FlexFit.tight)) {
error = 'RenderFlex children have non-zero flex but incoming $dimension constraints are unbounded.'; error = ErrorSummary('RenderFlex children have non-zero flex but incoming $dimension constraints are unbounded.');
message = 'When a $identity is in a parent that does not provide a finite $dimension constraint, for example ' message = ErrorDescription(
'if it is in a $axis scrollable, it will try to shrink-wrap its children along the $axis ' 'When a $identity is in a parent that does not provide a finite $dimension constraint, for example '
'axis. Setting a flex on a child (e.g. using Expanded) indicates that the child is to ' 'if it is in a $axis scrollable, it will try to shrink-wrap its children along the $axis '
'expand to fill the remaining space in the $axis direction.'; 'axis. Setting a flex on a child (e.g. using Expanded) indicates that the child is to '
final StringBuffer information = StringBuffer(); 'expand to fill the remaining space in the $axis direction.'
);
RenderBox node = this; RenderBox node = this;
switch (_direction) { switch (_direction) {
case Axis.horizontal: case Axis.horizontal:
...@@ -678,36 +679,38 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl ...@@ -678,36 +679,38 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
break; break;
} }
if (node != null) { if (node != null) {
information.writeln('The nearest ancestor providing an unbounded width constraint is:'); addendum.add(node.describeForError('The nearest ancestor providing an unbounded width constraint is'));
information.write(' ');
information.writeln(node.toStringShallow(joiner: '\n '));
} }
information.writeln('See also: https://flutter.dev/layout/'); addendum.add(ErrorHint('See also: https://flutter.dev/layout/'));
addendum = information.toString();
} else { } else {
return true; return true;
} }
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'$error\n' error,
'$message\n' message,
'These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child ' ErrorDescription(
'cannot simultaneously expand to fit its parent.\n' 'These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child '
'Consider setting mainAxisSize to MainAxisSize.min and using FlexFit.loose fits for the flexible ' 'cannot simultaneously expand to fit its parent.'
'children (using Flexible rather than Expanded). This will allow the flexible children ' ),
'to size themselves to less than the infinite remaining space they would otherwise be ' ErrorHint(
'forced to take, and then will cause the RenderFlex to shrink-wrap the children ' 'Consider setting mainAxisSize to MainAxisSize.min and using FlexFit.loose fits for the flexible '
'rather than expanding to fit the maximum constraints provided by the parent.\n' 'children (using Flexible rather than Expanded). This will allow the flexible children '
'The affected RenderFlex is:\n' 'to size themselves to less than the infinite remaining space they would otherwise be '
' $this\n' 'forced to take, and then will cause the RenderFlex to shrink-wrap the children '
'The creator information is set to:\n' 'rather than expanding to fit the maximum constraints provided by the parent.'
' $debugCreator\n' ),
'$addendum' ErrorDescription(
'If this message did not help you determine the problem, consider using debugDumpRenderTree():\n' 'If this message did not help you determine the problem, consider using debugDumpRenderTree():\n'
' https://flutter.dev/debugging/#rendering-layer\n' ' https://flutter.dev/debugging/#rendering-layer\n'
' http://docs.flutter.io/flutter/rendering/debugDumpRenderTree.html\n' ' http://docs.flutter.io/flutter/rendering/debugDumpRenderTree.html'
'If none of the above helps enough to fix this problem, please don\'t hesitate to file a bug:\n' ),
' https://github.com/flutter/flutter/issues/new?template=BUG.md' describeForError('The affected RenderFlex is', style: DiagnosticsTreeStyle.errorProperty),
); DiagnosticsProperty<dynamic>('The creator information is set to', debugCreator, style: DiagnosticsTreeStyle.errorProperty)
]..addAll(addendum)
..add(ErrorDescription(
'If none of the above helps enough to fix this problem, please don\'t hesitate to file a bug:\n'
' https://github.com/flutter/flutter/issues/new?template=BUG.md'
)));
}()); }());
totalFlex += childParentData.flex; totalFlex += childParentData.flex;
lastFlexChild = child; lastFlexChild = child;
...@@ -953,19 +956,28 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl ...@@ -953,19 +956,28 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
assert(() { assert(() {
// Only set this if it's null to save work. It gets reset to null if the // Only set this if it's null to save work. It gets reset to null if the
// _direction changes. // _direction changes.
final String debugOverflowHints = final List<DiagnosticsNode> debugOverflowHints = <DiagnosticsNode>[
'The overflowing $runtimeType has an orientation of $_direction.\n' ErrorDescription(
'The edge of the $runtimeType that is overflowing has been marked ' 'The overflowing $runtimeType has an orientation of $_direction.'
'in the rendering with a yellow and black striped pattern. This is ' ),
'usually caused by the contents being too big for the $runtimeType. ' ErrorDescription(
'Consider applying a flex factor (e.g. using an Expanded widget) to ' 'The edge of the $runtimeType that is overflowing has been marked '
'force the children of the $runtimeType to fit within the available ' 'in the rendering with a yellow and black striped pattern. This is '
'space instead of being sized to their natural size.\n' 'usually caused by the contents being too big for the $runtimeType.'
'This is considered an error condition because it indicates that there ' ),
'is content that cannot be seen. If the content is legitimately bigger ' ErrorHint(
'than the available space, consider clipping it with a ClipRect widget ' 'Consider applying a flex factor (e.g. using an Expanded widget) to '
'before putting it in the flex, or using a scrollable container rather ' 'force the children of the $runtimeType to fit within the available '
'than a Flex, like a ListView.'; 'space instead of being sized to their natural size.'
),
ErrorHint(
'This is considered an error condition because it indicates that there '
'is content that cannot be seen. If the content is legitimately bigger '
'than the available space, consider clipping it with a ClipRect widget '
'before putting it in the flex, or using a scrollable container rather '
'than a Flex, like a ListView.'
)
];
// Simulate a child rect that overflows by the right amount. This child // Simulate a child rect that overflows by the right amount. This child
// rect is never used for drawing, just for determining the overflow // rect is never used for drawing, just for determining the overflow
......
...@@ -529,13 +529,11 @@ class ContainerLayer extends Layer { ...@@ -529,13 +529,11 @@ class ContainerLayer extends Layer {
'See https://api.flutter.dev/flutter/rendering/debugCheckElevations.html ' 'See https://api.flutter.dev/flutter/rendering/debugCheckElevations.html '
'for more details.'), 'for more details.'),
library: 'rendering library', library: 'rendering library',
context: 'during compositing', context: ErrorDescription('during compositing'),
informationCollector: (StringBuffer buffer) { informationCollector: () sync* {
buffer.writeln('Attempted to composite layer:'); yield child.toDiagnosticsNode(name: 'Attempted to composite layer', style: DiagnosticsTreeStyle.errorProperty);
buffer.writeln(child); yield predecessor.toDiagnosticsNode(name: 'after layer', style: DiagnosticsTreeStyle.errorProperty);
buffer.writeln('after layer:'); yield ErrorDescription('which occupies the same area at a higher elevation.');
buffer.writeln(predecessor);
buffer.writeln('which occupies the same area at a higher elevation.');
} }
)); ));
return <PictureLayer>[ return <PictureLayer>[
......
...@@ -17,7 +17,7 @@ import 'binding.dart'; ...@@ -17,7 +17,7 @@ import 'binding.dart';
import 'debug.dart'; import 'debug.dart';
import 'layer.dart'; import 'layer.dart';
export 'package:flutter/foundation.dart' show FlutterError, InformationCollector, DiagnosticsNode, DiagnosticsProperty, StringProperty, DoubleProperty, EnumProperty, FlagProperty, IntProperty, DiagnosticPropertiesBuilder; export 'package:flutter/foundation.dart' show FlutterError, InformationCollector, DiagnosticsNode, ErrorSummary, ErrorDescription, ErrorHint, DiagnosticsProperty, StringProperty, DoubleProperty, EnumProperty, FlagProperty, IntProperty, DiagnosticPropertiesBuilder;
export 'package:flutter/gestures.dart' show HitTestEntry, HitTestResult; export 'package:flutter/gestures.dart' show HitTestEntry, HitTestResult;
export 'package:flutter/painting.dart'; export 'package:flutter/painting.dart';
...@@ -1187,38 +1187,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1187,38 +1187,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'rendering library', library: 'rendering library',
context: 'during $method()', context: ErrorDescription('during $method()'),
renderObject: this, renderObject: this,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('The following RenderObject was being processed when the exception was fired:'); yield describeForError('The following RenderObject was being processed when the exception was fired');
information.writeln(' ${toStringShallow(joiner: '\n ')}'); // TODO(jacobr): this error message has a code smell. Consider whether
final List<String> descendants = <String>[]; // displaying the truncated children is really useful for command line
const int maxDepth = 5; // users. Inspector users can see the full tree by clicking on the
int depth = 0; // render object so this may not be that useful.
const int maxLines = 25; yield describeForError('This RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
int lines = 0; }
void visitor(RenderObject child) {
if (lines < maxLines) {
depth += 1;
descendants.add('${" " * depth}$child');
if (depth < maxDepth)
child.visitChildren(visitor);
depth -= 1;
} else if (lines == maxLines) {
descendants.add(' ...(descendants list truncated after $lines lines)');
}
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');
},
)); ));
} }
...@@ -1558,7 +1536,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1558,7 +1536,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(constraints != null); assert(constraints != null);
assert(constraints.debugAssertIsValid( assert(constraints.debugAssertIsValid(
isAppliedConstraint: true, isAppliedConstraint: true,
informationCollector: (StringBuffer information) { informationCollector: () sync* {
final List<String> stack = StackTrace.current.toString().split('\n'); final List<String> stack = StackTrace.current.toString().split('\n');
int targetFrame; int targetFrame;
final Pattern layoutFramePattern = RegExp(r'^#[0-9]+ +RenderObject.layout \('); final Pattern layoutFramePattern = RegExp(r'^#[0-9]+ +RenderObject.layout \(');
...@@ -1569,18 +1547,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1569,18 +1547,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
} }
} }
if (targetFrame != null && targetFrame < stack.length) { if (targetFrame != null && targetFrame < stack.length) {
information.writeln( final Pattern targetFramePattern = RegExp(r'^#[0-9]+ +(.+)$');
final Match targetFrameMatch = targetFramePattern.matchAsPrefix(stack[targetFrame]);
final String problemFunction = (targetFrameMatch != null && targetFrameMatch.groupCount > 0) ? targetFrameMatch.group(1) : stack[targetFrame].trim();
// TODO(jacobr): this case is similar to displaying a single stack frame.
yield ErrorDescription(
'These invalid constraints were provided to $runtimeType\'s layout() ' 'These invalid constraints were provided to $runtimeType\'s layout() '
'function by the following function, which probably computed the ' 'function by the following function, which probably computed the '
'invalid constraints in question:' 'invalid constraints in question:\n'
' $problemFunction'
); );
final Pattern targetFramePattern = RegExp(r'^#[0-9]+ +(.+)$');
final Match targetFrameMatch = targetFramePattern.matchAsPrefix(stack[targetFrame]);
if (targetFrameMatch != null && targetFrameMatch.groupCount > 0) {
information.writeln(' ${targetFrameMatch.group(1)}');
} else {
information.writeln(stack[targetFrame]);
}
} }
}, },
)); ));
...@@ -2712,6 +2688,19 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2712,6 +2688,19 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
); );
} }
} }
/// Adds a debug representation of a [RenderObject] optimized for including in
/// error messages.
///
/// The default [style] of [DiagnosticsTreeStyle.shallow] ensures that all of
/// the properties of the render object are included in the error output but
/// none of the children of the object are.
///
/// You should always include a RenderObject in an error message if it is the
/// [RenderObject] causing the failure or contract violation of the error.
DiagnosticsNode describeForError(String name, { DiagnosticsTreeStyle style = DiagnosticsTreeStyle.shallow }) {
return toDiagnosticsNode(name: name, style: style);
}
} }
/// Generic mixin for render objects with one child. /// Generic mixin for render objects with one child.
...@@ -3108,7 +3097,7 @@ class FlutterErrorDetailsForRendering extends FlutterErrorDetails { ...@@ -3108,7 +3097,7 @@ class FlutterErrorDetailsForRendering extends FlutterErrorDetails {
dynamic exception, dynamic exception,
StackTrace stack, StackTrace stack,
String library, String library,
String context, DiagnosticsNode context,
this.renderObject, this.renderObject,
InformationCollector informationCollector, InformationCollector informationCollector,
bool silent = false, bool silent = false,
......
...@@ -421,10 +421,14 @@ class SliverConstraints extends Constraints { ...@@ -421,10 +421,14 @@ class SliverConstraints extends Constraints {
void verify(bool check, String message) { void verify(bool check, String message) {
if (check) if (check)
return; return;
final StringBuffer information = StringBuffer(); final List<DiagnosticsNode> information = <DiagnosticsNode>[];
if (informationCollector != null) information.add(ErrorSummary('$runtimeType is not valid: $message'));
informationCollector(information);
throw FlutterError('$runtimeType is not valid: $message\n${information}The offending constraints were:\n $this'); if (informationCollector != null) {
information.addAll(informationCollector());
}
information.add(DiagnosticsProperty<SliverConstraints>('The offending constraints were', this, style: DiagnosticsTreeStyle.errorProperty));
throw FlutterError.fromParts(information);
} }
verify(axis != null, 'The "axis" is null.'); verify(axis != null, 'The "axis" is null.');
verify(growthDirection != null, 'The "growthDirection" is null.'); verify(growthDirection != null, 'The "growthDirection" is null.');
...@@ -696,14 +700,20 @@ class SliverGeometry extends Diagnosticable { ...@@ -696,14 +700,20 @@ class SliverGeometry extends Diagnosticable {
InformationCollector informationCollector, InformationCollector informationCollector,
}) { }) {
assert(() { assert(() {
void verify(bool check, String message) { void verify(bool check, String summary, {List<DiagnosticsNode> details}) {
if (check) if (check)
return; return;
final StringBuffer information = StringBuffer(); final List<DiagnosticsNode> information = <DiagnosticsNode>[];
if (informationCollector != null) information.add(ErrorSummary('$runtimeType is not valid: $summary'));
informationCollector(information); if (details != null) {
throw FlutterError('$runtimeType is not valid: $message\n$information'); information.addAll(details);
}
if (informationCollector != null) {
information.addAll(informationCollector());
}
throw FlutterError.fromParts(information);
} }
verify(scrollExtent != null, 'The "scrollExtent" is null.'); verify(scrollExtent != null, 'The "scrollExtent" is null.');
verify(scrollExtent >= 0.0, 'The "scrollExtent" is negative.'); verify(scrollExtent >= 0.0, 'The "scrollExtent" is negative.');
verify(paintExtent != null, 'The "paintExtent" is null.'); verify(paintExtent != null, 'The "paintExtent" is null.');
...@@ -714,8 +724,8 @@ class SliverGeometry extends Diagnosticable { ...@@ -714,8 +724,8 @@ class SliverGeometry extends Diagnosticable {
verify(cacheExtent >= 0.0, 'The "cacheExtent" is negative.'); verify(cacheExtent >= 0.0, 'The "cacheExtent" is negative.');
if (layoutExtent > paintExtent) { if (layoutExtent > paintExtent) {
verify(false, verify(false,
'The "layoutExtent" exceeds the "paintExtent".\n' + 'The "layoutExtent" exceeds the "paintExtent".',
_debugCompareFloats('paintExtent', paintExtent, 'layoutExtent', layoutExtent), details: _debugCompareFloats('paintExtent', paintExtent, 'layoutExtent', layoutExtent),
); );
} }
verify(maxPaintExtent != null, 'The "maxPaintExtent" is null.'); verify(maxPaintExtent != null, 'The "maxPaintExtent" is null.');
...@@ -723,9 +733,10 @@ class SliverGeometry extends Diagnosticable { ...@@ -723,9 +733,10 @@ class SliverGeometry extends Diagnosticable {
// than precisionErrorTolerance, we will not throw the assert below. // than precisionErrorTolerance, we will not throw the assert below.
if (paintExtent - maxPaintExtent > precisionErrorTolerance) { if (paintExtent - maxPaintExtent > precisionErrorTolerance) {
verify(false, verify(false,
'The "maxPaintExtent" is less than the "paintExtent".\n' + 'The "maxPaintExtent" is less than the "paintExtent".',
_debugCompareFloats('maxPaintExtent', maxPaintExtent, 'paintExtent', paintExtent) + details:
'By definition, a sliver can\'t paint more than the maximum that it can paint!', _debugCompareFloats('maxPaintExtent', maxPaintExtent, 'paintExtent', paintExtent)
..add(ErrorDescription('By definition, a sliver can\'t paint more than the maximum that it can paint!'))
); );
} }
verify(hitTestExtent != null, 'The "hitTestExtent" is null.'); verify(hitTestExtent != null, 'The "hitTestExtent" is null.');
...@@ -866,14 +877,22 @@ class SliverPhysicalParentData extends ParentData { ...@@ -866,14 +877,22 @@ class SliverPhysicalParentData extends ParentData {
/// children using absolute coordinates. /// children using absolute coordinates.
class SliverPhysicalContainerParentData extends SliverPhysicalParentData with ContainerParentDataMixin<RenderSliver> { } class SliverPhysicalContainerParentData extends SliverPhysicalParentData with ContainerParentDataMixin<RenderSliver> { }
String _debugCompareFloats(String labelA, double valueA, String labelB, double valueB) { List<DiagnosticsNode> _debugCompareFloats(String labelA, double valueA, String labelB, double valueB) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
if (valueA.toStringAsFixed(1) != valueB.toStringAsFixed(1)) { if (valueA.toStringAsFixed(1) != valueB.toStringAsFixed(1)) {
return 'The $labelA is ${valueA.toStringAsFixed(1)}, but ' information..add(ErrorDescription(
'the $labelB is ${valueB.toStringAsFixed(1)}. '; 'The $labelA is ${valueA.toStringAsFixed(1)}, but '
'the $labelB is ${valueB.toStringAsFixed(1)}.'
));
} else {
information
..add(ErrorDescription('The $labelA is $valueA, but the $labelB is $valueB.'))
..add(ErrorHint(
'Maybe you have fallen prey to floating point rounding errors, and should explicitly '
'apply the min() or max() functions, or the clamp() method, to the $labelB?'
));
} }
return 'The $labelA is $valueA, but the $labelB is $valueB. ' return information;
'Maybe you have fallen prey to floating point rounding errors, and should explicitly '
'apply the min() or max() functions, or the clamp() method, to the $labelB? ';
} }
/// Base class for the render objects that implement scroll effects in viewports. /// Base class for the render objects that implement scroll effects in viewports.
...@@ -1036,28 +1055,30 @@ abstract class RenderSliver extends RenderObject { ...@@ -1036,28 +1055,30 @@ abstract class RenderSliver extends RenderObject {
(!sizedByParent && debugDoingThisLayout)) (!sizedByParent && debugDoingThisLayout))
return true; return true;
assert(!debugDoingThisResize); assert(!debugDoingThisResize);
String contract, violation, hint; DiagnosticsNode contract, violation, hint;
if (debugDoingThisLayout) { if (debugDoingThisLayout) {
assert(sizedByParent); assert(sizedByParent);
violation = 'It appears that the geometry setter was called from performLayout().'; violation = ErrorDescription('It appears that the geometry setter was called from performLayout().');
hint = '';
} else { } else {
violation = 'The geometry setter was called from outside layout (neither performResize() nor performLayout() were being run for this object).'; violation = ErrorDescription('The geometry setter was called from outside layout (neither performResize() nor performLayout() were being run for this object).');
if (owner != null && owner.debugDoingLayout) if (owner != null && owner.debugDoingLayout)
hint = 'Only the object itself can set its geometry. It is a contract violation for other objects to set it.'; hint = ErrorDescription('Only the object itself can set its geometry. It is a contract violation for other objects to set it.');
} }
if (sizedByParent) if (sizedByParent)
contract = 'Because this RenderSliver has sizedByParent set to true, it must set its geometry in performResize().'; contract = ErrorDescription('Because this RenderSliver has sizedByParent set to true, it must set its geometry in performResize().');
else else
contract = 'Because this RenderSliver has sizedByParent set to false, it must set its geometry in performLayout().'; contract = ErrorDescription('Because this RenderSliver has sizedByParent set to false, it must set its geometry in performLayout().');
throw FlutterError(
'RenderSliver geometry setter called incorrectly.\n' final List<DiagnosticsNode> information = <DiagnosticsNode>[
'$violation\n' ErrorSummary('RenderSliver geometry setter called incorrectly.'),
'$hint\n' violation
'$contract\n' ];
'The RenderSliver in question is:\n' if (hint != null)
' $this' information.add(hint);
); information.add(contract);
information.add(describeForError('The RenderSliver in question is'));
throw FlutterError.fromParts(information);
}()); }());
_geometry = value; _geometry = value;
} }
...@@ -1091,22 +1112,23 @@ abstract class RenderSliver extends RenderObject { ...@@ -1091,22 +1112,23 @@ abstract class RenderSliver extends RenderObject {
@override @override
void debugAssertDoesMeetConstraints() { void debugAssertDoesMeetConstraints() {
assert(geometry.debugAssertIsValid( assert(geometry.debugAssertIsValid(
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('The RenderSliver that returned the offending geometry was:'); yield describeForError('The RenderSliver that returned the offending geometry was');
information.writeln(' ${toStringShallow(joiner: '\n ')}'); }
},
)); ));
assert(() { assert(() {
if (geometry.paintExtent > constraints.remainingPaintExtent) { if (geometry.paintExtent > constraints.remainingPaintExtent) {
throw FlutterError( throw FlutterError.fromParts(<DiagnosticsNode>[
'SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.\n' ErrorSummary('SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.'),
'The render object whose geometry violates the constraints is the following:\n' describeForError('The render object whose geometry violates the constraints is the following')
' ${toStringShallow(joiner: '\n ')}\n' + ]..addAll(_debugCompareFloats(
_debugCompareFloats('remainingPaintExtent', constraints.remainingPaintExtent, 'remainingPaintExtent', constraints.remainingPaintExtent,
'paintExtent', geometry.paintExtent) + 'paintExtent', geometry.paintExtent,
))
..add(ErrorDescription(
'The paintExtent must cause the child sliver to paint within the viewport, and so ' 'The paintExtent must cause the child sliver to paint within the viewport, and so '
'cannot exceed the remainingPaintExtent.' 'cannot exceed the remainingPaintExtent.',
); )));
} }
return true; return true;
}()); }());
......
...@@ -393,14 +393,14 @@ mixin SchedulerBinding on BindingBase, ServicesBinding { ...@@ -393,14 +393,14 @@ mixin SchedulerBinding on BindingBase, ServicesBinding {
exception: exception, exception: exception,
stack: exceptionStack, stack: exceptionStack,
library: 'scheduler library', library: 'scheduler library',
context: 'during a task callback', context: ErrorDescription('during a task callback'),
informationCollector: (callbackStack == null) ? null : (StringBuffer information) { informationCollector: (callbackStack == null) ? null : () sync* {
information.writeln( yield DiagnosticsStackTrace(
'\nThis exception was thrown in the context of a task callback. ' '\nThis exception was thrown in the context of a scheduler callback. '
'When the task callback was _registered_ (as opposed to when the ' 'When the scheduler callback was _registered_ (as opposed to when the '
'exception was thrown), this was the stack:' 'exception was thrown), this was the stack',
callbackStack,
); );
FlutterError.defaultStackFilter(callbackStack.toString().trimRight().split('\n')).forEach(information.writeln);
}, },
)); ));
} }
...@@ -493,22 +493,22 @@ mixin SchedulerBinding on BindingBase, ServicesBinding { ...@@ -493,22 +493,22 @@ mixin SchedulerBinding on BindingBase, ServicesBinding {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: reason, exception: reason,
library: 'scheduler library', library: 'scheduler library',
informationCollector: (StringBuffer information) { informationCollector: () sync* {
if (count == 1) { if (count == 1) {
information.writeln( // TODO(jacobr): I have added an extra line break in this case.
yield ErrorDescription(
'There was one transient callback left. ' 'There was one transient callback left. '
'The stack trace for when it was registered is as follows:' 'The stack trace for when it was registered is as follows:'
); );
} else { } else {
information.writeln( yield ErrorDescription(
'There were $count transient callbacks left. ' 'There were $count transient callbacks left. '
'The stack traces for when they were registered are as follows:' 'The stack traces for when they were registered are as follows:'
); );
} }
for (int id in callbacks.keys) { for (int id in callbacks.keys) {
final _FrameCallbackEntry entry = callbacks[id]; final _FrameCallbackEntry entry = callbacks[id];
information.writeln('── callback $id ──'); yield DiagnosticsStackTrace('── callback $id ──', entry.debugStack, showSeparator: false);
FlutterError.defaultStackFilter(entry.debugStack.toString().trimRight().split('\n')).forEach(information.writeln);
} }
}, },
)); ));
...@@ -1015,14 +1015,14 @@ mixin SchedulerBinding on BindingBase, ServicesBinding { ...@@ -1015,14 +1015,14 @@ mixin SchedulerBinding on BindingBase, ServicesBinding {
exception: exception, exception: exception,
stack: exceptionStack, stack: exceptionStack,
library: 'scheduler library', library: 'scheduler library',
context: 'during a scheduler callback', context: ErrorDescription('during a scheduler callback'),
informationCollector: (callbackStack == null) ? null : (StringBuffer information) { informationCollector: (callbackStack == null) ? null : () sync* {
information.writeln( yield DiagnosticsStackTrace(
'\nThis exception was thrown in the context of a scheduler callback. ' '\nThis exception was thrown in the context of a scheduler callback. '
'When the scheduler callback was _registered_ (as opposed to when the ' 'When the scheduler callback was _registered_ (as opposed to when the '
'exception was thrown), this was the stack:' 'exception was thrown), this was the stack',
callbackStack,
); );
FlutterError.defaultStackFilter(callbackStack.toString().trimRight().split('\n')).forEach(information.writeln);
}, },
)); ));
} }
......
...@@ -493,7 +493,7 @@ class EventChannel { ...@@ -493,7 +493,7 @@ class EventChannel {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'services library', library: 'services library',
context: 'while activating platform stream on channel $name', context: ErrorDescription('while activating platform stream on channel $name'),
)); ));
} }
}, onCancel: () async { }, onCancel: () async {
...@@ -505,7 +505,7 @@ class EventChannel { ...@@ -505,7 +505,7 @@ class EventChannel {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'services library', library: 'services library',
context: 'while de-activating platform stream on channel $name', context: ErrorDescription('while de-activating platform stream on channel $name'),
)); ));
} }
}); });
......
...@@ -51,7 +51,7 @@ class BinaryMessages { ...@@ -51,7 +51,7 @@ class BinaryMessages {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'services library', library: 'services library',
context: 'during a platform message response callback', context: ErrorDescription('during a platform message response callback'),
)); ));
} }
}); });
...@@ -79,7 +79,7 @@ class BinaryMessages { ...@@ -79,7 +79,7 @@ class BinaryMessages {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'services library', library: 'services library',
context: 'during a platform message callback', context: ErrorDescription('during a platform message callback'),
)); ));
} finally { } finally {
callback(response); callback(response);
......
...@@ -937,7 +937,7 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje ...@@ -937,7 +937,7 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'widgets library', library: 'widgets library',
context: 'attaching to the render tree', context: ErrorDescription('attaching to the render tree'),
); );
FlutterError.reportError(details); FlutterError.reportError(details);
final Widget error = ErrorWidget.builder(details); final Widget error = ErrorWidget.builder(details);
......
...@@ -21,9 +21,9 @@ export 'package:flutter/foundation.dart' show ...@@ -21,9 +21,9 @@ export 'package:flutter/foundation.dart' show
protected, protected,
required, required,
visibleForTesting; visibleForTesting;
export 'package:flutter/foundation.dart' show FlutterError, debugPrint, debugPrintStack; export 'package:flutter/foundation.dart' show FlutterError, ErrorSummary, ErrorDescription, ErrorHint, debugPrint, debugPrintStack;
export 'package:flutter/foundation.dart' show VoidCallback, ValueChanged, ValueGetter, ValueSetter; export 'package:flutter/foundation.dart' show VoidCallback, ValueChanged, ValueGetter, ValueSetter;
export 'package:flutter/foundation.dart' show DiagnosticLevel; export 'package:flutter/foundation.dart' show DiagnosticsNode, DiagnosticLevel;
export 'package:flutter/foundation.dart' show Key, LocalKey, ValueKey; export 'package:flutter/foundation.dart' show Key, LocalKey, ValueKey;
export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpRenderTree, debugDumpLayerTree; export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpRenderTree, debugDumpLayerTree;
...@@ -197,16 +197,19 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ...@@ -197,16 +197,19 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
_debugIllFatedElements.clear(); _debugIllFatedElements.clear();
_debugReservations.clear(); _debugReservations.clear();
if (duplicates != null) { if (duplicates != null) {
final StringBuffer buffer = StringBuffer(); final List<DiagnosticsNode> information = <DiagnosticsNode>[];
buffer.writeln('Multiple widgets used the same GlobalKey.\n'); information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
for (GlobalKey key in duplicates.keys) { for (GlobalKey key in duplicates.keys) {
final Set<Element> elements = duplicates[key]; final Set<Element> elements = duplicates[key];
buffer.writeln('The key $key was used by ${elements.length} widgets:'); // TODO(jacobr): this will omit the '- ' before each widget name and
for (Element element in elements) // use the more standard whitespace style instead. Please let me know
buffer.writeln('- $element'); // if the '- ' style is a feature we want to maintain and we can add
// another tree style that supports it. I also see '* ' in some places
// so it would be nice to unify and normalize.
information.add(Element.describeElements('The key $key was used by ${elements.length} widgets', elements));
} }
buffer.write('A GlobalKey can only be specified on one widget at a time in the widget tree.'); information.add(ErrorDescription('A GlobalKey can only be specified on one widget at a time in the widget tree.'));
throw FlutterError(buffer.toString()); throw FlutterError.fromParts(information);
} }
return true; return true;
}()); }());
...@@ -2066,6 +2069,24 @@ abstract class BuildContext { ...@@ -2066,6 +2069,24 @@ abstract class BuildContext {
/// pull data down, than it is to use [visitChildElements] recursively to push /// pull data down, than it is to use [visitChildElements] recursively to push
/// data down to them. /// data down to them.
void visitChildElements(ElementVisitor visitor); void visitChildElements(ElementVisitor visitor);
/// Returns a description of an [Element] from the current build context.
DiagnosticsNode describeElement(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty});
/// Returns a description of the [Widget] associated with the current build context.
DiagnosticsNode describeWidget(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty});
/// Adds a description of a specific type of widget missing from the current
/// build context's ancestry tree.
///
/// You can find an example of using this method in [debugCheckHasMaterial].
List<DiagnosticsNode> describeMissingAncestor({ @required Type expectedAncestorType });
/// Adds a description of the ownership chain from a specific [Element]
/// to the error report.
///
/// The ownership chain is useful for debugging the source of an element.
DiagnosticsNode describeOwnershipChain(String name);
} }
/// Manager class for the widgets framework. /// Manager class for the widgets framework.
...@@ -2278,10 +2299,11 @@ class BuildOwner { ...@@ -2278,10 +2299,11 @@ class BuildOwner {
_dirtyElements[index].rebuild(); _dirtyElements[index].rebuild();
} catch (e, stack) { } catch (e, stack) {
_debugReportException( _debugReportException(
'while rebuilding dirty elements', e, stack, ErrorDescription('while rebuilding dirty elements'),
informationCollector: (StringBuffer information) { e,
information.writeln('The element being rebuilt at the time was index $index of $dirtyCount:'); stack,
information.write(' ${_dirtyElements[index]}'); informationCollector: () sync* {
yield _dirtyElements[index].describeElement('The element being rebuilt at the time was index $index of $dirtyCount');
}, },
); );
} }
...@@ -2442,7 +2464,7 @@ class BuildOwner { ...@@ -2442,7 +2464,7 @@ class BuildOwner {
return true; return true;
}()); }());
} catch (e, stack) { } catch (e, stack) {
_debugReportException('while finalizing the widget tree', e, stack); _debugReportException(ErrorSummary('while finalizing the widget tree'), e, stack);
} finally { } finally {
Timeline.finishSync(); Timeline.finishSync();
} }
...@@ -2642,6 +2664,60 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -2642,6 +2664,60 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return result; return result;
} }
@override
List<DiagnosticsNode> describeMissingAncestor({ @required Type expectedAncestorType }) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
final List<Element> ancestors = <Element>[];
visitAncestorElements((Element element) {
ancestors.add(element);
return true;
});
information.add(DiagnosticsProperty<Element>(
'The specific widget that could not find a $expectedAncestorType ancestor was',
this,
style: DiagnosticsTreeStyle.errorProperty,
));
if (ancestors.isNotEmpty) {
information.add(describeElements('The ancestors of this widget were', ancestors));
} else {
information.add(ErrorDescription(
'This widget is the root of the tree, so it has no '
'ancestors, let alone a "$expectedAncestorType" ancestor.'
));
}
return information;
}
/// Returns a list of [Element]s from the current build context to the error report.
static DiagnosticsNode describeElements(String name, Iterable<Element> elements) {
return DiagnosticsBlock(
name: name,
children: elements.map<DiagnosticsNode>((Element element) => DiagnosticsProperty<Element>('', element)).toList(),
allowTruncate: true,
);
}
@override
DiagnosticsNode describeElement(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) {
return DiagnosticsProperty<Element>(name, this, style: style);
}
@override
DiagnosticsNode describeWidget(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) {
return DiagnosticsProperty<Element>(name, this, style: style);
}
@override
DiagnosticsNode describeOwnershipChain(String name) {
// TODO(jacobr): make this structured so clients can support clicks on
// individual entries. For example, is this an iterable with arrows as
// separators?
return StringProperty(name, debugGetCreatorChain(10));
}
// This is used to verify that Element objects move through life in an // This is used to verify that Element objects move through life in an
// orderly fashion. // orderly fashion.
_ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial; _ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial;
...@@ -3600,6 +3676,7 @@ class ErrorWidget extends LeafRenderObjectWidget { ...@@ -3600,6 +3676,7 @@ class ErrorWidget extends LeafRenderObjectWidget {
/// Creates a widget that displays the given error message. /// Creates a widget that displays the given error message.
ErrorWidget(Object exception) ErrorWidget(Object exception)
: message = _stringify(exception), : message = _stringify(exception),
_flutterError = exception is FlutterError ? exception : null,
super(key: UniqueKey()); super(key: UniqueKey());
/// The configurable factory for [ErrorWidget]. /// The configurable factory for [ErrorWidget].
...@@ -3630,6 +3707,7 @@ class ErrorWidget extends LeafRenderObjectWidget { ...@@ -3630,6 +3707,7 @@ class ErrorWidget extends LeafRenderObjectWidget {
/// The message to display. /// The message to display.
final String message; final String message;
final FlutterError _flutterError;
static String _stringify(Object exception) { static String _stringify(Object exception) {
try { try {
...@@ -3646,7 +3724,10 @@ class ErrorWidget extends LeafRenderObjectWidget { ...@@ -3646,7 +3724,10 @@ class ErrorWidget extends LeafRenderObjectWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(StringProperty('message', message, quoted: false)); if (_flutterError == null)
properties.add(StringProperty('message', message, quoted: false));
else
properties.add(_flutterError.toDiagnosticsNode(style: DiagnosticsTreeStyle.whitespace));
} }
} }
...@@ -3739,7 +3820,7 @@ abstract class ComponentElement extends Element { ...@@ -3739,7 +3820,7 @@ abstract class ComponentElement extends Element {
built = build(); built = build();
debugWidgetBuilderValue(widget, built); debugWidgetBuilderValue(widget, built);
} catch (e, stack) { } catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
} finally { } finally {
// We delay marking the element as clean until after calling build() so // We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored. // that attempts to markNeedsBuild() during build() will be ignored.
...@@ -3750,7 +3831,7 @@ abstract class ComponentElement extends Element { ...@@ -3750,7 +3831,7 @@ abstract class ComponentElement extends Element {
_child = updateChild(_child, built, slot); _child = updateChild(_child, built, slot);
assert(_child != null); assert(_child != null);
} catch (e, stack) { } catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
_child = updateChild(null, built, slot); _child = updateChild(null, built, slot);
} }
...@@ -5002,7 +5083,7 @@ class _DebugCreator { ...@@ -5002,7 +5083,7 @@ class _DebugCreator {
} }
FlutterErrorDetails _debugReportException( FlutterErrorDetails _debugReportException(
String context, DiagnosticsNode context,
dynamic exception, dynamic exception,
StackTrace stack, { StackTrace stack, {
InformationCollector informationCollector, InformationCollector informationCollector,
......
...@@ -93,7 +93,7 @@ Future<void> precacheImage( ...@@ -93,7 +93,7 @@ Future<void> precacheImage(
onError(exception, stackTrace); onError(exception, stackTrace);
} else { } else {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
context: 'image failed to precache', context: ErrorDescription('image failed to precache'),
library: 'image resource service', library: 'image resource service',
exception: exception, exception: exception,
stack: stackTrace, stack: stackTrace,
......
...@@ -113,14 +113,14 @@ class _LayoutBuilderElement extends RenderObjectElement { ...@@ -113,14 +113,14 @@ class _LayoutBuilderElement extends RenderObjectElement {
built = widget.builder(this, constraints); built = widget.builder(this, constraints);
debugWidgetBuilderValue(widget, built); debugWidgetBuilderValue(widget, built);
} catch (e, stack) { } catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $widget', e, stack)); built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack));
} }
} }
try { try {
_child = updateChild(_child, built, null); _child = updateChild(_child, built, null);
assert(_child != null); assert(_child != null);
} catch (e, stack) { } catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $widget', e, stack)); built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack));
_child = updateChild(null, built, slot); _child = updateChild(null, built, slot);
} }
}); });
...@@ -226,7 +226,7 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren ...@@ -226,7 +226,7 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren
} }
FlutterErrorDetails _debugReportException( FlutterErrorDetails _debugReportException(
String context, DiagnosticsNode context,
dynamic exception, dynamic exception,
StackTrace stack, StackTrace stack,
) { ) {
......
...@@ -1297,8 +1297,7 @@ Widget _createErrorWidget(dynamic exception, StackTrace stackTrace) { ...@@ -1297,8 +1297,7 @@ Widget _createErrorWidget(dynamic exception, StackTrace stackTrace) {
exception: exception, exception: exception,
stack: stackTrace, stack: stackTrace,
library: 'widgets library', library: 'widgets library',
context: 'building', context: ErrorDescription('building'),
informationCollector: null,
); );
FlutterError.reportError(details); FlutterError.reportError(details);
return ErrorWidget.builder(details); return ErrorWidget.builder(details);
......
...@@ -56,19 +56,20 @@ Future<void> main() async { ...@@ -56,19 +56,20 @@ Future<void> main() async {
exception: getAssertionErrorWithMessage(), exception: getAssertionErrorWithMessage(),
stack: sampleStack, stack: sampleStack,
library: 'error handling test', library: 'error handling test',
context: 'testing the error handling logic', context: ErrorDescription('testing the error handling logic'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('line 1 of extra information'); yield ErrorDescription('line 1 of extra information');
information.writeln('line 2 of extra information\n'); // the double trailing newlines here are intentional yield ErrorHint('line 2 of extra information\n');
}, },
)); ));
expect(console.join('\n'), matches( expect(console.join('\n'), matches(
'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n' '^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n'
'The following assertion was thrown testing the error handling logic:\n' 'The following assertion was thrown testing the error handling logic:\n'
'Message goes here\\.\n' 'Message goes here\\.\n'
'\'[^\']+flutter/test/foundation/error_reporting_test\\.dart\': Failed assertion: line [0-9]+ pos [0-9]+: \'false\'\n' '\'[^\']+flutter/test/foundation/error_reporting_test\\.dart\':\n'
'Failed assertion: line [0-9]+ pos [0-9]+: \'false\'\n'
'\n' '\n'
'Either the assertion indicates an error in the framework itself, or we should provide substantially ' 'Either the assertion indicates an error in the framework itself, or we should provide substantially\n'
'more information in this error message to help you determine and fix the underlying cause\\.\n' 'more information in this error message to help you determine and fix the underlying cause\\.\n'
'In either case, please report this assertion by filing a bug on GitHub:\n' 'In either case, please report this assertion by filing a bug on GitHub:\n'
' https://github\\.com/flutter/flutter/issues/new\\?template=BUG\\.md\n' ' https://github\\.com/flutter/flutter/issues/new\\?template=BUG\\.md\n'
...@@ -102,14 +103,15 @@ Future<void> main() async { ...@@ -102,14 +103,15 @@ Future<void> main() async {
expect(console.join('\n'), matches( expect(console.join('\n'), matches(
'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n' '^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n'
'The following assertion was thrown:\n' 'The following assertion was thrown:\n'
'word word word word word word word word word word word word word word word word word word word word '
'word word word word word word word word word word word word word word word word word word word word '
'word word word word word word word word word word word word word word word word word word word word '
'word word word word word word word word word word word word word word word word word word word word '
'word word word word word word word word word word word word word word word word word word word word\n' 'word word word word word word word word word word word word word word word word word word word word\n'
'\'[^\']+flutter/test/foundation/error_reporting_test\\.dart\': Failed assertion: line [0-9]+ pos [0-9]+: \'false\'\n' 'word word word word word word word word word word word word word word word word word word word word\n'
'word word word word word word word word word word word word word word word word word word word word\n'
'word word word word word word word word word word word word word word word word word word word word\n'
'word word word word word word word word word word word word word word word word word word word word\n'
'\'[^\']+flutter/test/foundation/error_reporting_test\\.dart\':\n'
'Failed assertion: line [0-9]+ pos [0-9]+: \'false\'\n'
'\n' '\n'
'Either the assertion indicates an error in the framework itself, or we should provide substantially ' 'Either the assertion indicates an error in the framework itself, or we should provide substantially\n'
'more information in this error message to help you determine and fix the underlying cause\\.\n' 'more information in this error message to help you determine and fix the underlying cause\\.\n'
'In either case, please report this assertion by filing a bug on GitHub:\n' 'In either case, please report this assertion by filing a bug on GitHub:\n'
' https://github\\.com/flutter/flutter/issues/new\\?template=BUG\\.md\n' ' https://github\\.com/flutter/flutter/issues/new\\?template=BUG\\.md\n'
...@@ -138,18 +140,19 @@ Future<void> main() async { ...@@ -138,18 +140,19 @@ Future<void> main() async {
exception: getAssertionErrorWithoutMessage(), exception: getAssertionErrorWithoutMessage(),
stack: sampleStack, stack: sampleStack,
library: 'error handling test', library: 'error handling test',
context: 'testing the error handling logic', context: ErrorDescription('testing the error handling logic'),
informationCollector: (StringBuffer information) { informationCollector: () sync* {
information.writeln('line 1 of extra information'); yield ErrorDescription('line 1 of extra information');
information.writeln('line 2 of extra information\n'); // the double trailing newlines here are intentional yield ErrorDescription('line 2 of extra information\n'); // the trailing newlines here are intentional
}, }
)); ));
expect(console.join('\n'), matches( expect(console.join('\n'), matches(
'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n' '^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n'
'The following assertion was thrown testing the error handling logic:\n' 'The following assertion was thrown testing the error handling logic:\n'
'\'[^\']+flutter/test/foundation/error_reporting_test\\.dart\': Failed assertion: line [0-9]+ pos [0-9]+: \'false\': is not true\\.\n' '\'[^\']+flutter/test/foundation/error_reporting_test\\.dart\':[\n ]'
'Failed[\n ]assertion:[\n ]line[\n ][0-9]+[\n ]pos[\n ][0-9]+:[\n ]\'false\':[\n ]is[\n ]not[\n ]true\\.\n'
'\n' '\n'
'Either the assertion indicates an error in the framework itself, or we should provide substantially ' 'Either the assertion indicates an error in the framework itself, or we should provide substantially\n'
'more information in this error message to help you determine and fix the underlying cause\\.\n' 'more information in this error message to help you determine and fix the underlying cause\\.\n'
'In either case, please report this assertion by filing a bug on GitHub:\n' 'In either case, please report this assertion by filing a bug on GitHub:\n'
' https://github\\.com/flutter/flutter/issues/new\\?template=BUG\\.md\n' ' https://github\\.com/flutter/flutter/issues/new\\?template=BUG\\.md\n'
......
...@@ -145,7 +145,7 @@ void main() { ...@@ -145,7 +145,7 @@ void main() {
tearDownAll(() async { tearDownAll(() async {
// See widget_inspector_test.dart for tests of the ext.flutter.inspector // See widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions included in this count. // service extensions included in this count.
int widgetInspectorExtensionCount = 15; int widgetInspectorExtensionCount = 16;
if (WidgetInspectorService.instance.isWidgetCreationTracked()) { if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
// Some inspector extensions are only exposed if widget creation locations // Some inspector extensions are only exposed if widget creation locations
// are tracked. // are tracked.
......
...@@ -153,7 +153,10 @@ void main() { ...@@ -153,7 +153,10 @@ void main() {
)); ));
// the column overflows because we're forcing it to 600 pixels high // the column overflows because we're forcing it to 600 pixels high
expect(tester.takeException(), contains('A RenderFlex overflowed by')); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
expect(find.text('Gingerbread (0)'), findsOneWidget); expect(find.text('Gingerbread (0)'), findsOneWidget);
expect(find.text('Gingerbread (1)'), findsNothing); expect(find.text('Gingerbread (1)'), findsNothing);
...@@ -238,7 +241,11 @@ void main() { ...@@ -238,7 +241,11 @@ void main() {
), ),
)); ));
// the column overflows because we're forcing it to 600 pixels high // the column overflows because we're forcing it to 600 pixels high
expect(tester.takeException(), contains('A RenderFlex overflowed by')); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
expect(exception.diagnostics.first.toString(), contains('A RenderFlex overflowed by'));
expect(find.text('Rows per page:'), findsOneWidget); expect(find.text('Rows per page:'), findsOneWidget);
// Test that we will show some options in the drop down even if the lowest option is bigger than the source: // Test that we will show some options in the drop down even if the lowest option is bigger than the source:
assert(501 > source.rowCount); assert(501 > source.rowCount);
......
...@@ -33,7 +33,10 @@ void main() { ...@@ -33,7 +33,10 @@ void main() {
), ),
); );
expect(tester.takeException(), contains('A RenderUnconstrainedBox overflowed by')); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
expect(exception.diagnostics.first.toString(), startsWith('A RenderUnconstrainedBox overflowed by '));
expect(find.byType(UnconstrainedBox), paints..rect()); expect(find.byType(UnconstrainedBox), paints..rect());
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -82,7 +82,7 @@ void main() { ...@@ -82,7 +82,7 @@ void main() {
expect(error.toString(), contains('The following GlobalKey was specified multiple times')); expect(error.toString(), contains('The following GlobalKey was specified multiple times'));
// The following line is verifying the grammar is correct in this common case. // The following line is verifying the grammar is correct in this common case.
// We should probably also verify the three other combinations that can be generated... // We should probably also verify the three other combinations that can be generated...
expect(error.toString(), contains('This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key.')); expect(error.toString().split('\n').join(' '), contains('This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key.'));
expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]'));
expect(error.toString(), contains('Container')); expect(error.toString(), contains('Container'));
expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.'));
......
...@@ -105,7 +105,10 @@ void main() { ...@@ -105,7 +105,10 @@ void main() {
), ),
); );
expect(tester.takeException(), startsWith('A RenderFlex overflowed by ')); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
await expectLater( await expectLater(
find.byKey(key), find.byKey(key),
matchesGoldenFile('physical_model_overflow.png'), matchesGoldenFile('physical_model_overflow.png'),
......
...@@ -39,7 +39,10 @@ void main() { ...@@ -39,7 +39,10 @@ void main() {
), ),
)); ));
expect(tester.takeException(), contains('overflowed')); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
expect(exception.diagnostics.first.toString(), contains('overflowed'));
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
TestSemantics.root( TestSemantics.root(
...@@ -98,7 +101,10 @@ void main() { ...@@ -98,7 +101,10 @@ void main() {
), ),
)); ));
expect(tester.takeException(), contains('overflowed')); final dynamic exception = tester.takeException();
expect(exception, isInstanceOf<FlutterError>());
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
expect(exception.diagnostics.first.toString(), contains('overflowed'));
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
TestSemantics.root( TestSemantics.root(
......
...@@ -501,7 +501,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -501,7 +501,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
FlutterError.dumpErrorToConsole(FlutterErrorDetails( FlutterError.dumpErrorToConsole(FlutterErrorDetails(
exception: exception, exception: exception,
stack: _unmangle(stack), stack: _unmangle(stack),
context: 'running a test (but after the test had completed)', context: ErrorDescription('running a test (but after the test had completed)'),
library: 'Flutter test framework', library: 'Flutter test framework',
), forceReport: true); ), forceReport: true);
return; return;
...@@ -534,31 +534,33 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -534,31 +534,33 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// _this_ zone, the test framework would find this zone was the current // _this_ zone, the test framework would find this zone was the current
// zone and helpfully throw the error in this zone, causing us to be // zone and helpfully throw the error in this zone, causing us to be
// directly called again. // directly called again.
String treeDump; DiagnosticsNode treeDump;
try { try {
treeDump = renderViewElement?.toStringDeep() ?? '<no tree>'; treeDump = renderViewElement?.toDiagnosticsNode() ?? DiagnosticsNode.message('<no tree>');
// TODO(jacobr): this is a hack to make sure the tree can safely be fully dumped.
// Potentially everything is good enough without this case.
treeDump.toStringDeep();
} catch (exception) { } catch (exception) {
treeDump = '<additional error caught while dumping tree: $exception>'; treeDump = DiagnosticsNode.message('<additional error caught while dumping tree: $exception>', level: DiagnosticLevel.error);
} }
final StringBuffer expectLine = StringBuffer(); final List<DiagnosticsNode> omittedFrames = <DiagnosticsNode>[];
final int stackLinesToOmit = reportExpectCall(stack, expectLine); final int stackLinesToOmit = reportExpectCall(stack, omittedFrames);
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: exception, exception: exception,
stack: _unmangle(stack), stack: _unmangle(stack),
context: 'running a test', context: ErrorDescription('running a test'),
library: 'Flutter test framework', library: 'Flutter test framework',
stackFilter: (Iterable<String> frames) { stackFilter: (Iterable<String> frames) {
return FlutterError.defaultStackFilter(frames.skip(stackLinesToOmit)); return FlutterError.defaultStackFilter(frames.skip(stackLinesToOmit));
}, },
informationCollector: (StringBuffer information) { informationCollector: () sync* {
if (stackLinesToOmit > 0) if (stackLinesToOmit > 0)
information.writeln(expectLine.toString()); yield* omittedFrames;
if (showAppDumpInErrors) { if (showAppDumpInErrors) {
information.writeln('At the time of the failure, the widget tree looked as follows:'); yield DiagnosticsProperty<DiagnosticsNode>('At the time of the failure, the widget tree looked as follows', treeDump, linePrefix: '# ', style: DiagnosticsTreeStyle.flat);
information.writeln('# ${treeDump.split("\n").takeWhile((String s) => s != "").join("\n# ")}');
} }
if (description.isNotEmpty) if (description.isNotEmpty)
information.writeln('The test description was:\n$description'); yield DiagnosticsProperty<String>('The test description was', description, style: DiagnosticsTreeStyle.errorProperty);
}, },
)); ));
assert(_parentZone != null); assert(_parentZone != null);
...@@ -847,7 +849,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -847,7 +849,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'Flutter test framework', library: 'Flutter test framework',
context: 'while running async test code', context: ErrorDescription('while running async test code'),
)); ));
return null; return null;
}).whenComplete(() { }).whenComplete(() {
...@@ -1335,7 +1337,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1335,7 +1337,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
exception: error, exception: error,
stack: stack, stack: stack,
library: 'Flutter test framework', library: 'Flutter test framework',
context: 'while running async test code', context: ErrorSummary('while running async test code'),
)); ));
return null; return null;
} finally { } finally {
......
...@@ -754,7 +754,7 @@ class _EqualsIgnoringHashCodes extends Matcher { ...@@ -754,7 +754,7 @@ class _EqualsIgnoringHashCodes extends Matcher {
static final Object _mismatchedValueKey = Object(); static final Object _mismatchedValueKey = Object();
static String _normalize(String s) { static String _normalize(String s) {
return s.replaceAll(RegExp(r'#[0-9a-f]{5}'), '#00000'); return s.replaceAll(RegExp(r'#[0-9a-fA-F]{5}'), '#00000');
} }
@override @override
......
...@@ -4,14 +4,17 @@ ...@@ -4,14 +4,17 @@
// See also test_async_utils.dart which has some stack manipulation code. // See also test_async_utils.dart which has some stack manipulation code.
import 'package:flutter/foundation.dart';
/// Report call site for `expect()` call. Returns the number of frames that /// Report call site for `expect()` call. Returns the number of frames that
/// should be elided if a stack were to be modified to hide the expect call, or /// should be elided if a stack were to be modified to hide the expect call, or
/// zero if no such call was found. /// zero if no such call was found.
/// ///
/// If the head of the stack trace consists of a failure as a result of calling /// If the head of the stack trace consists of a failure as a result of calling
/// the test_widgets [expect] function, this will fill the given StringBuffer /// the test_widgets [expect] function, this will fill the given
/// with the precise file and line number that called that function. /// FlutterErrorBuilder with the precise file and line number that called that
int reportExpectCall(StackTrace stack, StringBuffer information) { /// function.
int reportExpectCall(StackTrace stack, List<DiagnosticsNode> information) {
final RegExp line0 = RegExp(r'^#0 +fail \(.+\)$'); final RegExp line0 = RegExp(r'^#0 +fail \(.+\)$');
final RegExp line1 = RegExp(r'^#1 +_expect \(.+\)$'); final RegExp line1 = RegExp(r'^#1 +_expect \(.+\)$');
final RegExp line2 = RegExp(r'^#2 +expect \(.+\)$'); final RegExp line2 = RegExp(r'^#2 +expect \(.+\)$');
...@@ -25,8 +28,11 @@ int reportExpectCall(StackTrace stack, StringBuffer information) { ...@@ -25,8 +28,11 @@ int reportExpectCall(StackTrace stack, StringBuffer information) {
final Match expectMatch = line4.firstMatch(stackLines[4]); final Match expectMatch = line4.firstMatch(stackLines[4]);
assert(expectMatch != null); assert(expectMatch != null);
assert(expectMatch.groupCount == 2); assert(expectMatch.groupCount == 2);
information.writeln('This was caught by the test expectation on the following line:'); information.add(DiagnosticsStackTrace.singleFrame(
information.writeln(' ${expectMatch.group(1)} line ${expectMatch.group(2)}'); 'This was caught by the test expectation on the following line',
frame: '${expectMatch.group(1)} line ${expectMatch.group(2)}',
));
return 4; return 4;
} }
return 0; return 0;
......
...@@ -149,7 +149,8 @@ void main() { ...@@ -149,7 +149,8 @@ void main() {
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#000000'))); expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#000000')));
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#123456'))); expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#123456')));
expect('Foo#A3b4D', isNot(equalsIgnoringHashCodes('Foo#00000'))); expect('FOO#A3b4D', equalsIgnoringHashCodes('FOO#00000'));
expect('FOO#A3b4J', isNot(equalsIgnoringHashCodes('FOO#00000')));
expect('Foo#12345(Bar#9110f)', expect('Foo#12345(Bar#9110f)',
equalsIgnoringHashCodes('Foo#00000(Bar#00000)')); equalsIgnoringHashCodes('Foo#00000(Bar#00000)'));
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
...@@ -10,9 +11,10 @@ void main() { ...@@ -10,9 +11,10 @@ void main() {
expect(false, isTrue); expect(false, isTrue);
throw 'unexpectedly did not throw'; throw 'unexpectedly did not throw';
} catch (e, stack) { } catch (e, stack) {
final StringBuffer information = StringBuffer(); final List<DiagnosticsNode> information = <DiagnosticsNode>[];
expect(reportExpectCall(stack, information), 4); expect(reportExpectCall(stack, information), 4);
final List<String> lines = information.toString().split('\n'); final TextTreeRenderer renderer = TextTreeRenderer();
final List<String> lines = information.map((DiagnosticsNode node) => renderer.render(node).trimRight()).join('\n').split('\n');
expect(lines[0], 'This was caught by the test expectation on the following line:'); expect(lines[0], 'This was caught by the test expectation on the following line:');
expect(lines[1], matches(r'^ .*stack_manipulation_test.dart line [0-9]+$')); expect(lines[1], matches(r'^ .*stack_manipulation_test.dart line [0-9]+$'));
} }
...@@ -20,9 +22,9 @@ void main() { ...@@ -20,9 +22,9 @@ void main() {
try { try {
throw null; throw null;
} catch (e, stack) { } catch (e, stack) {
final StringBuffer information = StringBuffer(); final List<DiagnosticsNode> information = <DiagnosticsNode>[];
expect(reportExpectCall(stack, information), 0); expect(reportExpectCall(stack, information), 0);
expect(information.toString(), ''); expect(information, isEmpty);
} }
}); });
} }
...@@ -122,7 +122,7 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire ...@@ -122,7 +122,7 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire
reason: 'Failure in $testName to compare to $fullTestExpectation', reason: 'Failure in $testName to compare to $fullTestExpectation',
); );
final String expectationLine = expectations[expectationLineNumber]; final String expectationLine = expectations[expectationLineNumber];
final String outputLine = output[outputLineNumber]; String outputLine = output[outputLineNumber];
if (expectationLine == '<<skip until matching line>>') { if (expectationLine == '<<skip until matching line>>') {
allowSkip = true; allowSkip = true;
expectationLineNumber += 1; expectationLineNumber += 1;
...@@ -139,6 +139,18 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire ...@@ -139,6 +139,18 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire
expect(haveSeenStdErrMarker, isFalse); expect(haveSeenStdErrMarker, isFalse);
haveSeenStdErrMarker = true; haveSeenStdErrMarker = true;
} }
if (!RegExp(expectationLine).hasMatch(outputLine) && outputLineNumber + 1 < output.length) {
// Check if the RegExp can match the next two lines in the output so
// that it is possible to write expectations that still hold even if a
// line is wrapped slightly differently due to for example a file name
// being longer on one platform than another.
final String mergedLines = '$outputLine\n${output[outputLineNumber+1]}';
if (RegExp(expectationLine).hasMatch(mergedLines)) {
outputLineNumber += 1;
outputLine = mergedLines;
}
}
expect(outputLine, matches(expectationLine), reason: 'Full output:\n- - - -----8<----- - - -\n${output.join("\n")}\n- - - -----8<----- - - -'); expect(outputLine, matches(expectationLine), reason: 'Full output:\n- - - -----8<----- - - -\n${output.join("\n")}\n- - - -----8<----- - - -');
expectationLineNumber += 1; expectationLineNumber += 1;
outputLineNumber += 1; outputLineNumber += 1;
......
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