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); |
| +} |