Skip to content

Commit b5ce70a

Browse files
authored
Migrate Color.toString() test, improves equalsIgnoringHashCodes (flutter#154934)
This migrates the last failing test for flutter/engine#54981. In order to effectively resolve that test I had to make `equalsIgnoringHashCodes` more usable by printing out the line that differs instead of just a huge blob of "expected" vs "actual. ## example Here's the output after the change. ### test ``` test('equalsIgnoringHashCodes - wrong line', () { expect( '1\n2\n3\n4\n5\n6\n7\n8\n9\n10', equalsIgnoringHashCodes('1\n2\n3\n4\n5\n6\na\n8\n9\n10'), ); }); ``` ### output ``` Expected: normalized value matches '1\n' '2\n' '3\n' '4\n' '5\n' '6\n' 'a\n' '8\n' '9\n' '10' Actual: '1\n' '2\n' '3\n' '4\n' '5\n' '6\n' '7\n' '8\n' '9\n' '10' Which: Lines 7 differed, expected: 'a' but got '7' ```
1 parent bcf1c5a commit b5ce70a

File tree

3 files changed

+83
-91
lines changed

3 files changed

+83
-91
lines changed

packages/flutter/test/widgets/keep_alive_test.dart

Lines changed: 20 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,12 @@ void main() {
212212
),
213213
);
214214
// The important lines below are the ones marked with "<----"
215-
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
215+
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info, wrapWidth: 600), equalsIgnoringHashCodes(
216216
'_ReusableRenderView#00000\n'
217217
' │ debug mode enabled - ${Platform.operatingSystem}\n'
218218
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
219219
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
220-
' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in\n'
221-
' │ logical pixels)\n'
220+
' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in logical pixels)\n'
222221
' │\n'
223222
' └─child: RenderRepaintBoundary#00000\n'
224223
' │ needs compositing\n'
@@ -227,19 +226,15 @@ void main() {
227226
' │ layer: OffsetLayer#00000\n'
228227
' │ size: Size(800.0, 600.0)\n'
229228
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
230-
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
231-
' │ repaints)\n'
229+
' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n'
232230
' │\n'
233231
' └─child: RenderCustomPaint#00000\n'
234232
' │ needs compositing\n'
235233
' │ parentData: <none> (can use size)\n'
236234
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
237235
' │ size: Size(800.0, 600.0)\n'
238236
' │ painter: null\n'
239-
' │ foregroundPainter:\n'
240-
' │ _GlowingOverscrollIndicatorPainter(_GlowController(color:\n'
241-
' │ Color(0xffffffff), axis: vertical), _GlowController(color:\n'
242-
' │ Color(0xffffffff), axis: vertical))\n'
237+
' │ foregroundPainter: _GlowingOverscrollIndicatorPainter(_GlowController(color: ${const Color(0xffffffff)}, axis: vertical), _GlowController(color: ${const Color(0xffffffff)}, axis: vertical))\n'
243238
' │\n'
244239
' └─child: RenderRepaintBoundary#00000\n'
245240
' │ needs compositing\n'
@@ -248,8 +243,7 @@ void main() {
248243
' │ layer: OffsetLayer#00000\n'
249244
' │ size: Size(800.0, 600.0)\n'
250245
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
251-
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
252-
' │ repaints)\n'
246+
' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n'
253247
' │\n'
254248
' └─child: _RenderScrollSemantics#00000\n'
255249
' │ needs compositing\n'
@@ -305,38 +299,20 @@ void main() {
305299
' │ size: Size(800.0, 600.0)\n'
306300
' │ axisDirection: down\n'
307301
' │ crossAxisDirection: right\n'
308-
' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n'
309-
' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
310-
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n'
311-
' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n'
312-
' │ ScrollDirection.idle)\n'
302+
' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range: 0.0..39400.0, viewport: 600.0, ScrollableState, AlwaysScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#00000, ScrollDirection.idle)\n'
313303
' │ anchor: 0.0\n'
314304
' │\n'
315305
' └─center child: RenderSliverPadding#00000 relayoutBoundary=up1\n'
316306
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
317-
' │ constraints: SliverConstraints(AxisDirection.down,\n'
318-
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
319-
' │ 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0,\n'
320-
' │ crossAxisExtent: 800.0, crossAxisDirection:\n'
321-
' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n'
322-
' │ remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n'
323-
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
324-
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
325-
' │ cacheExtent: 850.0)\n'
307+
' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n'
308+
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 850.0)\n'
326309
' │ padding: EdgeInsets.zero\n'
327310
' │ textDirection: ltr\n'
328311
' │\n'
329312
' └─child: RenderSliverFixedExtentList#00000 relayoutBoundary=up2\n'
330313
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
331-
' │ constraints: SliverConstraints(AxisDirection.down,\n'
332-
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
333-
' │ 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0,\n'
334-
' │ crossAxisExtent: 800.0, crossAxisDirection:\n'
335-
' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n'
336-
' │ remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n'
337-
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
338-
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
339-
' │ cacheExtent: 850.0)\n'
314+
' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n'
315+
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 850.0)\n'
340316
' │ currently live children: 0 to 2\n'
341317
' │\n'
342318
' ├─child with index 0: RenderLimitedBox#00000\n'
@@ -387,13 +363,12 @@ void main() {
387363
const GlobalObjectKey<_LeafState>(3).currentState!.setKeepAlive(true);
388364
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
389365
await tester.pump();
390-
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
366+
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info, wrapWidth: 600), equalsIgnoringHashCodes(
391367
'_ReusableRenderView#00000\n'
392368
' │ debug mode enabled - ${Platform.operatingSystem}\n'
393369
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
394370
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
395-
' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in\n'
396-
' │ logical pixels)\n'
371+
' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in logical pixels)\n'
397372
' │\n'
398373
' └─child: RenderRepaintBoundary#00000\n'
399374
' │ needs compositing\n'
@@ -402,19 +377,15 @@ void main() {
402377
' │ layer: OffsetLayer#00000\n'
403378
' │ size: Size(800.0, 600.0)\n'
404379
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
405-
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
406-
' │ repaints)\n'
380+
' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n'
407381
' │\n'
408382
' └─child: RenderCustomPaint#00000\n'
409383
' │ needs compositing\n'
410384
' │ parentData: <none> (can use size)\n'
411385
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
412386
' │ size: Size(800.0, 600.0)\n'
413387
' │ painter: null\n'
414-
' │ foregroundPainter:\n'
415-
' │ _GlowingOverscrollIndicatorPainter(_GlowController(color:\n'
416-
' │ Color(0xffffffff), axis: vertical), _GlowController(color:\n'
417-
' │ Color(0xffffffff), axis: vertical))\n'
388+
' │ foregroundPainter: _GlowingOverscrollIndicatorPainter(_GlowController(color: ${const Color(0xffffffff)}, axis: vertical), _GlowController(color: ${const Color(0xffffffff)}, axis: vertical))\n'
418389
' │\n'
419390
' └─child: RenderRepaintBoundary#00000\n'
420391
' │ needs compositing\n'
@@ -423,8 +394,7 @@ void main() {
423394
' │ layer: OffsetLayer#00000\n'
424395
' │ size: Size(800.0, 600.0)\n'
425396
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
426-
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
427-
' │ repaints)\n'
397+
' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n'
428398
' │\n'
429399
' └─child: _RenderScrollSemantics#00000\n'
430400
' │ needs compositing\n'
@@ -480,38 +450,20 @@ void main() {
480450
' │ size: Size(800.0, 600.0)\n'
481451
' │ axisDirection: down\n'
482452
' │ crossAxisDirection: right\n'
483-
' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n'
484-
' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
485-
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n'
486-
' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n'
487-
' │ ScrollDirection.idle)\n'
453+
' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0, range: 0.0..39400.0, viewport: 600.0, ScrollableState, AlwaysScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#00000, ScrollDirection.idle)\n'
488454
' │ anchor: 0.0\n'
489455
' │\n'
490456
' └─center child: RenderSliverPadding#00000 relayoutBoundary=up1\n'
491457
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
492-
' │ constraints: SliverConstraints(AxisDirection.down,\n'
493-
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
494-
' │ 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent:\n'
495-
' │ 600.0, crossAxisExtent: 800.0, crossAxisDirection:\n'
496-
' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n'
497-
' │ remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n'
498-
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
499-
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
500-
' │ cacheExtent: 1100.0)\n'
458+
' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n'
459+
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 1100.0)\n'
501460
' │ padding: EdgeInsets.zero\n'
502461
' │ textDirection: ltr\n'
503462
' │\n'
504463
' └─child: RenderSliverFixedExtentList#00000 relayoutBoundary=up2\n'
505464
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
506-
' │ constraints: SliverConstraints(AxisDirection.down,\n'
507-
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
508-
' │ 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent:\n'
509-
' │ 600.0, crossAxisExtent: 800.0, crossAxisDirection:\n'
510-
' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n'
511-
' │ remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n'
512-
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
513-
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
514-
' │ cacheExtent: 1100.0)\n'
465+
' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n'
466+
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 1100.0)\n'
515467
' │ currently live children: 4 to 7\n'
516468
' │\n'
517469
' ├─child with index 4: RenderLimitedBox#00000 NEEDS-PAINT\n'

packages/flutter_test/lib/src/matchers.dart

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/// @docImport '_goldens_io.dart';
66
library;
77

8+
import 'dart:convert' show LineSplitter;
89
import 'dart:math' as math;
910
import 'dart:ui' as ui;
1011

@@ -1198,19 +1199,24 @@ class _HasOneLineDescription extends Matcher {
11981199
}
11991200

12001201
class _EqualsIgnoringHashCodes extends Matcher {
1201-
_EqualsIgnoringHashCodes(Object v) : _value = _normalize(v);
1202+
_EqualsIgnoringHashCodes(Object v)
1203+
: _value = _normalize(v),
1204+
_stringValue = v is String ? _normalizeString(v) : null;
12021205

1203-
final Object _value;
1206+
final Iterable<String> _value;
1207+
final String? _stringValue;
12041208

1205-
static final Object _mismatchedValueKey = Object();
1209+
static final Object _lineNumberValueKey = Object();
1210+
static final Object _expectedLineValueKey = Object();
1211+
static final Object _seenLineValueKey = Object();
12061212

12071213
static String _normalizeString(String value) {
12081214
return value.replaceAll(RegExp(r'#[\da-fA-F]{5}'), '#00000');
12091215
}
12101216

1211-
static Object _normalize(Object value, {bool expected = true}) {
1217+
static Iterable<String> _normalize(Object value, {bool expected = true}) {
12121218
if (value is String) {
1213-
return _normalizeString(value);
1219+
return LineSplitter.split(value).map<String>((dynamic item) => _normalizeString(item.toString()));
12141220
}
12151221
if (value is Iterable<String>) {
12161222
return value.map<String>((dynamic item) => _normalizeString(item.toString()));
@@ -1222,20 +1228,33 @@ class _EqualsIgnoringHashCodes extends Matcher {
12221228

12231229
@override
12241230
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1225-
final Object normalized = _normalize(object as Object, expected: false);
1226-
if (!equals(_value).matches(normalized, matchState)) {
1227-
matchState[_mismatchedValueKey] = normalized;
1228-
return false;
1231+
final Iterable<String> normalized = _normalize(object as Object, expected: false);
1232+
final Iterator<String> expectedIt = _value.iterator;
1233+
final Iterator<String> seenIt = normalized.iterator;
1234+
1235+
int lineNumber = 1;
1236+
1237+
bool hasExpected = expectedIt.moveNext();
1238+
bool hasSeen = seenIt.moveNext();
1239+
while (hasExpected && hasSeen) {
1240+
if (!equals(expectedIt.current).matches(seenIt.current, matchState)) {
1241+
matchState[_lineNumberValueKey] = lineNumber;
1242+
matchState[_expectedLineValueKey] = expectedIt.current;
1243+
matchState[_seenLineValueKey] = seenIt.current;
1244+
return false;
1245+
}
1246+
1247+
lineNumber += 1;
1248+
hasExpected = expectedIt.moveNext();
1249+
hasSeen = seenIt.moveNext();
12291250
}
1230-
return true;
1251+
1252+
return !hasExpected && !hasSeen;
12311253
}
12321254

12331255
@override
12341256
Description describe(Description description) {
1235-
if (_value is String) {
1236-
return description.add('normalized value matches $_value');
1237-
}
1238-
return description.add('normalized value matches\n').addDescriptionOf(_value);
1257+
return description.add('normalized value matches\n').addDescriptionOf(_stringValue ?? _value);
12391258
}
12401259

12411260
@override
@@ -1245,16 +1264,17 @@ class _EqualsIgnoringHashCodes extends Matcher {
12451264
Map<dynamic, dynamic> matchState,
12461265
bool verbose,
12471266
) {
1248-
if (matchState.containsKey(_mismatchedValueKey)) {
1249-
final Object actualValue = matchState[_mismatchedValueKey] as Object;
1250-
// Leading whitespace is added so that lines in the multiline
1251-
// description returned by addDescriptionOf are all indented equally
1252-
// which makes the output easier to read for this case.
1253-
return mismatchDescription
1254-
.add('was expected to be normalized value\n')
1255-
.addDescriptionOf(_value)
1267+
if (matchState.containsKey(_lineNumberValueKey) &&
1268+
matchState.containsKey(_expectedLineValueKey) &&
1269+
matchState.containsKey(_seenLineValueKey)) {
1270+
final int lineNumber = matchState[_lineNumberValueKey] as int;
1271+
if (lineNumber > 1) {
1272+
mismatchDescription = mismatchDescription
1273+
.add('Lines $lineNumber differed, expected: \n')
1274+
.addDescriptionOf(matchState[_expectedLineValueKey])
12561275
.add('\nbut got\n')
1257-
.addDescriptionOf(actualValue);
1276+
.addDescriptionOf(matchState[_seenLineValueKey]);
1277+
}
12581278
}
12591279
return mismatchDescription;
12601280
}

packages/flutter_test/test/matchers_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,26 @@ void main() {
194194
);
195195
});
196196

197+
test('equalsIgnoringHashCodes - wrong line', () {
198+
TestFailure? failure;
199+
try {
200+
expect(
201+
'1\n2\n3\n4\n5\n6\n7\n8\n9\n10',
202+
equalsIgnoringHashCodes('1\n2\n3\n4\n5\n6\na\n8\n9\n10'),
203+
);
204+
} on TestFailure catch (e) {
205+
failure = e;
206+
}
207+
208+
expect(failure, isNotNull);
209+
if (failure != null) {
210+
final String? message = failure.message;
211+
expect(message, contains('Lines 7 differed'));
212+
expect(message, contains("'a'"));
213+
expect(message, contains("'7'"));
214+
}
215+
});
216+
197217
test('moreOrLessEquals', () {
198218
expect(0.0, moreOrLessEquals(1e-11));
199219
expect(1e-11, moreOrLessEquals(0.0));

0 commit comments

Comments
 (0)