diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e62f1..eb76db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.12.1+3 + +* Make `predicate` and `pairwiseCompare` generic methods to allow typed + functions to be passed to them as arguments. + +* Make internal implementations take better advantage of type promotion to avoid + dynamic call overhead. + ## 0.12.1+2 * Fixed small documentation issues. diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart index b513d5e..e8fdb11 100644 --- a/lib/src/core_matchers.dart +++ b/lib/src/core_matchers.dart @@ -110,6 +110,9 @@ Matcher equals(expected, [int limit = 100]) => expected is String ? new _StringEqualsMatcher(expected) : new _DeepMatcher(expected, limit); +typedef _RecursiveMatcher = List Function( + dynamic, dynamic, String, int); + class _DeepMatcher extends Matcher { final _expected; final int _limit; @@ -117,52 +120,60 @@ class _DeepMatcher extends Matcher { _DeepMatcher(this._expected, [int limit = 1000]) : this._limit = limit; // Returns a pair (reason, location) - List _compareIterables(expected, actual, matcher, depth, location) { - if (actual is! Iterable) return ['is not Iterable', location]; - - var expectedIterator = expected.iterator; - var actualIterator = actual.iterator; - for (var index = 0;; index++) { - // Advance in lockstep. - var expectedNext = expectedIterator.moveNext(); - var actualNext = actualIterator.moveNext(); - - // If we reached the end of both, we succeeded. - if (!expectedNext && !actualNext) return null; - - // Fail if their lengths are different. - var newLocation = '$location[$index]'; - if (!expectedNext) return ['longer than expected', newLocation]; - if (!actualNext) return ['shorter than expected', newLocation]; - - // Match the elements. - var rp = matcher( - expectedIterator.current, actualIterator.current, newLocation, depth); - if (rp != null) return rp; + List _compareIterables(Iterable expected, Object actual, + _RecursiveMatcher matcher, int depth, String location) { + if (actual is Iterable) { + var expectedIterator = expected.iterator; + var actualIterator = actual.iterator; + for (var index = 0;; index++) { + // Advance in lockstep. + var expectedNext = expectedIterator.moveNext(); + var actualNext = actualIterator.moveNext(); + + // If we reached the end of both, we succeeded. + if (!expectedNext && !actualNext) return null; + + // Fail if their lengths are different. + var newLocation = '$location[$index]'; + if (!expectedNext) return ['longer than expected', newLocation]; + if (!actualNext) return ['shorter than expected', newLocation]; + + // Match the elements. + var rp = matcher(expectedIterator.current, actualIterator.current, + newLocation, depth); + if (rp != null) return rp; + } + } else { + return ['is not Iterable', location]; } } - List _compareSets(Set expected, actual, matcher, depth, location) { - if (actual is! Iterable) return ['is not Iterable', location]; - actual = actual.toSet(); + List _compareSets(Set expected, Object actual, + _RecursiveMatcher matcher, int depth, String location) { + if (actual is Iterable) { + Set other = actual.toSet(); - for (var expectedElement in expected) { - if (actual.every((actualElement) => - matcher(expectedElement, actualElement, location, depth) != null)) { - return ['does not contain $expectedElement', location]; + for (var expectedElement in expected) { + if (other.every((actualElement) => + matcher(expectedElement, actualElement, location, depth) != null)) { + return ['does not contain $expectedElement', location]; + } } - } - if (actual.length > expected.length) { - return ['larger than expected', location]; - } else if (actual.length < expected.length) { - return ['smaller than expected', location]; + if (other.length > expected.length) { + return ['larger than expected', location]; + } else if (other.length < expected.length) { + return ['smaller than expected', location]; + } else { + return null; + } } else { - return null; + return ['is not Iterable', location]; } } - List _recursiveMatch(expected, actual, String location, int depth) { + List _recursiveMatch( + Object expected, Object actual, String location, int depth) { // If the expected value is a matcher, try to match it. if (expected is Matcher) { var matchState = {}; @@ -193,17 +204,16 @@ class _DeepMatcher extends Matcher { expected, actual, _recursiveMatch, depth + 1, location); } else if (expected is Map) { if (actual is! Map) return ['expected a map', location]; - - var err = (expected.length == actual.length) - ? '' - : 'has different length and '; + var map = (actual as Map); + var err = + (expected.length == map.length) ? '' : 'has different length and '; for (var key in expected.keys) { - if (!actual.containsKey(key)) { + if (!map.containsKey(key)) { return ["${err}is missing map key '$key'", location]; } } - for (var key in actual.keys) { + for (var key in map.keys) { if (!expected.containsKey(key)) { return ["${err}has extra map key '$key'", location]; } @@ -211,7 +221,7 @@ class _DeepMatcher extends Matcher { for (var key in expected.keys) { var rp = _recursiveMatch( - expected[key], actual[key], "$location['$key']", depth + 1); + expected[key], map[key], "$location['$key']", depth + 1); if (rp != null) return rp; } @@ -239,7 +249,7 @@ class _DeepMatcher extends Matcher { String _match(expected, actual, Map matchState) { var rp = _recursiveMatch(expected, actual, '', 0); if (rp == null) return null; - var reason; + String reason; if (rp[0].length > 0) { if (rp[1].length > 0) { reason = "${rp[0]} at location ${rp[1]}"; @@ -567,18 +577,19 @@ class _In extends Matcher { /// For example: /// /// expect(v, predicate((x) => ((x % 2) == 0), "is even")) -Matcher predicate(bool f(value), [String description = 'satisfies function']) => +Matcher predicate(bool f(T value), + [String description = 'satisfies function']) => new _Predicate(f, description); -typedef bool _PredicateFunction(value); +typedef bool _PredicateFunction(T value); -class _Predicate extends Matcher { - final _PredicateFunction _matcher; +class _Predicate extends Matcher { + final _PredicateFunction _matcher; final String _description; - const _Predicate(this._matcher, this._description); + _Predicate(this._matcher, this._description); - bool matches(item, Map matchState) => _matcher(item); + bool matches(item, Map matchState) => _matcher(item as T); Description describe(Description description) => description.add(_description); diff --git a/lib/src/iterable_matchers.dart b/lib/src/iterable_matchers.dart index 4f7785c..759c252 100644 --- a/lib/src/iterable_matchers.dart +++ b/lib/src/iterable_matchers.dart @@ -155,42 +155,45 @@ class _UnorderedMatches extends Matcher { : _expected = expected.map(wrapMatcher).toList(); String _test(item) { - if (item is! Iterable) return 'not iterable'; - item = item.toList(); - - // Check the lengths are the same. - if (_expected.length > item.length) { - return 'has too few elements (${item.length} < ${_expected.length})'; - } else if (_expected.length < item.length) { - return 'has too many elements (${item.length} > ${_expected.length})'; - } + if (item is Iterable) { + var list = item.toList(); + + // Check the lengths are the same. + if (_expected.length > list.length) { + return 'has too few elements (${list.length} < ${_expected.length})'; + } else if (_expected.length < list.length) { + return 'has too many elements (${list.length} > ${_expected.length})'; + } - var matched = new List.filled(item.length, false); - var expectedPosition = 0; - for (var expectedMatcher in _expected) { - var actualPosition = 0; - var gotMatch = false; - for (var actualElement in item) { - if (!matched[actualPosition]) { - if (expectedMatcher.matches(actualElement, {})) { - matched[actualPosition] = gotMatch = true; - break; + var matched = new List.filled(list.length, false); + var expectedPosition = 0; + for (var expectedMatcher in _expected) { + var actualPosition = 0; + var gotMatch = false; + for (var actualElement in list) { + if (!matched[actualPosition]) { + if (expectedMatcher.matches(actualElement, {})) { + matched[actualPosition] = gotMatch = true; + break; + } } + ++actualPosition; } - ++actualPosition; - } - if (!gotMatch) { - return new StringDescription() - .add('has no match for ') - .addDescriptionOf(expectedMatcher) - .add(' at index $expectedPosition') - .toString(); - } + if (!gotMatch) { + return new StringDescription() + .add('has no match for ') + .addDescriptionOf(expectedMatcher) + .add(' at index $expectedPosition') + .toString(); + } - ++expectedPosition; + ++expectedPosition; + } + return null; + } else { + return 'not iterable'; } - return null; } bool matches(item, Map mismatchState) => _test(item) == null; @@ -210,34 +213,37 @@ class _UnorderedMatches extends Matcher { /// The [comparator] function, taking an expected and an actual argument, and /// returning whether they match, will be applied to each pair in order. /// [description] should be a meaningful name for the comparator. -Matcher pairwiseCompare( - Iterable expected, bool comparator(a, b), String description) => +Matcher pairwiseCompare( + Iterable expected, bool comparator(S a, T b), String description) => new _PairwiseCompare(expected, comparator, description); -typedef bool _Comparator(a, b); +typedef bool _Comparator(S a, T b); -class _PairwiseCompare extends _IterableMatcher { - final Iterable _expected; - final _Comparator _comparator; +class _PairwiseCompare extends _IterableMatcher { + final Iterable _expected; + final _Comparator _comparator; final String _description; _PairwiseCompare(this._expected, this._comparator, this._description); bool matches(item, Map matchState) { - if (item is! Iterable) return false; - 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; + 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++; } - i++; + return true; + } else { + return false; } - return true; } Description describe(Description description) => diff --git a/lib/src/numeric_matchers.dart b/lib/src/numeric_matchers.dart index e8651de..c405391 100644 --- a/lib/src/numeric_matchers.dart +++ b/lib/src/numeric_matchers.dart @@ -17,11 +17,13 @@ class _IsCloseTo extends Matcher { const _IsCloseTo(this._value, this._delta); bool matches(item, Map matchState) { - if (item is! num) return false; - - var diff = item - _value; - if (diff < 0) diff = -diff; - return (diff <= _delta); + if (item is num) { + var diff = item - _value; + if (diff < 0) diff = -diff; + return (diff <= _delta); + } else { + return false; + } } Description describe(Description description) => description @@ -32,12 +34,12 @@ class _IsCloseTo extends Matcher { Description describeMismatch( item, Description mismatchDescription, Map matchState, bool verbose) { - if (item is! num) { - return mismatchDescription.add(' not numeric'); - } else { + 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'); } } } @@ -70,19 +72,20 @@ class _InRange extends Matcher { this._low, this._high, this._lowMatchValue, this._highMatchValue); bool matches(value, Map matchState) { - if (value is! num) { - return false; - } - if (value < _low || value > _high) { + if (value is num) { + if (value < _low || value > _high) { + return false; + } + if (value == _low) { + return _lowMatchValue; + } + if (value == _high) { + return _highMatchValue; + } + return true; + } else { return false; } - if (value == _low) { - return _lowMatchValue; - } - if (value == _high) { - return _highMatchValue; - } - return true; } Description describe(Description description) => diff --git a/lib/src/string_matchers.dart b/lib/src/string_matchers.dart index d8bbdb8..14e6fbb 100644 --- a/lib/src/string_matchers.dart +++ b/lib/src/string_matchers.dart @@ -118,15 +118,16 @@ class _StringContainsInOrder extends _StringMatcher { const _StringContainsInOrder(this._substrings); bool matches(item, Map matchState) { - if (!(item is String)) { + if (item is String) { + var from_index = 0; + for (var s in _substrings) { + from_index = item.indexOf(s, from_index); + if (from_index < 0) return false; + } + return true; + } else { return false; } - var from_index = 0; - for (var s in _substrings) { - from_index = item.indexOf(s, from_index); - if (from_index < 0) return false; - } - return true; } Description describe(Description description) => description.addAll( diff --git a/pubspec.yaml b/pubspec.yaml index ae72221..1d130cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: matcher -version: 0.12.1+2 +version: 0.12.1+3 author: Dart Team description: Support for specifying test expectations homepage: https://github.com/dart-lang/matcher environment: - sdk: '>=1.8.0 <2.0.0-dev.infinity' + sdk: '>=1.23.0 <2.0.0-dev.infinity' dependencies: stack_trace: '^1.2.0' dev_dependencies: