diff --git a/dev/automated_tests/flutter_test/print_user_created_ancestor_expectation.txt b/dev/automated_tests/flutter_test/print_user_created_ancestor_expectation.txt new file mode 100644 index 0000000000..6ee59cbdc8 --- /dev/null +++ b/dev/automated_tests/flutter_test/print_user_created_ancestor_expectation.txt @@ -0,0 +1,26 @@ +<> +══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ +The following assertion was thrown building +RawGestureDetector-\[LabeledGlobalKey#.+\]\(state: +RawGestureDetectorState#.+\(gestures: , behavior: opaque\)\): +'package:flutter\/src\/painting\/basic_types\.dart': Failed assertion: line 223 pos 10: 'textDirection +!= null': is not true\. + +Either the assertion indicates an error in the framework itself, or we should provide substantially +more information in this error message to help you determine and fix the underlying cause\. +In either case, please report this assertion by filing a bug on GitHub: + https:\/\/github\.com\/flutter\/flutter\/issues\/new\?template=BUG\.md + +User-created ancestor of the error-causing widget was: + CustomScrollView + file:\/\/\/.+print_user_created_ancestor_test\.dart:[0-9]+:7 + +When the exception was thrown, this was the stack: +<> +\(elided [0-9]+ frames from .+\) +════════════════════════════════════════════════════════════════════════════════════════════════════ +.*..:.. \+0 -1: Rendering Error * + Test failed\. See exception logs above\. + The test description was: Rendering Error + * +.*..:.. \+0 -1: Some tests failed\. * diff --git a/dev/automated_tests/flutter_test/print_user_created_ancestor_no_flag_expectation.txt b/dev/automated_tests/flutter_test/print_user_created_ancestor_no_flag_expectation.txt new file mode 100644 index 0000000000..1a67d54ab7 --- /dev/null +++ b/dev/automated_tests/flutter_test/print_user_created_ancestor_no_flag_expectation.txt @@ -0,0 +1,25 @@ +<> +══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ +The following assertion was thrown building +RawGestureDetector-\[LabeledGlobalKey#.+\]\(state: +RawGestureDetectorState#.+\(gestures: , behavior: opaque\)\): +'package:flutter\/src\/painting\/basic_types\.dart': Failed assertion: line 223 pos 10: 'textDirection +!= null': is not true\. + +Either the assertion indicates an error in the framework itself, or we should provide substantially +more information in this error message to help you determine and fix the underlying cause\. +In either case, please report this assertion by filing a bug on GitHub: + https:\/\/github\.com\/flutter\/flutter\/issues\/new\?template=BUG\.md + +Widget creation tracking is currently disabled. Enabling it enables improved error messages\. It can +be enabled by passing `--track-widget-creation` to `flutter run` or `flutter test`\. + +When the exception was thrown, this was the stack: +<> +\(elided [0-9]+ frames from .+\) +════════════════════════════════════════════════════════════════════════════════════════════════════ +.*..:.. \+0 -1: Rendering Error * + Test failed\. See exception logs above\. + The test description was: Rendering Error + * +.*..:.. \+0 -1: Some tests failed\. * diff --git a/dev/automated_tests/flutter_test/print_user_created_ancestor_no_flag_test.dart b/dev/automated_tests/flutter_test/print_user_created_ancestor_no_flag_test.dart new file mode 100644 index 0000000000..74ea6fdc76 --- /dev/null +++ b/dev/automated_tests/flutter_test/print_user_created_ancestor_no_flag_test.dart @@ -0,0 +1,19 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Rendering Error', (WidgetTester tester) async { + // this should fail + await tester.pumpWidget( + CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: Container()), + ] + ) + ); + }); +} diff --git a/dev/automated_tests/flutter_test/print_user_created_ancestor_test.dart b/dev/automated_tests/flutter_test/print_user_created_ancestor_test.dart new file mode 100644 index 0000000000..74ea6fdc76 --- /dev/null +++ b/dev/automated_tests/flutter_test/print_user_created_ancestor_test.dart @@ -0,0 +1,19 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Rendering Error', (WidgetTester tester) async { + // this should fail + await tester.pumpWidget( + CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: Container()), + ] + ) + ); + }); +} diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart index abcb076eff..13776e8099 100644 --- a/packages/flutter/lib/src/foundation/assertions.dart +++ b/packages/flutter/lib/src/foundation/assertions.dart @@ -12,6 +12,9 @@ import 'print.dart'; /// Signature for [FlutterError.onError] handler. typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details); +/// Signature for [DiagnosticPropertiesBuilder] transformer. +typedef DiagnosticPropertiesTransformer = Iterable Function(Iterable properties); + /// Signature for [FlutterErrorDetails.informationCollector] callback /// and other callbacks that collect information describing an error. typedef InformationCollector = Iterable Function(); @@ -212,6 +215,20 @@ class FlutterErrorDetails extends Diagnosticable { this.silent = false, }); + /// Transformers to transform [DiagnosticsNode] in [DiagnosticPropertiesBuilder] + /// into a more descriptive form. + /// + /// There are layers that attach certain [DiagnosticsNode] into + /// [FlutterErrorDetails] that require knowledge from other layers to parse. + /// To correctly interpret those [DiagnosticsNode], register transformers in + /// the layers that possess the knowledge. + /// + /// See also: + /// + /// * [WidgetsBinding.initInstances], which registers its transformer. + static final List propertiesTransformers = + []; + /// The exception. Often this will be an [AssertionError], maybe specifically /// a [FlutterError]. However, this could be any value at all. final dynamic exception; @@ -449,6 +466,15 @@ class FlutterErrorDetails extends Diagnosticable { String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) { return toDiagnosticsNode(style: DiagnosticsTreeStyle.error).toStringDeep(minLevel: minLevel); } + + @override + DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) { + return _FlutterErrorDetailsNode( + name: name, + value: this, + style: style, + ); + } } /// Error class used to report Flutter-specific assertion failures and @@ -777,3 +803,28 @@ class DiagnosticsStackTrace extends DiagnosticsBlock { return DiagnosticsNode.message(frame, allowWrap: false); } } + +class _FlutterErrorDetailsNode extends DiagnosticableNode { + _FlutterErrorDetailsNode({ + String name, + @required FlutterErrorDetails value, + @required DiagnosticsTreeStyle style, + }) : super( + name: name, + value: value, + style: style, + ); + + @override + DiagnosticPropertiesBuilder get builder { + final DiagnosticPropertiesBuilder builder = super.builder; + if (builder == null){ + return null; + } + Iterable properties = builder.properties; + for (DiagnosticPropertiesTransformer transformer in FlutterErrorDetails.propertiesTransformers) { + properties = transformer(properties); + } + return DiagnosticPropertiesBuilder.fromProperties(properties.toList()); + } +} diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart index ed5d2da71f..55ffc3f910 100644 --- a/packages/flutter/lib/src/foundation/diagnostics.dart +++ b/packages/flutter/lib/src/foundation/diagnostics.dart @@ -2774,7 +2774,10 @@ class DiagnosticableNode extends DiagnosticsNode { DiagnosticPropertiesBuilder _cachedBuilder; - DiagnosticPropertiesBuilder get _builder { + /// Retrieve the [DiagnosticPropertiesBuilder] of current node. + /// + /// It will cache the result to prevent duplicate operation. + DiagnosticPropertiesBuilder get builder { if (kReleaseMode) return null; if (_cachedBuilder == null) { @@ -2786,14 +2789,14 @@ class DiagnosticableNode extends DiagnosticsNode { @override DiagnosticsTreeStyle get style { - return kReleaseMode ? DiagnosticsTreeStyle.none : super.style ?? _builder.defaultDiagnosticsTreeStyle; + return kReleaseMode ? DiagnosticsTreeStyle.none : super.style ?? builder.defaultDiagnosticsTreeStyle; } @override - String get emptyBodyDescription => kReleaseMode ? '' : _builder.emptyBodyDescription; + String get emptyBodyDescription => kReleaseMode ? '' : builder.emptyBodyDescription; @override - List getProperties() => kReleaseMode ? const [] : _builder.properties; + List getProperties() => kReleaseMode ? const [] : builder.properties; @override List getChildren() { @@ -2875,6 +2878,13 @@ String describeEnum(Object enumEntry) { /// Builder to accumulate properties and configuration used to assemble a /// [DiagnosticsNode] from a [Diagnosticable] object. class DiagnosticPropertiesBuilder { + /// Creates a [DiagnosticPropertiesBuilder] with [properties] initialize to + /// an empty array. + DiagnosticPropertiesBuilder() : properties = []; + + /// Creates a [DiagnosticPropertiesBuilder] with a given [properties]. + DiagnosticPropertiesBuilder.fromProperties(this.properties); + /// Add a property to the list of properties. void add(DiagnosticsNode property) { if (!kReleaseMode) { @@ -2883,7 +2893,7 @@ class DiagnosticPropertiesBuilder { } /// List of properties accumulated so far. - final List properties = []; + final List properties; /// Default style to use for the [DiagnosticsNode] if no style is specified. DiagnosticsTreeStyle defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.sparse; diff --git a/packages/flutter/lib/src/rendering/debug_overflow_indicator.dart b/packages/flutter/lib/src/rendering/debug_overflow_indicator.dart index be8790a31d..e65203ba6b 100644 --- a/packages/flutter/lib/src/rendering/debug_overflow_indicator.dart +++ b/packages/flutter/lib/src/rendering/debug_overflow_indicator.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/painting.dart'; +import 'package:flutter/foundation.dart'; import 'object.dart'; import 'stack.dart'; @@ -248,6 +249,8 @@ mixin DebugOverflowIndicatorMixin on RenderObject { context: ErrorDescription('during layout'), renderObject: this, informationCollector: () sync* { + if (debugCreator != null) + yield DiagnosticsDebugCreator(debugCreator); yield* overflowHints; yield describeForError('The specific $runtimeType in question is'); // TODO(jacobr): this line is ascii art that it would be nice to diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 99bd26766d..3aeb6d83dd 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1189,6 +1189,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im context: ErrorDescription('during $method()'), renderObject: this, informationCollector: () sync* { + if (debugCreator != null) + yield DiagnosticsDebugCreator(debugCreator); 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 @@ -3675,3 +3677,20 @@ class _SemanticsGeometry { bool get markAsHidden => _markAsHidden; bool _markAsHidden = false; } + +/// A class that creates [DiagnosticsNode] by wrapping [RenderObject.debugCreator]. +/// +/// Attach a [DiagnosticsDebugCreator] into [FlutterErrorDetails.informationCollector] +/// when a [RenderObject.debugCreator] is available. This will lead to improved +/// error message. +class DiagnosticsDebugCreator extends DiagnosticsProperty { + /// Create a [DiagnosticsProperty] with its [value] initialized to input + /// [RenderObject.debugCreator]. + DiagnosticsDebugCreator(Object value): + assert(value != null), + super( + 'debugCreator', + value, + level: DiagnosticLevel.hidden + ); +} diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index bf91401a70..66d376ce44 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -256,6 +256,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation); SystemChannels.system.setMessageHandler(_handleSystemMessage); + FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator); } /// The current [WidgetsBinding], if one has been created. diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index b5c3907255..e0ee34124f 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2344,6 +2344,7 @@ class BuildOwner { e, stack, informationCollector: () sync* { + yield DiagnosticsDebugCreator(DebugCreator(_dirtyElements[index])); yield _dirtyElements[index].describeElement('The element being rebuilt at the time was index $index of $dirtyCount'); }, ); @@ -2771,7 +2772,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { return StringProperty(name, debugGetCreatorChain(10)); } - // This is used to verify that Element objects move through life in an // orderly fashion. _ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial; @@ -3933,7 +3933,16 @@ abstract class ComponentElement extends Element { built = build(); debugWidgetBuilderValue(widget, built); } catch (e, stack) { - built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack)); + built = ErrorWidget.builder( + _debugReportException( + ErrorDescription('building $this'), + e, + stack, + informationCollector: () sync* { + yield DiagnosticsDebugCreator(DebugCreator(this)); + }, + ) + ); } finally { // We delay marking the element as clean until after calling build() so // that attempts to markNeedsBuild() during build() will be ignored. @@ -3944,7 +3953,16 @@ abstract class ComponentElement extends Element { _child = updateChild(_child, built, slot); assert(_child != null); } catch (e, stack) { - built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack)); + built = ErrorWidget.builder( + _debugReportException( + ErrorDescription('building $this'), + e, + stack, + informationCollector: () sync* { + yield DiagnosticsDebugCreator(DebugCreator(this)); + }, + ) + ); _child = updateChild(null, built, slot); } @@ -4738,7 +4756,7 @@ abstract class RenderObjectElement extends Element { void _debugUpdateRenderObjectOwner() { assert(() { - _renderObject.debugCreator = _DebugCreator(this); + _renderObject.debugCreator = DebugCreator(this); return true; }()); } @@ -5219,9 +5237,17 @@ class MultiChildRenderObjectElement extends RenderObjectElement { } } -class _DebugCreator { - _DebugCreator(this.element); - final RenderObjectElement element; +/// A wrapper class for the [Element] that is the creator of a [RenderObject]. +/// +/// Attaching a [DebugCreator] attach the [RenderObject] will lead to better error +/// message. +class DebugCreator { + /// Create a [DebugCreator] instance with input [Element]. + DebugCreator(this.element); + + /// The creator of the [RenderObject]. + final Element element; + @override String toString() => element.debugGetCreatorChain(12); } diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index 68e31fae4d..c44827d146 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -113,14 +113,32 @@ class _LayoutBuilderElement extends RenderObjectElement { built = widget.builder(this, constraints); debugWidgetBuilderValue(widget, built); } catch (e, stack) { - built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack)); + built = ErrorWidget.builder( + _debugReportException( + ErrorDescription('building $widget'), + e, + stack, + informationCollector: () sync* { + yield DiagnosticsDebugCreator(DebugCreator(this)); + }, + ) + ); } } try { _child = updateChild(_child, built, null); assert(_child != null); } catch (e, stack) { - built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $widget'), e, stack)); + built = ErrorWidget.builder( + _debugReportException( + ErrorDescription('building $widget'), + e, + stack, + informationCollector: () sync* { + yield DiagnosticsDebugCreator(DebugCreator(this)); + }, + ) + ); _child = updateChild(null, built, slot); } }); @@ -228,13 +246,15 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin node is DiagnosticsDebugCreator; + +/// Transformer to parse and gather information about [DiagnosticsDebugCreator]. +/// +/// This function will be registered to [FlutterErrorDetails.propertiesTransformers] +/// in [WidgetsBinding.initInstances]. +Iterable transformDebugCreator(Iterable properties) sync* { + final List pending = []; + bool foundStackTrace = false; + for (DiagnosticsNode node in properties) { + if (!foundStackTrace && node is DiagnosticsStackTrace) + foundStackTrace = true; + if (_isDebugCreator(node)) { + yield* _parseDiagnosticsNode(node); + } else { + if (foundStackTrace) { + pending.add(node); + } else { + yield node; + } + } + } + yield* pending; +} + +/// Transform the input [DiagnosticsNode]. +/// +/// Return null if input [DiagnosticsNode] is not applicable. +Iterable _parseDiagnosticsNode(DiagnosticsNode node) { + if (!_isDebugCreator(node)) + return null; + final DebugCreator debugCreator = node.value; + final Element element = debugCreator.element; + return _describeRelevantUserCode(element); +} + +Iterable _describeRelevantUserCode(Element element) { + if (!WidgetInspectorService.instance.isWidgetCreationTracked()) { + return [ + ErrorDescription( + 'Widget creation tracking is currently disabled. Enabling ' + 'it enables improved error messages. It can be enabled by passing ' + '`--track-widget-creation` to `flutter run` or `flutter test`.', + ), + ErrorSpacer(), + ]; + } + final List nodes = []; + element.visitAncestorElements((Element ancestor) { + // TODO(chunhtai): should print out all the widgets that are about to cross + // package boundaries. + if (_isLocalCreationLocation(ancestor)) { + nodes.add( + DiagnosticsBlock( + name: 'User-created ancestor of the error-causing widget was', + children: [ + ErrorDescription('${ancestor.widget.toStringShort()} ${_describeCreationLocation(ancestor)}'), + ], + ) + ); + nodes.add(ErrorSpacer()); + return false; + } + return true; + }); + return nodes; +} + +/// Returns if an object is user created. +/// +/// This function will only work in debug mode builds when +/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is +/// required as injecting creation locations requires a +/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). +/// +/// Currently is local creation locations are only available for +/// [Widget] and [Element]. +bool _isLocalCreationLocation(Object object) { + final _Location location = _getCreationLocation(object); + if (location == null) + return false; + return WidgetInspectorService.instance._isLocalCreationLocation(location); +} + +/// Returns the creation location of an object in String format if one is available. +/// +/// ex: "file:///path/to/main.dart:4:3" +/// +/// Creation locations are only available for debug mode builds when +/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is +/// required as injecting creation locations requires a +/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). +/// +/// Currently creation locations are only available for [Widget] and [Element]. +String _describeCreationLocation(Object object) { + final _Location location = _getCreationLocation(object); + return location?.toString(); +} + /// Returns the creation location of an object if one is available. /// /// Creation locations are only available for debug mode builds when @@ -2712,7 +2817,7 @@ class _Location { /// required as injecting creation locations requires a /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). /// -/// Currently creation locations are only available for [Widget] and [Element] +/// Currently creation locations are only available for [Widget] and [Element]. _Location _getCreationLocation(Object object) { final Object candidate = object is Element ? object.widget : object; return candidate is _HasCreationLocation ? candidate._location : null; diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index 301ce841a4..7b8368e258 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -527,9 +527,9 @@ void main() { final List lines = errorDetails.toString().split('\n'); // The lines in the middle of the error message contain the stack trace // which will change depending on where the test is run. - expect(lines.length, greaterThan(9)); + expect(lines.length, greaterThan(7)); expect( - lines.take(9).join('\n'), + lines.take(7).join('\n'), equalsIgnoringHashCodes( '══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n' 'The following assertion was thrown building Stepper(dirty,\n' @@ -537,12 +537,9 @@ void main() { '_StepperState#00000):\n' 'Steppers must not be nested. The material specification advises\n' 'that one should avoid embedding steppers within steppers.\n' - 'https://material.io/archive/guidelines/components/steppers.html#steppers-usage\n' - '\n' - 'When the exception was thrown, this was the stack:' + 'https://material.io/archive/guidelines/components/steppers.html#steppers-usage' ), ); - }); ///https://github.com/flutter/flutter/issues/16920 diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 9a1c6d2417..c77591d5c2 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -853,6 +853,132 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { expect(paramB2['column'], equals(25)); }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag. + testWidgets('test transformDebugCreator will re-order if after stack trace', (WidgetTester tester) async { + final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: const [ + Text('a'), + Text('b', textDirection: TextDirection.ltr), + Text('c', textDirection: TextDirection.ltr), + ], + ), + ), + ); + final Element elementA = find.text('a').evaluate().first; + String pubRootTest; + if (widgetTracked) { + final Map jsonObject = json.decode( + service.getSelectedWidget(null, 'my-group')); + final Map creationLocation = jsonObject['creationLocation']; + expect(creationLocation, isNotNull); + final String fileA = creationLocation['file']; + expect(fileA, endsWith('widget_inspector_test.dart')); + expect(jsonObject, isNot(contains('createdByLocalProject'))); + final List segments = Uri + .parse(fileA) + .pathSegments; + // Strip a couple subdirectories away to generate a plausible pub root + // directory. + pubRootTest = '/' + + segments.take(segments.length - 2).join('/'); + service.setPubRootDirectories([pubRootTest]); + } + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + builder.add(StringProperty('dummy1', 'value')); + builder.add(StringProperty('dummy2', 'value')); + builder.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', null)); + builder.add(DiagnosticsDebugCreator(DebugCreator(elementA))); + + final List nodes = List.from(transformDebugCreator(builder.properties)); + expect(nodes.length, 5); + expect(nodes[0].runtimeType, StringProperty); + expect(nodes[0].name, 'dummy1'); + expect(nodes[1].runtimeType, StringProperty); + expect(nodes[1].name, 'dummy2'); + // transformed node should come in front of stack trace. + if (widgetTracked) { + expect(nodes[2].runtimeType, DiagnosticsBlock); + final DiagnosticsBlock node = nodes[2]; + final List children = node.getChildren(); + expect(children.length, 1); + final ErrorDescription child = children[0]; + expect(child.valueToString().contains(Uri.parse(pubRootTest).path), true); + } else { + expect(nodes[2].runtimeType, ErrorDescription); + final ErrorDescription node = nodes[2]; + expect(node.valueToString().startsWith('Widget creation tracking is currently disabled.'), true); + } + expect(nodes[3].runtimeType, ErrorSpacer); + expect(nodes[4].runtimeType, DiagnosticsStackTrace); + }); + + testWidgets('test transformDebugCreator will not re-order if before stack trace', (WidgetTester tester) async { + final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: const [ + Text('a'), + Text('b', textDirection: TextDirection.ltr), + Text('c', textDirection: TextDirection.ltr), + ], + ), + ), + ); + final Element elementA = find.text('a').evaluate().first; + String pubRootTest; + if (widgetTracked) { + final Map jsonObject = json.decode( + service.getSelectedWidget(null, 'my-group')); + final Map creationLocation = jsonObject['creationLocation']; + expect(creationLocation, isNotNull); + final String fileA = creationLocation['file']; + expect(fileA, endsWith('widget_inspector_test.dart')); + expect(jsonObject, isNot(contains('createdByLocalProject'))); + final List segments = Uri + .parse(fileA) + .pathSegments; + // Strip a couple subdirectories away to generate a plausible pub root + // directory. + pubRootTest = '/' + + segments.take(segments.length - 2).join('/'); + service.setPubRootDirectories([pubRootTest]); + } + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + builder.add(StringProperty('dummy1', 'value')); + builder.add(DiagnosticsDebugCreator(DebugCreator(elementA))); + builder.add(StringProperty('dummy2', 'value')); + builder.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', null)); + + final List nodes = List.from(transformDebugCreator(builder.properties)); + expect(nodes.length, 5); + expect(nodes[0].runtimeType, StringProperty); + expect(nodes[0].name, 'dummy1'); + // transformed node stays at original place. + if (widgetTracked) { + expect(nodes[1].runtimeType, DiagnosticsBlock); + final DiagnosticsBlock node = nodes[1]; + final List children = node.getChildren(); + expect(children.length, 1); + final ErrorDescription child = children[0]; + expect(child.valueToString().contains(Uri.parse(pubRootTest).path), true); + } else { + expect(nodes[1].runtimeType, ErrorDescription); + final ErrorDescription node = nodes[1]; + expect(node.valueToString().startsWith('Widget creation tracking is currently disabled.'), true); + } + expect(nodes[2].runtimeType, ErrorSpacer); + expect(nodes[3].runtimeType, StringProperty); + expect(nodes[3].name, 'dummy2'); + expect(nodes[4].runtimeType, DiagnosticsStackTrace); + }, skip: WidgetInspectorService.instance.isWidgetCreationTracked()); + testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async { await tester.pumpWidget( Directionality( diff --git a/packages/flutter_tools/test/commands/test_test.dart b/packages/flutter_tools/test/commands/test_test.dart index e8b5070188..d5710fa798 100644 --- a/packages/flutter_tools/test/commands/test_test.dart +++ b/packages/flutter_tools/test/commands/test_test.dart @@ -31,33 +31,44 @@ void main() { testUsingContext('report nice errors for exceptions thrown within testWidgets()', () async { Cache.flutterRoot = '../..'; return _testFile('exception_handling', automatedTestsDirectory, flutterTestDirectory); - }, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). testUsingContext('report a nice error when a guarded function was called without await', () async { Cache.flutterRoot = '../..'; return _testFile('test_async_utils_guarded', automatedTestsDirectory, flutterTestDirectory); - }, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). testUsingContext('report a nice error when an async function was called without await', () async { Cache.flutterRoot = '../..'; return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory); - }, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). testUsingContext('report a nice error when a Ticker is left running', () async { Cache.flutterRoot = '../..'; return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory); - }, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async { final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests'); Cache.flutterRoot = '../..'; return _testFile('trivial', missingDependencyTests, missingDependencyTests); - }, skip: io.Platform.isWindows); // Dart on Windows has trouble with unicode characters in output + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report which user created widget caused the error', () async { + Cache.flutterRoot = '../..'; + return _testFile('print_user_created_ancestor', automatedTestsDirectory, flutterTestDirectory, + extraArguments: const ['--track-widget-creation']); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). + + testUsingContext('report which user created widget caused the error - no flag', () async { + Cache.flutterRoot = '../..'; + return _testFile('print_user_created_ancestor_no_flag', automatedTestsDirectory, flutterTestDirectory); + }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425). testUsingContext('run a test when its name matches a regexp', () async { Cache.flutterRoot = '../..'; final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory, - extraArgs: const ['--name', 'inc.*de']); + extraArguments: const ['--name', 'inc.*de']); if (!result.stdout.contains('+1: All tests passed')) fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n'); expect(result.exitCode, 0); @@ -66,7 +77,7 @@ void main() { testUsingContext('run a test when its name contains a string', () async { Cache.flutterRoot = '../..'; final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory, - extraArgs: const ['--plain-name', 'include']); + extraArguments: const ['--plain-name', 'include']); if (!result.stdout.contains('+1: All tests passed')) fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n'); expect(result.exitCode, 0); @@ -75,7 +86,7 @@ void main() { testUsingContext('test runs to completion', () async { Cache.flutterRoot = '../..'; final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory, - extraArgs: const ['--verbose']); + extraArguments: const ['--verbose']); if ((!result.stdout.contains('+1: All tests passed')) || (!result.stdout.contains('test 0: starting shell process')) || (!result.stdout.contains('test 0: deleting temporary directory')) || @@ -90,7 +101,13 @@ void main() { }); } -Future _testFile(String testName, String workingDirectory, String testDirectory, { Matcher exitCode }) async { +Future _testFile( + String testName, + String workingDirectory, + String testDirectory, { + Matcher exitCode, + List extraArguments = const [], + }) async { exitCode ??= isNonZero; final String fullTestExpectation = fs.path.join(testDirectory, '${testName}_expectation.txt'); final File expectationFile = fs.file(fullTestExpectation); @@ -100,7 +117,12 @@ Future _testFile(String testName, String workingDirectory, String testDire while (_testExclusionLock != null) await _testExclusionLock; - final ProcessResult exec = await _runFlutterTest(testName, workingDirectory, testDirectory); + final ProcessResult exec = await _runFlutterTest( + testName, + workingDirectory, + testDirectory, + extraArguments: extraArguments, + ); expect(exec.exitCode, exitCode); final List output = exec.stdout.split('\n'); @@ -164,7 +186,7 @@ Future _runFlutterTest( String testName, String workingDirectory, String testDirectory, { - List extraArgs = const [], + List extraArguments = const [], }) async { final String testFilePath = fs.path.join(testDirectory, '${testName}_test.dart'); @@ -177,7 +199,7 @@ Future _runFlutterTest( fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')), 'test', '--no-color', - ...extraArgs, + ...extraArguments, testFilePath ];