Commit 944ee24b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Report GlobalKey duplicates (#8593)

parent 20d401de
......@@ -102,6 +102,42 @@ class FlutterErrorDetails {
/// error to the console on end-user devices, while still allowing a custom
/// error handler to see the errors even in release builds.
final bool silent;
/// Converts the [exception] to a string.
///
/// This applies some additional logic to
String exceptionAsString() {
String longMessage;
if (exception is AssertionError) {
// Regular _AssertionErrors thrown by assert() put the message last, after
// some code snippets. This leads to ugly messages. To avoid this, we move
// the assertion message up to before the code snippets, separated by a
// newline, if we recognise that format is being used.
final String message = exception.message;
final String fullMessage = exception.toString();
if (message is String && message != fullMessage) {
if (fullMessage.length > message.length) {
final int position = fullMessage.lastIndexOf(message);
if (position == fullMessage.length - message.length &&
position > 2 &&
fullMessage.substring(position - 2, position) == ': ') {
longMessage = '${message.trimRight()}\n${fullMessage.substring(0, position - 2)}';
}
}
}
longMessage ??= fullMessage;
} else if (exception is String) {
longMessage = exception;
} else if (exception is Error || exception is Exception) {
longMessage = exception.toString();
} else {
longMessage = ' ${exception.toString()}';
}
longMessage = longMessage.trimRight();
if (longMessage.isEmpty)
longMessage = ' <no message available>';
return longMessage;
}
}
/// Error class used to report Flutter-specific assertion failures and
......@@ -207,8 +243,14 @@ class FlutterError extends AssertionError {
} else {
errorName = '${details.exception.runtimeType} object';
}
debugPrint('The following $errorName was $verb:', wrapWidth: _kWrapWidth);
debugPrint('${details.exception}', wrapWidth: _kWrapWidth);
// Many exception classes put their type at the head of their message.
// This is redundant with the way we display exceptions, so attempt to
// strip out that header when we see it.
final String prefix = '${details.exception.runtimeType}: ';
String message = details.exceptionAsString();
if (message.startsWith(prefix))
message = message.substring(prefix.length);
debugPrint('The following $errorName was $verb:\n$message', wrapWidth: _kWrapWidth);
}
Iterable<String> stackLines = (details.stack != null) ? details.stack.toString().trimRight().split('\n') : null;
if ((details.exception is AssertionError) && (details.exception is! FlutterError)) {
......@@ -216,6 +258,7 @@ class FlutterError extends AssertionError {
if (stackLines != null) {
final List<String> stackList = stackLines.take(2).toList();
if (stackList.length >= 2) {
// TODO(ianh): This has bitrotted and is no longer matching. https://github.com/flutter/flutter/issues/4021
final RegExp throwPattern = new RegExp(r'^#0 +_AssertionError._throwNew \(dart:.+\)$');
final RegExp assertPattern = new RegExp(r'^#1 +[^(]+ \((.+?):([0-9]+)(?::[0-9]+)?\)$');
if (throwPattern.hasMatch(stackList[0])) {
......@@ -249,11 +292,11 @@ class FlutterError extends AssertionError {
if (details.informationCollector != null) {
final StringBuffer information = new StringBuffer();
details.informationCollector(information);
debugPrint('\n$information', wrapWidth: _kWrapWidth);
debugPrint('\n${information.toString().trimRight()}', wrapWidth: _kWrapWidth);
}
debugPrint(footer);
} else {
debugPrint('Another exception was thrown: ${details.exception.toString().split("\n")[0]}');
debugPrint('Another exception was thrown: ${details.exceptionAsString().split("\n")[0].trimLeft()}');
}
_errorCount += 1;
}
......
......@@ -2668,9 +2668,9 @@ abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, Parent
/// If `after` is null, then this inserts the child at the start of the list,
/// and the child becomes the new [firstChild].
void insert(ChildType child, { ChildType after }) {
assert(child != this);
assert(after != this);
assert(child != after);
assert(child != this, 'A RenderObject cannot be inserted into itself.');
assert(after != this, 'A RenderObject cannot simultaneously be both the parent and the sibling of another RenderObject.');
assert(child != after, 'A RenderObject cannot be inserted after itself.');
assert(child != _firstChild);
assert(child != _lastChild);
adoptChild(child);
......
......@@ -149,31 +149,37 @@ typedef void GlobalKeyRemoveListener(GlobalKey key);
/// Global keys are relatively expensive. If you don't need any of the features
/// listed above, consider using a [Key], [ValueKey], [ObjectKey], or
/// [UniqueKey] instead.
///
/// You cannot simultaneously include two widgets in the tree with the same
/// global key. Attempting to do so will assert at runtime.
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
/// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for debugging.
/// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
/// debugging.
///
/// The label is purely for debugging and not used for comparing the identity
/// of the key.
factory GlobalKey({ String debugLabel }) = LabeledGlobalKey<T>._; // the label is purely for debugging purposes and is otherwise ignored
factory GlobalKey({ String debugLabel }) = LabeledGlobalKey<T>._;
/// Creates a global key without a label.
///
/// Used by subclasss because the factory constructor shadows the implicit
/// Used by subclasses because the factory constructor shadows the implicit
/// constructor.
const GlobalKey.constructor() : super._();
static final Map<GlobalKey, Element> _registry = new Map<GlobalKey, Element>();
static final Map<GlobalKey, int> _debugDuplicates = new Map<GlobalKey, int>();
static final Map<GlobalKey, Set<GlobalKeyRemoveListener>> _removeListeners = new Map<GlobalKey, Set<GlobalKeyRemoveListener>>();
static final Set<GlobalKey> _removedKeys = new HashSet<GlobalKey>();
static final Set<Element> _debugIllFatedElements = new HashSet<Element>();
static final Map<GlobalKey, Element> _debugReservations = new Map<GlobalKey, Element>();
void _register(Element element) {
assert(() {
if (_registry.containsKey(this)) {
final int oldCount = _debugDuplicates.putIfAbsent(this, () => 1);
assert(oldCount >= 1);
_debugDuplicates[this] = oldCount + 1;
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
_debugIllFatedElements.add(_registry[this]);
}
return true;
});
......@@ -182,14 +188,10 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
void _unregister(Element element) {
assert(() {
if (_registry.containsKey(this) && _debugDuplicates.containsKey(this)) {
final int oldCount = _debugDuplicates[this];
assert(oldCount >= 2);
if (oldCount == 2) {
_debugDuplicates.remove(this);
} else {
_debugDuplicates[this] = oldCount - 1;
}
if (_registry.containsKey(this) && _registry[this] != element) {
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
}
return true;
});
......@@ -199,6 +201,73 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
}
}
void _debugReserveFor(Element parent) {
assert(() {
assert(parent != null);
if (_debugReservations.containsKey(this) && _debugReservations[this] != parent) {
// It's possible for an element to get built multiple times in one
// frame, in which case it'll reserve the same child's key multiple
// times. We catch multiple children of one widget having the same key
// by verifying that an element never steals elements from itself, so we
// don't care to verify that here as well.
final String older = _debugReservations[this].toString();
final String newer = parent.toString();
if (older != newer) {
throw new FlutterError(
'Multiple widgets used the same GlobalKey.\n'
'The key $this was used by multiple widgets. The parents of those widgets were:\n'
'- $older\n'
'- $newer\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
}
throw new FlutterError(
'Multiple widgets used the same GlobalKey.\n'
'The key $this was used by multiple widgets. The parents of those widgets were '
'different widgets that both had the following description:\n'
' $newer\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
}
_debugReservations[this] = parent;
return true;
});
}
static void _debugVerifyIllFatedPopulation() {
assert(() {
Map<GlobalKey, Set<Element>> duplicates;
for (Element element in _debugIllFatedElements) {
if (element._debugLifecycleState != _ElementLifecycle.defunct) {
assert(element != null);
assert(element.widget != null);
assert(element.widget.key != null);
final GlobalKey key = element.widget.key;
assert(_registry.containsKey(key));
duplicates ??= <GlobalKey, Set<Element>>{};
final Set<Element> elements = duplicates.putIfAbsent(key, () => new HashSet<Element>());
elements.add(element);
elements.add(_registry[key]);
}
}
_debugIllFatedElements.clear();
_debugReservations.clear();
if (duplicates != null) {
final StringBuffer buffer = new StringBuffer();
buffer.writeln('Multiple widgets used the same GlobalKey.\n');
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');
}
buffer.write('A GlobalKey can only be specified on one widget at a time in the widget tree.');
throw new FlutterError(buffer.toString());
}
return true;
});
}
Element get _currentElement => _registry[this];
/// The build context in which the widget with this key builds.
......@@ -255,21 +324,6 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
assert(removed);
}
static bool _debugCheckForDuplicates() {
String message = '';
for (GlobalKey key in _debugDuplicates.keys) {
message += 'The following GlobalKey was found multiple times among mounted elements: $key (${_debugDuplicates[key]} instances)\n';
message += 'The most recently registered instance is: ${_registry[key]}\n';
}
if (_debugDuplicates.isNotEmpty) {
throw new FlutterError(
'Incorrect GlobalKey usage.\n'
'$message'
);
}
return true;
}
static void _notifyListeners() {
if (_removedKeys.isEmpty)
return;
......@@ -1128,9 +1182,9 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
'$description has a $T ancestor, but there are other widgets between them:\n';
for (Widget ancestor in badAncestors) {
if (ancestor.runtimeType == runtimeType) {
result += ' $ancestor (this is a different $runtimeType than the one with the problem)\n';
result += '- $ancestor (this is a different $runtimeType than the one with the problem)\n';
} else {
result += ' $ancestor\n';
result += '- $ancestor\n';
}
}
result += 'These widgets cannot come between a $runtimeType and its $T.\n';
......@@ -1356,6 +1410,15 @@ class _InactiveElements {
_elements.remove(element);
assert(!element._active);
}
bool debugContains(Element element) {
bool result;
assert(() {
result = _elements.contains(element);
return true;
});
return result;
}
}
/// Signature for the callback to [BuildContext.visitChildElements].
......@@ -1774,6 +1837,7 @@ class BuildOwner {
context._debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
assert(_debugCurrentBuildTarget == context);
_debugCurrentBuildTarget = debugPreviousBuildTarget;
_debugElementWasRebuilt(context);
return true;
});
}
......@@ -1846,13 +1910,26 @@ class BuildOwner {
assert(_debugStateLockLevel >= 0);
}
Map<Element, Set<GlobalKey>> _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans;
void _debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(Element node, GlobalKey key) {
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans ??= new HashMap<Element, Set<GlobalKey>>();
final Set<GlobalKey> keys = _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans
.putIfAbsent(node, () => new HashSet<GlobalKey>());
keys.add(key);
}
void _debugElementWasRebuilt(Element node) {
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans?.remove(node);
}
/// Complete the element build pass by unmounting any elements that are no
/// longer active.
///
/// This is called by [WidgetsBinding.beginFrame].
///
/// In checked mode, this also verifies that each global key is used at most
/// once.
/// In debug mode, this also runs some sanity checks, for example checking for
/// duplicate global keys.
///
/// After the current call stack unwinds, a microtask that notifies listeners
/// about changes to global keys will run.
......@@ -1860,9 +1937,86 @@ class BuildOwner {
Timeline.startSync('Finalize tree');
try {
lockState(() {
_inactiveElements._unmountAll();
_inactiveElements._unmountAll(); // this unregisters the GlobalKeys
});
assert(() {
try {
GlobalKey._debugVerifyIllFatedPopulation();
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans != null &&
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans.isNotEmpty) {
final Set<GlobalKey> keys = new HashSet<GlobalKey>();
for (Element element in _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans.keys) {
if (element._debugLifecycleState != _ElementLifecycle.defunct)
keys.addAll(_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans[element]);
}
if (keys.isNotEmpty) {
final Map<String, int> keyStringCount = new HashMap<String, int>();
for (String key in keys.map<String>((GlobalKey key) => key.toString())) {
if (keyStringCount.containsKey(key)) {
keyStringCount[key] += 1;
} else {
keyStringCount[key] = 1;
}
}
final List<String> keyLabels = <String>[];
keyStringCount.forEach((String key, int count) {
if (count == 1) {
keyLabels.add(key);
} else {
keyLabels.add('$key ($count different affected keys had this toString representation)');
}
});
final Iterable<Element> elements = _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans.keys;
final Map<String, int> elementStringCount = new HashMap<String, int>();
for (String element in elements.map<String>((Element element) => element.toString())) {
if (elementStringCount.containsKey(element)) {
elementStringCount[element] += 1;
} else {
elementStringCount[element] = 1;
}
}
final List<String> elementLabels = <String>[];
elementStringCount.forEach((String element, int count) {
if (count == 1) {
elementLabels.add(element);
} else {
elementLabels.add('$element ($count different affected elements had this toString representation)');
}
});
assert(keyLabels.isNotEmpty);
final String the = keys.length == 1 ? ' the' : '';
final String s = keys.length == 1 ? '' : 's';
final String were = keys.length == 1 ? 'was' : 'were';
final String their = keys.length == 1 ? 'its' : 'their';
final String respective = elementLabels.length == 1 ? '' : ' respective';
final String those = keys.length == 1 ? 'that' : 'those';
final String s2 = elementLabels.length == 1 ? '' : 's';
final String those2 = elementLabels.length == 1 ? 'that' : 'those';
final String they = elementLabels.length == 1 ? 'it' : 'they';
final String think = elementLabels.length == 1 ? 'thinks' : 'think';
final String are = elementLabels.length == 1 ? 'is' : 'are';
throw new FlutterError(
'Duplicate GlobalKey$s detected in widget tree.\n'
'The following GlobalKey$s $were specified multiple times in the widget tree. This will lead to '
'parts of the widget tree being truncated unexpectedly, because the second time a key is seen, '
'the previous instance is moved to the new location. The key$s $were:\n'
'- ${keyLabels.join("\n ")}\n'
'This was determined by noticing that after$the widget$s with the above global key$s $were moved '
'out of $their$respective previous parent$s2, $those2 previous parent$s2 never updated during this frame, meaning '
'that $they either did not update at all or updated before the widget$s $were moved, in either case '
'implying that $they still $think that $they should have a child with $those global key$s.\n'
'The specific parent$s2 that did not update after having one or more children forcibly removed '
'due to GlobalKey reparenting $are:\n'
'- ${elementLabels.join("\n ")}\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
}
}
} finally {
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans?.clear();
}
return true;
});
assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners);
} catch (e, stack) {
_debugReportException('while finalizing the widget tree', e, stack);
......@@ -2078,6 +2232,13 @@ abstract class Element implements BuildContext {
/// </table>
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
assert(() {
if (newWidget != null && newWidget.key is GlobalKey) {
final GlobalKey key = newWidget.key;
key._debugReserveFor(this);
}
return true;
});
if (newWidget == null) {
if (child != null)
deactivateChild(child);
......@@ -2094,6 +2255,10 @@ abstract class Element implements BuildContext {
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner._debugElementWasRebuilt(child);
return true;
});
return child;
}
deactivateChild(child);
......@@ -2222,6 +2387,12 @@ abstract class Element implements BuildContext {
}
Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
// The "inactivity" of the element being retaken here may be forward-looking: if
// we are taking an element with a GlobalKey from an element that currently has
// it as a child, then we know that that element will soon no longer have that
// element as a child. The only way that assumption could be false is if the
// global key is being duplicated, and we'll try to track that using the
// _debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans call below.
final Element element = key._currentElement;
if (element == null)
return null;
......@@ -2234,6 +2405,23 @@ abstract class Element implements BuildContext {
});
final Element parent = element._parent;
if (parent != null) {
assert(() {
if (parent == this) {
throw new FlutterError(
'A GlobalKey was used multiple times inside one widget\'s child list.\n'
'The offending GlobalKey was: $key\n'
'The parent of the widgets with that key was:\n $parent\n'
'The first child to get instantiated with that key became:\n $element\n'
'The second child that was to be instantiated with that key was:\n $widget\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
}
parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent,
key,
);
return true;
});
parent.forgetChild(element);
parent.deactivateChild(element);
}
......@@ -2783,9 +2971,9 @@ abstract class BuildableElement extends Element {
owner.scheduleBuildFor(this);
}
/// Called by the binding when [BuildOwner.scheduleBuildFor] has been called
/// to mark this element dirty, and, in components, by [mount] when the
/// element is first built and by [update] when the widget has changed.
/// Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor] has been
/// called to mark this element dirty, by [mount] when the element is first
/// built, and by [update] when the widget has changed.
void rebuild() {
assert(_debugLifecycleState != _ElementLifecycle.initial);
if (!_active || !_dirty)
......
// Copyright 2017 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 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:test/test.dart';
dynamic getAssertionErrorWithMessage() {
try {
assert(false, 'Message goes here.');
} catch (e) {
return e;
}
throw 'assert failed';
}
dynamic getAssertionErrorWithoutMessage() {
try {
assert(false);
} catch (e) {
return e;
}
throw 'assert failed';
}
dynamic getAssertionErrorWithLongMessage() {
try {
assert(false, 'word ' * 100);
} catch (e) {
return e;
}
throw 'assert failed';
}
Future<StackTrace> getSampleStack() async {
return await new Future<StackTrace>.sync(() => StackTrace.current);
}
Future<Null> main() async {
final List<String> console = <String>[];
final StackTrace sampleStack = await getSampleStack();
test('Error reporting - pretest', () async {
expect(debugPrint, equals(debugPrintThrottled));
debugPrint = (String message, { int wrapWidth }) {
console.add(message);
};
});
test('Error reporting - assert with message', () async {
expect(console, isEmpty);
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
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
},
));
expect(console.join('\n'), matches(new RegExp(
'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n'
'The following assertion was thrown testing the error handling logic:\n'
'Message goes here\\.\n'
'\'[^\']+packages/flutter/test/foundation/error_reporting_test\\.dart\': 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 '
'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\n'
'\n'
'When the exception was thrown, this was the stack:\n'
'#0 getSampleStack\\.<anonymous closure> \\([^)]+packages/flutter/test/foundation/error_reporting_test\\.dart:[0-9]+:[0-9]+\\)\n'
'#2 getSampleStack \\([^)]+packages/flutter/test/foundation/error_reporting_test\\.dart:[0-9]+:[0-9]+\\)\n'
'<asynchronous suspension>\n' // TODO(ianh): https://github.com/flutter/flutter/issues/4021
'#3 main \\([^)]+packages/flutter/test/foundation/error_reporting_test\\.dart:[0-9]+:[0-9]+\\)\n'
'(.+\n)+' // TODO(ianh): when fixing #4021, also filter out frames from the test infrastructure below the first call to our main()
'\\(elided [0-9]+ frames from package dart:async\\)\n'
'\n'
'line 1 of extra information\n'
'line 2 of extra information\n'
'════════════════════════════════════════════════════════════════════════════════════════════════════\$',
)));
console.clear();
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: getAssertionErrorWithMessage(),
));
expect(console.join('\n'), 'Another exception was thrown: Message goes here.');
console.clear();
FlutterError.resetErrorCount();
});
test('Error reporting - assert with long message', () async {
expect(console, isEmpty);
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: getAssertionErrorWithLongMessage(),
));
expect(console.join('\n'), matches(new RegExp(
'^══╡ 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'
'\'[^\']+packages/flutter/test/foundation/error_reporting_test\\.dart\': 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 '
'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\n'
'════════════════════════════════════════════════════════════════════════════════════════════════════\$',
)));
console.clear();
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: getAssertionErrorWithLongMessage(),
));
expect(
console.join('\n'),
'Another exception was thrown: '
'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'
);
console.clear();
FlutterError.resetErrorCount();
});
test('Error reporting - assert with no message', () async {
expect(console, isEmpty);
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
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
},
));
expect(console.join('\n'), matches(new RegExp(
'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n'
'The following assertion was thrown testing the error handling logic:\n'
'\'[^\']+packages/flutter/test/foundation/error_reporting_test\\.dart\': Failed assertion: line [0-9]+ pos [0-9]+: \'false\': is not true\\.\n'
'\n'
'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\\.\n'
'In either case, please report this assertion by filing a bug on GitHub:\n'
' https://github\\.com/flutter/flutter/issues/new\n'
'\n'
'When the exception was thrown, this was the stack:\n'
'#0 getSampleStack\\.<anonymous closure> \\([^)]+packages/flutter/test/foundation/error_reporting_test\\.dart:[0-9]+:[0-9]+\\)\n'
'#2 getSampleStack \\([^)]+packages/flutter/test/foundation/error_reporting_test\\.dart:[0-9]+:[0-9]+\\)\n'
'<asynchronous suspension>\n' // TODO(ianh): https://github.com/flutter/flutter/issues/4021
'#3 main \\([^)]+packages/flutter/test/foundation/error_reporting_test\\.dart:[0-9]+:[0-9]+\\)\n'
'(.+\n)+' // TODO(ianh): when fixing #4021, also filter out frames from the test infrastructure below the first call to our main()
'\\(elided [0-9]+ frames from package dart:async\\)\n'
'\n'
'line 1 of extra information\n'
'line 2 of extra information\n'
'════════════════════════════════════════════════════════════════════════════════════════════════════\$',
)));
console.clear();
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: getAssertionErrorWithoutMessage(),
));
expect(console.join('\n'), matches('Another exception was thrown: \'[^\']+packages/flutter/test/foundation/error_reporting_test\\.dart\': Failed assertion: line [0-9]+ pos [0-9]+: \'false\': is not true\\.'));
console.clear();
FlutterError.resetErrorCount();
});
test('Error reporting - NoSuchMethodError', () async {
expect(console, isEmpty);
final dynamic exception = new NoSuchMethodError(5, #foo, <dynamic>[2, 4], null);
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: exception,
));
expect(console.join('\n'), matches(new RegExp(
'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n'
'The following NoSuchMethodError was thrown:\n'
'Receiver: 5\n'
'Tried calling: foo = 2, 4\n'
'════════════════════════════════════════════════════════════════════════════════════════════════════\$',
)));
console.clear();
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: exception,
));
expect(console.join('\n'), 'Another exception was thrown: NoSuchMethodError: Receiver: 5');
console.clear();
FlutterError.resetErrorCount();
});
test('Error reporting - NoSuchMethodError', () async {
expect(console, isEmpty);
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: 'hello',
));
expect(console.join('\n'), matches(new RegExp(
'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n'
'The following message was thrown:\n'
'hello\n'
'════════════════════════════════════════════════════════════════════════════════════════════════════\$',
)));
console.clear();
FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: 'hello again',
));
expect(console.join('\n'), 'Another exception was thrown: hello again');
console.clear();
FlutterError.resetErrorCount();
});
test('Error reporting - posttest', () async {
expect(console, isEmpty);
debugPrint = debugPrintThrottled;
});
}
......@@ -44,7 +44,24 @@ void main() {
expect(keyA, isNot(equals(keyB)));
});
testWidgets('GlobalKey duplication', (WidgetTester tester) async {
testWidgets('GlobalKey duplication 1 - double appearance', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(
key: const ValueKey<int>(1),
child: new SizedBox(key: key),
),
new Container(
key: const ValueKey<int>(2),
child: new Placeholder(key: key),
),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
......@@ -77,6 +94,315 @@ void main() {
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new SizedBox(key: key),
new Placeholder(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
new Placeholder(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Placeholder(key: key),
new Container(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 6 - splitting and not changing type', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
new Container(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 7 - appearing later', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
new Container(key: new ValueKey<int>(2)),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
new Container(key: new ValueKey<int>(2), child: new Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 8 - appearing earlier', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(1)),
new Container(key: new ValueKey<int>(2), child: new Container(key: key)),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
new Container(key: new ValueKey<int>(2), child: new Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 9 - moving and appearing later', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0), child: new Container(key: key)),
new Container(key: new ValueKey<int>(1)),
new Container(key: new ValueKey<int>(2)),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
new Container(key: new ValueKey<int>(2), child: new Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 10 - moving and appearing earlier', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(1)),
new Container(key: new ValueKey<int>(2)),
new Container(key: new ValueKey<int>(3), child: new Container(key: key)),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
new Container(key: new ValueKey<int>(2), child: new Container(key: key)),
new Container(key: new ValueKey<int>(3)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 11 - double sibling appearance', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
new Container(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 12 - all kinds of badness at once', (WidgetTester tester) async {
final Key key1 = new GlobalKey(debugLabel: 'problematic');
final Key key2 = new GlobalKey(debugLabel: 'problematic'); // intentionally the same label
final Key key3 = new GlobalKey(debugLabel: 'also problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key1),
new Container(key: key1),
new Container(key: key2),
new Container(key: key1),
new Container(key: key1),
new Container(key: key2),
new Container(key: key1),
new Container(key: key1),
new Row(
children: <Widget>[
new Container(key: key1),
new Container(key: key1),
new Container(key: key2),
new Container(key: key2),
new Container(key: key2),
new Container(key: key3),
new Container(key: key2),
],
),
new Row(
children: <Widget>[
new Container(key: key1),
new Container(key: key1),
new Container(key: key3),
],
),
new Container(key: key3),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async {
final Key key1 = new GlobalKey(debugLabel: 'problematic');
final Key key2 = new GlobalKey(debugLabel: 'problematic'); // intentionally the same label
final Key key3 = new GlobalKey(debugLabel: 'also problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key1),
new Container(key: key2),
new Container(key: key3),
]),
);
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key1),
new Container(key: key1),
new Container(key: key2),
new Container(key: key1),
new Container(key: key1),
new Container(key: key2),
new Container(key: key1),
new Container(key: key1),
new Row(
children: <Widget>[
new Container(key: key1),
new Container(key: key1),
new Container(key: key2),
new Container(key: key2),
new Container(key: key2),
new Container(key: key3),
new Container(key: key2),
],
),
new Row(
children: <Widget>[
new Container(key: key1),
new Container(key: key1),
new Container(key: key3),
],
),
new Container(key: key3),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 14 - moving during build - before', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1)),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
],
));
});
testWidgets('GlobalKey duplication 15 - duplicating during build - before', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1)),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: key),
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 16 - moving during build - after', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1)),
new Container(key: key),
],
));
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
],
));
});
testWidgets('GlobalKey duplication 17 - duplicating during build - after', (WidgetTester tester) async {
final Key key = new GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1)),
new Container(key: key),
],
));
int count = 0;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
expect(details.exception, isFlutterError);
count += 1;
};
await tester.pumpWidget(new Stack(
children: <Widget>[
new Container(key: new ValueKey<int>(0)),
new Container(key: new ValueKey<int>(1), child: new Container(key: key)),
new Container(key: key),
],
));
FlutterError.onError = oldHandler;
expect(count, 2);
});
testWidgets('GlobalKey notification exception handling', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
......
// Copyright 2015 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
// There's also some duplicate GlobalKey tests in the framework_test.dart file.
void main() {
testWidgets('GlobalKey children of one node', (WidgetTester tester) async {
// This is actually a test of the regular duplicate key logic, which
// happens before the duplicate GlobalKey logic.
await tester.pumpWidget(new Row(children: <Widget>[
new Container(key: new GlobalObjectKey(0)),
new Container(key: new GlobalObjectKey(0)),
]));
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), startsWith('Duplicate keys found.\n'));
expect(error.toString(), contains('Row'));
expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]'));
});
testWidgets('GlobalKey children of two nodes', (WidgetTester tester) async {
await tester.pumpWidget(new Row(children: <Widget>[
new Container(child: new Container(key: new GlobalObjectKey(0))),
new Container(child: new Container(key: new GlobalObjectKey(0))),
]));
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), startsWith('Multiple widgets used the same GlobalKey.\n'));
expect(error.toString(), contains('different widgets that both had the following description'));
expect(error.toString(), contains('Container'));
expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]'));
expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.'));
});
testWidgets('GlobalKey children of two different nodes', (WidgetTester tester) async {
await tester.pumpWidget(new Row(children: <Widget>[
new Container(child: new Container(key: new GlobalObjectKey(0))),
new Container(key: new Key('x'), child: new Container(key: new GlobalObjectKey(0))),
]));
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), startsWith('Multiple widgets used the same GlobalKey.\n'));
expect(error.toString(), isNot(contains('different widgets that both had the following description')));
expect(error.toString(), contains('Container()'));
expect(error.toString(), contains('Container([<\'x\'>])'));
expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]'));
expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.'));
});
testWidgets('GlobalKey children of two nodes', (WidgetTester tester) async {
StateSetter nestedSetState;
bool flag = false;
await tester.pumpWidget(new Row(children: <Widget>[
new Container(child: new Container(key: new GlobalObjectKey(0))),
new Container(child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
nestedSetState = setState;
if (flag)
return new Container(key: new GlobalObjectKey(0));
return new Container();
},
)),
]));
nestedSetState(() { flag = true; });
await tester.pump();
final dynamic error = tester.takeException();
expect(error.toString(), startsWith('Duplicate GlobalKey detected in widget tree.\n'));
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(), contains('[GlobalObjectKey int#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.'));
expect(error, isFlutterError);
});
}
......@@ -22,7 +22,7 @@ class StatefulLeaf extends StatefulWidget {
}
class StatefulLeafState extends State<StatefulLeaf> {
void test() { setState(() { }); }
void markNeedsBuild() { setState(() { }); }
@override
Widget build(BuildContext context) => new Text('leaf');
......@@ -38,7 +38,7 @@ class KeyedWrapper extends StatelessWidget {
return new Container(
key: key1,
child: new StatefulLeaf(
key: key2
key: key2,
)
);
}
......@@ -48,16 +48,16 @@ Widget builder() {
return new Column(
children: <Widget>[
new KeyedWrapper(items[1].key1, items[1].key2),
new KeyedWrapper(items[0].key1, items[0].key2)
]
new KeyedWrapper(items[0].key1, items[0].key2),
],
);
}
void main() {
testWidgets('duplicate key smoke test', (WidgetTester tester) async {
testWidgets('moving subtrees with global keys - smoketest', (WidgetTester tester) async {
await tester.pumpWidget(builder());
final StatefulLeafState leaf = tester.firstState(find.byType(StatefulLeaf));
leaf.test();
leaf.markNeedsBuild();
await tester.pump();
final Item lastItem = items[1];
items.remove(lastItem);
......
......@@ -79,10 +79,14 @@ void main() {
testWidgets('Clean then reparent with dependencies',
(WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
int layoutBuilderBuildCount = 0;
StateSetter keyedSetState;
StateSetter layoutBuilderSetState;
StateSetter childSetState;
final GlobalKey key = new GlobalKey();
final Widget keyedWidget = new StatefulBuilder(
key: key,
builder: (BuildContext context, StateSetter setState) {
......@@ -93,13 +97,8 @@ void main() {
);
Widget layoutBuilderChild = keyedWidget;
StateSetter layoutBuilderSetState;
StateSetter childSetState;
Widget deepChild = new Container();
int layoutBuilderBuildCount = 0;
await tester.pumpWidget(new MediaQuery(
data: new MediaQueryData.fromWindow(ui.window),
child: new Column(
......@@ -109,8 +108,8 @@ void main() {
layoutBuilderSetState = setState;
return new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
++layoutBuilderBuildCount;
return layoutBuilderChild;
layoutBuilderBuildCount += 1;
return layoutBuilderChild; // initially keyedWidget above, but then a new Container
},
);
}),
......@@ -123,7 +122,7 @@ void main() {
child: new StatefulBuilder(builder:
(BuildContext context, StateSetter setState) {
childSetState = setState;
return deepChild;
return deepChild; // initially a Container, but then the keyedWidget above
}),
),
),
......@@ -134,25 +133,24 @@ void main() {
],
),
));
expect(layoutBuilderBuildCount, 1);
// This call adds the element ot the dirty list.
keyedSetState(() {});
keyedSetState(() { /* Change nothing but add the element to the dirty list. */ });
childSetState(() {
// The deep child builds in the initial build phase. It takes the child
// from the LayoutBuilder before the LayoutBuilder has a chance to build.
deepChild = keyedWidget;
});
// The layout builder will build in a separate build scope. This delays the
// removal of the keyed child until this build scope.
layoutBuilderSetState(() {
// The layout builder will build in a separate build scope. This delays
// the removal of the keyed child until this build scope.
layoutBuilderChild = new Container();
});
// The essential part of this test is that this call to pump doesn't throw.
await tester.pump();
expect(layoutBuilderBuildCount, 2);
});
}
......@@ -512,26 +512,25 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
try {
debugBuildingDirtyElements = true;
buildOwner.buildScope(renderViewElement);
if (_phase == EnginePhase.build)
return;
if (_phase != EnginePhase.build) {
assert(renderView != null);
pipelineOwner.flushLayout();
if (_phase == EnginePhase.layout)
return;
if (_phase != EnginePhase.layout) {
pipelineOwner.flushCompositingBits();
if (_phase == EnginePhase.compositingBits)
return;
if (_phase != EnginePhase.compositingBits) {
pipelineOwner.flushPaint();
if (_phase == EnginePhase.paint)
return;
if (_phase != EnginePhase.paint) {
renderView.compositeFrame(); // this sends the bits to the GPU
if (_phase == EnginePhase.composite)
return;
if (_phase != EnginePhase.composite) {
pipelineOwner.flushSemantics();
if (_phase == EnginePhase.flushSemantics)
return;
} finally {
assert(_phase == EnginePhase.flushSemantics || _phase == EnginePhase.sendSemanticsTree);
}
}
}
}
}
buildOwner.finalizeTree();
} finally {
debugBuildingDirtyElements = false;
}
}
......
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