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>>
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
Guarded function conflict\. You must use "await" with all Future-returning test APIs\.
The guarded "guardedHelper" function was called from .*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart on line [0-9]+\.
Then, the "expect" function was called from .*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart on line [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\.
Guarded function conflict\.
You must use "await" with all Future-returning test APIs\.
The guarded "guardedHelper" function was called from
.*dev/automated_tests/flutter_test/test_async_utils_guarded_test\.dart[ \n]on[ \n]line[ \n][0-9]+\.
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:
<<skip until matching line>>
......
......@@ -2,10 +2,16 @@
<<skip until matching line>>
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
Guarded function conflict\. You must use "await" with all Future-returning test APIs\.
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]+\.
Then, it was called again from .*dev/automated_tests/flutter_test/test_async_utils_unguarded_test.dart on line [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\.
Guarded function conflict\.
You must use "await" with all Future-returning test APIs\.
The guarded method "pump" from class WidgetTester was called from
.*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:
<<skip until matching line>>
......
......@@ -16,7 +16,7 @@ void main() {
handleUncaughtError:(Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
FlutterError.reportError(FlutterErrorDetails(
exception: error,
context: 'In the Zone handleUncaughtError handler',
context: ErrorDescription('In the Zone handleUncaughtError handler'),
silent: false,
));
});
......
......@@ -129,10 +129,13 @@ mixin AnimationLocalListenersMixin {
exception: exception,
stack: stack,
library: 'animation library',
context: 'while notifying listeners for $runtimeType',
informationCollector: (StringBuffer information) {
information.writeln('The $runtimeType notifying listeners was:');
information.write(' $this');
context: ErrorDescription('while notifying listeners for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<AnimationLocalListenersMixin>(
'The $runtimeType notifying listeners was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
}
......@@ -195,10 +198,13 @@ mixin AnimationLocalStatusListenersMixin {
exception: exception,
stack: stack,
library: 'animation library',
context: 'while notifying status listeners for $runtimeType',
informationCollector: (StringBuffer information) {
information.writeln('The $runtimeType notifying status listeners was:');
information.write(' $this');
context: ErrorDescription('while notifying status listeners for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<AnimationLocalStatusListenersMixin>(
'The $runtimeType notifying status listeners was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
}
......
......@@ -522,7 +522,7 @@ abstract class BindingBase {
FlutterError.reportError(FlutterErrorDetails(
exception: caughtException,
stack: caughtStack,
context: 'during a service extension callback for "$method"',
context: ErrorDescription('during a service extension callback for "$method"'),
));
return developer.ServiceExtensionResponse.error(
developer.ServiceExtensionResponse.extensionError,
......
......@@ -209,10 +209,13 @@ class ChangeNotifier implements Listenable {
exception: exception,
stack: stack,
library: 'foundation library',
context: 'while dispatching notifications for $runtimeType',
informationCollector: (StringBuffer information) {
information.writeln('The $runtimeType sending notification was:');
information.write(' $this');
context: ErrorDescription('while dispatching notifications for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<ChangeNotifier>(
'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
}
......
......@@ -183,12 +183,11 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
exception: exception,
stack: stack,
library: 'gesture library',
context: 'while dispatching a non-hit-tested pointer event',
context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
event: event,
hitTestEntry: null,
informationCollector: (StringBuffer information) {
information.writeln('Event:');
information.writeln(' $event');
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
......@@ -202,14 +201,12 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
exception: exception,
stack: stack,
library: 'gesture library',
context: 'while dispatching a pointer event',
context: ErrorDescription('while dispatching a pointer event'),
event: event,
hitTestEntry: entry,
informationCollector: (StringBuffer information) {
information.writeln('Event:');
information.writeln(' $event');
information.writeln('Target:');
information.write(' ${entry.target}');
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
......@@ -244,7 +241,7 @@ class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails {
dynamic exception,
StackTrace stack,
String library,
String context,
DiagnosticsNode context,
this.event,
this.hitTestEntry,
InformationCollector informationCollector,
......
......@@ -76,13 +76,12 @@ class PointerRouter {
exception: exception,
stack: stack,
library: 'gesture library',
context: 'while routing a pointer event',
context: ErrorDescription('while routing a pointer event'),
router: this,
route: route,
event: event,
informationCollector: (StringBuffer information) {
information.writeln('Event:');
information.write(' $event');
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
......@@ -123,7 +122,7 @@ class FlutterErrorDetailsForPointerRouter extends FlutterErrorDetails {
dynamic exception,
StackTrace stack,
String library,
String context,
DiagnosticsNode context,
this.router,
this.route,
this.event,
......
......@@ -55,10 +55,9 @@ class PointerSignalResolver {
exception: exception,
stack: stack,
library: 'gesture library',
context: 'while resolving a PointerSignalEvent',
informationCollector: (StringBuffer information) {
information.writeln('Event:');
information.write(' $event');
context: ErrorDescription('while resolving a PointerSignalEvent'),
informationCollector: () sync* {
yield DiagnosticsProperty<PointerSignalEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
......
......@@ -169,12 +169,11 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
exception: exception,
stack: stack,
library: 'gesture',
context: 'while handling a gesture',
informationCollector: (StringBuffer information) {
information.writeln('Handler: $name');
information.writeln('Recognizer:');
information.writeln(' $this');
},
context: ErrorDescription('while handling a gesture'),
informationCollector: () sync* {
yield StringProperty('Handler', name);
yield DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty);
}
));
}
return result;
......
......@@ -362,7 +362,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
'The onRefresh callback returned null.\n'
'The RefreshIndicator onRefresh callback must return a Future.'
),
context: 'when calling onRefresh',
context: ErrorDescription('when calling onRefresh'),
library: 'material library',
));
return true;
......
......@@ -274,14 +274,12 @@ abstract class ImageProvider<T> {
imageCompleter.setError(
exception: exception,
stack: stack,
context: 'while resolving an image',
context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.writeln('Image configuration: $configuration');
if (obtainedKey != null) {
information.writeln('Image key: $obtainedKey');
}
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
yield DiagnosticsProperty<T>('Image key', obtainedKey, defaultValue: null);
},
);
}
......@@ -448,9 +446,9 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<AssetBundleImageKey>('Image key', key);
},
);
}
......@@ -505,9 +503,9 @@ class NetworkImage extends ImageProvider<NetworkImage> {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<NetworkImage>('Image key', key);
},
);
}
......@@ -579,8 +577,8 @@ class FileImage extends ImageProvider<FileImage> {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: (StringBuffer information) {
information.writeln('Path: ${file?.path}');
informationCollector: () sync* {
yield ErrorDescription('Path: ${file?.path}');
},
);
}
......@@ -815,7 +813,7 @@ class _ErrorImageCompleter extends ImageStreamCompleter {
_ErrorImageCompleter();
void setError({
String context,
DiagnosticsNode context,
dynamic exception,
StackTrace stack,
InformationCollector informationCollector,
......
......@@ -284,7 +284,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
listener(_currentImage, true);
} catch (exception, stack) {
reportError(
context: 'by a synchronously-called image listener',
context: ErrorDescription('by a synchronously-called image listener'),
exception: exception,
stack: stack,
);
......@@ -298,7 +298,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
FlutterErrorDetails(
exception: exception,
library: 'image resource service',
context: 'by a synchronously-called image error listener',
context: ErrorDescription('by a synchronously-called image error listener'),
stack: stack,
),
);
......@@ -336,7 +336,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
listener(image, false);
} catch (exception, stack) {
reportError(
context: 'by an image listener',
context: ErrorDescription('by an image listener'),
exception: exception,
stack: stack,
);
......@@ -374,7 +374,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// See [FlutterErrorDetails] for further details on these values.
@protected
void reportError({
String context,
DiagnosticsNode context,
dynamic exception,
StackTrace stack,
InformationCollector informationCollector,
......@@ -405,7 +405,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
} catch (exception, stack) {
FlutterError.reportError(
FlutterErrorDetails(
context: 'when reporting an error to an image listener',
context: ErrorDescription('when reporting an error to an image listener'),
library: 'image resource service',
exception: exception,
stack: stack,
......@@ -451,7 +451,7 @@ class OneFrameImageStreamCompleter extends ImageStreamCompleter {
: assert(image != null) {
image.then<void>(setImage, onError: (dynamic error, StackTrace stack) {
reportError(
context: 'resolving a single-frame image stream',
context: ErrorDescription('resolving a single-frame image stream'),
exception: error,
stack: stack,
informationCollector: informationCollector,
......@@ -511,7 +511,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_scale = scale {
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError(
context: 'resolving an image codec',
context: ErrorDescription('resolving an image codec'),
exception: error,
stack: stack,
informationCollector: informationCollector,
......@@ -579,7 +579,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
reportError(
context: 'resolving an image frame',
context: ErrorDescription('resolving an image frame'),
exception: exception,
stack: stack,
informationCollector: _informationCollector,
......
This diff is collapsed.
......@@ -198,16 +198,23 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
return regions;
}
void _reportOverflow(RelativeRect overflow, String overflowHints) {
overflowHints ??= 'The edge of the $runtimeType that is '
'overflowing has been marked in the rendering with a yellow and black '
'striped pattern. This is usually caused by the contents being too big '
'for the $runtimeType.\n'
'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.';
void _reportOverflow(RelativeRect overflow, List<DiagnosticsNode> overflowHints) {
overflowHints ??= <DiagnosticsNode>[];
if (overflowHints.isEmpty) {
overflowHints.add(ErrorDescription(
'The edge of the $runtimeType that is '
'overflowing has been marked in the rendering with a yellow and black '
'striped pattern. This is usually caused by the contents being too big '
'for the $runtimeType.'
));
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>[];
if (overflow.left > 0.0)
......@@ -232,18 +239,22 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
overflows[overflows.length - 1] = 'and ${overflows[overflows.length - 1]}';
overflowText = overflows.join(', ');
}
// TODO(jacobr): add the overflows in pixels as structured data so they can
// be visualized in debugging tools.
FlutterError.reportError(
FlutterErrorDetailsForRendering(
exception: 'A $runtimeType overflowed by $overflowText.',
exception: FlutterError('A $runtimeType overflowed by $overflowText.'),
library: 'rendering library',
context: 'during layout',
context: ErrorDescription('during layout'),
renderObject: this,
informationCollector: (StringBuffer information) {
information.writeln(overflowHints);
information.writeln('The specific $runtimeType in question is:');
information.writeln(' ${toStringShallow(joiner: '\n ')}');
information.writeln('◢◤' * (FlutterError.wrapWidth ~/ 2));
},
informationCollector: () sync* {
yield* overflowHints;
yield describeForError('The specific $runtimeType in question is');
// TODO(jacobr): this line is ascii art that it would be nice to
// 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 {
Offset offset,
Rect containerRect,
Rect childRect, {
String overflowHints,
List<DiagnosticsNode> overflowHints,
}) {
final RelativeRect overflow = RelativeRect.fromRect(containerRect, childRect);
......
......@@ -653,15 +653,16 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
final String identity = _direction == Axis.horizontal ? 'row' : 'column';
final String axis = _direction == Axis.horizontal ? 'horizontal' : 'vertical';
final String dimension = _direction == Axis.horizontal ? 'width' : 'height';
String error, message;
String addendum = '';
DiagnosticsNode error, message;
final List<DiagnosticsNode> addendum = <DiagnosticsNode>[];
if (!canFlex && (mainAxisSize == MainAxisSize.max || _getFit(child) == FlexFit.tight)) {
error = '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 '
'if it is in a $axis scrollable, it will try to shrink-wrap its children along the $axis '
'axis. Setting a flex on a child (e.g. using Expanded) indicates that the child is to '
'expand to fill the remaining space in the $axis direction.';
final StringBuffer information = StringBuffer();
error = ErrorSummary('RenderFlex children have non-zero flex but incoming $dimension constraints are unbounded.');
message = ErrorDescription(
'When a $identity is in a parent that does not provide a finite $dimension constraint, for example '
'if it is in a $axis scrollable, it will try to shrink-wrap its children along the $axis '
'axis. Setting a flex on a child (e.g. using Expanded) indicates that the child is to '
'expand to fill the remaining space in the $axis direction.'
);
RenderBox node = this;
switch (_direction) {
case Axis.horizontal:
......@@ -678,36 +679,38 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
break;
}
if (node != null) {
information.writeln('The nearest ancestor providing an unbounded width constraint is:');
information.write(' ');
information.writeln(node.toStringShallow(joiner: '\n '));
addendum.add(node.describeForError('The nearest ancestor providing an unbounded width constraint is'));
}
information.writeln('See also: https://flutter.dev/layout/');
addendum = information.toString();
addendum.add(ErrorHint('See also: https://flutter.dev/layout/'));
} else {
return true;
}
throw FlutterError(
'$error\n'
'$message\n'
'These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child '
'cannot simultaneously expand to fit its parent.\n'
'Consider setting mainAxisSize to MainAxisSize.min and using FlexFit.loose fits for the flexible '
'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 '
'forced to take, and then will cause the RenderFlex to shrink-wrap the children '
'rather than expanding to fit the maximum constraints provided by the parent.\n'
'The affected RenderFlex is:\n'
' $this\n'
'The creator information is set to:\n'
' $debugCreator\n'
'$addendum'
'If this message did not help you determine the problem, consider using debugDumpRenderTree():\n'
' https://flutter.dev/debugging/#rendering-layer\n'
' http://docs.flutter.io/flutter/rendering/debugDumpRenderTree.html\n'
'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'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
error,
message,
ErrorDescription(
'These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child '
'cannot simultaneously expand to fit its parent.'
),
ErrorHint(
'Consider setting mainAxisSize to MainAxisSize.min and using FlexFit.loose fits for the flexible '
'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 '
'forced to take, and then will cause the RenderFlex to shrink-wrap the children '
'rather than expanding to fit the maximum constraints provided by the parent.'
),
ErrorDescription(
'If this message did not help you determine the problem, consider using debugDumpRenderTree():\n'
' https://flutter.dev/debugging/#rendering-layer\n'
' http://docs.flutter.io/flutter/rendering/debugDumpRenderTree.html'
),
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;
lastFlexChild = child;
......@@ -953,19 +956,28 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
assert(() {
// Only set this if it's null to save work. It gets reset to null if the
// _direction changes.
final String debugOverflowHints =
'The overflowing $runtimeType has an orientation of $_direction.\n'
'The edge of the $runtimeType that is overflowing has been marked '
'in the rendering with a yellow and black striped pattern. This is '
'usually caused by the contents being too big for the $runtimeType. '
'Consider applying a flex factor (e.g. using an Expanded widget) to '
'force the children of the $runtimeType to fit within the available '
'space instead of being sized to their natural size.\n'
'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.';
final List<DiagnosticsNode> debugOverflowHints = <DiagnosticsNode>[
ErrorDescription(
'The overflowing $runtimeType has an orientation of $_direction.'
),
ErrorDescription(
'The edge of the $runtimeType that is overflowing has been marked '
'in the rendering with a yellow and black striped pattern. This is '
'usually caused by the contents being too big for the $runtimeType.'
),
ErrorHint(
'Consider applying a flex factor (e.g. using an Expanded widget) to '
'force the children of the $runtimeType to fit within the available '
'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
// rect is never used for drawing, just for determining the overflow
......
......@@ -529,13 +529,11 @@ class ContainerLayer extends Layer {
'See https://api.flutter.dev/flutter/rendering/debugCheckElevations.html '
'for more details.'),
library: 'rendering library',
context: 'during compositing',
informationCollector: (StringBuffer buffer) {
buffer.writeln('Attempted to composite layer:');
buffer.writeln(child);
buffer.writeln('after layer:');
buffer.writeln(predecessor);
buffer.writeln('which occupies the same area at a higher elevation.');
context: ErrorDescription('during compositing'),
informationCollector: () sync* {
yield child.toDiagnosticsNode(name: 'Attempted to composite layer', style: DiagnosticsTreeStyle.errorProperty);
yield predecessor.toDiagnosticsNode(name: 'after layer', style: DiagnosticsTreeStyle.errorProperty);
yield ErrorDescription('which occupies the same area at a higher elevation.');
}
));
return <PictureLayer>[
......
......@@ -17,7 +17,7 @@ import 'binding.dart';
import 'debug.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/painting.dart';
......@@ -1187,38 +1187,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
exception: exception,
stack: stack,
library: 'rendering library',
context: 'during $method()',
context: ErrorDescription('during $method()'),
renderObject: this,
informationCollector: (StringBuffer information) {
information.writeln('The following RenderObject was being processed when the exception was fired:');
information.writeln(' ${toStringShallow(joiner: '\n ')}');
final List<String> descendants = <String>[];
const int maxDepth = 5;
int depth = 0;
const int maxLines = 25;
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');
},
informationCollector: () sync* {
yield describeForError('The following RenderObject was being processed when the exception was fired');
// TODO(jacobr): this error message has a code smell. Consider whether
// displaying the truncated children is really useful for command line
// users. Inspector users can see the full tree by clicking on the
// render object so this may not be that useful.
yield describeForError('This RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
}
));
}
......@@ -1558,7 +1536,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(constraints != null);
assert(constraints.debugAssertIsValid(
isAppliedConstraint: true,
informationCollector: (StringBuffer information) {
informationCollector: () sync* {
final List<String> stack = StackTrace.current.toString().split('\n');
int targetFrame;
final Pattern layoutFramePattern = RegExp(r'^#[0-9]+ +RenderObject.layout \(');
......@@ -1569,18 +1547,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}
}
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() '
'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
);
}
}
/// 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.
......@@ -3108,7 +3097,7 @@ class FlutterErrorDetailsForRendering extends FlutterErrorDetails {
dynamic exception,
StackTrace stack,
String library,
String context,
DiagnosticsNode context,
this.renderObject,
InformationCollector informationCollector,
bool silent = false,
......
......@@ -421,10 +421,14 @@ class SliverConstraints extends Constraints {
void verify(bool check, String message) {
if (check)
return;
final StringBuffer information = StringBuffer();
if (informationCollector != null)
informationCollector(information);
throw FlutterError('$runtimeType is not valid: $message\n${information}The offending constraints were:\n $this');
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
information.add(ErrorSummary('$runtimeType is not valid: $message'));
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(growthDirection != null, 'The "growthDirection" is null.');
......@@ -696,14 +700,20 @@ class SliverGeometry extends Diagnosticable {
InformationCollector informationCollector,
}) {
assert(() {
void verify(bool check, String message) {
void verify(bool check, String summary, {List<DiagnosticsNode> details}) {
if (check)
return;
final StringBuffer information = StringBuffer();
if (informationCollector != null)
informationCollector(information);
throw FlutterError('$runtimeType is not valid: $message\n$information');
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
information.add(ErrorSummary('$runtimeType is not valid: $summary'));
if (details != null) {
information.addAll(details);
}
if (informationCollector != null) {
information.addAll(informationCollector());
}
throw FlutterError.fromParts(information);
}
verify(scrollExtent != null, 'The "scrollExtent" is null.');
verify(scrollExtent >= 0.0, 'The "scrollExtent" is negative.');
verify(paintExtent != null, 'The "paintExtent" is null.');
......@@ -714,8 +724,8 @@ class SliverGeometry extends Diagnosticable {
verify(cacheExtent >= 0.0, 'The "cacheExtent" is negative.');
if (layoutExtent > paintExtent) {
verify(false,
'The "layoutExtent" exceeds the "paintExtent".\n' +
_debugCompareFloats('paintExtent', paintExtent, 'layoutExtent', layoutExtent),
'The "layoutExtent" exceeds the "paintExtent".',
details: _debugCompareFloats('paintExtent', paintExtent, 'layoutExtent', layoutExtent),
);
}
verify(maxPaintExtent != null, 'The "maxPaintExtent" is null.');
......@@ -723,9 +733,10 @@ class SliverGeometry extends Diagnosticable {
// than precisionErrorTolerance, we will not throw the assert below.
if (paintExtent - maxPaintExtent > precisionErrorTolerance) {
verify(false,
'The "maxPaintExtent" is less than the "paintExtent".\n' +
_debugCompareFloats('maxPaintExtent', maxPaintExtent, 'paintExtent', paintExtent) +
'By definition, a sliver can\'t paint more than the maximum that it can paint!',
'The "maxPaintExtent" is less than the "paintExtent".',
details:
_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.');
......@@ -866,14 +877,22 @@ class SliverPhysicalParentData extends ParentData {
/// children using absolute coordinates.
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)) {
return 'The $labelA is ${valueA.toStringAsFixed(1)}, but '
'the $labelB is ${valueB.toStringAsFixed(1)}. ';
information..add(ErrorDescription(
'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. '
'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 information;
}
/// Base class for the render objects that implement scroll effects in viewports.
......@@ -1036,28 +1055,30 @@ abstract class RenderSliver extends RenderObject {
(!sizedByParent && debugDoingThisLayout))
return true;
assert(!debugDoingThisResize);
String contract, violation, hint;
DiagnosticsNode contract, violation, hint;
if (debugDoingThisLayout) {
assert(sizedByParent);
violation = 'It appears that the geometry setter was called from performLayout().';
hint = '';
violation = ErrorDescription('It appears that the geometry setter was called from performLayout().');
} 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)
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)
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
contract = 'Because this RenderSliver has sizedByParent set to false, it must set its geometry in performLayout().';
throw FlutterError(
'RenderSliver geometry setter called incorrectly.\n'
'$violation\n'
'$hint\n'
'$contract\n'
'The RenderSliver in question is:\n'
' $this'
);
contract = ErrorDescription('Because this RenderSliver has sizedByParent set to false, it must set its geometry in performLayout().');
final List<DiagnosticsNode> information = <DiagnosticsNode>[
ErrorSummary('RenderSliver geometry setter called incorrectly.'),
violation
];
if (hint != null)
information.add(hint);
information.add(contract);
information.add(describeForError('The RenderSliver in question is'));
throw FlutterError.fromParts(information);
}());
_geometry = value;
}
......@@ -1091,22 +1112,23 @@ abstract class RenderSliver extends RenderObject {
@override
void debugAssertDoesMeetConstraints() {
assert(geometry.debugAssertIsValid(
informationCollector: (StringBuffer information) {
information.writeln('The RenderSliver that returned the offending geometry was:');
information.writeln(' ${toStringShallow(joiner: '\n ')}');
},
informationCollector: () sync* {
yield describeForError('The RenderSliver that returned the offending geometry was');
}
));
assert(() {
if (geometry.paintExtent > constraints.remainingPaintExtent) {
throw FlutterError(
'SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.\n'
'The render object whose geometry violates the constraints is the following:\n'
' ${toStringShallow(joiner: '\n ')}\n' +
_debugCompareFloats('remainingPaintExtent', constraints.remainingPaintExtent,
'paintExtent', geometry.paintExtent) +
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.'),
describeForError('The render object whose geometry violates the constraints is the following')
]..addAll(_debugCompareFloats(
'remainingPaintExtent', constraints.remainingPaintExtent,
'paintExtent', geometry.paintExtent,
))
..add(ErrorDescription(
'The paintExtent must cause the child sliver to paint within the viewport, and so '
'cannot exceed the remainingPaintExtent.'
);
'cannot exceed the remainingPaintExtent.',
)));
}
return true;
}());
......
......@@ -393,14 +393,14 @@ mixin SchedulerBinding on BindingBase, ServicesBinding {
exception: exception,
stack: exceptionStack,
library: 'scheduler library',
context: 'during a task callback',
informationCollector: (callbackStack == null) ? null : (StringBuffer information) {
information.writeln(
'\nThis exception was thrown in the context of a task callback. '
'When the task callback was _registered_ (as opposed to when the '
'exception was thrown), this was the stack:'
context: ErrorDescription('during a task callback'),
informationCollector: (callbackStack == null) ? null : () sync* {
yield DiagnosticsStackTrace(
'\nThis exception was thrown in the context of a scheduler callback. '
'When the scheduler callback was _registered_ (as opposed to when the '
'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 {
FlutterError.reportError(FlutterErrorDetails(
exception: reason,
library: 'scheduler library',
informationCollector: (StringBuffer information) {
informationCollector: () sync* {
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. '
'The stack trace for when it was registered is as follows:'
);
} else {
information.writeln(
yield ErrorDescription(
'There were $count transient callbacks left. '
'The stack traces for when they were registered are as follows:'
);
}
for (int id in callbacks.keys) {
final _FrameCallbackEntry entry = callbacks[id];
information.writeln('── callback $id ──');
FlutterError.defaultStackFilter(entry.debugStack.toString().trimRight().split('\n')).forEach(information.writeln);
yield DiagnosticsStackTrace('── callback $id ──', entry.debugStack, showSeparator: false);
}
},
));
......@@ -1015,14 +1015,14 @@ mixin SchedulerBinding on BindingBase, ServicesBinding {
exception: exception,
stack: exceptionStack,
library: 'scheduler library',
context: 'during a scheduler callback',
informationCollector: (callbackStack == null) ? null : (StringBuffer information) {
information.writeln(
context: ErrorDescription('during a scheduler callback'),
informationCollector: (callbackStack == null) ? null : () sync* {
yield DiagnosticsStackTrace(
'\nThis exception was thrown in the context of a scheduler callback. '
'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 {
exception: exception,
stack: stack,
library: 'services library',
context: 'while activating platform stream on channel $name',
context: ErrorDescription('while activating platform stream on channel $name'),
));
}
}, onCancel: () async {
......@@ -505,7 +505,7 @@ class EventChannel {
exception: exception,
stack: stack,
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 {
exception: exception,
stack: stack,
library: 'services library',
context: 'during a platform message response callback',
context: ErrorDescription('during a platform message response callback'),
));
}
});
......@@ -79,7 +79,7 @@ class BinaryMessages {
exception: exception,
stack: stack,
library: 'services library',
context: 'during a platform message callback',
context: ErrorDescription('during a platform message callback'),
));
} finally {
callback(response);
......
......@@ -937,7 +937,7 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
exception: exception,
stack: stack,
library: 'widgets library',
context: 'attaching to the render tree',
context: ErrorDescription('attaching to the render tree'),
);
FlutterError.reportError(details);
final Widget error = ErrorWidget.builder(details);
......
......@@ -21,9 +21,9 @@ export 'package:flutter/foundation.dart' show
protected,
required,
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 DiagnosticLevel;
export 'package:flutter/foundation.dart' show DiagnosticsNode, DiagnosticLevel;
export 'package:flutter/foundation.dart' show Key, LocalKey, ValueKey;
export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpRenderTree, debugDumpLayerTree;
......@@ -197,16 +197,19 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
_debugIllFatedElements.clear();
_debugReservations.clear();
if (duplicates != null) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('Multiple widgets used the same GlobalKey.\n');
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
for (GlobalKey key in duplicates.keys) {
final Set<Element> elements = duplicates[key];
buffer.writeln('The key $key was used by ${elements.length} widgets:');
for (Element element in elements)
buffer.writeln('- $element');
// TODO(jacobr): this will omit the '- ' before each widget name and
// use the more standard whitespace style instead. Please let me know
// 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.');
throw FlutterError(buffer.toString());
information.add(ErrorDescription('A GlobalKey can only be specified on one widget at a time in the widget tree.'));
throw FlutterError.fromParts(information);
}
return true;
}());
......@@ -2066,6 +2069,24 @@ abstract class BuildContext {
/// pull data down, than it is to use [visitChildElements] recursively to push
/// data down to them.
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.
......@@ -2278,10 +2299,11 @@ class BuildOwner {
_dirtyElements[index].rebuild();
} catch (e, stack) {
_debugReportException(
'while rebuilding dirty elements', e, stack,
informationCollector: (StringBuffer information) {
information.writeln('The element being rebuilt at the time was index $index of $dirtyCount:');
information.write(' ${_dirtyElements[index]}');
ErrorDescription('while rebuilding dirty elements'),
e,
stack,
informationCollector: () sync* {
yield _dirtyElements[index].describeElement('The element being rebuilt at the time was index $index of $dirtyCount');
},
);
}
......@@ -2442,7 +2464,7 @@ class BuildOwner {
return true;
}());
} catch (e, stack) {
_debugReportException('while finalizing the widget tree', e, stack);
_debugReportException(ErrorSummary('while finalizing the widget tree'), e, stack);
} finally {
Timeline.finishSync();
}
......@@ -2642,6 +2664,60 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
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
// orderly fashion.
_ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial;
......@@ -3600,6 +3676,7 @@ class ErrorWidget extends LeafRenderObjectWidget {
/// Creates a widget that displays the given error message.
ErrorWidget(Object exception)
: message = _stringify(exception),
_flutterError = exception is FlutterError ? exception : null,
super(key: UniqueKey());
/// The configurable factory for [ErrorWidget].
......@@ -3630,6 +3707,7 @@ class ErrorWidget extends LeafRenderObjectWidget {
/// The message to display.
final String message;
final FlutterError _flutterError;
static String _stringify(Object exception) {
try {
......@@ -3646,7 +3724,10 @@ class ErrorWidget extends LeafRenderObjectWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder 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 {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
} finally {
// We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored.
......@@ -3750,7 +3831,7 @@ abstract class ComponentElement extends Element {
_child = updateChild(_child, built, slot);
assert(_child != null);
} 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);
}
......@@ -5002,7 +5083,7 @@ class _DebugCreator {
}
FlutterErrorDetails _debugReportException(
String context,
DiagnosticsNode context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector,
......
......@@ -93,7 +93,7 @@ Future<void> precacheImage(
onError(exception, stackTrace);
} else {
FlutterError.reportError(FlutterErrorDetails(
context: 'image failed to precache',
context: ErrorDescription('image failed to precache'),
library: 'image resource service',
exception: exception,
stack: stackTrace,
......
......@@ -113,14 +113,14 @@ class _LayoutBuilderElement extends RenderObjectElement {
built = widget.builder(this, constraints);
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $widget', e, stack));
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack));
}
}
try {
_child = updateChild(_child, built, null);
assert(_child != null);
} 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);
}
});
......@@ -226,7 +226,7 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren
}
FlutterErrorDetails _debugReportException(
String context,
DiagnosticsNode context,
dynamic exception,
StackTrace stack,
) {
......
......@@ -1297,8 +1297,7 @@ Widget _createErrorWidget(dynamic exception, StackTrace stackTrace) {
exception: exception,
stack: stackTrace,
library: 'widgets library',
context: 'building',
informationCollector: null,
context: ErrorDescription('building'),
);
FlutterError.reportError(details);
return ErrorWidget.builder(details);
......
......@@ -56,19 +56,20 @@ Future<void> main() async {
exception: getAssertionErrorWithMessage(),
stack: sampleStack,
library: 'error handling test',
context: 'testing the error handling logic',
informationCollector: (StringBuffer information) {
information.writeln('line 1 of extra information');
information.writeln('line 2 of extra information\n'); // the double trailing newlines here are intentional
context: ErrorDescription('testing the error handling logic'),
informationCollector: () sync* {
yield ErrorDescription('line 1 of extra information');
yield ErrorHint('line 2 of extra information\n');
},
));
expect(console.join('\n'), matches(
'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n'
'The following assertion was thrown testing the error handling logic:\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'
'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'
'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'
......@@ -102,14 +103,15 @@ Future<void> main() async {
expect(console.join('\n'), matches(
'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\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'
'\'[^\']+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'
'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'
'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'
......@@ -138,18 +140,19 @@ Future<void> main() async {
exception: getAssertionErrorWithoutMessage(),
stack: sampleStack,
library: 'error handling test',
context: 'testing the error handling logic',
informationCollector: (StringBuffer information) {
information.writeln('line 1 of extra information');
information.writeln('line 2 of extra information\n'); // the double trailing newlines here are intentional
},
context: ErrorDescription('testing the error handling logic'),
informationCollector: () sync* {
yield ErrorDescription('line 1 of extra information');
yield ErrorDescription('line 2 of extra information\n'); // the trailing newlines here are intentional
}
));
expect(console.join('\n'), matches(
'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\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'
'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'
'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'
......
......@@ -145,7 +145,7 @@ void main() {
tearDownAll(() async {
// See widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions included in this count.
int widgetInspectorExtensionCount = 15;
int widgetInspectorExtensionCount = 16;
if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
// Some inspector extensions are only exposed if widget creation locations
// are tracked.
......
......@@ -153,7 +153,10 @@ void main() {
));
// 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 (1)'), findsNothing);
......@@ -238,7 +241,11 @@ void main() {
),
));
// 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);
// 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);
......
......@@ -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());
await tester.pumpWidget(
......
......@@ -82,7 +82,7 @@ void main() {
expect(error.toString(), contains('The following GlobalKey was specified multiple times'));
// 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...
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('Container'));
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() {
),
);
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(
find.byKey(key),
matchesGoldenFile('physical_model_overflow.png'),
......
......@@ -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(
TestSemantics.root(
......@@ -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(
TestSemantics.root(
......
......@@ -501,7 +501,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
FlutterError.dumpErrorToConsole(FlutterErrorDetails(
exception: exception,
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',
), forceReport: true);
return;
......@@ -534,31 +534,33 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// _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
// directly called again.
String treeDump;
DiagnosticsNode treeDump;
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) {
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 int stackLinesToOmit = reportExpectCall(stack, expectLine);
final List<DiagnosticsNode> omittedFrames = <DiagnosticsNode>[];
final int stackLinesToOmit = reportExpectCall(stack, omittedFrames);
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: _unmangle(stack),
context: 'running a test',
context: ErrorDescription('running a test'),
library: 'Flutter test framework',
stackFilter: (Iterable<String> frames) {
return FlutterError.defaultStackFilter(frames.skip(stackLinesToOmit));
},
informationCollector: (StringBuffer information) {
informationCollector: () sync* {
if (stackLinesToOmit > 0)
information.writeln(expectLine.toString());
yield* omittedFrames;
if (showAppDumpInErrors) {
information.writeln('At the time of the failure, the widget tree looked as follows:');
information.writeln('# ${treeDump.split("\n").takeWhile((String s) => s != "").join("\n# ")}');
yield DiagnosticsProperty<DiagnosticsNode>('At the time of the failure, the widget tree looked as follows', treeDump, linePrefix: '# ', style: DiagnosticsTreeStyle.flat);
}
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);
......@@ -847,7 +849,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
exception: exception,
stack: stack,
library: 'Flutter test framework',
context: 'while running async test code',
context: ErrorDescription('while running async test code'),
));
return null;
}).whenComplete(() {
......@@ -1335,7 +1337,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
exception: error,
stack: stack,
library: 'Flutter test framework',
context: 'while running async test code',
context: ErrorSummary('while running async test code'),
));
return null;
} finally {
......
......@@ -754,7 +754,7 @@ class _EqualsIgnoringHashCodes extends Matcher {
static final Object _mismatchedValueKey = Object();
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
......
......@@ -4,14 +4,17 @@
// 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
/// should be elided if a stack were to be modified to hide the expect call, or
/// zero if no such call was found.
///
/// 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
/// with the precise file and line number that called that function.
int reportExpectCall(StackTrace stack, StringBuffer information) {
/// the test_widgets [expect] function, this will fill the given
/// FlutterErrorBuilder with the precise file and line number that called that
/// function.
int reportExpectCall(StackTrace stack, List<DiagnosticsNode> information) {
final RegExp line0 = RegExp(r'^#0 +fail \(.+\)$');
final RegExp line1 = RegExp(r'^#1 +_expect \(.+\)$');
final RegExp line2 = RegExp(r'^#2 +expect \(.+\)$');
......@@ -25,8 +28,11 @@ int reportExpectCall(StackTrace stack, StringBuffer information) {
final Match expectMatch = line4.firstMatch(stackLines[4]);
assert(expectMatch != null);
assert(expectMatch.groupCount == 2);
information.writeln('This was caught by the test expectation on the following line:');
information.writeln(' ${expectMatch.group(1)} line ${expectMatch.group(2)}');
information.add(DiagnosticsStackTrace.singleFrame(
'This was caught by the test expectation on the following line',
frame: '${expectMatch.group(1)} line ${expectMatch.group(2)}',
));
return 4;
}
return 0;
......
......@@ -149,7 +149,8 @@ void main() {
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#000000')));
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)',
equalsIgnoringHashCodes('Foo#00000(Bar#00000)'));
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......@@ -10,9 +11,10 @@ void main() {
expect(false, isTrue);
throw 'unexpectedly did not throw';
} catch (e, stack) {
final StringBuffer information = StringBuffer();
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
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[1], matches(r'^ .*stack_manipulation_test.dart line [0-9]+$'));
}
......@@ -20,9 +22,9 @@ void main() {
try {
throw null;
} catch (e, stack) {
final StringBuffer information = StringBuffer();
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
expect(reportExpectCall(stack, information), 0);
expect(information.toString(), '');
expect(information, isEmpty);
}
});
}
......@@ -122,7 +122,7 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire
reason: 'Failure in $testName to compare to $fullTestExpectation',
);
final String expectationLine = expectations[expectationLineNumber];
final String outputLine = output[outputLineNumber];
String outputLine = output[outputLineNumber];
if (expectationLine == '<<skip until matching line>>') {
allowSkip = true;
expectationLineNumber += 1;
......@@ -139,6 +139,18 @@ Future<void> _testFile(String testName, String workingDirectory, String testDire
expect(haveSeenStdErrMarker, isFalse);
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<----- - - -');
expectationLineNumber += 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