From 7ca775b665ccdf1a6c4a84672a2e1fb2bdfe9ae0 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Wed, 13 Jun 2018 13:55:44 -0700 Subject: [PATCH] fix: Improve the output of many matchers that expect specific types - Add a package-private FeatureMatcher class to generalize type checking - Use it across many of the existing Matcher implementations - Update tests validate new, more consistent failure messages - Add a few new tests for `isIn` --- CHANGELOG.md | 2 + lib/src/core_matchers.dart | 64 +++++++++-------- lib/src/equals_matcher.dart | 81 +++++++++++----------- lib/src/feature_matcher.dart | 32 +++++++++ lib/src/iterable_matchers.dart | 114 ++++++++++++------------------- lib/src/numeric_matchers.dart | 61 ++++++----------- lib/src/string_matchers.dart | 76 +++++++-------------- test/core_matchers_test.dart | 31 ++++----- test/iterable_matchers_test.dart | 50 +++++++++----- test/numeric_matchers_test.dart | 15 ++-- test/pretty_print_test.dart | 9 +-- test/string_matchers_test.dart | 15 ++-- test/test_utils.dart | 5 ++ 13 files changed, 268 insertions(+), 287 deletions(-) create mode 100644 lib/src/feature_matcher.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f238d5..7e3ffd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead. +- Improved the output of `Matcher` instances that fail due to type errors. + ## 0.12.2+1 - Updated SDK version to 2.0.0-dev.17.0 diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart index 1de4b8d..73570e3 100644 --- a/lib/src/core_matchers.dart +++ b/lib/src/core_matchers.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'feature_matcher.dart'; import 'interfaces.dart'; import 'type_matcher.dart'; import 'util.dart'; @@ -70,15 +71,17 @@ const Matcher isNaN = const _IsNaN(); /// A matcher that matches any non-NaN value. const Matcher isNotNaN = const _IsNotNaN(); -class _IsNaN extends Matcher { +class _IsNaN extends FeatureMatcher { const _IsNaN(); - bool matches(item, Map matchState) => double.nan.compareTo(item) == 0; + bool typedMatches(num item, Map matchState) => + double.nan.compareTo(item) == 0; Description describe(Description description) => description.add('NaN'); } -class _IsNotNaN extends Matcher { +class _IsNotNaN extends FeatureMatcher { const _IsNotNaN(); - bool matches(item, Map matchState) => double.nan.compareTo(item) != 0; + bool typedMatches(num item, Map matchState) => + double.nan.compareTo(item) != 0; Description describe(Description description) => description.add('not NaN'); } @@ -122,10 +125,10 @@ class isInstanceOf extends TypeMatcher { /// a wrapper will have to be created. const Matcher returnsNormally = const _ReturnsNormally(); -class _ReturnsNormally extends Matcher { +class _ReturnsNormally extends FeatureMatcher { const _ReturnsNormally(); - bool matches(f, Map matchState) { + bool typedMatches(Function f, Map matchState) { try { f(); return true; @@ -138,8 +141,8 @@ class _ReturnsNormally extends Matcher { Description describe(Description description) => description.add("return normally"); - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { + Description describeTypedMismatch(Function item, + Description mismatchDescription, Map matchState, bool verbose) { mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']); if (verbose) { mismatchDescription.add(' at ').add(matchState['stack'].toString()); @@ -210,11 +213,12 @@ class _Contains extends Matcher { const _Contains(this._expected); bool matches(item, Map matchState) { + var expected = _expected; if (item is String) { - return item.contains((_expected as Pattern)); + return expected is Pattern && item.contains(expected); } else if (item is Iterable) { - if (_expected is Matcher) { - return item.any((e) => (_expected as Matcher).matches(e, matchState)); + if (expected is Matcher) { + return item.any((e) => expected.matches(e, matchState)); } else { return item.contains(_expected); } @@ -240,27 +244,29 @@ class _Contains extends Matcher { /// Returns a matcher that matches if the match argument is in /// the expected value. This is the converse of [contains]. -Matcher isIn(expected) => new _In(expected); +Matcher isIn(expected) { + if (expected is Iterable) { + return new _In(expected, expected.contains); + } else if (expected is String) { + return new _In(expected, expected.contains); + } else if (expected is Map) { + return new _In(expected, expected.containsKey); + } -class _In extends Matcher { - final Object _expected; + throw new ArgumentError.value( + expected, 'expected', 'Only Iterable, Map, and String are supported.'); +} - const _In(this._expected); +class _In extends FeatureMatcher { + final Object _source; + final bool Function(T) _containsFunction; - bool matches(item, Map matchState) { - var expected = _expected; - if (expected is String) { - return expected.contains(item as Pattern); - } else if (expected is Iterable) { - return expected.contains(item); - } else if (expected is Map) { - return expected.containsKey(item); - } - return false; - } + const _In(this._source, this._containsFunction); + + bool typedMatches(T item, Map matchState) => _containsFunction(item); Description describe(Description description) => - description.add('is in ').addDescriptionOf(_expected); + description.add('is in ').addDescriptionOf(_source); } /// Returns a matcher that uses an arbitrary function that returns @@ -275,13 +281,13 @@ Matcher predicate(bool f(T value), typedef bool _PredicateFunction(T value); -class _Predicate extends Matcher { +class _Predicate extends FeatureMatcher { final _PredicateFunction _matcher; final String _description; _Predicate(this._matcher, this._description); - bool matches(item, Map matchState) => _matcher(item as T); + bool typedMatches(T item, Map matchState) => _matcher(item); Description describe(Description description) => description.add(_description); diff --git a/lib/src/equals_matcher.dart b/lib/src/equals_matcher.dart index 28aee51..31e369b 100644 --- a/lib/src/equals_matcher.dart +++ b/lib/src/equals_matcher.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'description.dart'; +import 'feature_matcher.dart'; import 'interfaces.dart'; import 'util.dart'; @@ -16,67 +17,61 @@ import 'util.dart'; /// handle cyclic structures a recursion depth [limit] can be provided. The /// default limit is 100. [Set]s will be compared order-independently. Matcher equals(expected, [int limit = 100]) => expected is String - ? new _StringEqualsMatcher(expected) as Matcher + ? new _StringEqualsMatcher(expected) : new _DeepMatcher(expected, limit); typedef _RecursiveMatcher = List Function( dynamic, dynamic, String, int); /// A special equality matcher for strings. -class _StringEqualsMatcher extends Matcher { +class _StringEqualsMatcher extends FeatureMatcher { final String _value; _StringEqualsMatcher(this._value); - bool get showActualValue => true; - - bool matches(item, Map matchState) => _value == item; + bool typedMatches(String item, Map matchState) => _value == item; Description describe(Description description) => description.addDescriptionOf(_value); - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is! String) { - return mismatchDescription.addDescriptionOf(item).add('is not a string'); - } else { - var buff = new StringBuffer(); - buff.write('is different.'); - var escapedItem = escape(item); - var escapedValue = escape(_value); - var minLength = escapedItem.length < escapedValue.length - ? escapedItem.length - : escapedValue.length; - var start = 0; - for (; start < minLength; start++) { - if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) { - break; - } + Description describeTypedMismatch(String item, + Description mismatchDescription, Map matchState, bool verbose) { + var buff = new StringBuffer(); + buff.write('is different.'); + var escapedItem = escape(item); + var escapedValue = escape(_value); + var minLength = escapedItem.length < escapedValue.length + ? escapedItem.length + : escapedValue.length; + var start = 0; + for (; start < minLength; start++) { + if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) { + break; } - if (start == minLength) { - if (escapedValue.length < escapedItem.length) { - buff.write(' Both strings start the same, but the actual value also' - ' has the following trailing characters: '); - _writeTrailing(buff, escapedItem, escapedValue.length); - } else { - buff.write(' Both strings start the same, but the actual value is' - ' missing the following trailing characters: '); - _writeTrailing(buff, escapedValue, escapedItem.length); - } + } + if (start == minLength) { + if (escapedValue.length < escapedItem.length) { + buff.write(' Both strings start the same, but the actual value also' + ' has the following trailing characters: '); + _writeTrailing(buff, escapedItem, escapedValue.length); } else { - buff.write('\nExpected: '); - _writeLeading(buff, escapedValue, start); - _writeTrailing(buff, escapedValue, start); - buff.write('\n Actual: '); - _writeLeading(buff, escapedItem, start); - _writeTrailing(buff, escapedItem, start); - buff.write('\n '); - for (var i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' '); - buff.write('^\n Differ at offset $start'); + buff.write(' Both strings start the same, but the actual value is' + ' missing the following trailing characters: '); + _writeTrailing(buff, escapedValue, escapedItem.length); } - - return mismatchDescription.add(buff.toString()); + } else { + buff.write('\nExpected: '); + _writeLeading(buff, escapedValue, start); + _writeTrailing(buff, escapedValue, start); + buff.write('\n Actual: '); + _writeLeading(buff, escapedItem, start); + _writeTrailing(buff, escapedItem, start); + buff.write('\n '); + for (var i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' '); + buff.write('^\n Differ at offset $start'); } + + return mismatchDescription.add(buff.toString()); } static void _writeLeading(StringBuffer buff, String s, int start) { diff --git a/lib/src/feature_matcher.dart b/lib/src/feature_matcher.dart new file mode 100644 index 0000000..520e441 --- /dev/null +++ b/lib/src/feature_matcher.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'interfaces.dart'; +import 'type_matcher.dart'; + +/// A package-private [TypeMatcher] implementation that makes it easy for +/// subclasses to validate aspects of specific types while providing consistent +/// type checking. +abstract class FeatureMatcher extends TypeMatcher { + const FeatureMatcher(); + + bool matches(item, Map matchState) => + super.matches(item, matchState) && typedMatches(item, matchState); + + bool typedMatches(T item, Map matchState); + + Description describeMismatch( + item, Description mismatchDescription, Map matchState, bool verbose) { + if (item is T) { + return describeTypedMismatch( + item, mismatchDescription, matchState, verbose); + } + + return super.describe(mismatchDescription.add('not an ')); + } + + Description describeTypedMismatch(T item, Description mismatchDescription, + Map matchState, bool verbose) => + mismatchDescription; +} diff --git a/lib/src/iterable_matchers.dart b/lib/src/iterable_matchers.dart index f13525d..02c41aa 100644 --- a/lib/src/iterable_matchers.dart +++ b/lib/src/iterable_matchers.dart @@ -4,6 +4,7 @@ import 'description.dart'; import 'equals_matcher.dart'; +import 'feature_matcher.dart'; import 'interfaces.dart'; import 'util.dart'; @@ -16,10 +17,7 @@ class _EveryElement extends _IterableMatcher { _EveryElement(this._matcher); - bool matches(item, Map matchState) { - if (item is! Iterable) { - return false; - } + bool typedMatches(Iterable item, Map matchState) { var i = 0; for (var element in item) { if (!_matcher.matches(element, matchState)) { @@ -34,7 +32,7 @@ class _EveryElement extends _IterableMatcher { Description describe(Description description) => description.add('every element(').addDescriptionOf(_matcher).add(')'); - Description describeMismatch( + Description describeTypedMismatch( item, Description mismatchDescription, Map matchState, bool verbose) { if (matchState['index'] != null) { var index = matchState['index']; @@ -69,9 +67,8 @@ class _AnyElement extends _IterableMatcher { _AnyElement(this._matcher); - bool matches(item, Map matchState) { - return item.any((e) => _matcher.matches(e, matchState)); - } + bool typedMatches(Iterable item, Map matchState) => + item.any((e) => _matcher.matches(e, matchState)); Description describe(Description description) => description.add('some element ').addDescriptionOf(_matcher); @@ -83,28 +80,22 @@ class _AnyElement extends _IterableMatcher { /// This is equivalent to [equals] but does not recurse. Matcher orderedEquals(Iterable expected) => new _OrderedEquals(expected); -class _OrderedEquals extends Matcher { +class _OrderedEquals extends _IterableMatcher { final Iterable _expected; - Matcher _matcher; + final Matcher _matcher; - _OrderedEquals(this._expected) { - _matcher = equals(_expected, 1); - } + _OrderedEquals(this._expected) : _matcher = equals(_expected, 1); - bool matches(item, Map matchState) => - (item is Iterable) && _matcher.matches(item, matchState); + bool typedMatches(Iterable item, Map matchState) => + _matcher.matches(item, matchState); Description describe(Description description) => description.add('equals ').addDescriptionOf(_expected).add(' ordered'); - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is! Iterable) { - return mismatchDescription.add('is not an Iterable'); - } else { - return _matcher.describeMismatch( - item, mismatchDescription, matchState, verbose); - } + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + return _matcher.describeMismatch( + item, mismatchDescription, matchState, verbose); } } @@ -130,17 +121,8 @@ class _UnorderedEquals extends _UnorderedMatches { /// Iterable matchers match against [Iterable]s. We add this intermediate /// class to give better mismatch error messages than the base Matcher class. -abstract class _IterableMatcher extends Matcher { +abstract class _IterableMatcher extends FeatureMatcher { const _IterableMatcher(); - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is! Iterable) { - return mismatchDescription.addDescriptionOf(item).add(' not an Iterable'); - } else { - return super - .describeMismatch(item, mismatchDescription, matchState, verbose); - } - } } /// Returns a matcher which matches [Iterable]s whose elements match the @@ -150,7 +132,7 @@ abstract class _IterableMatcher extends Matcher { /// only be used on small iterables. Matcher unorderedMatches(Iterable expected) => new _UnorderedMatches(expected); -class _UnorderedMatches extends Matcher { +class _UnorderedMatches extends _IterableMatcher { final List _expected; final bool _allowUnmatchedValues; @@ -158,14 +140,7 @@ class _UnorderedMatches extends Matcher { : _expected = expected.map(wrapMatcher).toList(), _allowUnmatchedValues = allowUnmatchedValues ?? false; - String _test(item) { - if (item is Iterable) { - return _testCore(item.toList()); - } - return 'not iterable'; - } - - String _testCore(List values) { + String _test(List values) { // Check the lengths are the same. if (_expected.length > values.length) { return 'has too few elements (${values.length} < ${_expected.length})'; @@ -208,16 +183,17 @@ class _UnorderedMatches extends Matcher { return null; } - bool matches(item, Map mismatchState) => _test(item) == null; + bool typedMatches(Iterable item, Map mismatchState) => + _test(item.toList()) == null; Description describe(Description description) => description .add('matches ') .addAll('[', ', ', ']', _expected) .add(' unordered'); - Description describeMismatch(item, Description mismatchDescription, + Description describeTypedMismatch(item, Description mismatchDescription, Map matchState, bool verbose) => - mismatchDescription.add(_test(item)); + mismatchDescription.add(_test(item.toList())); /// Returns `true` if the value at [valueIndex] can be paired with some /// unmatched matcher and updates the state of [matched]. @@ -263,34 +239,28 @@ class _PairwiseCompare extends _IterableMatcher { _PairwiseCompare(this._expected, this._comparator, this._description); - bool matches(item, Map matchState) { - if (item is Iterable) { - if (item.length != _expected.length) return false; - var iterator = item.iterator; - var i = 0; - for (var e in _expected) { - iterator.moveNext(); - if (!_comparator(e, iterator.current)) { - addStateInfo(matchState, - {'index': i, 'expected': e, 'actual': iterator.current}); - return false; - } - i++; + bool typedMatches(Iterable item, Map matchState) { + if (item.length != _expected.length) return false; + var iterator = item.iterator; + var i = 0; + for (var e in _expected) { + iterator.moveNext(); + if (!_comparator(e, iterator.current as T)) { + addStateInfo(matchState, + {'index': i, 'expected': e, 'actual': iterator.current}); + return false; } - return true; - } else { - return false; + i++; } + return true; } Description describe(Description description) => description.add('pairwise $_description ').addDescriptionOf(_expected); - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is! Iterable) { - return mismatchDescription.add('is not an Iterable'); - } else if (item.length != _expected.length) { + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + if (item.length != _expected.length) { return mismatchDescription .add('has length ${item.length} instead of ${_expected.length}'); } else { @@ -345,13 +315,12 @@ class _ContainsAll extends _UnorderedMatches { Matcher containsAllInOrder(Iterable expected) => new _ContainsAllInOrder(expected); -class _ContainsAllInOrder implements Matcher { +class _ContainsAllInOrder extends _IterableMatcher { final Iterable _expected; _ContainsAllInOrder(this._expected); - String _test(item, Map matchState) { - if (item is! Iterable) return 'not an iterable'; + String _test(Iterable item, Map matchState) { var matchers = _expected.map(wrapMatcher).toList(); var matcherIndex = 0; for (var value in item) { @@ -366,7 +335,8 @@ class _ContainsAllInOrder implements Matcher { } @override - bool matches(item, Map matchState) => _test(item, matchState) == null; + bool typedMatches(Iterable item, Map matchState) => + _test(item, matchState) == null; @override Description describe(Description description) => description @@ -375,7 +345,7 @@ class _ContainsAllInOrder implements Matcher { .add(')'); @override - Description describeMismatch(item, Description mismatchDescription, - Map matchState, bool verbose) => + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) => mismatchDescription.add(_test(item, matchState)); } diff --git a/lib/src/numeric_matchers.dart b/lib/src/numeric_matchers.dart index c405391..4187078 100644 --- a/lib/src/numeric_matchers.dart +++ b/lib/src/numeric_matchers.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'feature_matcher.dart'; import 'interfaces.dart'; /// Returns a matcher which matches if the match argument is within [delta] @@ -11,19 +12,15 @@ import 'interfaces.dart'; /// than or equal [value]-[delta] and less than or equal to [value]+[delta]. Matcher closeTo(num value, num delta) => new _IsCloseTo(value, delta); -class _IsCloseTo extends Matcher { +class _IsCloseTo extends FeatureMatcher { final num _value, _delta; const _IsCloseTo(this._value, this._delta); - bool matches(item, Map matchState) { - if (item is num) { - var diff = item - _value; - if (diff < 0) diff = -diff; - return (diff <= _delta); - } else { - return false; - } + bool typedMatches(item, Map matchState) { + var diff = item - _value; + if (diff < 0) diff = -diff; + return (diff <= _delta); } Description describe(Description description) => description @@ -32,15 +29,11 @@ class _IsCloseTo extends Matcher { .add(' of ') .addDescriptionOf(_value); - Description describeMismatch( + Description describeTypedMismatch( item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is num) { - var diff = item - _value; - if (diff < 0) diff = -diff; - return mismatchDescription.add(' differs by ').addDescriptionOf(diff); - } else { - return mismatchDescription.add(' not numeric'); - } + var diff = item - _value; + if (diff < 0) diff = -diff; + return mismatchDescription.add(' differs by ').addDescriptionOf(diff); } } @@ -64,42 +57,28 @@ Matcher inOpenClosedRange(num low, num high) => Matcher inClosedOpenRange(num low, num high) => new _InRange(low, high, true, false); -class _InRange extends Matcher { +class _InRange extends FeatureMatcher { final num _low, _high; final bool _lowMatchValue, _highMatchValue; const _InRange( this._low, this._high, this._lowMatchValue, this._highMatchValue); - bool matches(value, Map matchState) { - if (value is num) { - if (value < _low || value > _high) { - return false; - } - if (value == _low) { - return _lowMatchValue; - } - if (value == _high) { - return _highMatchValue; - } - return true; - } else { + bool typedMatches(value, Map matchState) { + if (value < _low || value > _high) { return false; } + if (value == _low) { + return _lowMatchValue; + } + if (value == _high) { + return _highMatchValue; + } + return true; } Description describe(Description description) => description.add("be in range from " "$_low (${_lowMatchValue ? 'inclusive' : 'exclusive'}) to " "$_high (${_highMatchValue ? 'inclusive' : 'exclusive'})"); - - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is! num) { - return mismatchDescription.addDescriptionOf(item).add(' not numeric'); - } else { - return super - .describeMismatch(item, mismatchDescription, matchState, verbose); - } - } } diff --git a/lib/src/string_matchers.dart b/lib/src/string_matchers.dart index d82e830..47703c3 100644 --- a/lib/src/string_matchers.dart +++ b/lib/src/string_matchers.dart @@ -2,13 +2,14 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'feature_matcher.dart'; import 'interfaces.dart'; /// Returns a matcher which matches if the match argument is a string and /// is equal to [value] when compared case-insensitively. Matcher equalsIgnoringCase(String value) => new _IsEqualIgnoringCase(value); -class _IsEqualIgnoringCase extends _StringMatcher { +class _IsEqualIgnoringCase extends FeatureMatcher { final String _value; final String _matchValue; @@ -16,8 +17,8 @@ class _IsEqualIgnoringCase extends _StringMatcher { : _value = value, _matchValue = value.toLowerCase(); - bool matches(item, Map matchState) => - item is String && _matchValue == item.toLowerCase(); + bool typedMatches(String item, Map matchState) => + _matchValue == item.toLowerCase(); Description describe(Description description) => description.addDescriptionOf(_value).add(' ignoring case'); @@ -43,7 +44,7 @@ class _IsEqualIgnoringCase extends _StringMatcher { Matcher equalsIgnoringWhitespace(String value) => new _IsEqualIgnoringWhitespace(value); -class _IsEqualIgnoringWhitespace extends _StringMatcher { +class _IsEqualIgnoringWhitespace extends FeatureMatcher { final String _value; final String _matchValue; @@ -51,23 +52,18 @@ class _IsEqualIgnoringWhitespace extends _StringMatcher { : _value = value, _matchValue = collapseWhitespace(value); - bool matches(item, Map matchState) => - item is String && _matchValue == collapseWhitespace(item); + bool typedMatches(String item, Map matchState) => + _matchValue == collapseWhitespace(item); Description describe(Description description) => description.addDescriptionOf(_matchValue).add(' ignoring whitespace'); - Description describeMismatch( + Description describeTypedMismatch( item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is String) { - return mismatchDescription - .add('is ') - .addDescriptionOf(collapseWhitespace(item)) - .add(' with whitespace compressed'); - } else { - return super - .describeMismatch(item, mismatchDescription, matchState, verbose); - } + return mismatchDescription + .add('is ') + .addDescriptionOf(collapseWhitespace(item)) + .add(' with whitespace compressed'); } } @@ -75,13 +71,12 @@ class _IsEqualIgnoringWhitespace extends _StringMatcher { /// starts with [prefixString]. Matcher startsWith(String prefixString) => new _StringStartsWith(prefixString); -class _StringStartsWith extends _StringMatcher { +class _StringStartsWith extends FeatureMatcher { final String _prefix; const _StringStartsWith(this._prefix); - bool matches(item, Map matchState) => - item is String && item.startsWith(_prefix); + bool typedMatches(item, Map matchState) => item.startsWith(_prefix); Description describe(Description description) => description.add('a string starting with ').addDescriptionOf(_prefix); @@ -91,13 +86,12 @@ class _StringStartsWith extends _StringMatcher { /// ends with [suffixString]. Matcher endsWith(String suffixString) => new _StringEndsWith(suffixString); -class _StringEndsWith extends _StringMatcher { +class _StringEndsWith extends FeatureMatcher { final String _suffix; const _StringEndsWith(this._suffix); - bool matches(item, Map matchState) => - item is String && item.endsWith(_suffix); + bool typedMatches(item, Map matchState) => item.endsWith(_suffix); Description describe(Description description) => description.add('a string ending with ').addDescriptionOf(_suffix); @@ -112,22 +106,18 @@ class _StringEndsWith extends _StringMatcher { Matcher stringContainsInOrder(List substrings) => new _StringContainsInOrder(substrings); -class _StringContainsInOrder extends _StringMatcher { +class _StringContainsInOrder extends FeatureMatcher { final List _substrings; const _StringContainsInOrder(this._substrings); - bool matches(item, Map matchState) { - if (item is String) { - var fromIndex = 0; - for (var s in _substrings) { - fromIndex = item.indexOf(s, fromIndex); - if (fromIndex < 0) return false; - } - return true; - } else { - return false; + bool typedMatches(item, Map matchState) { + var fromIndex = 0; + for (var s in _substrings) { + fromIndex = item.indexOf(s, fromIndex); + if (fromIndex < 0) return false; } + return true; } Description describe(Description description) => description.addAll( @@ -141,7 +131,7 @@ class _StringContainsInOrder extends _StringMatcher { /// used to create a RegExp instance. Matcher matches(re) => new _MatchesRegExp(re); -class _MatchesRegExp extends _StringMatcher { +class _MatchesRegExp extends FeatureMatcher { RegExp _regexp; _MatchesRegExp(re) { @@ -154,28 +144,12 @@ class _MatchesRegExp extends _StringMatcher { } } - bool matches(item, Map matchState) => - item is String ? _regexp.hasMatch(item) : false; + bool typedMatches(item, Map matchState) => _regexp.hasMatch(item); Description describe(Description description) => description.add("match '${_regexp.pattern}'"); } -// String matchers match against a string. We add this intermediate -// class to give better mismatch error messages than the base Matcher class. -abstract class _StringMatcher extends Matcher { - const _StringMatcher(); - Description describeMismatch( - item, Description mismatchDescription, Map matchState, bool verbose) { - if (!(item is String)) { - return mismatchDescription.addDescriptionOf(item).add(' not a string'); - } else { - return super - .describeMismatch(item, mismatchDescription, matchState, verbose); - } - } -} - /// Utility function to collapse whitespace runs to single spaces /// and strip leading/trailing whitespace. String collapseWhitespace(String string) { diff --git a/test/core_matchers_test.dart b/test/core_matchers_test.dart index 48e7672..927cc7b 100644 --- a/test/core_matchers_test.dart +++ b/test/core_matchers_test.dart @@ -32,27 +32,13 @@ void main() { test('isNaN', () { shouldPass(double.nan, isNaN); shouldFail(3.1, isNaN, "Expected: NaN Actual: <3.1>"); - - shouldFail( - 'not a num', - isNaN, - anyOf( - contains("type 'String' is not a subtype of type 'num'"), - // For dart2js - will be fixed in follow-up - contains('TypeError'))); + shouldFail('not a num', isNaN, endsWith('not an ')); }); test('isNotNaN', () { shouldPass(3.1, isNotNaN); shouldFail(double.nan, isNotNaN, "Expected: not NaN Actual: "); - - shouldFail( - 'not a num', - isNotNaN, - anyOf( - contains("type 'String' is not a subtype of type 'num'"), - // For dart2js - will be fixed in follow-up - contains('TypeError'))); + shouldFail('not a num', isNotNaN, endsWith('not an ')); }); test('same', () { @@ -107,9 +93,8 @@ void main() { matches(r"Expected: return normally" r" Actual: " r" Which: threw StateError:")); - - shouldFail( - 'not a function', returnsNormally, contains('NoSuchMethodError')); + shouldFail('not a function', returnsNormally, + contains('not an ')); }); test('hasLength', () { @@ -233,6 +218,14 @@ void main() { shouldFail(0, predicate((x) => x is String, "an instance of String"), "Expected: an instance of String Actual: <0>"); shouldPass('cow', predicate((x) => x is String, "an instance of String")); + + if (isDart2) { + // With Dart2 semantics, predicate picks up a type argument of `bool` + // and we get nice type checking. + // Without Dart2 semantics a gnarly type error is thrown. + shouldFail(0, predicate((bool x) => x, "bool value is true"), + endsWith("not an ")); + } }); }); } diff --git a/test/iterable_matchers_test.dart b/test/iterable_matchers_test.dart index 6ed51a4..7c28338 100644 --- a/test/iterable_matchers_test.dart +++ b/test/iterable_matchers_test.dart @@ -26,6 +26,9 @@ void main() { contains(0), "Expected: contains <0> " "Actual: [1, 2]"); + + shouldFail( + 'String', contains(42), "Expected: contains <42> Actual: 'String'"); }); test('equals with matcher element', () { @@ -40,9 +43,22 @@ void main() { }); test('isIn', () { - var d = [1, 2]; - shouldPass(1, isIn(d)); - shouldFail(0, isIn(d), "Expected: is in [1, 2] Actual: <0>"); + // Iterable + shouldPass(1, isIn([1, 2])); + shouldFail(0, isIn([1, 2]), "Expected: is in [1, 2] Actual: <0>"); + + // Map + shouldPass(1, isIn({1: null})); + shouldFail(0, isIn({1: null}), "Expected: is in {1: null} Actual: <0>"); + + // String + shouldPass('42', isIn('1421')); + shouldFail('42', isIn('41'), "Expected: is in '41' Actual: '42'"); + shouldFail( + 0, isIn('a string'), endsWith('not an ')); + + // Invalid arg + expect(() => isIn(42), throwsArgumentError); }); test('everyElement', () { @@ -55,7 +71,8 @@ void main() { "Actual: [1, 2] " "Which: has value <2> which doesn't match <1> at index 1"); shouldPass(e, everyElement(1)); - shouldFail('not iterable', everyElement(1), contains('not an Iterable')); + shouldFail('not iterable', everyElement(1), + endsWith('not an ')); }); test('nested everyElement', () { @@ -110,7 +127,8 @@ void main() { shouldPass(d, anyElement(2)); shouldFail( e, anyElement(2), "Expected: some element <2> Actual: [1, 1, 1]"); - shouldFail('not an iterable', anyElement(2), contains('not an Iterable')); + shouldFail('not an iterable', anyElement(2), + endsWith('not an ')); }); test('orderedEquals', () { @@ -123,8 +141,8 @@ void main() { "Expected: equals [2, 1] ordered " "Actual: [1, 2] " "Which: was <1> instead of <2> at location [0]"); - shouldFail( - 'not an iterable', orderedEquals([1]), contains('not an iterable')); + shouldFail('not an iterable', orderedEquals([1]), + endsWith('not an ')); }); test('unorderedEquals', () { @@ -155,8 +173,8 @@ void main() { "Actual: [1, 2] " "Which: has no match for <3> at index 0" " along with 1 other unmatched"); - shouldFail( - 'not an iterable', unorderedEquals([1]), contains('not an iterable')); + shouldFail('not an iterable', unorderedEquals([1]), + endsWith('not an ')); }); test('unorderedMatches', () { @@ -204,7 +222,7 @@ void main() { "Actual: [1, 2] " "Which: has no match for a value greater than <3> at index 0"); shouldFail('not an iterable', unorderedMatches([greaterThan(1)]), - contains('not an iterable')); + endsWith('not an ')); }); test('containsAll', () { @@ -224,7 +242,7 @@ void main() { containsAll([1]), "Expected: contains all of [1] " "Actual: <1> " - "Which: not iterable"); + "Which: not an "); shouldFail( [-1, 2], containsAll([greaterThan(0), greaterThan(1)]), @@ -232,8 +250,8 @@ void main() { ">] " "Actual: [-1, 2] " "Which: has no match for a value greater than <1> at index 1"); - shouldFail( - 'not an iterable', containsAll([1, 2, 3]), contains('not an iterable')); + shouldFail('not an iterable', containsAll([1, 2, 3]), + endsWith('not an ')); }); test('containsAllInOrder', () { @@ -267,7 +285,7 @@ void main() { containsAllInOrder([1]), "Expected: contains in order([1]) " "Actual: <1> " - "Which: not an iterable"); + "Which: not an "); }); test('pairwise compare', () { @@ -279,7 +297,7 @@ void main() { pairwiseCompare(e, (int e, int a) => a <= e, "less than or equal"), "Expected: pairwise less than or equal [1, 4, 9] " "Actual: 'x' " - "Which: is not an Iterable"); + "Which: not an "); shouldFail( c, pairwiseCompare(e, (int e, int a) => a <= e, "less than or equal"), @@ -304,7 +322,7 @@ void main() { shouldFail( 'not an iterable', pairwiseCompare(e, (e, a) => a + a == e, "double"), - contains('not an Iterable')); + endsWith('not an ')); }); test('isEmpty', () { diff --git a/test/numeric_matchers_test.dart b/test/numeric_matchers_test.dart index 0cfd3cd..a85f13b 100644 --- a/test/numeric_matchers_test.dart +++ b/test/numeric_matchers_test.dart @@ -24,7 +24,8 @@ void main() { "Expected: a numeric value within <1> of <0> " "Actual: <-1.001> " "Which: differs by <1.001>"); - shouldFail('not a num', closeTo(0, 1), contains('not numeric')); + shouldFail( + 'not a num', closeTo(0, 1), endsWith('not an ')); }); test('inInclusiveRange', () { @@ -41,7 +42,8 @@ void main() { inInclusiveRange(0, 2), "Expected: be in range from 0 (inclusive) to 2 (inclusive) " "Actual: <3>"); - shouldFail('not a num', inInclusiveRange(0, 1), contains('not numeric')); + shouldFail('not a num', inInclusiveRange(0, 1), + endsWith('not an ')); }); test('inExclusiveRange', () { @@ -56,7 +58,8 @@ void main() { inExclusiveRange(0, 2), "Expected: be in range from 0 (exclusive) to 2 (exclusive) " "Actual: <2>"); - shouldFail('not a num', inExclusiveRange(0, 1), contains('not numeric')); + shouldFail('not a num', inExclusiveRange(0, 1), + endsWith('not an ')); }); test('inOpenClosedRange', () { @@ -67,7 +70,8 @@ void main() { "Actual: <0>"); shouldPass(1, inOpenClosedRange(0, 2)); shouldPass(2, inOpenClosedRange(0, 2)); - shouldFail('not a num', inOpenClosedRange(0, 1), contains('not numeric')); + shouldFail('not a num', inOpenClosedRange(0, 1), + endsWith('not an ')); }); test('inClosedOpenRange', () { @@ -78,6 +82,7 @@ void main() { inClosedOpenRange(0, 2), "Expected: be in range from 0 (inclusive) to 2 (exclusive) " "Actual: <2>"); - shouldFail('not a num', inClosedOpenRange(0, 1), contains('not numeric')); + shouldFail('not a num', inClosedOpenRange(0, 1), + endsWith('not an ')); }); } diff --git a/test/pretty_print_test.dart b/test/pretty_print_test.dart index 7c9c44b..59a5950 100644 --- a/test/pretty_print_test.dart +++ b/test/pretty_print_test.dart @@ -8,6 +8,8 @@ import 'package:matcher/matcher.dart'; import 'package:matcher/src/pretty_print.dart'; import 'package:test/test.dart' show group, test, expect; +import 'test_utils.dart'; + class DefaultToString {} class CustomToString { @@ -246,7 +248,7 @@ void main() { test("that's not a list", () { expect( prettyPrint([1, 2, 3, 4].map((n) => n * 2)), - equals(_isDart2 + equals(isDart2 ? "MappedListIterable:[2, 4, 6, 8]" : "MappedListIterable:[2, 4, 6, 8]")); }); @@ -260,8 +262,3 @@ void main() { expect(prettyPrint(''.runtimeType), 'Type:'); }); } - -final _isDart2 = () { - Type checkType() => T; - return checkType() == String; -}(); diff --git a/test/string_matchers_test.dart b/test/string_matchers_test.dart index ad0ef71..834df8c 100644 --- a/test/string_matchers_test.dart +++ b/test/string_matchers_test.dart @@ -55,7 +55,8 @@ void main() { shouldPass('hello', equalsIgnoringCase('HELLO')); shouldFail('hi', equalsIgnoringCase('HELLO'), "Expected: 'HELLO' ignoring case Actual: 'hi'"); - shouldFail(42, equalsIgnoringCase('HELLO'), contains('not a string')); + shouldFail(42, equalsIgnoringCase('HELLO'), + endsWith('not an ')); }); test('equalsIgnoringWhitespace', () { @@ -66,7 +67,8 @@ void main() { "Expected: 'hello world' ignoring whitespace " "Actual: ' helloworld ' " "Which: is 'helloworld' with whitespace compressed"); - shouldFail(42, equalsIgnoringWhitespace('HELLO'), contains('not a string')); + shouldFail(42, equalsIgnoringWhitespace('HELLO'), + endsWith('not an ')); }); test('startsWith', () { @@ -78,7 +80,8 @@ void main() { startsWith('hello '), "Expected: a string starting with 'hello ' " "Actual: 'hello'"); - shouldFail(42, startsWith('hello '), contains('not a string')); + shouldFail( + 42, startsWith('hello '), endsWith('not an ')); }); test('endsWith', () { @@ -90,7 +93,8 @@ void main() { endsWith(' hello'), "Expected: a string ending with ' hello' " "Actual: 'hello'"); - shouldFail(42, startsWith('hello '), contains('not a string')); + shouldFail( + 42, startsWith('hello '), endsWith('not an ')); }); test('contains', () { @@ -128,6 +132,7 @@ void main() { shouldPass('c0d', matches(new RegExp('[a-z][0-9][a-z]'))); shouldFail('cOd', matches('[a-z][0-9][a-z]'), "Expected: match '[a-z][0-9][a-z]' Actual: 'cOd'"); - shouldFail(42, matches('[a-z][0-9][a-z]'), contains('not a string')); + shouldFail(42, matches('[a-z][0-9][a-z]'), + endsWith('not an ')); }); } diff --git a/test/test_utils.dart b/test/test_utils.dart index c22e223..22ad681 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -4,6 +4,11 @@ import 'package:test/test.dart'; +final bool isDart2 = () { + Type checkType() => T; + return checkType() == String; +}(); + void shouldFail(value, Matcher matcher, expected) { var failed = false; try {