Chromium Code Reviews| Index: lib/unittest/core_matchers.dart | 
| =================================================================== | 
| --- lib/unittest/core_matchers.dart (revision 8828) | 
| +++ lib/unittest/core_matchers.dart (working copy) | 
| @@ -78,15 +78,114 @@ | 
| description.add('same instance as ').addDescriptionOf(_expected); | 
| } | 
| -/** Returns a matcher that matches if two objects are equal (==). */ | 
| -Matcher equals(expected) => new _IsEqual(expected); | 
| +/** | 
| + * 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 pr]ovided. The default limit is 100. | 
| 
 
Bob Nystrom
2012/06/19 23:38:33
That ] is in a weird place.
 
gram
2012/06/20 17:44:14
Telekinesis
 
 | 
| + */ | 
| +Matcher equals(expected, [limit=100]) => | 
| + new _DeepMatcher(expected, limit); | 
| -class _IsEqual extends BaseMatcher { | 
| +class _DeepMatcher extends BaseMatcher { | 
| final _expected; | 
| - const _IsEqual(this._expected); | 
| - bool matches(item) => item == _expected; | 
| + final int _limit; | 
| + var count; | 
| + | 
| + _DeepMatcher(this._expected, [limit = 1000]) : this._limit = limit; | 
| + | 
| + // A utility function for comparing iterators | 
| 
 
Bob Nystrom
2012/06/19 23:38:33
This comment doesn't add a lot of value.
 
gram
2012/06/20 17:44:14
Removed
 
 | 
| + | 
| + String _compareIterables(expected, actual, matcher, depth) { | 
| + if (actual is !Iterable) { | 
| + return 'is not Iterable'; | 
| + } | 
| + var expectedIterator = expected.iterator(); | 
| + var actualIterator = actual.iterator(); | 
| + var position = 0; | 
| + String reason = null; | 
| + while (reason == null) { | 
| + if (expectedIterator.hasNext()) { | 
| + if (actualIterator.hasNext()) { | 
| + reason = matcher(expectedIterator.next(), | 
| + actualIterator.next(), | 
| + 'mismatch at position ${position}', | 
| + depth); | 
| + ++position; | 
| + } else { | 
| + reason = 'shorter than expected'; | 
| + } | 
| + } else if (actualIterator.hasNext()) { | 
| + reason = 'longer than expected'; | 
| + } else { | 
| + return null; | 
| + } | 
| + } | 
| + return reason; | 
| + } | 
| + | 
| + Description _recursiveMatch(expected, actual, String location, int depth) { | 
| + Description reason = null; | 
| + // If _limit is 1 we can only recurse one level into object | 
| 
 
Bob Nystrom
2012/06/19 23:38:33
Add a "."
 
gram
2012/06/20 17:44:14
Done.
 
 | 
| + bool canRecurse = depth == 0 || _limit > 1; | 
| + if (expected != actual) { | 
| 
 
Bob Nystrom
2012/06/19 23:38:33
This is some painfully nested flow control. Can yo
 
gram
2012/06/20 17:44:14
Done.
 
 | 
| + if (depth > _limit) { | 
| + reason = new StringDescription('recursion depth limit exceeded'); | 
| 
 
Bob Nystrom
2012/06/19 23:38:33
Extra space before "new".
 
gram
2012/06/20 17:44:14
Done.
 
 | 
| + } else { | 
| + if (expected is Iterable && canRecurse) { | 
| + String r = _compareIterables(expected, actual, | 
| + _recursiveMatch, depth+1); | 
| + if (r != null) reason = new StringDescription(r); | 
| + } else if (expected is Map && canRecurse) { | 
| + if (actual is !Map) { | 
| + reason = new StringDescription('expected a map'); | 
| + } else if (expected.length != actual.length) { | 
| + reason = new StringDescription('different map lengths'); | 
| + } else { | 
| + for (var key in expected.getKeys()) { | 
| + if (!actual.containsKey(key)) { | 
| + reason = new StringDescription('missing map key '); | 
| + reason.addDescriptionOf(key); | 
| + break; | 
| + } | 
| + reason = _recursiveMatch(expected[key], actual[key], | 
| + 'with key <${key}> ${location}', depth+1); | 
| + if (reason != null) { | 
| + break; | 
| + } | 
| + } | 
| + } | 
| + } else { | 
| + // If we have recursed, show the expected value too; if not, | 
| + // expect() will show it for us. | 
| + reason = new StringDescription(); | 
| + if (depth > 1) { | 
| + reason.add('expected ').addDescriptionOf(expected).add(' but was '). | 
| + addDescriptionOf(actual); | 
| + } else { | 
| + reason.add('was ').addDescriptionOf(actual); | 
| + } | 
| + } | 
| + } | 
| + } | 
| + if (reason == null) { | 
| + return null; | 
| + } else { | 
| + return reason.add(' ').add(location); | 
| + } | 
| + } | 
| + | 
| + String _match(expected, actual) { | 
| + Description reason = _recursiveMatch(expected, actual, '', 0); | 
| + return reason == null ? null : reason.toString(); | 
| + } | 
| + | 
| + bool matches(item) => _match(_expected, item) == null; | 
| + | 
| Description describe(Description description) => | 
| - description.addDescriptionOf(_expected); | 
| + description.addDescriptionOf(_expected); | 
| + | 
| + Description describeMismatch(item, Description mismatchDescription) => | 
| + mismatchDescription.add(_match(_expected, item)); | 
| } | 
| /** A matcher that matches any value. */ | 
| @@ -240,42 +339,142 @@ | 
| } | 
| } | 
| +/* | 
| + * Matchers for different exception types. Ideally we should just be able to | 
| + * use something like: | 
| + * | 
| + * final Matcher throwsException = | 
| + * const _Throws(const isInstanceOf<Exception>()); | 
| + * | 
| + * Unfortunately instanceOf is not working with dart2js. | 
| + * | 
| + * Alternatively, if static functions could be used in const expressions, | 
| + * we could use: | 
| + * | 
| + * bool _isException(x) => x is Exception; | 
| + * final Matcher isException = const _Predicate(_isException, "Exception"); | 
| + * final Matcher throwsException = const _Throws(isException); | 
| + * | 
| + * But currently using static functions in const expressions is not supported. | 
| + * For now the only solution for all platforms seems to be separate classes | 
| + * for each exception type. | 
| + */ | 
| + | 
| +/* abstract */ class _ExceptionMatcher extends BaseMatcher { | 
| + final String _name; | 
| + const _ExceptionMatcher(this._name); | 
| + Description describe(Description description) => | 
| + description.add(_name); | 
| +} | 
| + | 
| +/** A matcher for BadNumberFormatExceptions. */ | 
| +final isBadNumberFormatException = const _BadNumberFormatException(); | 
| + | 
| /** A matcher for functions that throw BadNumberFormatException */ | 
| final Matcher throwsBadNumberFormatException = | 
| - const _Throws(const isInstanceOf<BadNumberFormatException>()); | 
| + const _Throws(isBadNumberFormatException); | 
| -/** A matcher for functions that throw an Exception */ | 
| -final Matcher throwsException = | 
| - const _Throws(const isInstanceOf<Exception>()); | 
| +class _BadNumberFormatException extends _ExceptionMatcher { | 
| + const _BadNumberFormatException() : super("BadNumberFormatException"); | 
| + bool matches(item) => item is BadNumberFormatException; | 
| +} | 
| -/** A matcher for functions that throw an IllegalArgumentException */ | 
| +/** A matcher for Exceptions. */ | 
| +final isException = const _Exception(); | 
| + | 
| +/** A matcher for functions that throw Exception */ | 
| +final Matcher throwsException = const _Throws(isException); | 
| + | 
| +class _Exception extends _ExceptionMatcher { | 
| + const _Exception() : super("Exception"); | 
| + bool matches(item) => item is Exception; | 
| +} | 
| + | 
| +/** A matcher for IllegalArgumentExceptions. */ | 
| +final isIllegalArgumentException = const _IllegalArgumentException(); | 
| + | 
| +/** A matcher for functions that throw IllegalArgumentException */ | 
| final Matcher throwsIllegalArgumentException = | 
| - const _Throws(const isInstanceOf<IllegalArgumentException>()); | 
| + const _Throws(isIllegalArgumentException); | 
| -/** A matcher for functions that throw an IllegalJSRegExpException */ | 
| +class _IllegalArgumentException extends _ExceptionMatcher { | 
| + const _IllegalArgumentException() : super("IllegalArgumentException"); | 
| + bool matches(item) => item is IllegalArgumentException; | 
| +} | 
| + | 
| +/** A matcher for IllegalJSRegExpExceptions. */ | 
| +final isIllegalJSRegExpException = const _IllegalJSRegExpException(); | 
| + | 
| +/** A matcher for functions that throw IllegalJSRegExpException */ | 
| final Matcher throwsIllegalJSRegExpException = | 
| - const _Throws(const isInstanceOf<IllegalJSRegExpException>()); | 
| + const _Throws(isIllegalJSRegExpException); | 
| -/** A matcher for functions that throw an IndexOutOfRangeException */ | 
| +class _IllegalJSRegExpException extends _ExceptionMatcher { | 
| + const _IllegalJSRegExpException() : super("IllegalJSRegExpException"); | 
| + bool matches(item) => item is IllegalJSRegExpException; | 
| +} | 
| + | 
| +/** A matcher for IndexOutOfRangeExceptions. */ | 
| +final isIndexOutOfRangeException = const _IndexOutOfRangeException(); | 
| + | 
| +/** A matcher for functions that throw IndexOutOfRangeException */ | 
| final Matcher throwsIndexOutOfRangeException = | 
| - const _Throws(const isInstanceOf<IndexOutOfRangeException>()); | 
| + const _Throws(isIndexOutOfRangeException); | 
| -/** A matcher for functions that throw a NoSuchMethodException */ | 
| +class _IndexOutOfRangeException extends _ExceptionMatcher { | 
| + const _IndexOutOfRangeException() : super("IndexOutOfRangeException"); | 
| + bool matches(item) => item is IndexOutOfRangeException; | 
| +} | 
| + | 
| +/** A matcher for NoSuchMethodExceptions. */ | 
| +final isNoSuchMethodException = const _NoSuchMethodException(); | 
| + | 
| +/** A matcher for functions that throw NoSuchMethodException */ | 
| final Matcher throwsNoSuchMethodException = | 
| - const _Throws(const isInstanceOf<NoSuchMethodException>()); | 
| + const _Throws(isNoSuchMethodException); | 
| -/** A matcher for functions that throw a NotImplementedException */ | 
| +class _NoSuchMethodException extends _ExceptionMatcher { | 
| + const _NoSuchMethodException() : super("NoSuchMethodException"); | 
| + bool matches(item) => item is NoSuchMethodException; | 
| +} | 
| + | 
| +/** A matcher for NotImplementedExceptions. */ | 
| +final isNotImplementedException = const _NotImplementedException(); | 
| + | 
| +/** A matcher for functions that throw Exception */ | 
| final Matcher throwsNotImplementedException = | 
| - const _Throws(const isInstanceOf<NotImplementedException>()); | 
| + const _Throws(isNotImplementedException); | 
| -/** A matcher for functions that throw a NullPointerException */ | 
| +class _NotImplementedException extends _ExceptionMatcher { | 
| + const _NotImplementedException() : super("NotImplementedException"); | 
| + bool matches(item) => item is NotImplementedException; | 
| +} | 
| + | 
| +/** A matcher for NullPointerExceptions. */ | 
| +final isNullPointerException = const _NullPointerException(); | 
| + | 
| +/** A matcher for functions that throw NotNullPointerException */ | 
| final Matcher throwsNullPointerException = | 
| - const _Throws(const isInstanceOf<NullPointerException>()); | 
| + const _Throws(isNullPointerException); | 
| -/** A matcher for functions that throw an UnsupportedOperationException */ | 
| +class _NullPointerException extends _ExceptionMatcher { | 
| + const _NullPointerException() : super("NullPointerException"); | 
| + bool matches(item) => item is NullPointerException; | 
| +} | 
| + | 
| +/** A matcher for UnsupportedOperationExceptions. */ | 
| +final isUnsupportedOperationException = const _UnsupportedOperationException(); | 
| + | 
| +/** A matcher for functions that throw UnsupportedOperationException */ | 
| final Matcher throwsUnsupportedOperationException = | 
| - const _Throws(const isInstanceOf<UnsupportedOperationException>()); | 
| + const _Throws(isUnsupportedOperationException); | 
| +class _UnsupportedOperationException extends _ExceptionMatcher { | 
| + const _UnsupportedOperationException() : | 
| + super("UnsupportedOperationException"); | 
| + bool matches(item) => item is UnsupportedOperationException; | 
| +} | 
| + | 
| /** | 
| * Returns a matcher that matches if an object has a length property | 
| * that matches [matcher]. | 
| @@ -312,100 +511,6 @@ | 
| } | 
| /** | 
| - * Returns a matcher that does a deep recursive match. This only works | 
| - * with scalars, Maps and Iterables. To handle cyclic structures an | 
| - * item [limit] can be provided; if after [limit] items have been | 
| - * compared and the process is not complete this will be treated as | 
| - * a mismatch. The default limit is 1000. | 
| - */ | 
| -Matcher recursivelyMatches(expected, [limit=1000]) => | 
| - new _DeepMatcher(expected, limit); | 
| - | 
| -// A utility function for comparing iterators | 
| - | 
| -String _compareIterables(expected, actual, matcher) { | 
| - if (actual is !Iterable) { | 
| - return 'is not Iterable'; | 
| - } | 
| - var expectedIterator = expected.iterator(); | 
| - var actualIterator = actual.iterator(); | 
| - var position = 0; | 
| - String reason = null; | 
| - while (reason == null) { | 
| - if (expectedIterator.hasNext()) { | 
| - if (actualIterator.hasNext()) { | 
| - reason = matcher(expectedIterator.next(), | 
| - actualIterator.next(), | 
| - 'mismatch at position ${position}'); | 
| - ++position; | 
| - } else { | 
| - reason = 'shorter than expected'; | 
| - } | 
| - } else if (actualIterator.hasNext()) { | 
| - reason = 'longer than expected'; | 
| - } else { | 
| - return null; | 
| - } | 
| - } | 
| - return reason; | 
| -} | 
| - | 
| -class _DeepMatcher extends BaseMatcher { | 
| - final _expected; | 
| - final int _limit; | 
| - var count; | 
| - | 
| - _DeepMatcher(this._expected, [limit = 1000]) : this._limit = limit; | 
| - | 
| - String _recursiveMatch(expected, actual, String location) { | 
| - String reason = null; | 
| - if (++count >= _limit) { | 
| - reason = 'item comparison limit exceeded'; | 
| - } else if (expected is Iterable) { | 
| - reason = _compareIterables(expected, actual, _recursiveMatch); | 
| - } else if (expected is Map) { | 
| - if (actual is !Map) { | 
| - reason = 'expected a map'; | 
| - } else if (expected.length != actual.length) { | 
| - reason = 'different map lengths'; | 
| - } else { | 
| - for (var key in expected.getKeys()) { | 
| - if (!actual.containsKey(key)) { | 
| - reason = 'missing map key ${key}'; | 
| - break; | 
| - } | 
| - reason = _recursiveMatch(expected[key], actual[key], | 
| - 'with key ${key} ${location}'); | 
| - if (reason != null) { | 
| - break; | 
| - } | 
| - } | 
| - } | 
| - } else if (expected != actual) { | 
| - reason = 'expected ${expected} but got ${actual}'; | 
| - } | 
| - if (reason == null) { | 
| - return null; | 
| - } else { | 
| - return '${reason} ${location}'; | 
| - } | 
| - } | 
| - | 
| - String _match(expected, actual) { | 
| - count = 0; | 
| - return _recursiveMatch(expected, actual, ''); | 
| - } | 
| - | 
| - bool matches(item) => _match(_expected, item) == null; | 
| - | 
| - Description describe(Description description) => | 
| - description.add('recursively matches ').addDescriptionOf(_expected); | 
| - | 
| - Description describeMismatch(item, Description mismatchDescription) => | 
| - mismatchDescription.add(_match(_expected, item)); | 
| -} | 
| - | 
| -/** | 
| * Returns a matcher that matches if the match argument contains | 
| * the expected value. For [String]s this means substring matching; | 
| * for [Map]s is means the map has the key, and for [Collection]s it | 
| @@ -439,3 +544,49 @@ | 
| description.add('contains ').addDescriptionOf(_expected); | 
| } | 
| +/** | 
| + * 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); | 
| + | 
| 
 
Sean Eagan
2012/06/20 18:00:22
Thanks for adding this!
Just a small gripe, I thi
 
 | 
| +class _In extends BaseMatcher { | 
| + | 
| + final _expected; | 
| + | 
| + const _In(this._expected); | 
| + | 
| + bool matches(item) { | 
| + if (_expected is String) { | 
| + return _expected.indexOf(item) >= 0; | 
| + } else if (_expected is Collection) { | 
| + return _expected.some((e) => e == item); | 
| + } else if (_expected is Map) { | 
| + return _expected.containsKey(item); | 
| + } | 
| + return false; | 
| + } | 
| + | 
| + Description describe(Description description) => | 
| + description.add('is in ').addDescriptionOf(_expected); | 
| +} | 
| + | 
| +/** | 
| + * Returns a matcher that uses an arbitrary function that returns | 
| + * true or false for the actual value. | 
| + */ | 
| +Matcher predicate(f, [description = 'satisfies function']) => | 
| + new _Predicate(f, description); | 
| + | 
| +class _Predicate extends BaseMatcher { | 
| + | 
| + final _matcher; | 
| + final String _description; | 
| + | 
| + const _Predicate(this._matcher, this._description); | 
| + | 
| + bool matches(item) => _matcher(item); | 
| + | 
| + Description describe(Description description) => | 
| + description.add(_description); | 
| +} |