diff --git a/pkgs/matcher/CHANGELOG.md b/pkgs/matcher/CHANGELOG.md index 614e35a5b..54420762b 100644 --- a/pkgs/matcher/CHANGELOG.md +++ b/pkgs/matcher/CHANGELOG.md @@ -3,6 +3,7 @@ * Remove some dynamic invocations. * Add explicit casts from `dynamic` values. * Require Dart 3.5 +* Add `isSorted` and related matchers for iterables. ## 0.12.17 diff --git a/pkgs/matcher/lib/src/iterable_matchers.dart b/pkgs/matcher/lib/src/iterable_matchers.dart index abc9688e8..d9d2db3e2 100644 --- a/pkgs/matcher/lib/src/iterable_matchers.dart +++ b/pkgs/matcher/lib/src/iterable_matchers.dart @@ -132,7 +132,7 @@ 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 FeatureMatcher { +abstract class _IterableMatcher extends FeatureMatcher> { const _IterableMatcher(); } @@ -414,3 +414,119 @@ class _ContainsOnce extends _IterableMatcher { Description mismatchDescription, Map matchState, bool verbose) => mismatchDescription.add(_test(item, matchState)!); } + +/// Matches [Iterable]s which are sorted. +Matcher isSorted>() => + _IsSorted((t) => t, (a, b) => a.compareTo(b)); + +/// Matches [Iterable]s which are [compare]-sorted. +Matcher isSortedUsing(Comparator compare) => + _IsSorted((t) => t, compare); + +/// Matches [Iterable]s which are sorted by the [keyOf] property. +Matcher isSortedBy>(K Function(T) keyOf) => + _IsSorted(keyOf, (a, b) => a.compareTo(b)); + +/// Matches [Iterable]s which are [compare]-sorted by their [keyOf] property. +Matcher isSortedByCompare(K Function(T) keyOf, Comparator compare) => + _IsSorted(keyOf, compare); + +class _IsSorted extends _IterableMatcher { + final K Function(T) _keyOf; + final Comparator _compare; + + _IsSorted(K Function(T) keyOf, Comparator compare) + : _keyOf = keyOf, + _compare = compare; + + @override + bool typedMatches(Iterable item, Map matchState) { + var iterator = item.iterator; + if (!iterator.moveNext()) return true; + var previousElement = iterator.current; + K previousKey; + try { + previousKey = _keyOf(previousElement); + } catch (e) { + addStateInfo(matchState, { + 'index': 0, + 'element': previousElement, + 'error': e, + 'keyError': true + }); + return false; + } + + var index = 0; + while (iterator.moveNext()) { + final element = iterator.current; + final K key; + try { + key = _keyOf(element); + } catch (e) { + addStateInfo(matchState, + {'index': index, 'element': element, 'error': e, 'keyError': true}); + return false; + } + + final int comparison; + try { + comparison = _compare(previousKey, key); + } catch (e) { + addStateInfo(matchState, { + 'index': index, + 'first': previousElement, + 'second': element, + 'error': e, + 'compareError': true + }); + return false; + } + + if (comparison > 0) { + addStateInfo(matchState, + {'index': index, 'first': previousElement, 'second': element}); + return false; + } + previousElement = element; + previousKey = key; + index++; + } + return true; + } + + @override + Description describe(Description description) => description.add('is sorted'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + if (matchState.containsKey('error')) { + mismatchDescription + .add('got error ') + .addDescriptionOf(matchState['error']) + .add(' at ') + .addDescriptionOf(matchState['index']); + + if (matchState.containsKey('compareError')) { + return mismatchDescription + .add(' when comparing ') + .addDescriptionOf(matchState['first']) + .add(' and ') + .addDescriptionOf(matchState['second']); + } else { + return mismatchDescription + .add(' when getting key of ') + .addDescriptionOf(matchState['element']); + } + } + + return mismatchDescription + .add('found elements out of order at ') + .addDescriptionOf(matchState['index']) + .add(': ') + .addDescriptionOf(matchState['first']) + .add(' and ') + .addDescriptionOf(matchState['second']); + } +} diff --git a/pkgs/matcher/test/iterable_matchers_test.dart b/pkgs/matcher/test/iterable_matchers_test.dart index 3cd78d71a..6a4f3b6df 100644 --- a/pkgs/matcher/test/iterable_matchers_test.dart +++ b/pkgs/matcher/test/iterable_matchers_test.dart @@ -392,4 +392,110 @@ void main() { 'Actual: SimpleIterable:[3, 2, 1] ' 'Which: does not contain <5>'); }); + + test('isSorted', () { + final sorted = [4, 8, 15, 16, 23, 42]; + final mismatchAtStart = [8, 4, 15, 16, 23, 42]; + final mismatchInMiddle = [4, 8, 16, 15, 23, 42]; + final mismatchAtEnd = [4, 8, 15, 16, 42, 23]; + final singleElement = [42]; + final twoElementsSorted = [42, 143]; + final twoElementsUnsorted = [143, 42]; + + shouldPass(sorted, isSorted()); + shouldFail( + mismatchAtStart, + isSorted(), + 'Expected: is sorted ' + 'Actual: [8, 4, 15, 16, 23, 42] ' + 'Which: found elements out of order at <0>: <8> and <4>'); + shouldFail( + mismatchInMiddle, + isSorted(), + 'Expected: is sorted ' + 'Actual: [4, 8, 16, 15, 23, 42] ' + 'Which: found elements out of order at <2>: <16> and <15>'); + shouldFail( + mismatchAtEnd, + isSorted(), + 'Expected: is sorted ' + 'Actual: [4, 8, 15, 16, 42, 23] ' + 'Which: found elements out of order at <4>: <42> and <23>'); + shouldPass(singleElement, isSorted()); + shouldPass(twoElementsSorted, isSorted()); + shouldFail( + twoElementsUnsorted, + isSorted(), + 'Expected: is sorted ' + 'Actual: [143, 42] ' + 'Which: found elements out of order at <0>: <143> and <42>'); + }); + + test('isSortedUsing', () { + final sorted = [1, 2, 3]; + final unsorted = [1, 3, 2]; + final reverseSorted = [3, 2, 1]; + + int alwaysEqualCompare(int x, int y) => 0; + int throwingCompare(int x, int y) => throw Error(); + + shouldPass(sorted, isSortedUsing((int x, int y) => x - y)); + shouldFail( + unsorted, + isSortedUsing((int x, int y) => x - y), + 'Expected: is sorted ' + 'Actual: [1, 3, 2] ' + 'Which: found elements out of order at <1>: <3> and <2>'); + shouldPass(reverseSorted, isSortedUsing((int x, int y) => y - x)); + + shouldPass(unsorted, isSortedUsing(alwaysEqualCompare)); + + shouldFail( + sorted, + isSortedUsing(throwingCompare), + 'Expected: is sorted ' + 'Actual: [1, 2, 3] ' + 'Which: got error at <0> ' + 'when comparing <1> and <2>'); + }); + + test('isSortedBy', () { + final sorted = ['y', 'zz', 'bbbb', 'aaaa']; + final unsorted = ['y', 'bbbb', 'aaaa', 'zz']; + final sortedDueToSameKey = ['zzz', 'abc', 'def', 'aaa']; + + num throwingKey(String s) => throw Error(); + + shouldPass(sorted, isSortedBy((String s) => s.length)); + shouldFail( + unsorted, + isSortedBy((String s) => s.length), + 'Expected: is sorted ' + 'Actual: [\'y\', \'bbbb\', \'aaaa\', \'zz\'] ' + 'Which: found elements out of order at <2>: \'aaaa\' and \'zz\''); + shouldPass( + sortedDueToSameKey, isSortedBy((String s) => s.length)); + + shouldFail( + sorted, + isSortedBy(throwingKey), + 'Expected: is sorted ' + 'Actual: [\'y\', \'zz\', \'bbbb\', \'aaaa\'] ' + 'Which: got error at <0> ' + 'when getting key of \'y\''); + }); + + test('isSortedByCompare', () { + final sorted = ['aaaa', 'bbbb', 'zz', 'y']; + final unsorted = ['y', 'bbbb', 'aaaa', 'zz']; + + shouldPass(sorted, + isSortedByCompare((String s) => s.length, (a, b) => b.compareTo(a))); + shouldFail( + unsorted, + isSortedByCompare((String s) => s.length, (a, b) => b.compareTo(a)), + 'Expected: is sorted ' + 'Actual: [\'y\', \'bbbb\', \'aaaa\', \'zz\'] ' + 'Which: found elements out of order at <0>: \'y\' and \'bbbb\''); + }); }