Add hasAGoodToStringDeep and equalsIgnoringHashCodes methods. (#10935)
* Add hasAGoodToStringDeep and equalsIgnoringHashCodes methods. Methods simplify testing of toStringDeep calls and other cases where methods return strings containing hash codes.
This commit is contained in:
parent
f4f81e9ac7
commit
8f07a5864b
@ -117,6 +117,23 @@ const Matcher isNotInCard = const _IsNotInCard();
|
|||||||
/// empty, and does not contain the default `Instance of ...` string.
|
/// empty, and does not contain the default `Instance of ...` string.
|
||||||
const Matcher hasOneLineDescription = const _HasOneLineDescription();
|
const Matcher hasOneLineDescription = const _HasOneLineDescription();
|
||||||
|
|
||||||
|
/// Asserts that an object's toStringDeep() is a plausible multi-line
|
||||||
|
/// description.
|
||||||
|
///
|
||||||
|
/// Specifically, this matcher checks that an object's
|
||||||
|
/// `toStringDeep(prefixLineOne, prefixOtherLines)`:
|
||||||
|
///
|
||||||
|
/// * Does not have leading or trailing whitespace.
|
||||||
|
/// * Does not contain the default `Instance of ...` string.
|
||||||
|
/// * The last line has characters other than tree connector characters and
|
||||||
|
/// whitespace. For example: the line ` │ ║ ╎` has only tree connector
|
||||||
|
/// characters and whitespace.
|
||||||
|
/// * Does not contain lines with trailing white space.
|
||||||
|
/// * Has multiple lines.
|
||||||
|
/// * The first line starts with `prefixLineOne`
|
||||||
|
/// * All subsequent lines start with `prefixOtherLines`.
|
||||||
|
const Matcher hasAGoodToStringDeep = const _HasGoodToStringDeep();
|
||||||
|
|
||||||
/// A matcher for functions that throw [FlutterError].
|
/// A matcher for functions that throw [FlutterError].
|
||||||
///
|
///
|
||||||
/// This is equivalent to `throwsA(const isInstanceOf<FlutterError>())`.
|
/// This is equivalent to `throwsA(const isInstanceOf<FlutterError>())`.
|
||||||
@ -179,6 +196,23 @@ Matcher moreOrLessEquals(double value, { double epsilon: 1e-10 }) {
|
|||||||
return new _MoreOrLessEquals(value, epsilon);
|
return new _MoreOrLessEquals(value, epsilon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asserts that two [String]s are equal after normalizing likely hash codes.
|
||||||
|
///
|
||||||
|
/// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code
|
||||||
|
/// and is normalized to #00000.
|
||||||
|
///
|
||||||
|
/// See Also:
|
||||||
|
///
|
||||||
|
/// * [describeIdentity], a method that generates short descriptions of objects
|
||||||
|
/// with ids that match the pattern #[0-9a-f]{5}.
|
||||||
|
/// * [shortHash], a method that generates a 5 character long hexadecimal
|
||||||
|
/// [String] based on [Object.hashCode].
|
||||||
|
/// * [TreeDiagnosticsMixin.toStringDeep], a method that returns a [String]
|
||||||
|
/// typically containing multiple hash codes.
|
||||||
|
Matcher equalsIgnoringHashCodes(String value) {
|
||||||
|
return new _EqualsIgnoringHashCodes(value);
|
||||||
|
}
|
||||||
|
|
||||||
class _FindsWidgetMatcher extends Matcher {
|
class _FindsWidgetMatcher extends Matcher {
|
||||||
const _FindsWidgetMatcher(this.min, this.max);
|
const _FindsWidgetMatcher(this.min, this.max);
|
||||||
|
|
||||||
@ -352,6 +386,185 @@ class _HasOneLineDescription extends Matcher {
|
|||||||
Description describe(Description description) => description.add('one line description');
|
Description describe(Description description) => description.add('one line description');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _EqualsIgnoringHashCodes extends Matcher {
|
||||||
|
_EqualsIgnoringHashCodes(String v) : _value = _normalize(v);
|
||||||
|
|
||||||
|
final String _value;
|
||||||
|
|
||||||
|
static final Object _mismatchedValueKey = new Object();
|
||||||
|
|
||||||
|
static String _normalize(String s) {
|
||||||
|
return s.replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#00000');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
|
||||||
|
final String description = _normalize(object);
|
||||||
|
if (_value != description) {
|
||||||
|
matchState[_mismatchedValueKey] = description;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) {
|
||||||
|
return description.add('multi line description equals $_value');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describeMismatch(
|
||||||
|
dynamic item,
|
||||||
|
Description mismatchDescription,
|
||||||
|
Map<dynamic, dynamic> matchState,
|
||||||
|
bool verbose
|
||||||
|
) {
|
||||||
|
if (matchState.containsKey(_mismatchedValueKey)) {
|
||||||
|
final String actualValue = matchState[_mismatchedValueKey];
|
||||||
|
// Leading whitespace is added so that lines in the multi-line
|
||||||
|
// description returned by addDescriptionOf are all indented equally
|
||||||
|
// which makes the output easier to read for this case.
|
||||||
|
return mismatchDescription
|
||||||
|
.add('expected normalized value\n ')
|
||||||
|
.addDescriptionOf(_value)
|
||||||
|
.add('\nbut got\n ')
|
||||||
|
.addDescriptionOf(actualValue);
|
||||||
|
}
|
||||||
|
return mismatchDescription;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if [c] represents a whitespace code unit.
|
||||||
|
bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020;
|
||||||
|
|
||||||
|
/// Returns `true` if [c] represents a vertical line unicode line art code unit.
|
||||||
|
///
|
||||||
|
/// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only
|
||||||
|
/// specifies vertical line art code units currently used by Flutter line art.
|
||||||
|
/// There are other line art characters that technically also represent vertical
|
||||||
|
/// lines.
|
||||||
|
bool _isVerticalLine(int c) {
|
||||||
|
return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether a [line] is all vertical tree connector characters.
|
||||||
|
///
|
||||||
|
/// Example vertical tree connector characters: `│ ║ ╎`.
|
||||||
|
/// The last line of a text tree contains only vertical tree connector
|
||||||
|
/// characters indicates a poorly formatted tree.
|
||||||
|
bool _isAllTreeConnectorCharacters(String line) {
|
||||||
|
for (int i = 0; i < line.length; ++i) {
|
||||||
|
final int c = line.codeUnitAt(i);
|
||||||
|
if (!_isWhitespace(c) && !_isVerticalLine(c))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HasGoodToStringDeep extends Matcher {
|
||||||
|
const _HasGoodToStringDeep();
|
||||||
|
|
||||||
|
static final Object _toStringDeepErrorDescriptionKey = new Object();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
|
||||||
|
final List<String> issues = <String>[];
|
||||||
|
String description = object.toStringDeep();
|
||||||
|
if (description.endsWith('\n')) {
|
||||||
|
// Trim off trailing \n as the remaining calculations assume
|
||||||
|
// the description does not end with a trailing \n.
|
||||||
|
description = description.substring(0, description.length - 1);
|
||||||
|
} else {
|
||||||
|
issues.add('Not terminated with a line break.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description.trim() != description)
|
||||||
|
issues.add('Has trailing whitespace.');
|
||||||
|
|
||||||
|
final List<String> lines = description.split('\n');
|
||||||
|
if (lines.length < 2)
|
||||||
|
issues.add('Does not have multiple lines.');
|
||||||
|
|
||||||
|
if (description.contains('Instance of '))
|
||||||
|
issues.add('Contains text "Instance of ".');
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.length; ++i) {
|
||||||
|
final String line = lines[i];
|
||||||
|
if (line.isEmpty)
|
||||||
|
issues.add('Line ${i+1} is empty.');
|
||||||
|
|
||||||
|
if (line.trimRight() != line)
|
||||||
|
issues.add('Line ${i+1} has trailing whitespace.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isAllTreeConnectorCharacters(lines.last))
|
||||||
|
issues.add('Last line is all tree connector characters.');
|
||||||
|
|
||||||
|
// If a toStringDeep method doesn't properly handle nested values that
|
||||||
|
// contain line breaks it can fail to add the required prefixes to all
|
||||||
|
// lined when toStringDeep is called specifying prefixes.
|
||||||
|
final String prefixLineOne = 'PREFIX_LINE_ONE____';
|
||||||
|
final String prefixOtherLines = 'PREFIX_OTHER_LINES_';
|
||||||
|
final List<String> prefixIssues = <String>[];
|
||||||
|
String descriptionWithPrefixes =
|
||||||
|
object.toStringDeep(prefixLineOne, prefixOtherLines);
|
||||||
|
if (descriptionWithPrefixes.endsWith('\n')) {
|
||||||
|
// Trim off trailing \n as the remaining calculations assume
|
||||||
|
// the description does not end with a trailing \n.
|
||||||
|
descriptionWithPrefixes = descriptionWithPrefixes.substring(
|
||||||
|
0, descriptionWithPrefixes.length - 1);
|
||||||
|
}
|
||||||
|
final List<String> linesWithPrefixes = descriptionWithPrefixes.split('\n');
|
||||||
|
if (!linesWithPrefixes.first.startsWith(prefixLineOne))
|
||||||
|
prefixIssues.add('First line does not contain expected prefix.');
|
||||||
|
|
||||||
|
for (int i = 1; i < linesWithPrefixes.length; ++i) {
|
||||||
|
if (!linesWithPrefixes[i].startsWith(prefixOtherLines))
|
||||||
|
prefixIssues.add('Line ${i+1} does not contain the expected prefix.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final StringBuffer errorDescription = new StringBuffer();
|
||||||
|
if (issues.isNotEmpty) {
|
||||||
|
errorDescription.writeln('Bad toStringDeep():');
|
||||||
|
errorDescription.writeln(description);
|
||||||
|
errorDescription.writeAll(issues, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefixIssues.isNotEmpty) {
|
||||||
|
errorDescription.writeln(
|
||||||
|
'Bad toStringDeep("$prefixLineOne", "$prefixOtherLines"):');
|
||||||
|
errorDescription.writeln(descriptionWithPrefixes);
|
||||||
|
errorDescription.writeAll(prefixIssues, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorDescription.isNotEmpty) {
|
||||||
|
matchState[_toStringDeepErrorDescriptionKey] =
|
||||||
|
errorDescription.toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describeMismatch(
|
||||||
|
dynamic item,
|
||||||
|
Description mismatchDescription,
|
||||||
|
Map<dynamic, dynamic> matchState,
|
||||||
|
bool verbose
|
||||||
|
) {
|
||||||
|
if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) {
|
||||||
|
return mismatchDescription.add(
|
||||||
|
matchState[_toStringDeepErrorDescriptionKey]);
|
||||||
|
}
|
||||||
|
return mismatchDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) {
|
||||||
|
return description.add('multi line description');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _MoreOrLessEquals extends Matcher {
|
class _MoreOrLessEquals extends Matcher {
|
||||||
const _MoreOrLessEquals(this.value, this.epsilon);
|
const _MoreOrLessEquals(this.value, this.epsilon);
|
||||||
|
|
||||||
|
@ -4,6 +4,43 @@
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Class that makes it easy to mock common toStringDeep behavior.
|
||||||
|
class _MockToStringDeep {
|
||||||
|
_MockToStringDeep(String str) {
|
||||||
|
final List<String> lines = str.split('\n');
|
||||||
|
_lines = <String>[];
|
||||||
|
for (int i = 0; i < lines.length - 1; ++i)
|
||||||
|
_lines.add('${lines[i]}\n');
|
||||||
|
|
||||||
|
// If the last line is empty, that really just means that the previous
|
||||||
|
// line was terminated with a line break.
|
||||||
|
if (lines.isNotEmpty && lines.last.isNotEmpty) {
|
||||||
|
_lines.add(lines.last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_MockToStringDeep.fromLines(this._lines);
|
||||||
|
|
||||||
|
/// Lines in the message to display when [toStringDeep] is called.
|
||||||
|
/// For correct toStringDeep behavior, each line should be terminated with a
|
||||||
|
/// line break.
|
||||||
|
List<String> _lines;
|
||||||
|
|
||||||
|
String toStringDeep([String prefixLineOne="", String prefixOtherLines=""]) {
|
||||||
|
final StringBuffer sb = new StringBuffer();
|
||||||
|
if (_lines.isNotEmpty)
|
||||||
|
sb.write('$prefixLineOne${_lines.first}');
|
||||||
|
|
||||||
|
for (int i = 1; i < _lines.length; ++i)
|
||||||
|
sb.write('$prefixOtherLines${_lines[i]}');
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => toStringDeep();
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('hasOneLineDescription', () {
|
test('hasOneLineDescription', () {
|
||||||
expect('Hello', hasOneLineDescription);
|
expect('Hello', hasOneLineDescription);
|
||||||
@ -13,6 +50,113 @@ void main() {
|
|||||||
expect(new Object(), isNot(hasOneLineDescription));
|
expect(new Object(), isNot(hasOneLineDescription));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('hasAGoodToStringDeep', () {
|
||||||
|
expect(new _MockToStringDeep('Hello\n World\n'), hasAGoodToStringDeep);
|
||||||
|
// Not terminated with a line break.
|
||||||
|
expect(new _MockToStringDeep('Hello\n World'), isNot(hasAGoodToStringDeep));
|
||||||
|
// Trailing whitespace on last line.
|
||||||
|
expect(new _MockToStringDeep('Hello\n World \n'),
|
||||||
|
isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('Hello\n World\t\n'),
|
||||||
|
isNot(hasAGoodToStringDeep));
|
||||||
|
// Leading whitespace on line 1.
|
||||||
|
expect(new _MockToStringDeep(' Hello\n World \n'),
|
||||||
|
isNot(hasAGoodToStringDeep));
|
||||||
|
|
||||||
|
// Single line.
|
||||||
|
expect(new _MockToStringDeep('Hello World'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('Hello World\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
|
||||||
|
expect(new _MockToStringDeep('Hello: World\nFoo: bar\n'),
|
||||||
|
hasAGoodToStringDeep);
|
||||||
|
expect(new _MockToStringDeep('Hello: World\nFoo: 42\n'),
|
||||||
|
hasAGoodToStringDeep);
|
||||||
|
// Contains default Object.toString().
|
||||||
|
expect(new _MockToStringDeep('Hello: World\nFoo: ${new Object()}\n'),
|
||||||
|
isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n'), hasAGoodToStringDeep);
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n╘══════\n'), hasAGoodToStringDeep);
|
||||||
|
// Last line is all whitespace or vertical line art.
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n│\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n│\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n│\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n╎\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n║\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n │\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n ╎\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n ║\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
expect(new _MockToStringDeep('A\n├─B\n ││\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
|
||||||
|
expect(new _MockToStringDeep(
|
||||||
|
'A\n'
|
||||||
|
'├─B\n'
|
||||||
|
'│\n'
|
||||||
|
'└─C\n'), hasAGoodToStringDeep);
|
||||||
|
// Last line is all whitespace or vertical line art.
|
||||||
|
expect(new _MockToStringDeep(
|
||||||
|
'A\n'
|
||||||
|
'├─B\n'
|
||||||
|
'│\n'), isNot(hasAGoodToStringDeep));
|
||||||
|
|
||||||
|
expect(new _MockToStringDeep.fromLines(
|
||||||
|
<String>['Paragraph#00000\n',
|
||||||
|
' │ size: (400x200)\n',
|
||||||
|
' ╘═╦══ text ═══\n',
|
||||||
|
' ║ TextSpan:\n',
|
||||||
|
' ║ "I polished up that handle so carefullee\n',
|
||||||
|
' ║ That now I am the Ruler of the Queen\'s Navee!"\n',
|
||||||
|
' ╚═══════════\n']), hasAGoodToStringDeep);
|
||||||
|
|
||||||
|
// Text span
|
||||||
|
expect(new _MockToStringDeep.fromLines(
|
||||||
|
<String>['Paragraph#00000\n',
|
||||||
|
' │ size: (400x200)\n',
|
||||||
|
' ╘═╦══ text ═══\n',
|
||||||
|
' ║ TextSpan:\n',
|
||||||
|
' ║ "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen\'s Navee!"\n',
|
||||||
|
' ╚═══════════\n']), isNot(hasAGoodToStringDeep));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeHashCodesEquals', () {
|
||||||
|
expect('Foo#34219', equalsIgnoringHashCodes('Foo#00000'));
|
||||||
|
expect('Foo#34219', equalsIgnoringHashCodes('Foo#12345'));
|
||||||
|
expect('Foo#34219', equalsIgnoringHashCodes('Foo#abcdf'));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo')));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#')));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#0')));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#00')));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#00000 ')));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#000000')));
|
||||||
|
expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#123456')));
|
||||||
|
|
||||||
|
expect('Foo#34219:', equalsIgnoringHashCodes('Foo#00000:'));
|
||||||
|
expect('Foo#34219:', isNot(equalsIgnoringHashCodes('Foo#00000')));
|
||||||
|
|
||||||
|
expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#00000'));
|
||||||
|
expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#12345'));
|
||||||
|
expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#abcdf'));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo')));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#')));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#0')));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#00')));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#00000 ')));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#000000')));
|
||||||
|
expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#123456')));
|
||||||
|
|
||||||
|
expect('Foo#A3b4D', isNot(equalsIgnoringHashCodes('Foo#00000')));
|
||||||
|
|
||||||
|
expect('Foo#12345(Bar#9110f)',
|
||||||
|
equalsIgnoringHashCodes('Foo#00000(Bar#00000)'));
|
||||||
|
expect('Foo#12345(Bar#9110f)',
|
||||||
|
isNot(equalsIgnoringHashCodes('Foo#00000(Bar#)')));
|
||||||
|
|
||||||
|
expect('Foo', isNot(equalsIgnoringHashCodes('Foo#00000')));
|
||||||
|
expect('Foo#', isNot(equalsIgnoringHashCodes('Foo#00000')));
|
||||||
|
expect('Foo#3421', isNot(equalsIgnoringHashCodes('Foo#00000')));
|
||||||
|
expect('Foo#342193', isNot(equalsIgnoringHashCodes('Foo#00000')));
|
||||||
|
});
|
||||||
|
|
||||||
test('moreOrLessEquals', () {
|
test('moreOrLessEquals', () {
|
||||||
expect(0.0, moreOrLessEquals(1e-11));
|
expect(0.0, moreOrLessEquals(1e-11));
|
||||||
expect(1e-11, moreOrLessEquals(0.0));
|
expect(1e-11, moreOrLessEquals(0.0));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user