Skip to content

Commit

Permalink
Fix unorderedMatches for poorly ordered input
Browse files Browse the repository at this point in the history
Fixes #73

Re-implements the unordered matcher using a recursive search assuming
that matchers can matche multiple values in the input rather than a
greedy algorithm which allows a value to "consume" a matcher that is
needed for some other matcher.

User visible differences:
- May match inputs that would have previously been (incorrectly)
  rejected.
- The failure description no longer includes the index of the matcher
  which is unmatched, and all unmatched matchers are printed rather than
  the first.
  • Loading branch information
natebosch committed Apr 11, 2018
1 parent 3b2755e commit 1746fef
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 25 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.12.2

* Fixed `unorderedMatches` in cases where the matchers may match more than one
element and order of the elements doesn't line up with the order of the
matchers.

## 0.12.1+4

* Fixed SDK constraint to allow edge builds.
Expand Down
69 changes: 49 additions & 20 deletions lib/src/iterable_matchers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class _OrderedEquals extends Matcher {
/// Returns a matcher which matches [Iterable]s that have the same length and
/// the same elements as [expected], but not necessarily in the same order.
///
/// Note that this is O(n^2) so should only be used on small objects.
/// Note that this is O(n^2) so should only be used on small iterables.
Matcher unorderedEquals(Iterable expected) => new _UnorderedEquals(expected);

class _UnorderedEquals extends _UnorderedMatches {
Expand Down Expand Up @@ -145,7 +145,7 @@ abstract class _IterableMatcher extends Matcher {
/// Returns a matcher which matches [Iterable]s whose elements match the
/// matchers in [expected], but not necessarily in the same order.
///
/// Note that this is `O(n^2)` and so should only be used on small objects.
/// Note that this is `O(n^2)` and so should only be used on small iterables.
Matcher unorderedMatches(Iterable expected) => new _UnorderedMatches(expected);

class _UnorderedMatches extends Matcher {
Expand All @@ -165,30 +165,39 @@ class _UnorderedMatches extends Matcher {
return 'has too many elements (${list.length} > ${_expected.length})';
}

var matched = new List<bool>.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;
}
var adjacency = list
.map((_) => new List.filled(_expected.length, false, growable: false))
.toList(growable: false);
for (int v = 0; v < list.length; v++) {
for (int m = 0; m < _expected.length; m++) {
if (_expected[m].matches(list[v], {})) {
adjacency[v][m] = true;
}
++actualPosition;
}

if (!gotMatch) {
}
// The index into `values` matched with each matcher
var matched = new List<int>.filled(_expected.length, -1, growable: false);
for (int valueIndex = 0; valueIndex < list.length; valueIndex++) {
_findPairing(adjacency, valueIndex, new Set<int>(), matched);
}
var unmatched = <Matcher>[];
for (int matcherIndex = 0;
matcherIndex < _expected.length;
matcherIndex++) {
if (matched[matcherIndex] < 0) unmatched.add(_expected[matcherIndex]);
}
if (unmatched.isNotEmpty) {
if (unmatched.length > 1) {
return new StringDescription()
.add('has no match for any of ')
.addAll('(', ', ', ')', unmatched)
.toString();
} else {
return new StringDescription()
.add('has no match for ')
.addDescriptionOf(expectedMatcher)
.add(' at index $expectedPosition')
.addDescriptionOf(unmatched.single)
.toString();
}

++expectedPosition;
}
return null;
} else {
Expand All @@ -206,6 +215,26 @@ class _UnorderedMatches extends Matcher {
Description describeMismatch(item, Description mismatchDescription,
Map matchState, bool verbose) =>
mismatchDescription.add(_test(item));

/// Returns [true] if the value at [valueIndex] can be paired with some
/// unmatched matcher.
///
/// Recursively looks for new pairings whenever there is a conflict. [seen]
/// tracks the matchers that have already been consumed within this search.
bool _findPairing(List<List<bool>> adjacency, int valueIndex, Set<int> seen,
List<int> matched) {
for (int i = 0; i < matched.length; i++) {
if (adjacency[valueIndex][i] && !seen.contains(i)) {
seen.add(i);
if (matched[i] < 0 ||
_findPairing(adjacency, matched[i], seen, matched)) {
matched[i] = valueIndex;
return true;
}
}
}
return false;
}
}

/// A pairwise matcher for [Iterable]s.
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: matcher
version: 0.12.1+4
version: 0.12.2-dev
author: Dart Team <misc@dartlang.org>
description: Support for specifying test expectations
homepage: https://github.com/dart-lang/matcher
Expand Down
17 changes: 13 additions & 4 deletions test/iterable_matchers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,22 @@ void main() {
unorderedEquals([3, 1]),
"Expected: equals [3, 1] unordered "
"Actual: [1, 2] "
"Which: has no match for <3> at index 0");
"Which: has no match for <3>");
shouldFail(
d,
unorderedEquals([3, 4]),
"Expected: equals [3, 4] unordered "
"Actual: [1, 2] "
"Which: has no match for any of (<3>, <4>)");
});

test('unorderedMatchess', () {
test('unorderedMatches', () {
var d = [1, 2];
shouldPass(d, unorderedMatches([2, 1]));
shouldPass(d, unorderedMatches([greaterThan(1), greaterThan(0)]));
shouldPass(d, unorderedMatches([greaterThan(0), greaterThan(1)]));
shouldPass([2, 1], unorderedMatches([greaterThan(1), greaterThan(0)]));
shouldPass([2, 1], unorderedMatches([greaterThan(0), greaterThan(1)]));
shouldFail(
d,
unorderedMatches([greaterThan(0)]),
Expand All @@ -167,14 +176,14 @@ void main() {
unorderedMatches([3, 1]),
"Expected: matches [<3>, <1>] unordered "
"Actual: [1, 2] "
"Which: has no match for <3> at index 0");
"Which: has no match for <3>");
shouldFail(
d,
unorderedMatches([greaterThan(3), greaterThan(0)]),
"Expected: matches [a value greater than <3>, a value greater than "
"<0>] unordered "
"Actual: [1, 2] "
"Which: has no match for a value greater than <3> at index 0");
"Which: has no match for a value greater than <3>");
});

test('containsAllInOrder', () {
Expand Down

0 comments on commit 1746fef

Please sign in to comment.