From e5aa6f254c9e89a138a0686f6ab3afe97a116f3e Mon Sep 17 00:00:00 2001 From: "rnystrom@google.com" <rnystrom@google.com> Date: Fri, 28 Feb 2014 17:30:56 +0000 Subject: [PATCH] Support nested matchers in deep equality matching. R=kevmoo@google.com, sigmund@google.com Review URL: https://codereview.chromium.org//184103002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/unittest@33164 260f80e4-7a28-3924-810f-c04153c831b5 --- CHANGELOG.md | 1 + lib/src/core_matchers.dart | 179 ++++++++++++++++++------------------ lib/src/pretty_print.dart | 8 ++ test/matchers_test.dart | 20 ++++ test/pretty_print_test.dart | 15 +++ 5 files changed, 133 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be796e4b..316b851b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,5 +19,6 @@ and `protectAsync2` * `runTests`, `tearDown`, `setUp`, `test`, `group`, `solo_test`, and `solo_group` now throw a `StateError` if called while tests are running. * `rerunTests` has been removed. +* `equals` now allows a nested matcher as an expected list element or map value when doing deep matching. ##0.9.3 - 2014-01-13 diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart index 86f51f36..e55738d4 100644 --- a/lib/src/core_matchers.dart +++ b/lib/src/core_matchers.dart @@ -67,7 +67,7 @@ class _IsFalse extends Matcher { /** * Returns a matches that matches if the value is the same instance - * as [object] (`===`). + * as [expected], using [identical]. */ Matcher same(expected) => new _IsSameAs(expected); @@ -81,9 +81,15 @@ class _IsSameAs extends Matcher { } /** - * Returns a matcher that does a deep recursive match. This only works - * with scalars, Maps and Iterables. To handle cyclic structures a - * recursion depth [limit] can be provided. The default limit is 100. + * Returns a matcher that matches if the value is structurally equal to + * [expected]. + * + * If [expected] is a [Matcher], then it matches using that. Otherwise it tests + * for equality using `==` on the expected value. + * + * For [Iterable]s and [Map]s, this will recursively match the elements. To + * handle cyclic structures a recursion depth [limit] can be provided. The + * default limit is 100. */ Matcher equals(expected, [limit=100]) => expected is String @@ -99,106 +105,99 @@ class _DeepMatcher extends Matcher { // Returns a pair (reason, location) List _compareIterables(expected, actual, matcher, depth, location) { - if (actual is! Iterable) { - return ['is not Iterable', location]; - } + if (actual is! Iterable) return ['is not Iterable', location]; + var expectedIterator = expected.iterator; var actualIterator = actual.iterator; - var index = 0; - while (true) { + 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 (expectedIterator.moveNext()) { - if (actualIterator.moveNext()) { - var rp = matcher(expectedIterator.current, - actualIterator.current, newLocation, - depth); - if (rp != null) return rp; - ++index; - } else { - return ['shorter than expected', newLocation]; - } - } else if (actualIterator.moveNext()) { - return ['longer than expected', newLocation]; - } else { - return null; - } + 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; } - return null; } List _recursiveMatch(expected, actual, String location, int depth) { - String reason = null; + // If the expected value is a matcher, try to match it. + if (expected is Matcher) { + var matchState = {}; + if (expected.matches(actual, matchState)) return null; + + var description = new StringDescription(); + expected.describe(description); + return ['does not match $description', location]; + } else { + // Otherwise, test for equality. + try { + if (expected == actual) return null; + } catch (e, s) { + // TODO(gram): Add a test for this case. + return ['== threw "$e"', location]; + } + } + + if (depth > _limit) return ['recursion depth limit exceeded', location]; + // If _limit is 1 we can only recurse one level into object. bool canRecurse = depth == 0 || _limit > 1; - bool equal; - try { - equal = (expected == actual); - } catch (e, s) { - // TODO(gram): Add a test for this case. - reason = '== threw "$e"'; - return [reason, location]; + + if (expected is Iterable && canRecurse) { + return _compareIterables(expected, actual, _recursiveMatch, depth + 1, + location); } - if (equal) { - // Do nothing. - } else if (depth > _limit) { - reason = 'recursion depth limit exceeded'; - } else { - if (expected is Iterable && canRecurse) { - List result = _compareIterables(expected, actual, - _recursiveMatch, depth + 1, location); - if (result != null) { - reason = result[0]; - location = result[1]; - } - } else if (expected is Map && canRecurse) { - if (actual is! Map) { - reason = 'expected a map'; - } else { - var err = (expected.length == actual.length) ? '' : - 'has different length and '; - for (var key in expected.keys) { - if (!actual.containsKey(key)) { - reason = '${err}is missing map key \'$key\''; - break; - } - } - if (reason == null) { - for (var key in actual.keys) { - if (!expected.containsKey(key)) { - reason = '${err}has extra map key \'$key\''; - break; - } - } - if (reason == null) { - for (var key in expected.keys) { - var rp = _recursiveMatch(expected[key], actual[key], - "${location}['${key}']", depth + 1); - if (rp != null) { - reason = rp[0]; - location = rp[1]; - break; - } - } - } - } + + if (expected is Map && canRecurse) { + if (actual is! Map) return ['expected a map', location]; + + var err = (expected.length == actual.length) ? '' : + 'has different length and '; + for (var key in expected.keys) { + if (!actual.containsKey(key)) { + return ["${err}is missing map key '$key'", location]; } - } else { - var description = new StringDescription(); - // If we have recursed, show the expected value too; if not, - // expect() will show it for us. - if (depth > 0) { - description.add('was '). - addDescriptionOf(actual). - add(' instead of '). - addDescriptionOf(expected); - reason = description.toString(); - } else { - reason = ''; // We're not adding any value to the actual value. + } + + for (var key in actual.keys) { + if (!expected.containsKey(key)) { + return ["${err}has extra map key '$key'", location]; } } + + for (var key in expected.keys) { + var rp = _recursiveMatch(expected[key], actual[key], + "${location}['${key}']", depth + 1); + if (rp != null) return rp; + } + + return null; } - if (reason == null) return null; - return [reason, location]; + + var description = new StringDescription(); + + // If we have recursed, show the expected value too; if not, expect() will + // show it for us. + if (depth > 0) { + description.add('was '). + addDescriptionOf(actual). + add(' instead of '). + addDescriptionOf(expected); + return [description.toString(), location]; + } + + // We're not adding any value to the actual value. + return ["", location]; } String _match(expected, actual, Map matchState) { diff --git a/lib/src/pretty_print.dart b/lib/src/pretty_print.dart index 14a0bb1f..a748ec35 100644 --- a/lib/src/pretty_print.dart +++ b/lib/src/pretty_print.dart @@ -4,6 +4,7 @@ library unittest.pretty_print; +import '../matcher.dart'; import 'utils.dart'; /** @@ -19,6 +20,13 @@ import 'utils.dart'; */ String prettyPrint(object, {int maxLineLength, int maxItems}) { String _prettyPrint(object, int indent, Set seen, bool top) { + // If the object is a matcher, use its description. + if (object is Matcher) { + var description = new StringDescription(); + object.describe(description); + return "<$description>"; + } + // Avoid looping infinitely on recursively-nested data structures. if (seen.contains(object)) return "(recursive)"; seen = seen.union(new Set.from([object])); diff --git a/test/matchers_test.dart b/test/matchers_test.dart index 9f8e1219..dc2b37ae 100644 --- a/test/matchers_test.dart +++ b/test/matchers_test.dart @@ -401,6 +401,15 @@ void main() { "Actual: [1, 2]"); }); + test('equals with matcher element', () { + var d = ['foo', 'bar']; + shouldPass(d, equals(['foo', startsWith('ba')])); + shouldFail(d, equals(['foo', endsWith('ba')]), + "Expected: ['foo', <a string ending with 'ba'>] " + "Actual: ['foo', 'bar'] " + "Which: does not match a string ending with 'ba' at location [1]"); + }); + test('isIn', () { var d = [1, 2]; shouldPass(1, isIn(d)); @@ -593,6 +602,17 @@ void main() { "Which: has different length and is missing map key 'foo'"); }); + test('equals with matcher value', () { + var a = new Map(); + a['foo'] = 'bar'; + shouldPass(a, equals({'foo': startsWith('ba')})); + shouldFail(a, equals({'foo': endsWith('ba')}), + "Expected: {'foo': <a string ending with 'ba'>} " + "Actual: {'foo': 'bar'} " + "Which: does not match a string ending with 'ba' " + "at location ['foo']"); + }); + test('contains', () { var a = new Map(); a['foo'] = 'bar'; diff --git a/test/pretty_print_test.dart b/test/pretty_print_test.dart index a0608ac0..826c3451 100644 --- a/test/pretty_print_test.dart +++ b/test/pretty_print_test.dart @@ -48,6 +48,11 @@ void main() { "]")); }); + test('containing a matcher', () { + expect(prettyPrint(['foo', endsWith('qux')]), + equals("['foo', <a string ending with 'qux'>]")); + }); + test("that's under maxLineLength", () { expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 30), equals("[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]")); @@ -138,6 +143,16 @@ void main() { "}")); }); + test('containing a matcher key', () { + expect(prettyPrint({endsWith('bar'): 'qux'}), + equals("{<a string ending with 'bar'>: 'qux'}")); + }); + + test('containing a matcher value', () { + expect(prettyPrint({'foo': endsWith('qux')}), + equals("{'foo': <a string ending with 'qux'>}")); + }); + test("that's under maxLineLength", () { expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 32), equals("{'0': 1, '2': 3, '4': 5, '6': 7}")); -- GitLab