Chromium Code Reviews| Index: lib/unittest/core_matchers.dart |
| =================================================================== |
| --- lib/unittest/core_matchers.dart (revision 0) |
| +++ lib/unittest/core_matchers.dart (revision 0) |
| @@ -0,0 +1,371 @@ |
| +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| + |
| +/** |
| + * Returns a matcher that matches empty strings, maps or collections. |
| + */ |
| +final Matcher isEmpty = const _Empty(); |
| + |
| +class _Empty extends BaseMatcher { |
| + const _Empty(); |
| + bool matches(item) { |
| + if (item is Map || item is Collection) { |
| + return item.isEmpty(); |
| + } else if (item is String) { |
| + return item.length == 0; |
| + } else { |
| + return false; |
| + } |
| + } |
| + Description describe(Description description) => |
| + description.add('empty'); |
| +} |
| + |
| +/** A matcher that matches any null value. */ |
| +final Matcher isNull = const _IsNull(); |
| + |
| +/** A matcher that matches any non-null value. */ |
| +final Matcher isNotNull = const _IsNotNull(); |
| + |
| +class _IsNull extends BaseMatcher { |
| + const _IsNull(); |
| + bool matches(item) => item == null; |
| + Description describe(Description description) => |
| + description.add('null'); |
| +} |
| + |
| +class _IsNotNull extends BaseMatcher { |
| + const _IsNotNull(); |
| + bool matches(item) => item != null; |
| + Description describe(Description description) => |
| + description.add('not null'); |
| +} |
| + |
| +/** A matcher that matches the Boolean value true. */ |
| +final Matcher isTrue = const _IsTrue(); |
| + |
| +/** A matcher that matches anything except the Boolean value true. */ |
| +final Matcher isFalse = const _IsFalse(); |
| + |
| +class _IsTrue extends BaseMatcher { |
| + const _IsTrue(); |
| + bool matches(item) => item == true; |
| + Description describe(Description description) => |
| + description.add('true'); |
| +} |
| + |
| +class _IsFalse extends BaseMatcher { |
| + const _IsFalse(); |
| + bool matches(item) => item != true; |
| + Description describe(Description description) => |
| + description.add('false'); |
| +} |
| + |
| +/** |
| + * Returns a matches that matches if the value is the same instance |
| + * as [object] (`===`). |
| + */ |
| +Matcher same(expected) => new _IsSameAs(expected); |
| + |
| +class _IsSameAs extends BaseMatcher { |
| + final _expected; |
| + const _IsSameAs(this._expected); |
| + bool matches(item) => item === _expected; |
| + // If all types were hashable we could show a hash here. |
| + Description describe(Description description) => |
| + description.add('same instance as ').addDescriptionOf(_expected); |
| +} |
| + |
| +/** Returns a matcher that matches if two objects are equal (==). */ |
| +Matcher equals(expected) => new _IsEqual(expected); |
| + |
| +class _IsEqual extends BaseMatcher { |
| + final _expected; |
| + const _IsEqual(this._expected); |
| + bool matches(item) => item == _expected; |
| + Description describe(Description description) => |
| + description.addDescriptionOf(_expected); |
| +} |
| + |
| +/** A matcher that matches any value. */ |
| +final Matcher anything = const _IsAnything(); |
| + |
| +class _IsAnything extends BaseMatcher { |
| + const _IsAnything(); |
| + bool matches(item) => true; |
| + Description describe(Description description) => |
| + description.add('anything'); |
| +} |
| + |
| +/** |
| + * A matcher that matches functions that throw exceptions when called. |
| + * The value passed to expect() should be a reference to the function. |
| + * Note that the function cannot take arguments; to handle this |
| + * a wrapper will have to be created. |
| + * The function will be called once upon success, or twice upon failure |
| + * (the second time to get the failure description). |
| + */ |
| +final Matcher throws = const _Throws(); |
| + |
| +/** |
| + * Returns a matcher that matches a function call against an exception, |
| + * which is in turn constrained by a [matcher]. |
| + * The value passed to expect() should be a reference to the function. |
| + * Note that the function cannot take arguments; to handle this |
| + * a wrapper will have to be created. |
| + * The function will be called once upon success, or twice upon failure |
| + * (the second time to get the failure description). |
| + */ |
| +Matcher throwsA(Matcher matcher) => new _Throws(matcher); |
| + |
| +/** |
| + * A matcher that matches a function call against no exception. |
| + * The function will be called once. Any exceptions will be silently swallowed. |
| + * The value passed to expect() should be a reference to the function. |
| + * Note that the function cannot take arguments; to handle this |
| + * a wrapper will have to be created. |
| + */ |
| +final Matcher returnsNormally = const _ReturnsNormally(); |
| + |
| +class _Throws extends BaseMatcher { |
| + final Matcher _matcher; |
| + |
| + const _Throws([Matcher this._matcher = null]); |
| + |
| + bool matches(item) { |
| + try { |
| + item(); |
| + return false; |
| + } catch (final e) { |
| + return _matcher == null || _matcher.matches(e); |
| + } |
| + } |
| + |
| + Description describe(Description description) { |
| + if (_matcher == null) { |
| + return description.add("throws an exception"); |
| + } else { |
| + return description.add('throws an exception which matches '). |
| + addDescriptionOf(_matcher); |
| + } |
| + } |
| + |
| + Description describeMismatch(item, Description mismatchDescription) { |
| + try { |
| + item(); |
| + return mismatchDescription.add(' no exception'); |
| + } catch (final e) { |
| + return mismatchDescription.add(' exception does not match '). |
| + addDescriptionOf(_matcher); |
| + } |
| + } |
| +} |
| + |
| +class _ReturnsNormally extends BaseMatcher { |
| + |
| + const _ReturnsNormally(); |
| + |
| + bool matches(f) { |
| + try { |
| + f(); |
| + return true; |
| + } catch (final e) { |
| + return false; |
| + } |
| + } |
| + |
| + Description describe(Description description) => |
| + description.add("return normally"); |
| + |
| + Description describeMismatch(item, Description mismatchDescription) { |
| + return mismatchDescription.add(' threw exception'); |
| + } |
| +} |
| + |
| +/** |
| + * Returns a matcher that matches if an object is an instance |
| + * of [type] (or a subtype). |
| + * |
| + * As types are not first class objects in Dart we can only |
| + * approximate this test by using a generic wrapper class. |
| + * |
| + * For example, to test whether 'bar' is an instance of type |
| + * 'Foo', we would write: |
| + * |
| + * expect(bar, new isInstanceOf<Foo>()); |
| + * |
| + * To get better error message, supply a name when creating the |
| + * Type wrapper; e.g.: |
| + * |
| + * expect(bar, new isInstanceOf<Foo>('Foo')); |
| + */ |
| +class isInstanceOf<T> extends BaseMatcher { |
| + final String _name; |
| + const isInstanceOf([this._name = 'specified type']); |
| + bool matches(obj) => obj is T; |
| + // The description here is lame :-( |
| + Description describe(Description description) => |
| + description.add('an instance of ${_name}'); |
| +} |
| + |
| +/** |
| + * Returns a matcher that matches if an object has a length property |
| + * that matches [matcher]. |
| + */ |
| +Matcher hasLength(Matcher matcher) => |
| + new _HasLength(wrapMatcher(matcher)); |
| + |
| +class _HasLength extends BaseMatcher { |
| + final Matcher _matcher; |
| + const _HasLength([Matcher this._matcher = null]); |
| + |
| + bool matches(item) { |
| + return _matcher.matches(item.length); |
| + } |
| + |
| + Description describe(Description description) => |
| + description.add('an object with length of '). |
| + addDescriptionOf(_matcher); |
| + |
| + Description describeMismatch(item, Description mismatchDescription) { |
| + super.describeMismatch(item, mismatchDescription); |
| + try { |
| + if (item.length * item.length >= 0) { // hack to will throw if no length |
|
Bob Nystrom
2012/06/04 20:04:09
This comment no grammar. Why is this hack needed?
gram
2012/06/05 16:25:46
Basically we want to generate a different descript
Bob Nystrom
2012/06/05 16:50:37
OK. Can you move that comment to the preceding lin
gram
2012/06/05 16:59:53
Was done already :-)
On 2012/06/05 16:50:37, Bob N
|
| + return mismatchDescription.add(' with length of '). |
| + addDescriptionOf(item.length); |
| + } |
| + } catch (var e) { |
| + return mismatchDescription.add(' has no length property'); |
| + } |
| + } |
| +} |
| + |
| +/** |
| + * 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, [this._limit = 1000]); |
| + |
| + 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 |
| + * means the collection has a matching element. In the case of collections, |
| + * [expected] can itself be a matcher. |
| + */ |
| +Matcher contains(expected) => new _Contains(expected); |
| + |
| +class _Contains extends BaseMatcher { |
| + |
| + final _expected; |
| + |
| + const _Contains(this._expected); |
| + |
| + bool matches(item) { |
| + if (item is String) { |
| + return item.indexOf(_expected) >= 0; |
| + } else if (item is Collection) { |
| + if (_expected is Matcher) { |
| + return item.some((e) => _expected.matches(e)); |
| + } else { |
| + return item.some((e) => e == _expected); |
| + } |
| + } else if (item is Map) { |
| + return item.containsKey(_expected); |
| + } |
| + return false; |
| + } |
| + |
| + Description describe(Description description) => |
| + description.add('contains ').addDescriptionOf(_expected); |
| +} |