pkg/matcher
Copied from unittest. Unittest will update to use this package once it has been published.
R=blois@google.com
Review URL: https://codereview.chromium.org//208823005
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/matcher@34282 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/matcher.dart b/lib/matcher.dart
new file mode 100644
index 0000000..85694ef
--- /dev/null
+++ b/lib/matcher.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2014, 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.
+
+/**
+ * Support for specifying test expectations, such as for unit tests.
+ *
+ * The matcher library provides a third-generation assertion mechanism, drawing
+ * inspiration from [Hamcrest](http://code.google.com/p/hamcrest/).
+ *
+ * For more information, see
+ * [Unit Testing with Dart]
+ * (http://www.dartlang.org/articles/dart-unit-tests/).
+ */
+library matcher;
+
+import 'dart:async';
+
+import 'src/description.dart';
+import 'src/interfaces.dart';
+
+export 'src/description.dart';
+export 'src/interfaces.dart';
+
+part 'src/core_matchers.dart';
+part 'src/expect.dart';
+part 'src/future_matchers.dart';
+part 'src/iterable_matchers.dart';
+part 'src/map_matchers.dart';
+part 'src/numeric_matchers.dart';
+part 'src/operator_matchers.dart';
+part 'src/string_matchers.dart';
diff --git a/lib/mirror_matchers.dart b/lib/mirror_matchers.dart
new file mode 100644
index 0000000..c836bc9
--- /dev/null
+++ b/lib/mirror_matchers.dart
@@ -0,0 +1,80 @@
+// Copyright (c) 2014, 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.
+
+/**
+ * The mirror matchers library provides some additional matchers that
+ * make use of `dart:mirrors`.
+ */
+library matcher.mirror_matchers;
+
+import 'dart:mirrors';
+
+import 'matcher.dart';
+
+/**
+ * Returns a matcher that checks if a class instance has a property
+ * with name [name], and optionally, if that property in turn satisfies
+ * a [matcher].
+ */
+Matcher hasProperty(String name, [matcher]) =>
+ new _HasProperty(name, matcher == null ? null : wrapMatcher(matcher));
+
+class _HasProperty extends Matcher {
+ final String _name;
+ final Matcher _matcher;
+
+ const _HasProperty(this._name, [this._matcher]);
+
+ bool matches(item, Map matchState) {
+ var mirror = reflect(item);
+ var classMirror = mirror.type;
+ var symbol = new Symbol(_name);
+ var candidate = classMirror.declarations[symbol];
+ if (candidate == null) {
+ addStateInfo(matchState, {'reason': 'has no property named "$_name"'});
+ return false;
+ }
+ bool isInstanceField = candidate is VariableMirror && !candidate.isStatic;
+ bool isInstanceGetter =
+ candidate is MethodMirror && candidate.isGetter && !candidate.isStatic;
+ if (!(isInstanceField || isInstanceGetter)) {
+ addStateInfo(matchState, {'reason':
+ 'has a member named "$_name", but it is not an instance property'});
+ return false;
+ }
+ if (_matcher == null) return true;
+ var result = mirror.getField(symbol);
+ var resultMatches = _matcher.matches(result.reflectee, matchState);
+ if (!resultMatches) {
+ addStateInfo(matchState, {'value': result.reflectee});
+ }
+ return resultMatches;
+ }
+
+ Description describe(Description description) {
+ description.add('has property "$_name"');
+ if (_matcher != null) {
+ description.add(' which matches ').addDescriptionOf(_matcher);
+ }
+ return description;
+ }
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ var reason = matchState == null ? null : matchState['reason'];
+ if (reason != null) {
+ mismatchDescription.add(reason);
+ } else {
+ mismatchDescription.add('has property "$_name" with value ').
+ addDescriptionOf(matchState['value']);
+ var innerDescription = new StringDescription();
+ _matcher.describeMismatch(matchState['value'], innerDescription,
+ matchState['state'], verbose);
+ if (innerDescription.length > 0) {
+ mismatchDescription.add(' which ').add(innerDescription.toString());
+ }
+ }
+ return mismatchDescription;
+ }
+}
diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart
new file mode 100644
index 0000000..7acd7b4
--- /dev/null
+++ b/lib/src/core_matchers.dart
@@ -0,0 +1,873 @@
+// 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.
+
+part of matcher;
+
+/**
+ * Returns a matcher that matches empty strings, maps or iterables
+ * (including collections).
+ */
+const Matcher isEmpty = const _Empty();
+
+class _Empty extends Matcher {
+ const _Empty();
+ bool matches(item, Map matchState) {
+ if (item is Map || item is Iterable) {
+ 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. */
+const Matcher isNull = const _IsNull();
+
+/** A matcher that matches any non-null value. */
+const Matcher isNotNull = const _IsNotNull();
+
+class _IsNull extends Matcher {
+ const _IsNull();
+ bool matches(item, Map matchState) => item == null;
+ Description describe(Description description) => description.add('null');
+}
+
+class _IsNotNull extends Matcher {
+ const _IsNotNull();
+ bool matches(item, Map matchState) => item != null;
+ Description describe(Description description) => description.add('not null');
+}
+
+/** A matcher that matches the Boolean value true. */
+const Matcher isTrue = const _IsTrue();
+
+/** A matcher that matches anything except the Boolean value true. */
+const Matcher isFalse = const _IsFalse();
+
+class _IsTrue extends Matcher {
+ const _IsTrue();
+ bool matches(item, Map matchState) => item == true;
+ Description describe(Description description) => description.add('true');
+}
+
+class _IsFalse extends Matcher {
+ const _IsFalse();
+ bool matches(item, Map matchState) => item == false;
+ Description describe(Description description) => description.add('false');
+}
+
+/**
+ * Returns a matches that matches if the value is the same instance
+ * as [expected], using [identical].
+ */
+Matcher same(expected) => new _IsSameAs(expected);
+
+class _IsSameAs extends Matcher {
+ final _expected;
+ const _IsSameAs(this._expected);
+ bool matches(item, Map matchState) => identical(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 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
+ ? new _StringEqualsMatcher(expected)
+ : new _DeepMatcher(expected, limit);
+
+class _DeepMatcher extends Matcher {
+ final _expected;
+ final int _limit;
+ var count;
+
+ _DeepMatcher(this._expected, [limit = 1000]): this._limit = limit;
+
+ // Returns a pair (reason, location)
+ List _compareIterables(expected, actual, matcher, depth, location) {
+ if (actual is! Iterable) return ['is not Iterable', location];
+
+ var expectedIterator = expected.iterator;
+ var actualIterator = actual.iterator;
+ 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 (!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;
+ }
+ }
+
+ List _recursiveMatch(expected, actual, String location, int depth) {
+ // 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;
+
+ if (expected is Iterable && canRecurse) {
+ return _compareIterables(expected, actual, _recursiveMatch, depth + 1,
+ location);
+ }
+
+ 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];
+ }
+ }
+
+ 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;
+ }
+
+ 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) {
+ var rp = _recursiveMatch(expected, actual, '', 0);
+ if (rp == null) return null;
+ var reason;
+ if (rp[0].length > 0) {
+ if (rp[1].length > 0) {
+ reason = "${rp[0]} at location ${rp[1]}";
+ } else {
+ reason = rp[0];
+ }
+ } else {
+ reason = '';
+ }
+ // Cache the failure reason in the matchState.
+ addStateInfo(matchState, {'reason': reason});
+ return reason;
+ }
+
+ bool matches(item, Map matchState) =>
+ _match(_expected, item, matchState) == null;
+
+ Description describe(Description description) =>
+ description.addDescriptionOf(_expected);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ var reason = matchState['reason'];
+ // If we didn't get a good reason, that would normally be a
+ // simple 'is <value>' message. We only add that if the mismatch
+ // description is non empty (so we are supplementing the mismatch
+ // description).
+ if (reason.length == 0 && mismatchDescription.length > 0) {
+ mismatchDescription.add('is ').addDescriptionOf(item);
+ } else {
+ mismatchDescription.add(reason);
+ }
+ return mismatchDescription;
+ }
+}
+
+/** A special equality matcher for strings. */
+class _StringEqualsMatcher extends Matcher {
+ final String _value;
+
+ _StringEqualsMatcher(this._value);
+
+ bool get showActualValue => true;
+
+ bool matches(item, Map matchState) => _value == item;
+
+ Description describe(Description description) =>
+ description.addDescriptionOf(_value);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is! String) {
+ return mismatchDescription.addDescriptionOf(item).add('is not a string');
+ } else {
+ var buff = new StringBuffer();
+ buff.write('is different.');
+ var escapedItem = _escape(item);
+ var escapedValue = _escape(_value);
+ int minLength = escapedItem.length < escapedValue.length ?
+ escapedItem.length : escapedValue.length;
+ int start;
+ for (start = 0; start < minLength; start++) {
+ if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
+ break;
+ }
+ }
+ if (start == minLength) {
+ if (escapedValue.length < escapedItem.length) {
+ buff.write(' Both strings start the same, but the given value also'
+ ' has the following trailing characters: ');
+ _writeTrailing(buff, escapedItem, escapedValue.length);
+ } else {
+ buff.write(' Both strings start the same, but the given value is'
+ ' missing the following trailing characters: ');
+ _writeTrailing(buff, escapedValue, escapedItem.length);
+ }
+ } else {
+ buff.write('\nExpected: ');
+ _writeLeading(buff, escapedValue, start);
+ _writeTrailing(buff, escapedValue, start);
+ buff.write('\n Actual: ');
+ _writeLeading(buff, escapedItem, start);
+ _writeTrailing(buff, escapedItem, start);
+ buff.write('\n ');
+ for (int i = (start > 10 ? 14 : start); i > 0; i--) buff.write(' ');
+ buff.write('^\n Differ at offset $start');
+ }
+
+ return mismatchDescription.replace(buff.toString());
+ }
+ }
+
+ static String _escape(String s) =>
+ s.replaceAll('\n', '\\n').replaceAll('\r', '\\r').replaceAll('\t', '\\t');
+
+ static void _writeLeading(StringBuffer buff, String s, int start) {
+ if (start > 10) {
+ buff.write('... ');
+ buff.write(s.substring(start - 10, start));
+ } else {
+ buff.write(s.substring(0, start));
+ }
+ }
+
+ static void _writeTrailing(StringBuffer buff, String s, int start) {
+ if (start + 10 > s.length) {
+ buff.write(s.substring(start));
+ } else {
+ buff.write(s.substring(start, start + 10));
+ buff.write(' ...');
+ }
+ }
+}
+
+/** A matcher that matches any value. */
+const Matcher anything = const _IsAnything();
+
+class _IsAnything extends Matcher {
+ const _IsAnything();
+ bool matches(item, Map matchState) => true;
+ Description describe(Description description) => description.add('anything');
+}
+
+/**
+ * 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'));
+ *
+ * Note that this does not currently work in dart2js; it will
+ * match any type, and isNot(new isInstanceof<T>()) will always
+ * fail. This is because dart2js currently ignores template type
+ * parameters.
+ */
+class isInstanceOf<T> extends Matcher {
+ final String _name;
+ const isInstanceOf([name = 'specified type']): this._name = name;
+ bool matches(obj, Map matchState) => obj is T;
+ // The description here is lame :-(
+ Description describe(Description description) =>
+ description.add('an instance of ${_name}');
+}
+
+/**
+ * This can be used to match two kinds of objects:
+ *
+ * * A [Function] that throws an exception when called. The function cannot
+ * take any arguments. If you want to test that a function expecting
+ * arguments throws, wrap it in another zero-argument function that calls
+ * the one you want to test.
+ *
+ * * A [Future] that completes with an exception. Note that this creates an
+ * asynchronous expectation. The call to `expect()` that includes this will
+ * return immediately and execution will continue. Later, when the future
+ * completes, the actual expectation will run.
+ */
+const Matcher throws = const Throws();
+
+/**
+ * This can be used to match two kinds of objects:
+ *
+ * * A [Function] that throws an exception when called. The function cannot
+ * take any arguments. If you want to test that a function expecting
+ * arguments throws, wrap it in another zero-argument function that calls
+ * the one you want to test.
+ *
+ * * A [Future] that completes with an exception. Note that this creates an
+ * asynchronous expectation. The call to `expect()` that includes this will
+ * return immediately and execution will continue. Later, when the future
+ * completes, the actual expectation will run.
+ *
+ * In both cases, when an exception is thrown, this will test that the exception
+ * object matches [matcher]. If [matcher] is not an instance of [Matcher], it
+ * will implicitly be treated as `equals(matcher)`.
+ */
+Matcher throwsA(matcher) => new Throws(wrapMatcher(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.
+ */
+const Matcher returnsNormally = const _ReturnsNormally();
+
+class Throws extends Matcher {
+ final Matcher _matcher;
+
+ const Throws([Matcher matcher]): this._matcher = matcher;
+
+ bool matches(item, Map matchState) {
+ if (item is! Function && item is! Future) return false;
+ if (item is Future) {
+ var done = wrapAsync((fn) => fn());
+
+ // Queue up an asynchronous expectation that validates when the future
+ // completes.
+ item.then((value) {
+ done(() => fail("Expected future to fail, but succeeded with '$value'."));
+ }, onError: (error, trace) {
+ done(() {
+ if (_matcher == null) return;
+ var reason;
+ if (trace != null) {
+ var stackTrace = trace.toString();
+ stackTrace = " ${stackTrace.replaceAll("\n", "\n ")}";
+ reason = "Actual exception trace:\n$stackTrace";
+ }
+ expect(error, _matcher, reason: reason);
+ });
+ });
+ // It hasn't failed yet.
+ return true;
+ }
+
+ try {
+ item();
+ return false;
+ } catch (e, s) {
+ if (_matcher == null || _matcher.matches(e, matchState)) {
+ return true;
+ } else {
+ addStateInfo(matchState, {'exception': e, 'stack': s});
+ return false;
+ }
+ }
+ }
+
+ Description describe(Description description) {
+ if (_matcher == null) {
+ return description.add("throws");
+ } else {
+ return description.add('throws ').addDescriptionOf(_matcher);
+ }
+ }
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState,
+ bool verbose) {
+ if (item is! Function && item is! Future) {
+ return mismatchDescription.add('is not a Function or Future');
+ } else if (_matcher == null || matchState['exception'] == null) {
+ return mismatchDescription.add('did not throw');
+ } else {
+ mismatchDescription. add('threw ').
+ addDescriptionOf(matchState['exception']);
+ if (verbose) {
+ mismatchDescription.add(' at ').add(matchState['stack'].toString());
+ }
+ return mismatchDescription;
+ }
+ }
+}
+
+class _ReturnsNormally extends Matcher {
+ const _ReturnsNormally();
+
+ bool matches(f, Map matchState) {
+ try {
+ f();
+ return true;
+ } catch (e, s) {
+ addStateInfo(matchState, {'exception': e, 'stack': s});
+ return false;
+ }
+ }
+
+ Description describe(Description description) =>
+ description.add("return normally");
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState,
+ bool verbose) {
+ mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
+ if (verbose) {
+ mismatchDescription.add(' at ').add(matchState['stack'].toString());
+ }
+ return mismatchDescription;
+ }
+}
+
+/*
+ * 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 TypeMatcher extends Matcher {
+ final String _name;
+ const TypeMatcher(this._name);
+ Description describe(Description description) => description.add(_name);
+}
+
+/** A matcher for FormatExceptions. */
+const isFormatException = const _FormatException();
+
+/** A matcher for functions that throw FormatException. */
+const Matcher throwsFormatException = const Throws(isFormatException);
+
+class _FormatException extends TypeMatcher {
+ const _FormatException(): super("FormatException");
+ bool matches(item, Map matchState) => item is FormatException;
+}
+
+/** A matcher for Exceptions. */
+const isException = const _Exception();
+
+/** A matcher for functions that throw Exception. */
+const Matcher throwsException = const Throws(isException);
+
+class _Exception extends TypeMatcher {
+ const _Exception(): super("Exception");
+ bool matches(item, Map matchState) => item is Exception;
+}
+
+/** A matcher for ArgumentErrors. */
+const isArgumentError = const _ArgumentError();
+
+/** A matcher for functions that throw ArgumentError. */
+const Matcher throwsArgumentError = const Throws(isArgumentError);
+
+class _ArgumentError extends TypeMatcher {
+ const _ArgumentError(): super("ArgumentError");
+ bool matches(item, Map matchState) => item is ArgumentError;
+}
+
+/** A matcher for RangeErrors. */
+const isRangeError = const _RangeError();
+
+/** A matcher for functions that throw RangeError. */
+const Matcher throwsRangeError = const Throws(isRangeError);
+
+class _RangeError extends TypeMatcher {
+ const _RangeError(): super("RangeError");
+ bool matches(item, Map matchState) => item is RangeError;
+}
+
+/** A matcher for NoSuchMethodErrors. */
+const isNoSuchMethodError = const _NoSuchMethodError();
+
+/** A matcher for functions that throw NoSuchMethodError. */
+const Matcher throwsNoSuchMethodError = const Throws(isNoSuchMethodError);
+
+class _NoSuchMethodError extends TypeMatcher {
+ const _NoSuchMethodError(): super("NoSuchMethodError");
+ bool matches(item, Map matchState) => item is NoSuchMethodError;
+}
+
+/** A matcher for UnimplementedErrors. */
+const isUnimplementedError = const _UnimplementedError();
+
+/** A matcher for functions that throw Exception. */
+const Matcher throwsUnimplementedError = const Throws(isUnimplementedError);
+
+class _UnimplementedError extends TypeMatcher {
+ const _UnimplementedError(): super("UnimplementedError");
+ bool matches(item, Map matchState) => item is UnimplementedError;
+}
+
+/** A matcher for UnsupportedError. */
+const isUnsupportedError = const _UnsupportedError();
+
+/** A matcher for functions that throw UnsupportedError. */
+const Matcher throwsUnsupportedError = const Throws(isUnsupportedError);
+
+class _UnsupportedError extends TypeMatcher {
+ const _UnsupportedError(): super("UnsupportedError");
+ bool matches(item, Map matchState) => item is UnsupportedError;
+}
+
+/** A matcher for StateErrors. */
+const isStateError = const _StateError();
+
+/** A matcher for functions that throw StateError. */
+const Matcher throwsStateError = const Throws(isStateError);
+
+class _StateError extends TypeMatcher {
+ const _StateError(): super("StateError");
+ bool matches(item, Map matchState) => item is StateError;
+}
+
+/** A matcher for FallThroughError. */
+const isFallThroughError = const _FallThroughError();
+
+/** A matcher for functions that throw FallThroughError. */
+const Matcher throwsFallThroughError = const Throws(isFallThroughError);
+
+class _FallThroughError extends TypeMatcher {
+ const _FallThroughError(): super("FallThroughError");
+ bool matches(item, Map matchState) => item is FallThroughError;
+}
+
+/** A matcher for NullThrownError. */
+const isNullThrownError = const _NullThrownError();
+
+/** A matcher for functions that throw NullThrownError. */
+const Matcher throwsNullThrownError = const Throws(isNullThrownError);
+
+class _NullThrownError extends TypeMatcher {
+ const _NullThrownError(): super("NullThrownError");
+ bool matches(item, Map matchState) => item is NullThrownError;
+}
+
+/** A matcher for ConcurrentModificationError. */
+const isConcurrentModificationError = const _ConcurrentModificationError();
+
+/** A matcher for functions that throw ConcurrentModificationError. */
+const Matcher throwsConcurrentModificationError =
+ const Throws(isConcurrentModificationError);
+
+class _ConcurrentModificationError extends TypeMatcher {
+ const _ConcurrentModificationError(): super("ConcurrentModificationError");
+ bool matches(item, Map matchState) => item is ConcurrentModificationError;
+}
+
+/** A matcher for AbstractClassInstantiationError. */
+const isAbstractClassInstantiationError =
+ const _AbstractClassInstantiationError();
+
+/** A matcher for functions that throw AbstractClassInstantiationError. */
+const Matcher throwsAbstractClassInstantiationError =
+ const Throws(isAbstractClassInstantiationError);
+
+class _AbstractClassInstantiationError extends TypeMatcher {
+ const _AbstractClassInstantiationError() :
+ super("AbstractClassInstantiationError");
+ bool matches(item, Map matchState) => item is AbstractClassInstantiationError;
+}
+
+/** A matcher for CyclicInitializationError. */
+const isCyclicInitializationError = const _CyclicInitializationError();
+
+/** A matcher for functions that throw CyclicInitializationError. */
+const Matcher throwsCyclicInitializationError =
+ const Throws(isCyclicInitializationError);
+
+class _CyclicInitializationError extends TypeMatcher {
+ const _CyclicInitializationError(): super("CyclicInitializationError");
+ bool matches(item, Map matchState) => item is CyclicInitializationError;
+}
+
+/** A matcher for Map types. */
+const isMap = const _IsMap();
+
+class _IsMap extends TypeMatcher {
+ const _IsMap(): super("Map");
+ bool matches(item, Map matchState) => item is Map;
+}
+
+/** A matcher for List types. */
+const isList = const _IsList();
+
+class _IsList extends TypeMatcher {
+ const _IsList(): super("List");
+ bool matches(item, Map matchState) => item is List;
+}
+
+/**
+ * Returns a matcher that matches if an object has a length property
+ * that matches [matcher].
+ */
+Matcher hasLength(matcher) => new _HasLength(wrapMatcher(matcher));
+
+class _HasLength extends Matcher {
+ final Matcher _matcher;
+ const _HasLength([Matcher matcher = null]): this._matcher = matcher;
+
+ bool matches(item, Map matchState) {
+ try {
+ // This is harmless code that will throw if no length property
+ // but subtle enough that an optimizer shouldn't strip it out.
+ if (item.length * item.length >= 0) {
+ return _matcher.matches(item.length, matchState);
+ }
+ } catch (e) {}
+ return false;
+ }
+
+ Description describe(Description description) =>
+ description.add('an object with length of ').
+ addDescriptionOf(_matcher);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ try {
+ // We want to generate a different description if there is no length
+ // property; we use the same trick as in matches().
+ if (item.length * item.length >= 0) {
+ return mismatchDescription.add('has length of ').
+ addDescriptionOf(item.length);
+ }
+ } catch (e) {}
+ return mismatchDescription.add('has no length property');
+ }
+}
+
+/**
+ * Returns a matcher that matches if the match argument contains
+ * the expected value. For [String]s this means substring matching;
+ * for [Map]s it means the map has the key, and for [Iterable]s
+ * (including [Iterable]s) it means the iterable has a matching
+ * element. In the case of iterables, [expected] can itself be a
+ * matcher.
+ */
+Matcher contains(expected) => new _Contains(expected);
+
+class _Contains extends Matcher {
+
+ final _expected;
+
+ const _Contains(this._expected);
+
+ bool matches(item, Map matchState) {
+ if (item is String) {
+ return item.indexOf(_expected) >= 0;
+ } else if (item is Iterable) {
+ if (_expected is Matcher) {
+ return item.any((e) => _expected.matches(e, matchState));
+ } else {
+ return item.contains(_expected);
+ }
+ } else if (item is Map) {
+ return item.containsKey(_expected);
+ }
+ return false;
+ }
+
+ Description describe(Description description) =>
+ description.add('contains ').addDescriptionOf(_expected);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is String || item is Iterable || item is Map) {
+ return super.describeMismatch(item, mismatchDescription, matchState,
+ verbose);
+ } else {
+ return mismatchDescription.add('is not a string, map or iterable');
+ }
+ }
+}
+
+/**
+ * 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);
+
+class _In extends Matcher {
+
+ final _expected;
+
+ const _In(this._expected);
+
+ bool matches(item, Map matchState) {
+ if (_expected is String) {
+ return _expected.indexOf(item) >= 0;
+ } else if (_expected is Iterable) {
+ return _expected.any((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. For example:
+ *
+ * expect(v, predicate((x) => ((x % 2) == 0), "is even"))
+ */
+Matcher predicate(Function f, [description = 'satisfies function']) =>
+ new _Predicate(f, description);
+
+class _Predicate extends Matcher {
+
+ final Function _matcher;
+ final String _description;
+
+ const _Predicate(this._matcher, this._description);
+
+ bool matches(item, Map matchState) => _matcher(item);
+
+ Description describe(Description description) =>
+ description.add(_description);
+}
+
+/**
+ * A useful utility class for implementing other matchers through inheritance.
+ * Derived classes should call the base constructor with a feature name and
+ * description, and an instance matcher, and should implement the
+ * [featureValueOf] abstract method.
+ *
+ * The feature description will typically describe the item and the feature,
+ * while the feature name will just name the feature. For example, we may
+ * have a Widget class where each Widget has a price; we could make a
+ * [CustomMatcher] that can make assertions about prices with:
+ *
+ * class HasPrice extends CustomMatcher {
+ * const HasPrice(matcher) :
+ * super("Widget with price that is", "price", matcher);
+ * featureValueOf(actual) => actual.price;
+ * }
+ *
+ * and then use this for example like:
+ *
+ * expect(inventoryItem, new HasPrice(greaterThan(0)));
+ */
+class CustomMatcher extends Matcher {
+ final String _featureDescription;
+ final String _featureName;
+ final Matcher _matcher;
+
+ CustomMatcher(this._featureDescription, this._featureName, matcher)
+ : this._matcher = wrapMatcher(matcher);
+
+ /** Override this to extract the interesting feature.*/
+ featureValueOf(actual) => actual;
+
+ bool matches(item, Map matchState) {
+ var f = featureValueOf(item);
+ if (_matcher.matches(f, matchState)) return true;
+ addStateInfo(matchState, {'feature': f});
+ return false;
+ }
+
+ Description describe(Description description) =>
+ description.add(_featureDescription).add(' ').addDescriptionOf(_matcher);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ mismatchDescription.add('has ').add(_featureName).add(' with value ').
+ addDescriptionOf(matchState['feature']);
+ var innerDescription = new StringDescription();
+ _matcher.describeMismatch(matchState['feature'], innerDescription,
+ matchState['state'], verbose);
+ if (innerDescription.length > 0) {
+ mismatchDescription.add(' which ').add(innerDescription.toString());
+ }
+ return mismatchDescription;
+ }
+}
diff --git a/lib/src/description.dart b/lib/src/description.dart
new file mode 100644
index 0000000..f8ebf09
--- /dev/null
+++ b/lib/src/description.dart
@@ -0,0 +1,82 @@
+// 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.
+
+library matcher.description;
+
+import 'interfaces.dart';
+import 'pretty_print.dart';
+import 'utils.dart';
+
+/**
+ * The default implementation of IDescription. This should rarely need
+ * substitution, although conceivably it is a place where other languages
+ * could be supported.
+ */
+class StringDescription implements Description {
+ var _out;
+
+ /** Initialize the description with initial contents [init]. */
+ StringDescription([String init = '']) {
+ _out = init;
+ }
+
+ int get length => _out.length;
+
+ /** Get the description as a string. */
+ String toString() => _out;
+
+ /** Append [text] to the description. */
+ Description add(text) {
+ _out = '${_out}${text}';
+ return this;
+ }
+
+ /** Change the value of the description. */
+ Description replace(String text) {
+ _out = text;
+ return this;
+ }
+
+ /**
+ * Appends a description of [value]. If it is an IMatcher use its
+ * describe method; if it is a string use its literal value after
+ * escaping any embedded control characters; otherwise use its
+ * toString() value and wrap it in angular "quotes".
+ */
+ Description addDescriptionOf(value) {
+ if (value is Matcher) {
+ value.describe(this);
+ } else {
+ add(prettyPrint(value, maxLineLength: 80, maxItems: 25));
+ }
+ return this;
+ }
+
+ /**
+ * Append an [Iterable] [list] of objects to the description, using the
+ * specified [separator] and framing the list with [start]
+ * and [end].
+ */
+ Description addAll(String start, String separator, String end,
+ Iterable list) {
+ var separate = false;
+ add(start);
+ for (var item in list) {
+ if (separate) {
+ add(separator);
+ }
+ addDescriptionOf(item);
+ separate = true;
+ }
+ add(end);
+ return this;
+ }
+
+ /** Escape the control characters in [string] so that they are visible. */
+ _addEscapedString(String string) {
+ add("'");
+ add(escapeString(string));
+ add("'");
+ }
+}
diff --git a/lib/src/expect.dart b/lib/src/expect.dart
new file mode 100644
index 0000000..2994163
--- /dev/null
+++ b/lib/src/expect.dart
@@ -0,0 +1,176 @@
+// 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.
+
+part of matcher;
+
+/** The objects thrown by the default failure handler. */
+class TestFailure extends Error {
+ final String message;
+
+ TestFailure(this.message);
+
+ String toString() => message;
+}
+
+/**
+ * Useful utility for nesting match states.
+ */
+
+void addStateInfo(Map matchState, Map values) {
+ var innerState = new Map.from(matchState);
+ matchState.clear();
+ matchState['state'] = innerState;
+ matchState.addAll(values);
+}
+
+/**
+ * Some matchers, like those for Futures and exception testing,
+ * can fail in asynchronous sections, and throw exceptions.
+ * A user of this library will typically want to catch and handle
+ * such exceptions. The [wrapAsync] property is a function that
+ * can wrap callbacks used by these Matchers so that they can be
+ * used safely. For example, the unittest library will set this
+ * to be `expectAsync`. By default this is an identity function.
+ */
+Function wrapAsync = (Function f, [id]) => f;
+
+/**
+ * This is the main assertion function. It asserts that [actual]
+ * matches the [matcher]. [reason] is optional and is typically not
+ * supplied, as a reason is generated from the matcher; if [reason]
+ * is included it is appended to the reason generated by the matcher.
+ *
+ * [matcher] can be a value in which case it will be wrapped in an
+ * [equals] matcher.
+ *
+ * If the assertion fails, then the default behavior is to throw a
+ * [TestFailure], but this behavior can be changed by calling
+ * [configureExpectFailureHandler] and providing an alternative handler that
+ * implements the [IFailureHandler] interface. It is also possible to
+ * pass a [failureHandler] to [expect] as a final parameter for fine-
+ * grained control.
+ *
+ * In some cases extra diagnostic info can be produced on failure (for
+ * example, stack traces on mismatched exceptions). To enable these,
+ * [verbose] should be specified as true;
+ */
+void expect(actual, matcher, {String reason, FailureHandler failureHandler,
+ bool verbose : false}) {
+ matcher = wrapMatcher(matcher);
+ bool doesMatch;
+ var matchState = {};
+ try {
+ doesMatch = matcher.matches(actual, matchState);
+ } catch (e, trace) {
+ doesMatch = false;
+ if (reason == null) {
+ reason = '${(e is String) ? e : e.toString()} at $trace';
+ }
+ }
+ if (!doesMatch) {
+ if (failureHandler == null) {
+ failureHandler = getOrCreateExpectFailureHandler();
+ }
+ failureHandler.failMatch(actual, matcher, reason, matchState, verbose);
+ }
+}
+
+void fail(String message, {FailureHandler failureHandler}) {
+ if (failureHandler == null) {
+ failureHandler = getOrCreateExpectFailureHandler();
+ }
+ failureHandler.fail(message);
+}
+
+/**
+ * Takes an argument and returns an equivalent matcher.
+ * If the argument is already a matcher this does nothing,
+ * else if the argument is a function, it generates a predicate
+ * function matcher, else it generates an equals matcher.
+ */
+Matcher wrapMatcher(x) {
+ if (x is Matcher) {
+ return x;
+ } else if (x is Function) {
+ return predicate(x);
+ } else {
+ return equals(x);
+ }
+}
+
+// The handler for failed asserts.
+FailureHandler _assertFailureHandler = null;
+
+// The default failure handler that throws [TestFailure]s.
+class DefaultFailureHandler implements FailureHandler {
+ DefaultFailureHandler() {
+ if (_assertErrorFormatter == null) {
+ _assertErrorFormatter = _defaultErrorFormatter;
+ }
+ }
+ void fail(String reason) {
+ throw new TestFailure(reason);
+ }
+ void failMatch(actual, Matcher matcher, String reason,
+ Map matchState, bool verbose) {
+ fail(_assertErrorFormatter(actual, matcher, reason, matchState, verbose));
+ }
+}
+
+/**
+ * Changes or resets to the default the failure handler for expect()
+ * [handler] is a reference to the new handler; if this is omitted
+ * or null then the failure handler is reset to the default, which
+ * throws [TestFailure]s on [expect] assertion failures.
+ */
+void configureExpectFailureHandler([FailureHandler handler = null]) {
+ if (handler == null) {
+ handler = new DefaultFailureHandler();
+ }
+ _assertFailureHandler = handler;
+}
+
+FailureHandler getOrCreateExpectFailureHandler() {
+ if (_assertFailureHandler == null) {
+ configureExpectFailureHandler();
+ }
+ return _assertFailureHandler;
+}
+
+// The error message formatter for failed asserts.
+ErrorFormatter _assertErrorFormatter = null;
+
+// The default error formatter implementation.
+String _defaultErrorFormatter(actual, Matcher matcher, String reason,
+ Map matchState, bool verbose) {
+ var description = new StringDescription();
+ description.add('Expected: ').addDescriptionOf(matcher).add('\n');
+ description.add(' Actual: ').addDescriptionOf(actual).add('\n');
+
+ var mismatchDescription = new StringDescription();
+ matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
+
+ if (mismatchDescription.length > 0) {
+ description.add(' Which: ${mismatchDescription}\n');
+ }
+ if (reason != null) {
+ description.add(reason).add('\n');
+ }
+ return description.toString();
+}
+
+/**
+ * Changes or resets to default the failure message formatter for expect().
+ * [formatter] is a reference to the new formatter; if this is omitted or
+ * null then the failure formatter is reset to the default. The new
+ * formatter is returned; this allows custom expect handlers to easily
+ * get a reference to the default formatter.
+ */
+ErrorFormatter configureExpectFormatter([ErrorFormatter formatter = null]) {
+ if (formatter == null) {
+ formatter = _defaultErrorFormatter;
+ }
+ return _assertErrorFormatter = formatter;
+}
+
diff --git a/lib/src/future_matchers.dart b/lib/src/future_matchers.dart
new file mode 100644
index 0000000..c9e2742
--- /dev/null
+++ b/lib/src/future_matchers.dart
@@ -0,0 +1,68 @@
+// 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.
+
+part of matcher;
+
+/**
+ * Matches a [Future] that completes successfully with a value. Note that this
+ * creates an asynchronous expectation. The call to `expect()` that includes
+ * this will return immediately and execution will continue. Later, when the
+ * future completes, the actual expectation will run.
+ *
+ * To test that a Future completes with an exception, you can use [throws] and
+ * [throwsA].
+ */
+final Matcher completes = const _Completes(null, '');
+
+/**
+ * Matches a [Future] that completes succesfully with a value that matches
+ * [matcher]. Note that this creates an asynchronous expectation. The call to
+ * `expect()` that includes this will return immediately and execution will
+ * continue. Later, when the future completes, the actual expectation will run.
+ *
+ * To test that a Future completes with an exception, you can use [throws] and
+ * [throwsA].
+ *
+ * [id] is an optional tag that can be used to identify the completion matcher
+ * in error messages.
+ */
+Matcher completion(matcher, [String id = '']) =>
+ new _Completes(wrapMatcher(matcher), id);
+
+class _Completes extends Matcher {
+ final Matcher _matcher;
+ final String _id;
+
+ const _Completes(this._matcher, this._id);
+
+ bool matches(item, Map matchState) {
+ if (item is! Future) return false;
+ var done = wrapAsync((fn) => fn(), _id);
+
+ item.then((value) {
+ done(() { if (_matcher != null) expect(value, _matcher); });
+ }, onError: (error, trace) {
+ var id = _id == '' ? '' : '${_id} ';
+ var reason = 'Expected future ${id}to complete successfully, '
+ 'but it failed with ${error}';
+ if (trace != null) {
+ var stackTrace = trace.toString();
+ stackTrace = ' ${stackTrace.replaceAll('\n', '\n ')}';
+ reason = '$reason\nStack trace:\n$stackTrace';
+ }
+ done(() => fail(reason));
+ });
+
+ return true;
+ }
+
+ Description describe(Description description) {
+ if (_matcher == null) {
+ description.add('completes successfully');
+ } else {
+ description.add('completes to a value that ').addDescriptionOf(_matcher);
+ }
+ return description;
+ }
+}
diff --git a/lib/src/interfaces.dart b/lib/src/interfaces.dart
new file mode 100644
index 0000000..71079e5
--- /dev/null
+++ b/lib/src/interfaces.dart
@@ -0,0 +1,103 @@
+// 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.
+
+library matcher.interfaces;
+
+// To decouple the reporting of errors, and allow for extensibility of
+// matchers, we make use of some interfaces.
+
+/**
+ * The ErrorFormatter type is used for functions that
+ * can be used to build up error reports upon [expect] failures.
+ * There is one built-in implementation ([defaultErrorFormatter])
+ * which is used by the default failure handler. If the failure handler
+ * is replaced it may be desirable to replace the [stringDescription]
+ * error formatter with another.
+ */
+typedef String ErrorFormatter(actual, Matcher matcher, String reason,
+ Map matchState, bool verbose);
+
+/**
+ * Matchers build up their error messages by appending to
+ * Description objects. This interface is implemented by
+ * StringDescription. This interface is unlikely to need
+ * other implementations, but could be useful to replace in
+ * some cases - e.g. language conversion.
+ */
+abstract class Description {
+ int get length;
+
+ /** Change the value of the description. */
+ Description replace(String text);
+
+ /** This is used to add arbitrary text to the description. */
+ Description add(String text);
+
+ /** This is used to add a meaningful description of a value. */
+ Description addDescriptionOf(value);
+
+ /**
+ * This is used to add a description of an [Iterable] [list],
+ * with appropriate [start] and [end] markers and inter-element [separator].
+ */
+ Description addAll(String start, String separator, String end, Iterable list);
+}
+
+/**
+ * [expect] Matchers must implement/extend the Matcher class.
+ * The base Matcher class has a generic implementation of [describeMismatch]
+ * so this does not need to be provided unless a more clear description is
+ * required. The other two methods ([matches] and [describe])
+ * must always be provided as they are highly matcher-specific.
+ */
+abstract class Matcher {
+ const Matcher();
+
+ /**
+ * This does the matching of the actual vs expected values.
+ * [item] is the actual value. [matchState] can be supplied
+ * and may be used to add details about the mismatch that are too
+ * costly to determine in [describeMismatch].
+ */
+ bool matches(item, Map matchState);
+
+ /** This builds a textual description of the matcher. */
+ Description describe(Description description);
+
+ /**
+ * This builds a textual description of a specific mismatch. [item]
+ * is the value that was tested by [matches]; [matchState] is
+ * the [Map] that was passed to and supplemented by [matches]
+ * with additional information about the mismact, and [mismatchDescription]
+ * is the [Description] that is being built to decribe the mismatch.
+ * A few matchers make use of the [verbose] flag to provide detailed
+ * information that is not typically included but can be of help in
+ * diagnosing failures, such as stack traces.
+ */
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) => mismatchDescription;
+}
+
+/**
+ * Failed matches are reported using a default IFailureHandler.
+ * The default implementation simply throws [TestFailure]s;
+ * this can be replaced by some other implementation of
+ * IFailureHandler by calling configureExpectHandler.
+ */
+abstract class FailureHandler {
+ /** This handles failures given a textual decription */
+ void fail(String reason);
+
+ /**
+ * This handles failures given the actual [value], the [matcher]
+ * the [reason] (argument from [expect]), some additonal [matchState]
+ * generated by the [matcher], and a verbose flag which controls in
+ * some cases how much [matchState] information is used. It will use
+ * these to create a detailed error message (typically by calling
+ * an [ErrorFormatter]) and then call [fail] with this message.
+ */
+ void failMatch(actual, Matcher matcher, String reason,
+ Map matchState, bool verbose);
+}
+
diff --git a/lib/src/iterable_matchers.dart b/lib/src/iterable_matchers.dart
new file mode 100644
index 0000000..1922aa1
--- /dev/null
+++ b/lib/src/iterable_matchers.dart
@@ -0,0 +1,273 @@
+// 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.
+
+part of matcher;
+
+/**
+ * Returns a matcher which matches [Iterable]s in which all elements
+ * match the given [matcher].
+ */
+Matcher everyElement(matcher) => new _EveryElement(wrapMatcher(matcher));
+
+class _EveryElement extends _IterableMatcher {
+ Matcher _matcher;
+
+ _EveryElement(Matcher this._matcher);
+
+ bool matches(item, Map matchState) {
+ if (item is! Iterable) {
+ return false;
+ }
+ var i = 0;
+ for (var element in item) {
+ if (!_matcher.matches(element, matchState)) {
+ addStateInfo(matchState, {'index': i, 'element': element});
+ return false;
+ }
+ ++i;
+ }
+ return true;
+ }
+
+ Description describe(Description description) =>
+ description.add('every element(').addDescriptionOf(_matcher).add(')');
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (matchState['index'] != null) {
+ var index = matchState['index'];
+ var element = matchState['element'];
+ mismatchDescription.add('has value ').addDescriptionOf(element).
+ add(' which ');
+ var subDescription = new StringDescription();
+ _matcher.describeMismatch(element, subDescription,
+ matchState['state'], verbose);
+ if (subDescription.length > 0) {
+ mismatchDescription.add(subDescription);
+ } else {
+ mismatchDescription.add("doesn't match ");
+ _matcher.describe(mismatchDescription);
+ }
+ mismatchDescription.add(' at index $index');
+ return mismatchDescription;
+ }
+ return super.describeMismatch(item, mismatchDescription,
+ matchState, verbose);
+ }
+}
+
+/**
+ * Returns a matcher which matches [Iterable]s in which at least one
+ * element matches the given [matcher].
+ */
+Matcher anyElement(matcher) => new _AnyElement(wrapMatcher(matcher));
+
+class _AnyElement extends _IterableMatcher {
+ Matcher _matcher;
+
+ _AnyElement(this._matcher);
+
+ bool matches(item, Map matchState) {
+ return item.any((e) => _matcher.matches(e, matchState));
+ }
+
+ Description describe(Description description) =>
+ description.add('some element ').addDescriptionOf(_matcher);
+}
+
+/**
+ * Returns a matcher which matches [Iterable]s that have the same
+ * length and the same elements as [expected], and in the same order.
+ * This is equivalent to equals but does not recurse.
+ */
+
+Matcher orderedEquals(Iterable expected) => new _OrderedEquals(expected);
+
+class _OrderedEquals extends Matcher {
+ final Iterable _expected;
+ Matcher _matcher;
+
+ _OrderedEquals(this._expected) {
+ _matcher = equals(_expected, 1);
+ }
+
+ bool matches(item, Map matchState) =>
+ (item is Iterable) && _matcher.matches(item, matchState);
+
+ Description describe(Description description) =>
+ description.add('equals ').addDescriptionOf(_expected).add(' ordered');
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is !Iterable) {
+ return mismatchDescription.add('is not an Iterable');
+ } else {
+ return _matcher.describeMismatch(item, mismatchDescription,
+ matchState, verbose);
+ }
+ }
+}
+
+/**
+ * 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.
+ */
+Matcher unorderedEquals(Iterable expected) => new _UnorderedEquals(expected);
+
+class _UnorderedEquals extends _UnorderedMatches {
+ final List _expectedValues;
+
+ _UnorderedEquals(Iterable expected)
+ : super(expected.map(equals)),
+ _expectedValues = expected.toList();
+
+ Description describe(Description description) =>
+ description
+ .add('equals ')
+ .addDescriptionOf(_expectedValues)
+ .add(' unordered');
+}
+
+/**
+ * Iterable matchers match against [Iterable]s. We add this intermediate
+ * class to give better mismatch error messages than the base Matcher class.
+ */
+abstract class _IterableMatcher extends Matcher {
+ const _IterableMatcher();
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is! Iterable) {
+ return mismatchDescription.
+ addDescriptionOf(item).
+ add(' not an Iterable');
+ } else {
+ return super.describeMismatch(item, mismatchDescription, matchState,
+ verbose);
+ }
+ }
+}
+
+/**
+ * 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.
+ */
+Matcher unorderedMatches(Iterable expected) => new _UnorderedMatches(expected);
+
+class _UnorderedMatches extends Matcher {
+ final List<Matcher> _expected;
+
+ _UnorderedMatches(Iterable expected)
+ : _expected = expected.map(wrapMatcher).toList();
+
+ String _test(item) {
+ if (item is! Iterable) return 'not iterable';
+ item = item.toList();
+
+ // Check the lengths are the same.
+ if (_expected.length > item.length) {
+ return 'has too few elements (${item.length} < ${_expected.length})';
+ } else if (_expected.length < item.length) {
+ return 'has too many elements (${item.length} > ${_expected.length})';
+ }
+
+ var matched = new List<bool>.filled(item.length, false);
+ var expectedPosition = 0;
+ for (var expectedMatcher in _expected) {
+ var actualPosition = 0;
+ var gotMatch = false;
+ for (var actualElement in item) {
+ if (!matched[actualPosition]) {
+ if (expectedMatcher.matches(actualElement, {})) {
+ matched[actualPosition] = gotMatch = true;
+ break;
+ }
+ }
+ ++actualPosition;
+ }
+
+ if (!gotMatch) {
+ return new StringDescription()
+ .add('has no match for ')
+ .addDescriptionOf(expectedMatcher)
+ .add(' at index ${expectedPosition}')
+ .toString();
+ }
+
+ ++expectedPosition;
+ }
+ return null;
+ }
+
+ bool matches(item, Map mismatchState) => _test(item) == null;
+
+ Description describe(Description description) =>
+ description
+ .add('matches ')
+ .addAll('[', ', ', ']', _expected)
+ .add(' unordered');
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) =>
+ mismatchDescription.add(_test(item));
+}
+
+/**
+ * A pairwise matcher for iterable. You can pass an arbitrary [comparator]
+ * function that takes an expected and actual argument which will be applied
+ * to each pair in order. [description] should be a meaningful name for
+ * the comparator.
+ */
+Matcher pairwiseCompare(Iterable expected, Function comparator,
+ String description) =>
+ new _PairwiseCompare(expected, comparator, description);
+
+class _PairwiseCompare extends _IterableMatcher {
+ Iterable _expected;
+ Function _comparator;
+ String _description;
+
+ _PairwiseCompare(this._expected, this._comparator, this._description);
+
+ bool matches(item, Map matchState) {
+ if (item is! Iterable) return false;
+ if (item.length != _expected.length) return false;
+ var iterator = item.iterator;
+ var i = 0;
+ for (var e in _expected) {
+ iterator.moveNext();
+ if (!_comparator(e, iterator.current)) {
+ addStateInfo(matchState, {'index': i, 'expected': e,
+ 'actual': iterator.current});
+ return false;
+ }
+ i++;
+ }
+ return true;
+ }
+
+ Description describe(Description description) =>
+ description.add('pairwise $_description ').addDescriptionOf(_expected);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is !Iterable) {
+ return mismatchDescription.add('is not an Iterable');
+ } else if (item.length != _expected.length) {
+ return mismatchDescription.
+ add('has length ${item.length} instead of ${_expected.length}');
+ } else {
+ return mismatchDescription.
+ add('has ').
+ addDescriptionOf(matchState["actual"]).
+ add(' which is not $_description ').
+ addDescriptionOf(matchState["expected"]).
+ add(' at index ${matchState["index"]}');
+ }
+ }
+}
+
diff --git a/lib/src/map_matchers.dart b/lib/src/map_matchers.dart
new file mode 100644
index 0000000..033e906
--- /dev/null
+++ b/lib/src/map_matchers.dart
@@ -0,0 +1,57 @@
+// 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.
+
+part of matcher;
+
+/**
+ * Returns a matcher which matches maps containing the given [value].
+ */
+Matcher containsValue(value) => new _ContainsValue(value);
+
+class _ContainsValue extends Matcher {
+ final _value;
+
+ const _ContainsValue(this._value);
+
+ bool matches(item, Map matchState) => item.containsValue(_value);
+ Description describe(Description description) =>
+ description.add('contains value ').addDescriptionOf(_value);
+}
+
+/**
+ * Returns a matcher which matches maps containing the key-value pair
+ * with [key] => [value].
+ */
+Matcher containsPair(key, value) =>
+ new _ContainsMapping(key, wrapMatcher(value));
+
+class _ContainsMapping extends Matcher {
+ final _key;
+ final Matcher _valueMatcher;
+
+ const _ContainsMapping(this._key, Matcher this._valueMatcher);
+
+ bool matches(item, Map matchState) =>
+ item.containsKey(_key) &&
+ _valueMatcher.matches(item[_key], matchState);
+
+ Description describe(Description description) {
+ return description.add('contains pair ').addDescriptionOf(_key).
+ add(' => ').addDescriptionOf(_valueMatcher);
+ }
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (!item.containsKey(_key)) {
+ return mismatchDescription.add(" doesn't contain key ")
+ .addDescriptionOf(_key);
+ } else {
+ mismatchDescription.add(' contains key ').addDescriptionOf(_key).
+ add(' but with value ');
+ _valueMatcher.describeMismatch(item[_key], mismatchDescription,
+ matchState, verbose);
+ return mismatchDescription;
+ }
+ }
+}
diff --git a/lib/src/numeric_matchers.dart b/lib/src/numeric_matchers.dart
new file mode 100644
index 0000000..507a682
--- /dev/null
+++ b/lib/src/numeric_matchers.dart
@@ -0,0 +1,234 @@
+// 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.
+
+part of matcher;
+
+/**
+ * Returns a matcher which matches if the match argument is greater
+ * than the given [value].
+ */
+Matcher greaterThan(value) =>
+ new _OrderingComparison(value, false, false, true, 'a value greater than');
+
+/**
+ * Returns a matcher which matches if the match argument is greater
+ * than or equal to the given [value].
+ */
+Matcher greaterThanOrEqualTo(value) =>
+ new _OrderingComparison(value, true, false, true,
+ 'a value greater than or equal to');
+
+/**
+ * Returns a matcher which matches if the match argument is less
+ * than the given [value].
+ */
+Matcher lessThan(value) =>
+ new _OrderingComparison(value, false, true, false, 'a value less than');
+
+/**
+ * Returns a matcher which matches if the match argument is less
+ * than or equal to the given [value].
+ */
+Matcher lessThanOrEqualTo(value) =>
+ new _OrderingComparison(value, true, true, false,
+ 'a value less than or equal to');
+
+/**
+ * A matcher which matches if the match argument is zero.
+ */
+const Matcher isZero =
+ const _OrderingComparison(0, true, false, false, 'a value equal to');
+
+
+/**
+ * A matcher which matches if the match argument is non-zero.
+ */
+const Matcher isNonZero =
+ const _OrderingComparison(0, false, true, true, 'a value not equal to');
+
+/**
+ * A matcher which matches if the match argument is positive.
+ */
+const Matcher isPositive =
+ const _OrderingComparison(0, false, false, true, 'a positive value', false);
+
+/**
+ * A matcher which matches if the match argument is zero or negative.
+ */
+const Matcher isNonPositive =
+ const _OrderingComparison(0, true, true, false,
+ 'a non-positive value', false);
+
+/**
+ * A matcher which matches if the match argument is negative.
+ */
+const Matcher isNegative =
+ const _OrderingComparison(0, false, true, false, 'a negative value', false);
+
+/**
+ * A matcher which matches if the match argument is zero or positive.
+ */
+const Matcher isNonNegative =
+ const _OrderingComparison(0, true, false, true,
+ 'a non-negative value', false);
+
+bool _isNumeric(value) {
+ return value is num;
+}
+
+class _OrderingComparison extends Matcher {
+ /** Expected value. */
+ final _value;
+ /** What to return if actual == expected */
+ final bool _equalValue;
+ /** What to return if actual < expected */
+ final bool _lessThanValue;
+ /** What to return if actual > expected */
+ final bool _greaterThanValue;
+ /** Textual name of the inequality */
+ final String _comparisonDescription;
+ /** Whether to include the expected value in the description */
+ final bool _valueInDescription;
+
+ const _OrderingComparison(
+ this._value,
+ this._equalValue,
+ this._lessThanValue,
+ this._greaterThanValue,
+ this._comparisonDescription,
+ [valueInDescription = true]) :
+ this._valueInDescription = valueInDescription;
+
+ bool matches(item, Map matchState) {
+ if (item == _value) {
+ return _equalValue;
+ } else if (item < _value) {
+ return _lessThanValue;
+ } else {
+ return _greaterThanValue;
+ }
+ }
+
+ Description describe(Description description) {
+ if (_valueInDescription) {
+ return description.add(_comparisonDescription).add(' ').
+ addDescriptionOf(_value);
+ } else {
+ return description.add(_comparisonDescription);
+ }
+ }
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ mismatchDescription.add('is not ');
+ return describe(mismatchDescription);
+ }
+}
+
+/**
+ * Returns a matcher which matches if the match argument is within [delta]
+ * of some [value]; i.e. if the match argument is greater than
+ * than or equal [value]-[delta] and less than or equal to [value]+[delta].
+ */
+Matcher closeTo(value, delta) => new _IsCloseTo(value, delta);
+
+class _IsCloseTo extends Matcher {
+ final num _value, _delta;
+
+ const _IsCloseTo(this._value, this._delta);
+
+ bool matches(item, Map matchState) {
+ if (!_isNumeric(item)) {
+ return false;
+ }
+ var diff = item - _value;
+ if (diff < 0) diff = -diff;
+ return (diff <= _delta);
+ }
+
+ Description describe(Description description) =>
+ description.add('a numeric value within ').
+ addDescriptionOf(_delta).
+ add(' of ').
+ addDescriptionOf(_value);
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is !num) {
+ return mismatchDescription.add(' not numeric');
+ } else {
+ var diff = item - _value;
+ if (diff < 0) diff = -diff;
+ return mismatchDescription.
+ add(' differs by ').
+ addDescriptionOf(diff);
+ }
+ }
+}
+
+/**
+ * Returns a matcher which matches if the match argument is greater
+ * than or equal to [low] and less than or equal to [high].
+ */
+Matcher inInclusiveRange(low, high) => new _InRange(low, high, true, true);
+
+/**
+ * Returns a matcher which matches if the match argument is greater
+ * than [low] and less than [high].
+ */
+Matcher inExclusiveRange(low, high) => new _InRange(low, high, false, false);
+
+/**
+ * Returns a matcher which matches if the match argument is greater
+ * than [low] and less than or equal to [high].
+ */
+Matcher inOpenClosedRange(low, high) => new _InRange(low, high, false, true);
+
+/**
+ * Returns a matcher which matches if the match argument is greater
+ * than or equal to a [low] and less than [high].
+ */
+Matcher inClosedOpenRange(low, high) => new _InRange(low, high, true, false);
+
+class _InRange extends Matcher {
+ final num _low, _high;
+ final bool _lowMatchValue, _highMatchValue;
+
+ const _InRange(this._low, this._high,
+ this._lowMatchValue, this._highMatchValue);
+
+ bool matches(value, Map matchState) {
+ if (value is! num) {
+ return false;
+ }
+ if (value < _low || value > _high) {
+ return false;
+ }
+ if (value == _low) {
+ return _lowMatchValue;
+ }
+ if (value == _high) {
+ return _highMatchValue;
+ }
+ return true;
+ }
+
+ Description describe(Description description) =>
+ description.add("be in range from "
+ "$_low (${_lowMatchValue ? 'inclusive' : 'exclusive'}) to "
+ "$_high (${_highMatchValue ? 'inclusive' : 'exclusive'})");
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is !num) {
+ return mismatchDescription.
+ addDescriptionOf(item).
+ add(' not numeric');
+ } else {
+ return super.describeMismatch(item, mismatchDescription,
+ matchState, verbose);
+ }
+ }
+}
+
diff --git a/lib/src/operator_matchers.dart b/lib/src/operator_matchers.dart
new file mode 100644
index 0000000..fa17aba
--- /dev/null
+++ b/lib/src/operator_matchers.dart
@@ -0,0 +1,176 @@
+// 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.
+
+part of matcher;
+
+/**
+ * This returns a matcher that inverts [matcher] to its logical negation.
+ */
+Matcher isNot(matcher) => new _IsNot(wrapMatcher(matcher));
+
+class _IsNot extends Matcher {
+ final Matcher _matcher;
+
+ const _IsNot(Matcher this._matcher);
+
+ bool matches(item, Map matchState) => !_matcher.matches(item, matchState);
+
+ Description describe(Description description) =>
+ description.add('not ').addDescriptionOf(_matcher);
+}
+
+/**
+ * This returns a matcher that matches if all of the matchers passed as
+ * arguments (up to 7) match. Instead of passing the matchers separately
+ * they can be passed as a single List argument.
+ * Any argument that is not a matcher is implicitly wrapped in a
+ * Matcher to check for equality.
+*/
+Matcher allOf(arg0,
+ [arg1 = null,
+ arg2 = null,
+ arg3 = null,
+ arg4 = null,
+ arg5 = null,
+ arg6 = null]) {
+ if (arg0 is List) {
+ expect(arg1, isNull);
+ expect(arg2, isNull);
+ expect(arg3, isNull);
+ expect(arg4, isNull);
+ expect(arg5, isNull);
+ expect(arg6, isNull);
+ for (int i = 0; i < arg0.length; i++) {
+ arg0[i] = wrapMatcher(arg0[i]);
+ }
+ return new _AllOf(arg0);
+ } else {
+ List matchers = new List();
+ if (arg0 != null) {
+ matchers.add(wrapMatcher(arg0));
+ }
+ if (arg1 != null) {
+ matchers.add(wrapMatcher(arg1));
+ }
+ if (arg2 != null) {
+ matchers.add(wrapMatcher(arg2));
+ }
+ if (arg3 != null) {
+ matchers.add(wrapMatcher(arg3));
+ }
+ if (arg4 != null) {
+ matchers.add(wrapMatcher(arg4));
+ }
+ if (arg5 != null) {
+ matchers.add(wrapMatcher(arg5));
+ }
+ if (arg6 != null) {
+ matchers.add(wrapMatcher(arg6));
+ }
+ return new _AllOf(matchers);
+ }
+}
+
+class _AllOf extends Matcher {
+ final Iterable<Matcher> _matchers;
+
+ const _AllOf(this._matchers);
+
+ bool matches(item, Map matchState) {
+ for (var matcher in _matchers) {
+ if (!matcher.matches(item, matchState)) {
+ addStateInfo(matchState, {'matcher': matcher});
+ return false;
+ }
+ }
+ return true;
+ }
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ var matcher = matchState['matcher'];
+ matcher.describeMismatch(item, mismatchDescription,
+ matchState['state'], verbose);
+ return mismatchDescription;
+ }
+
+ Description describe(Description description) =>
+ description.addAll('(', ' and ', ')', _matchers);
+}
+
+/**
+ * Matches if any of the given matchers evaluate to true. The
+ * arguments can be a set of matchers as separate parameters
+ * (up to 7), or a List of matchers.
+ *
+ * The matchers are evaluated from left to right using short-circuit
+ * evaluation, so evaluation stops as soon as a matcher returns true.
+ *
+ * Any argument that is not a matcher is implicitly wrapped in a
+ * Matcher to check for equality.
+*/
+
+Matcher anyOf(arg0,
+ [arg1 = null,
+ arg2 = null,
+ arg3 = null,
+ arg4 = null,
+ arg5 = null,
+ arg6 = null]) {
+ if (arg0 is List) {
+ expect(arg1, isNull);
+ expect(arg2, isNull);
+ expect(arg3, isNull);
+ expect(arg4, isNull);
+ expect(arg5, isNull);
+ expect(arg6, isNull);
+ for (int i = 0; i < arg0.length; i++) {
+ arg0[i] = wrapMatcher(arg0[i]);
+ }
+ return new _AnyOf(arg0);
+ } else {
+ List matchers = new List();
+ if (arg0 != null) {
+ matchers.add(wrapMatcher(arg0));
+ }
+ if (arg1 != null) {
+ matchers.add(wrapMatcher(arg1));
+ }
+ if (arg2 != null) {
+ matchers.add(wrapMatcher(arg2));
+ }
+ if (arg3 != null) {
+ matchers.add(wrapMatcher(arg3));
+ }
+ if (arg4 != null) {
+ matchers.add(wrapMatcher(arg4));
+ }
+ if (arg5 != null) {
+ matchers.add(wrapMatcher(arg5));
+ }
+ if (arg6 != null) {
+ matchers.add(wrapMatcher(arg6));
+ }
+ return new _AnyOf(matchers);
+ }
+}
+
+class _AnyOf extends Matcher {
+ final Iterable<Matcher> _matchers;
+
+ const _AnyOf(this._matchers);
+
+ bool matches(item, Map matchState) {
+ for (var matcher in _matchers) {
+ if (matcher.matches(item, matchState)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Description describe(Description description) =>
+ description.addAll('(', ' or ', ')', _matchers);
+}
+
diff --git a/lib/src/pretty_print.dart b/lib/src/pretty_print.dart
new file mode 100644
index 0000000..7deae06
--- /dev/null
+++ b/lib/src/pretty_print.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2013, 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.
+
+library matcher.pretty_print;
+
+import 'description.dart';
+import 'interfaces.dart';
+import 'utils.dart';
+
+/**
+ * Returns a pretty-printed representation of [object].
+ *
+ * If [maxLineLength] is passed, this will attempt to ensure that each line is
+ * no longer than [maxLineLength] characters long. This isn't guaranteed, since
+ * individual objects may have string representations that are too long, but
+ * most lines will be less than [maxLineLength] long.
+ *
+ * If [maxItems] is passed, [Iterable]s and [Map]s will only print their first
+ * [maxItems] members or key/value pairs, respectively.
+ */
+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]));
+ String pp(child) => _prettyPrint(child, indent + 2, seen, false);
+
+ if (object is Iterable) {
+ // Print the type name for non-List iterables.
+ var type = object is List ? "" : typeName(object) + ":";
+
+ // Truncate the list of strings if it's longer than [maxItems].
+ var strings = object.map(pp).toList();
+ if (maxItems != null && strings.length > maxItems) {
+ strings.replaceRange(maxItems - 1, strings.length, ['...']);
+ }
+
+ // If the printed string is short and doesn't contain a newline, print it
+ // as a single line.
+ var singleLine = "$type[${strings.join(', ')}]";
+ if ((maxLineLength == null ||
+ singleLine.length + indent <= maxLineLength) &&
+ !singleLine.contains("\n")) {
+ return singleLine;
+ }
+
+ // Otherwise, print each member on its own line.
+ return "$type[\n" + strings.map((string) {
+ return _indent(indent + 2) + string;
+ }).join(",\n") + "\n" + _indent(indent) + "]";
+ } else if (object is Map) {
+ // Convert the contents of the map to string representations.
+ var strings = object.keys.map((key) {
+ return '${pp(key)}: ${pp(object[key])}';
+ }).toList();
+
+ // Truncate the list of strings if it's longer than [maxItems].
+ if (maxItems != null && strings.length > maxItems) {
+ strings.replaceRange(maxItems - 1, strings.length, ['...']);
+ }
+
+ // If the printed string is short and doesn't contain a newline, print it
+ // as a single line.
+ var singleLine = "{${strings.join(", ")}}";
+ if ((maxLineLength == null ||
+ singleLine.length + indent <= maxLineLength) &&
+ !singleLine.contains("\n")) {
+ return singleLine;
+ }
+
+ // Otherwise, print each key/value pair on its own line.
+ return "{\n" + strings.map((string) {
+ return _indent(indent + 2) + string;
+ }).join(",\n") + "\n" + _indent(indent) + "}";
+ } else if (object is String) {
+ // Escape strings and print each line on its own line.
+ var lines = object.split("\n");
+ return "'" + lines.map(escapeString)
+ .join("\\n'\n${_indent(indent + 2)}'") + "'";
+ } else {
+ var value = object.toString().replaceAll("\n", _indent(indent) + "\n");
+ var defaultToString = value.startsWith("Instance of ");
+
+ // If this is the top-level call to [prettyPrint], wrap the value on angle
+ // brackets to set it apart visually.
+ if (top) value = "<$value>";
+
+ // Print the type of objects with custom [toString] methods. Primitive
+ // objects and objects that don't implement a custom [toString] don't need
+ // to have their types printed.
+ if (object is num || object is bool || object is Function ||
+ object == null || defaultToString) {
+ return value;
+ } else {
+ return "${typeName(object)}:$value";
+ }
+ }
+ }
+
+ return _prettyPrint(object, 0, new Set(), true);
+}
+
+String _indent(int length) => new List.filled(length, ' ').join('');
diff --git a/lib/src/string_matchers.dart b/lib/src/string_matchers.dart
new file mode 100644
index 0000000..f5e32ff
--- /dev/null
+++ b/lib/src/string_matchers.dart
@@ -0,0 +1,204 @@
+// 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.
+
+part of matcher;
+
+/**
+ * Returns a matcher which matches if the match argument is a string and
+ * is equal to [value] when compared case-insensitively.
+ */
+Matcher equalsIgnoringCase(String value) => new _IsEqualIgnoringCase(value);
+
+class _IsEqualIgnoringCase extends _StringMatcher {
+ final String _value;
+ String _matchValue;
+
+ _IsEqualIgnoringCase(this._value) {
+ _matchValue = _value.toLowerCase();
+ }
+
+ bool matches(item, Map matchState) =>
+ item is String && _matchValue == item.toLowerCase();
+
+ Description describe(Description description) =>
+ description.addDescriptionOf(_value).add(' ignoring case');
+}
+
+/**
+ * Returns a matcher which matches if the match argument is a string and
+ * is equal to [value] when compared with all runs of whitespace
+ * collapsed to single spaces and leading and trailing whitespace removed.
+ *
+ * For example, `equalsIgnoringCase("hello world")` will match
+ * "hello world", " hello world" and "hello world ".
+ */
+Matcher equalsIgnoringWhitespace(_string) =>
+ new _IsEqualIgnoringWhitespace(_string);
+
+class _IsEqualIgnoringWhitespace extends _StringMatcher {
+ final String _value;
+ String _matchValue;
+
+ _IsEqualIgnoringWhitespace(this._value) {
+ _matchValue = collapseWhitespace(_value);
+ }
+
+ bool matches(item, Map matchState) =>
+ item is String && _matchValue == collapseWhitespace(item);
+
+ Description describe(Description description) =>
+ description.addDescriptionOf(_matchValue).add(' ignoring whitespace');
+
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (item is String) {
+ return mismatchDescription.add('is ').
+ addDescriptionOf(collapseWhitespace(item)).
+ add(' with whitespace compressed');
+ } else {
+ return super.describeMismatch(item, mismatchDescription,
+ matchState, verbose);
+ }
+ }
+}
+
+/**
+ * Utility function to collapse whitespace runs to single spaces
+ * and strip leading/trailing whitespace.
+ */
+String collapseWhitespace(_string) {
+ bool isWhitespace(String ch) => (' \n\r\t'.indexOf(ch) >= 0);
+ StringBuffer result = new StringBuffer();
+ bool skipSpace = true;
+ for (var i = 0; i < _string.length; i++) {
+ var character = _string[i];
+ if (isWhitespace(character)) {
+ if (!skipSpace) {
+ result.write(' ');
+ skipSpace = true;
+ }
+ } else {
+ result.write(character);
+ skipSpace = false;
+ }
+ }
+ return result.toString().trim();
+}
+
+/**
+ * Returns a matcher that matches if the match argument is a string and
+ * starts with [prefixString].
+ */
+Matcher startsWith(String prefixString) => new _StringStartsWith(prefixString);
+
+class _StringStartsWith extends _StringMatcher {
+ final String _prefix;
+
+ const _StringStartsWith(this._prefix);
+
+ bool matches(item, Map matchState) =>
+ item is String && item.startsWith(_prefix);
+
+ Description describe(Description description) =>
+ description.add('a string starting with ').addDescriptionOf(_prefix);
+}
+
+/**
+ * Returns a matcher that matches if the match argument is a string and
+ * ends with [suffixString].
+ */
+Matcher endsWith(String suffixString) => new _StringEndsWith(suffixString);
+
+class _StringEndsWith extends _StringMatcher {
+
+ final String _suffix;
+
+ const _StringEndsWith(this._suffix);
+
+ bool matches(item, Map matchState) =>
+ item is String && item.endsWith(_suffix);
+
+ Description describe(Description description) =>
+ description.add('a string ending with ').addDescriptionOf(_suffix);
+}
+
+/**
+ * Returns a matcher that matches if the match argument is a string and
+ * contains a given list of [substrings] in relative order.
+ *
+ * For example, `stringContainsInOrder(["a", "e", "i", "o", "u"])` will match
+ * "abcdefghijklmnopqrstuvwxyz".
+ */
+
+Matcher stringContainsInOrder(substrings) =>
+ new _StringContainsInOrder(substrings);
+
+class _StringContainsInOrder extends _StringMatcher {
+
+ final List<String> _substrings;
+
+ const _StringContainsInOrder(this._substrings);
+
+ bool matches(item, Map matchState) {
+ if (!(item is String)) {
+ return false;
+ }
+ var from_index = 0;
+ for (var s in _substrings) {
+ from_index = item.indexOf(s, from_index);
+ if (from_index < 0)
+ return false;
+ }
+ return true;
+ }
+
+ Description describe(Description description) =>
+ description.addAll('a string containing ', ', ', ' in order',
+ _substrings);
+}
+
+/**
+ * Returns a matcher that matches if the match argument is a string and
+ * matches the regular expression given by [re]. [re] can be a RegExp
+ * instance or a string; in the latter case it will be used to create
+ * a RegExp instance.
+ */
+Matcher matches(re) => new _MatchesRegExp(re);
+
+class _MatchesRegExp extends _StringMatcher {
+ RegExp _regexp;
+
+ _MatchesRegExp(re) {
+ if (re is String) {
+ _regexp = new RegExp(re);
+ } else if (re is RegExp) {
+ _regexp = re;
+ } else {
+ throw new ArgumentError('matches requires a regexp or string');
+ }
+ }
+
+ bool matches(item, Map matchState) =>
+ item is String ? _regexp.hasMatch(item) : false;
+
+ Description describe(Description description) =>
+ description.add("match '${_regexp.pattern}'");
+}
+
+// String matchers match against a string. We add this intermediate
+// class to give better mismatch error messages than the base Matcher class.
+abstract class _StringMatcher extends Matcher {
+ const _StringMatcher();
+ Description describeMismatch(item, Description mismatchDescription,
+ Map matchState, bool verbose) {
+ if (!(item is String)) {
+ return mismatchDescription.
+ addDescriptionOf(item).
+ add(' not a string');
+ } else {
+ return super.describeMismatch(item, mismatchDescription,
+ matchState, verbose);
+ }
+ }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..c2182c2
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2013, 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.
+
+library matcher.utils;
+
+/**
+ * Returns the name of the type of [x], or "Unknown" if the type name can't be
+ * determined.
+ */
+String typeName(x) {
+ // dart2js blows up on some objects (e.g. window.navigator).
+ // So we play safe here.
+ try {
+ if (x == null) return "null";
+ var type = x.runtimeType.toString();
+ // TODO(nweiz): if the object's type is private, find a public superclass to
+ // display once there's a portable API to do that.
+ return type.startsWith("_") ? "?" : type;
+ } catch (e) {
+ return "?";
+ }
+}
+
+/**
+ * Returns [source] with any control characters replaced by their escape
+ * sequences.
+ *
+ * This doesn't add quotes to the string, but it does escape single quote
+ * characters so that single quotes can be applied externally.
+ */
+String escapeString(String source) =>
+ source.split("").map(_escapeChar).join("");
+
+/** Return the escaped form of a character [ch]. */
+String _escapeChar(String ch) {
+ if (ch == "'")
+ return "\\'";
+ else if (ch == '\n')
+ return '\\n';
+ else if (ch == '\r')
+ return '\\r';
+ else if (ch == '\t')
+ return '\\t';
+ else
+ return ch;
+}
+
+/** Indent each line in [str] by two spaces. */
+String indent(String str) =>
+ str.replaceAll(new RegExp("^", multiLine: true), " ");
+
+/** A pair of values. */
+class Pair<E, F> {
+ E first;
+ F last;
+
+ Pair(this.first, this.last);
+
+ String toString() => '($first, $last)';
+
+ bool operator ==(other) {
+ if (other is! Pair) return false;
+ return other.first == first && other.last == last;
+ }
+
+ int get hashCode => first.hashCode ^ last.hashCode;
+}
+
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..63f8191
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,10 @@
+name: matcher
+version: 0.10.0
+author: Dart Team <misc@dartlang.org>
+description: Support for specifying test expectations
+homepage: http://www.dartlang.org
+environment:
+ sdk: '>=1.0.0 <2.0.0'
+documentation: http://api.dartlang.org/docs/pkg/matcher
+dev_dependencies:
+ unittest: '>=0.10.0 <0.12.0'
diff --git a/test/matchers_minified_test.dart b/test/matchers_minified_test.dart
new file mode 100644
index 0000000..e071c09
--- /dev/null
+++ b/test/matchers_minified_test.dart
@@ -0,0 +1,135 @@
+// 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.
+
+// This file is for matcher tests that rely on the names of various Dart types.
+// These tests normally fail when run in minified dart2js, since the names will
+// be mangled. This version of the file is modified to expect minified names.
+
+import 'package:matcher/matcher.dart';
+import 'package:unittest/unittest.dart' as ut;
+
+import 'test_common.dart';
+import 'test_utils.dart';
+
+// A regexp fragment matching a minified name.
+final _minifiedName = r"[A-Za-z0-9]{1,3}";
+
+void main() {
+ initUtils();
+
+ ut.group('Core matchers', () {
+ ut.test('throwsFormatException', () {
+ shouldPass(() { throw new FormatException(''); },
+ throwsFormatException);
+ shouldFail(() { throw new Exception(); },
+ throwsFormatException,
+ matches(
+ r"Expected: throws FormatException +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+
+ ut.test('throwsArgumentError', () {
+ shouldPass(() { throw new ArgumentError(''); },
+ throwsArgumentError);
+ shouldFail(() { throw new Exception(); },
+ throwsArgumentError,
+ matches(
+ r"Expected: throws ArgumentError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+
+ ut.test('throwsRangeError', () {
+ shouldPass(() { throw new RangeError(0); },
+ throwsRangeError);
+ shouldFail(() { throw new Exception(); },
+ throwsRangeError,
+ matches(
+ r"Expected: throws RangeError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+
+ ut.test('throwsNoSuchMethodError', () {
+ shouldPass(() {
+ throw new NoSuchMethodError(null, const Symbol(''), null, null);
+ }, throwsNoSuchMethodError);
+ shouldFail(() { throw new Exception(); },
+ throwsNoSuchMethodError,
+ matches(
+ r"Expected: throws NoSuchMethodError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+
+ ut.test('throwsUnimplementedError', () {
+ shouldPass(() { throw new UnimplementedError(''); },
+ throwsUnimplementedError);
+ shouldFail(() { throw new Exception(); },
+ throwsUnimplementedError,
+ matches(
+ r"Expected: throws UnimplementedError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+
+ ut.test('throwsUnsupportedError', () {
+ shouldPass(() { throw new UnsupportedError(''); },
+ throwsUnsupportedError);
+ shouldFail(() { throw new Exception(); },
+ throwsUnsupportedError,
+ matches(
+ r"Expected: throws UnsupportedError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+
+ ut.test('throwsStateError', () {
+ shouldPass(() { throw new StateError(''); },
+ throwsStateError);
+ shouldFail(() { throw new Exception(); },
+ throwsStateError,
+ matches(
+ r"Expected: throws StateError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw " + _minifiedName + r":<Exception>"));
+ });
+ });
+
+ ut.group('Iterable Matchers', () {
+ ut.test('isEmpty', () {
+ var d = new SimpleIterable(0);
+ var e = new SimpleIterable(1);
+ shouldPass(d, isEmpty);
+ shouldFail(e, isEmpty,
+ matches(r"Expected: empty +Actual: " + _minifiedName + r":\[1\]"));
+ });
+
+ ut.test('contains', () {
+ var d = new SimpleIterable(3);
+ shouldPass(d, contains(2));
+ shouldFail(d, contains(5),
+ matches(
+ r"Expected: contains <5> +"
+ r"Actual: " + _minifiedName + r":\[3, 2, 1\]"));
+ });
+ });
+
+ ut.group('Feature Matchers', () {
+ ut.test("Feature Matcher", () {
+ var w = new Widget();
+ w.price = 10;
+ shouldPass(w, new HasPrice(10));
+ shouldPass(w, new HasPrice(greaterThan(0)));
+ shouldFail(w, new HasPrice(greaterThan(10)),
+ matches(
+ r"Expected: Widget with a price that is a value greater than "
+ r"<10> +"
+ r"Actual: <Instance of '" + _minifiedName + r"'> +"
+ r"Which: has price with value <10> which is not "
+ r"a value greater than <10>"));
+ });
+ });
+}
diff --git a/test/matchers_test.dart b/test/matchers_test.dart
new file mode 100644
index 0000000..3d75bd9
--- /dev/null
+++ b/test/matchers_test.dart
@@ -0,0 +1,809 @@
+// 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.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:matcher/matcher.dart';
+import 'package:unittest/unittest.dart' as ut;
+
+import 'test_utils.dart';
+
+void main() {
+
+ initUtils();
+
+ // Core matchers
+
+ ut.group('Core matchers', () {
+
+ ut.test('isTrue', () {
+ shouldPass(true, isTrue);
+ shouldFail(false, isTrue, "Expected: true Actual: <false>");
+ });
+
+ ut.test('isFalse', () {
+ shouldPass(false, isFalse);
+ shouldFail(10, isFalse, "Expected: false Actual: <10>");
+ shouldFail(true, isFalse, "Expected: false Actual: <true>");
+ });
+
+ ut.test('isNull', () {
+ shouldPass(null, isNull);
+ shouldFail(false, isNull, "Expected: null Actual: <false>");
+ });
+
+ ut.test('isNotNull', () {
+ shouldPass(false, isNotNull);
+ shouldFail(null, isNotNull, "Expected: not null Actual: <null>");
+ });
+
+ ut.test('same', () {
+ var a = new Map();
+ var b = new Map();
+ shouldPass(a, same(a));
+ shouldFail(b, same(a), "Expected: same instance as {} Actual: {}");
+ });
+
+ ut.test('equals', () {
+ var a = new Map();
+ var b = new Map();
+ shouldPass(a, equals(a));
+ shouldPass(a, equals(b));
+ });
+
+ ut.test('anything', () {
+ var a = new Map();
+ shouldPass(0, anything);
+ shouldPass(null, anything);
+ shouldPass(a, anything);
+ shouldFail(a, isNot(anything), "Expected: not anything Actual: {}");
+ });
+
+ ut.test('throws', () {
+ shouldFail(doesNotThrow, throws,
+ matches(
+ r"Expected: throws"
+ r" Actual: <Closure(: \(\) => dynamic "
+ r"from Function 'doesNotThrow': static\.)?>"
+ r" Which: did not throw"));
+ shouldPass(doesThrow, throws);
+ shouldFail(true, throws,
+ "Expected: throws"
+ " Actual: <true>"
+ " Which: is not a Function or Future");
+ });
+
+ ut.test('throwsA', () {
+ shouldPass(doesThrow, throwsA(equals('X')));
+ shouldFail(doesThrow, throwsA(equals('Y')),
+ matches(
+ r"Expected: throws 'Y'"
+ r" Actual: <Closure(: \(\) => dynamic "
+ r"from Function 'doesThrow': static\.)?>"
+ r" Which: threw 'X'"));
+ });
+
+ ut.test('returnsNormally', () {
+ shouldPass(doesNotThrow, returnsNormally);
+ shouldFail(doesThrow, returnsNormally,
+ matches(
+ r"Expected: return normally"
+ r" Actual: <Closure(: \(\) => dynamic "
+ r"from Function 'doesThrow': static\.)?>"
+ r" Which: threw 'X'"));
+ });
+
+
+ ut.test('hasLength', () {
+ var a = new Map();
+ var b = new List();
+ shouldPass(a, hasLength(0));
+ shouldPass(b, hasLength(0));
+ shouldPass('a', hasLength(1));
+ shouldFail(0, hasLength(0), new PrefixMatcher(
+ "Expected: an object with length of <0> "
+ "Actual: <0> "
+ "Which: has no length property"));
+
+ b.add(0);
+ shouldPass(b, hasLength(1));
+ shouldFail(b, hasLength(2),
+ "Expected: an object with length of <2> "
+ "Actual: [0] "
+ "Which: has length of <1>");
+
+ b.add(0);
+ shouldFail(b, hasLength(1),
+ "Expected: an object with length of <1> "
+ "Actual: [0, 0] "
+ "Which: has length of <2>");
+ shouldPass(b, hasLength(2));
+ });
+
+ ut.test('scalar type mismatch', () {
+ shouldFail('error', equals(5.1),
+ "Expected: <5.1> "
+ "Actual: 'error'");
+ });
+
+ ut.test('nested type mismatch', () {
+ shouldFail(['error'], equals([5.1]),
+ "Expected: [5.1] "
+ "Actual: ['error'] "
+ "Which: was 'error' instead of <5.1> at location [0]");
+ });
+
+ ut.test('doubly-nested type mismatch', () {
+ shouldFail([['error']], equals([[5.1]]),
+ "Expected: [[5.1]] "
+ "Actual: [['error']] "
+ "Which: was 'error' instead of <5.1> at location [0][0]");
+ });
+
+ ut.test('doubly nested inequality', () {
+ var actual1 = [['foo', 'bar'], ['foo'], 3, []];
+ var expected1 = [['foo', 'bar'], ['foo'], 4, []];
+ var reason1 = "Expected: [['foo', 'bar'], ['foo'], 4, []] "
+ "Actual: [['foo', 'bar'], ['foo'], 3, []] "
+ "Which: was <3> instead of <4> at location [2]";
+
+ var actual2 = [['foo', 'barry'], ['foo'], 4, []];
+ var expected2 = [['foo', 'bar'], ['foo'], 4, []];
+ var reason2 = "Expected: [['foo', 'bar'], ['foo'], 4, []] "
+ "Actual: [['foo', 'barry'], ['foo'], 4, []] "
+ "Which: was 'barry' instead of 'bar' at location [0][1]";
+
+ var actual3 = [['foo', 'bar'], ['foo'], 4, {'foo':'bar'}];
+ var expected3 = [['foo', 'bar'], ['foo'], 4, {'foo':'barry'}];
+ var reason3 = "Expected: [['foo', 'bar'], ['foo'], 4, {'foo': 'barry'}] "
+ "Actual: [['foo', 'bar'], ['foo'], 4, {'foo': 'bar'}] "
+ "Which: was 'bar' instead of 'barry' at location [3]['foo']";
+
+ shouldFail(actual1, equals(expected1), reason1);
+ shouldFail(actual2, equals(expected2), reason2);
+ shouldFail(actual3, equals(expected3), reason3);
+ });
+ });
+
+ ut.group('Numeric Matchers', () {
+
+ ut.test('greaterThan', () {
+ shouldPass(10, greaterThan(9));
+ shouldFail(9, greaterThan(10),
+ "Expected: a value greater than <10> "
+ "Actual: <9> "
+ "Which: is not a value greater than <10>");
+ });
+
+ ut.test('greaterThanOrEqualTo', () {
+ shouldPass(10, greaterThanOrEqualTo(10));
+ shouldFail(9, greaterThanOrEqualTo(10),
+ "Expected: a value greater than or equal to <10> "
+ "Actual: <9> "
+ "Which: is not a value greater than or equal to <10>");
+ });
+
+ ut.test('lessThan', () {
+ shouldFail(10, lessThan(9),
+ "Expected: a value less than <9> "
+ "Actual: <10> "
+ "Which: is not a value less than <9>");
+ shouldPass(9, lessThan(10));
+ });
+
+ ut.test('lessThanOrEqualTo', () {
+ shouldPass(10, lessThanOrEqualTo(10));
+ shouldFail(11, lessThanOrEqualTo(10),
+ "Expected: a value less than or equal to <10> "
+ "Actual: <11> "
+ "Which: is not a value less than or equal to <10>");
+ });
+
+ ut.test('isZero', () {
+ shouldPass(0, isZero);
+ shouldFail(1, isZero,
+ "Expected: a value equal to <0> "
+ "Actual: <1> "
+ "Which: is not a value equal to <0>");
+ });
+
+ ut.test('isNonZero', () {
+ shouldFail(0, isNonZero,
+ "Expected: a value not equal to <0> "
+ "Actual: <0> "
+ "Which: is not a value not equal to <0>");
+ shouldPass(1, isNonZero);
+ });
+
+ ut.test('isPositive', () {
+ shouldFail(-1, isPositive,
+ "Expected: a positive value "
+ "Actual: <-1> "
+ "Which: is not a positive value");
+ shouldFail(0, isPositive,
+ "Expected: a positive value "
+ "Actual: <0> "
+ "Which: is not a positive value");
+ shouldPass(1, isPositive);
+ });
+
+ ut.test('isNegative', () {
+ shouldPass(-1, isNegative);
+ shouldFail(0, isNegative,
+ "Expected: a negative value "
+ "Actual: <0> "
+ "Which: is not a negative value");
+ });
+
+ ut.test('isNonPositive', () {
+ shouldPass(-1, isNonPositive);
+ shouldPass(0, isNonPositive);
+ shouldFail(1, isNonPositive,
+ "Expected: a non-positive value "
+ "Actual: <1> "
+ "Which: is not a non-positive value");
+ });
+
+ ut.test('isNonNegative', () {
+ shouldPass(1, isNonNegative);
+ shouldPass(0, isNonNegative);
+ shouldFail(-1, isNonNegative,
+ "Expected: a non-negative value "
+ "Actual: <-1> "
+ "Which: is not a non-negative value");
+ });
+
+ ut.test('closeTo', () {
+ shouldPass(0, closeTo(0, 1));
+ shouldPass(-1, closeTo(0, 1));
+ shouldPass(1, closeTo(0, 1));
+ shouldFail(1.001, closeTo(0, 1),
+ "Expected: a numeric value within <1> of <0> "
+ "Actual: <1.001> "
+ "Which: differs by <1.001>");
+ shouldFail(-1.001, closeTo(0, 1),
+ "Expected: a numeric value within <1> of <0> "
+ "Actual: <-1.001> "
+ "Which: differs by <1.001>");
+ });
+
+ ut.test('inInclusiveRange', () {
+ shouldFail(-1, inInclusiveRange(0,2),
+ "Expected: be in range from 0 (inclusive) to 2 (inclusive) "
+ "Actual: <-1>");
+ shouldPass(0, inInclusiveRange(0,2));
+ shouldPass(1, inInclusiveRange(0,2));
+ shouldPass(2, inInclusiveRange(0,2));
+ shouldFail(3, inInclusiveRange(0,2),
+ "Expected: be in range from 0 (inclusive) to 2 (inclusive) "
+ "Actual: <3>");
+ });
+
+ ut.test('inExclusiveRange', () {
+ shouldFail(0, inExclusiveRange(0,2),
+ "Expected: be in range from 0 (exclusive) to 2 (exclusive) "
+ "Actual: <0>");
+ shouldPass(1, inExclusiveRange(0,2));
+ shouldFail(2, inExclusiveRange(0,2),
+ "Expected: be in range from 0 (exclusive) to 2 (exclusive) "
+ "Actual: <2>");
+ });
+
+ ut.test('inOpenClosedRange', () {
+ shouldFail(0, inOpenClosedRange(0,2),
+ "Expected: be in range from 0 (exclusive) to 2 (inclusive) "
+ "Actual: <0>");
+ shouldPass(1, inOpenClosedRange(0,2));
+ shouldPass(2, inOpenClosedRange(0,2));
+ });
+
+ ut.test('inClosedOpenRange', () {
+ shouldPass(0, inClosedOpenRange(0,2));
+ shouldPass(1, inClosedOpenRange(0,2));
+ shouldFail(2, inClosedOpenRange(0,2),
+ "Expected: be in range from 0 (inclusive) to 2 (exclusive) "
+ "Actual: <2>");
+ });
+ });
+
+ ut.group('String Matchers', () {
+
+ ut.test('isEmpty', () {
+ shouldPass('', isEmpty);
+ shouldFail(null, isEmpty,
+ "Expected: empty Actual: <null>");
+ shouldFail(0, isEmpty,
+ "Expected: empty Actual: <0>");
+ shouldFail('a', isEmpty, "Expected: empty Actual: 'a'");
+ });
+
+ ut.test('equalsIgnoringCase', () {
+ shouldPass('hello', equalsIgnoringCase('HELLO'));
+ shouldFail('hi', equalsIgnoringCase('HELLO'),
+ "Expected: 'HELLO' ignoring case Actual: 'hi'");
+ });
+
+ ut.test('equalsIgnoringWhitespace', () {
+ shouldPass(' hello world ', equalsIgnoringWhitespace('hello world'));
+ shouldFail(' helloworld ', equalsIgnoringWhitespace('hello world'),
+ "Expected: 'hello world' ignoring whitespace "
+ "Actual: ' helloworld ' "
+ "Which: is 'helloworld' with whitespace compressed");
+ });
+
+ ut.test('startsWith', () {
+ shouldPass('hello', startsWith(''));
+ shouldPass('hello', startsWith('hell'));
+ shouldPass('hello', startsWith('hello'));
+ shouldFail('hello', startsWith('hello '),
+ "Expected: a string starting with 'hello ' "
+ "Actual: 'hello'");
+ });
+
+ ut.test('endsWith', () {
+ shouldPass('hello', endsWith(''));
+ shouldPass('hello', endsWith('lo'));
+ shouldPass('hello', endsWith('hello'));
+ shouldFail('hello', endsWith(' hello'),
+ "Expected: a string ending with ' hello' "
+ "Actual: 'hello'");
+ });
+
+ ut.test('contains', () {
+ shouldPass('hello', contains(''));
+ shouldPass('hello', contains('h'));
+ shouldPass('hello', contains('o'));
+ shouldPass('hello', contains('hell'));
+ shouldPass('hello', contains('hello'));
+ shouldFail('hello', contains(' '),
+ "Expected: contains ' ' Actual: 'hello'");
+ });
+
+ ut.test('stringContainsInOrder', () {
+ shouldPass('goodbye cruel world', stringContainsInOrder(['']));
+ shouldPass('goodbye cruel world', stringContainsInOrder(['goodbye']));
+ shouldPass('goodbye cruel world', stringContainsInOrder(['cruel']));
+ shouldPass('goodbye cruel world', stringContainsInOrder(['world']));
+ shouldPass('goodbye cruel world',
+ stringContainsInOrder(['good', 'bye', 'world']));
+ shouldPass('goodbye cruel world',
+ stringContainsInOrder(['goodbye', 'cruel']));
+ shouldPass('goodbye cruel world',
+ stringContainsInOrder(['cruel', 'world']));
+ shouldPass('goodbye cruel world',
+ stringContainsInOrder(['goodbye', 'cruel', 'world']));
+ shouldFail('goodbye cruel world',
+ stringContainsInOrder(['goo', 'cruel', 'bye']),
+ "Expected: a string containing 'goo', 'cruel', 'bye' in order "
+ "Actual: 'goodbye cruel world'");
+ });
+
+ ut.test('matches', () {
+ shouldPass('c0d', matches('[a-z][0-9][a-z]'));
+ shouldPass('c0d', matches(new RegExp('[a-z][0-9][a-z]')));
+ shouldFail('cOd', matches('[a-z][0-9][a-z]'),
+ "Expected: match '[a-z][0-9][a-z]' Actual: 'cOd'");
+ });
+ });
+
+ ut.group('Iterable Matchers', () {
+
+ ut.test('isEmpty', () {
+ shouldPass([], isEmpty);
+ shouldFail([1], isEmpty, "Expected: empty Actual: [1]");
+ });
+
+ ut.test('contains', () {
+ var d = [1, 2];
+ shouldPass(d, contains(1));
+ shouldFail(d, contains(0), "Expected: contains <0> "
+ "Actual: [1, 2]");
+ });
+
+ ut.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]");
+ });
+
+ ut.test('isIn', () {
+ var d = [1, 2];
+ shouldPass(1, isIn(d));
+ shouldFail(0, isIn(d), "Expected: is in [1, 2] Actual: <0>");
+ });
+
+ ut.test('everyElement', () {
+ var d = [1, 2];
+ var e = [1, 1, 1];
+ shouldFail(d, everyElement(1),
+ "Expected: every element(<1>) "
+ "Actual: [1, 2] "
+ "Which: has value <2> which doesn't match <1> at index 1");
+ shouldPass(e, everyElement(1));
+ });
+
+ ut.test('nested everyElement', () {
+ var d = [['foo', 'bar'], ['foo'], []];
+ var e = [['foo', 'bar'], ['foo'], 3, []];
+ shouldPass(d, everyElement(anyOf(isEmpty, contains('foo'))));
+ shouldFail(d, everyElement(everyElement(equals('foo'))),
+ "Expected: every element(every element('foo')) "
+ "Actual: [['foo', 'bar'], ['foo'], []] "
+ "Which: has value ['foo', 'bar'] which has value 'bar' "
+ "which is different. Expected: foo Actual: bar ^ "
+ "Differ at offset 0 at index 1 at index 0");
+ shouldFail(d, everyElement(allOf(hasLength(greaterThan(0)),
+ contains('foo'))),
+ "Expected: every element((an object with length of a value "
+ "greater than <0> and contains 'foo')) "
+ "Actual: [['foo', 'bar'], ['foo'], []] "
+ "Which: has value [] which has length of <0> at index 2");
+ shouldFail(d, everyElement(allOf(contains('foo'),
+ hasLength(greaterThan(0)))),
+ "Expected: every element((contains 'foo' and "
+ "an object with length of a value greater than <0>)) "
+ "Actual: [['foo', 'bar'], ['foo'], []] "
+ "Which: has value [] which doesn't match (contains 'foo' and "
+ "an object with length of a value greater than <0>) at index 2");
+ shouldFail(e, everyElement(allOf(contains('foo'),
+ hasLength(greaterThan(0)))),
+ "Expected: every element((contains 'foo' and an object with "
+ "length of a value greater than <0>)) "
+ "Actual: [['foo', 'bar'], ['foo'], 3, []] "
+ "Which: has value <3> which is not a string, map or iterable "
+ "at index 2");
+ });
+
+ ut.test('anyElement', () {
+ var d = [1, 2];
+ var e = [1, 1, 1];
+ shouldPass(d, anyElement(2));
+ shouldFail(e, anyElement(2),
+ "Expected: some element <2> Actual: [1, 1, 1]");
+ });
+
+ ut.test('orderedEquals', () {
+ shouldPass([null], orderedEquals([null]));
+ var d = [1, 2];
+ shouldPass(d, orderedEquals([1, 2]));
+ shouldFail(d, orderedEquals([2, 1]),
+ "Expected: equals [2, 1] ordered "
+ "Actual: [1, 2] "
+ "Which: was <1> instead of <2> at location [0]");
+ });
+
+ ut.test('unorderedEquals', () {
+ var d = [1, 2];
+ shouldPass(d, unorderedEquals([2, 1]));
+ shouldFail(d, unorderedEquals([1]),
+ "Expected: equals [1] unordered "
+ "Actual: [1, 2] "
+ "Which: has too many elements (2 > 1)");
+ shouldFail(d, unorderedEquals([3, 2, 1]),
+ "Expected: equals [3, 2, 1] unordered "
+ "Actual: [1, 2] "
+ "Which: has too few elements (2 < 3)");
+ shouldFail(d, unorderedEquals([3, 1]),
+ "Expected: equals [3, 1] unordered "
+ "Actual: [1, 2] "
+ "Which: has no match for <3> at index 0");
+ });
+
+ ut.test('unorderedMatchess', () {
+ var d = [1, 2];
+ shouldPass(d, unorderedMatches([2, 1]));
+ shouldPass(d, unorderedMatches([greaterThan(1), greaterThan(0)]));
+ shouldFail(d, unorderedMatches([greaterThan(0)]),
+ "Expected: matches [a value greater than <0>] unordered "
+ "Actual: [1, 2] "
+ "Which: has too many elements (2 > 1)");
+ shouldFail(d, unorderedMatches([3, 2, 1]),
+ "Expected: matches [<3>, <2>, <1>] unordered "
+ "Actual: [1, 2] "
+ "Which: has too few elements (2 < 3)");
+ shouldFail(d, unorderedMatches([3, 1]),
+ "Expected: matches [<3>, <1>] unordered "
+ "Actual: [1, 2] "
+ "Which: has no match for <3> at index 0");
+ 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");
+ });
+
+ ut.test('pairwise compare', () {
+ var c = [1, 2];
+ var d = [1, 2, 3];
+ var e = [1, 4, 9];
+ shouldFail('x', pairwiseCompare(e, (e,a) => a <= e,
+ "less than or equal"),
+ "Expected: pairwise less than or equal [1, 4, 9] "
+ "Actual: 'x' "
+ "Which: is not an Iterable");
+ shouldFail(c, pairwiseCompare(e, (e,a) => a <= e, "less than or equal"),
+ "Expected: pairwise less than or equal [1, 4, 9] "
+ "Actual: [1, 2] "
+ "Which: has length 2 instead of 3");
+ shouldPass(d, pairwiseCompare(e, (e,a) => a <= e, "less than or equal"));
+ shouldFail(d, pairwiseCompare(e, (e,a) => a < e, "less than"),
+ "Expected: pairwise less than [1, 4, 9] "
+ "Actual: [1, 2, 3] "
+ "Which: has <1> which is not less than <1> at index 0");
+ shouldPass(d, pairwiseCompare(e, (e,a) => a * a == e, "square root of"));
+ shouldFail(d, pairwiseCompare(e, (e,a) => a + a == e, "double"),
+ "Expected: pairwise double [1, 4, 9] "
+ "Actual: [1, 2, 3] "
+ "Which: has <1> which is not double <1> at index 0");
+ });
+ });
+
+ ut.group('Map Matchers', () {
+
+ ut.test('isEmpty', () {
+ var a = new Map();
+ shouldPass({}, isEmpty);
+ shouldPass(a, isEmpty);
+ a['foo'] = 'bar';
+ shouldFail(a, isEmpty, "Expected: empty "
+ "Actual: {'foo': 'bar'}");
+ });
+
+ ut.test('equals', () {
+ var a = new Map();
+ a['foo'] = 'bar';
+ var b = new Map();
+ b['foo'] = 'bar';
+ var c = new Map();
+ c['bar'] = 'foo';
+ shouldPass(a, equals(b));
+ shouldFail(b, equals(c),
+ "Expected: {'bar': 'foo'} "
+ "Actual: {'foo': 'bar'} "
+ "Which: is missing map key 'bar'");
+ });
+
+ ut.test('equals with different lengths', () {
+ var a = new LinkedHashMap();
+ a['foo'] = 'bar';
+ var b = new LinkedHashMap();
+ b['foo'] = 'bar';
+ b['bar'] = 'foo';
+ var c = new LinkedHashMap();
+ c['bar'] = 'foo';
+ c['barrista'] = 'caffeine';
+ shouldFail(a, equals(b),
+ "Expected: {'foo': 'bar', 'bar': 'foo'} "
+ "Actual: {'foo': 'bar'} "
+ "Which: has different length and is missing map key 'bar'");
+ shouldFail(b, equals(a),
+ "Expected: {'foo': 'bar'} "
+ "Actual: {'foo': 'bar', 'bar': 'foo'} "
+ "Which: has different length and has extra map key 'bar'");
+ shouldFail(b, equals(c),
+ "Expected: {'bar': 'foo', 'barrista': 'caffeine'} "
+ "Actual: {'foo': 'bar', 'bar': 'foo'} "
+ "Which: is missing map key 'barrista'");
+ shouldFail(c, equals(b),
+ "Expected: {'foo': 'bar', 'bar': 'foo'} "
+ "Actual: {'bar': 'foo', 'barrista': 'caffeine'} "
+ "Which: is missing map key 'foo'");
+ shouldFail(a, equals(c),
+ "Expected: {'bar': 'foo', 'barrista': 'caffeine'} "
+ "Actual: {'foo': 'bar'} "
+ "Which: has different length and is missing map key 'bar'");
+ shouldFail(c, equals(a),
+ "Expected: {'foo': 'bar'} "
+ "Actual: {'bar': 'foo', 'barrista': 'caffeine'} "
+ "Which: has different length and is missing map key 'foo'");
+ });
+
+ ut.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']");
+ });
+
+ ut.test('contains', () {
+ var a = new Map();
+ a['foo'] = 'bar';
+ var b = new Map();
+ shouldPass(a, contains('foo'));
+ shouldFail(b, contains('foo'),
+ "Expected: contains 'foo' Actual: {}");
+ shouldFail(10, contains('foo'),
+ "Expected: contains 'foo' Actual: <10> "
+ "Which: is not a string, map or iterable");
+ });
+
+ ut.test('containsValue', () {
+ var a = new Map();
+ a['foo'] = 'bar';
+ shouldPass(a, containsValue('bar'));
+ shouldFail(a, containsValue('ba'),
+ "Expected: contains value 'ba' "
+ "Actual: {'foo': 'bar'}");
+ });
+
+ ut.test('containsPair', () {
+ var a = new Map();
+ a['foo'] = 'bar';
+ shouldPass(a, containsPair('foo', 'bar'));
+ shouldFail(a, containsPair('foo', 'ba'),
+ "Expected: contains pair 'foo' => 'ba' "
+ "Actual: {'foo': 'bar'} "
+ "Which: is different. Both strings start the same, but "
+ "the given value also has the following trailing characters: r");
+ shouldFail(a, containsPair('fo', 'bar'),
+ "Expected: contains pair 'fo' => 'bar' "
+ "Actual: {'foo': 'bar'} "
+ "Which: doesn't contain key 'fo'");
+ });
+
+ ut.test('hasLength', () {
+ var a = new Map();
+ a['foo'] = 'bar';
+ var b = new Map();
+ shouldPass(a, hasLength(1));
+ shouldFail(b, hasLength(1),
+ "Expected: an object with length of <1> "
+ "Actual: {} "
+ "Which: has length of <0>");
+ });
+ });
+
+ ut.group('Operator Matchers', () {
+
+ ut.test('anyOf', () {
+ shouldFail(0, anyOf([equals(1), equals(2)]),
+ "Expected: (<1> or <2>) Actual: <0>");
+ shouldPass(1, anyOf([equals(1), equals(2)]));
+ });
+
+ ut.test('allOf', () {
+ shouldPass(1, allOf([lessThan(10), greaterThan(0)]));
+ shouldFail(-1, allOf([lessThan(10), greaterThan(0)]),
+ "Expected: (a value less than <10> and a value greater than <0>) "
+ "Actual: <-1> "
+ "Which: is not a value greater than <0>");
+ });
+ });
+
+ ut.group('Future Matchers', () {
+
+ ut.test('completes - unexpected error', () {
+ var completer = new Completer();
+ completer.completeError('X');
+ shouldFail(completer.future, completes,
+ contains('Expected future to complete successfully, '
+ 'but it failed with X'),
+ isAsync: true);
+ });
+
+ ut.test('completes - successfully', () {
+ var completer = new Completer();
+ completer.complete('1');
+ shouldPass(completer.future, completes, isAsync: true);
+ });
+
+ ut.test('throws - unexpected to see normal completion', () {
+ var completer = new Completer();
+ completer.complete('1');
+ shouldFail(completer.future, throws,
+ contains("Expected future to fail, but succeeded with '1'"),
+ isAsync: true);
+ });
+
+ ut.test('throws - expected to see exception', () {
+ var completer = new Completer();
+ completer.completeError('X');
+ shouldPass(completer.future, throws, isAsync: true);
+ });
+
+ ut.test('throws - expected to see exception thrown later on', () {
+ var completer = new Completer();
+ var chained = completer.future.then((_) { throw 'X'; });
+ shouldPass(chained, throws, isAsync: true);
+ completer.complete('1');
+ });
+
+ ut.test('throwsA - unexpected normal completion', () {
+ var completer = new Completer();
+ completer.complete('1');
+ shouldFail(completer.future, throwsA(equals('X')),
+ contains("Expected future to fail, but succeeded with '1'"),
+ isAsync: true);
+ });
+
+ ut.test('throwsA - correct error', () {
+ var completer = new Completer();
+ completer.completeError('X');
+ shouldPass(completer.future, throwsA(equals('X')), isAsync: true);
+ });
+
+ ut.test('throwsA - wrong error', () {
+ var completer = new Completer();
+ completer.completeError('X');
+ shouldFail(completer.future, throwsA(equals('Y')),
+ "Expected: 'Y' Actual: 'X' "
+ "Which: is different. "
+ "Expected: Y Actual: X ^ Differ at offset 0",
+ isAsync: true);
+ });
+ });
+
+ ut.group('Predicate Matchers', () {
+ ut.test('isInstanceOf', () {
+ shouldFail(0, predicate((x) => x is String, "an instance of String"),
+ "Expected: an instance of String Actual: <0>");
+ shouldPass('cow', predicate((x) => x is String, "an instance of String"));
+ });
+ });
+
+ ut.group('exception/error matchers', () {
+ // TODO(gram): extend this to more types; for now this is just
+ // the types being added in this CL.
+
+ // TODO: enable this test when it works.
+ // See issue 12052.
+ ut.skip_test('throwsCyclicInitializationError', () {
+ expect(() => new Bicycle(), throwsCyclicInitializationError);
+ });
+
+ ut.test('throwsAbstractClassInstantiationError', () {
+ expect(() => new Abstraction(), throwsAbstractClassInstantiationError);
+ });
+
+ ut.test('throwsConcurrentModificationError', () {
+ expect(() {
+ var a = { 'foo': 'bar' };
+ for (var k in a.keys) {
+ a.remove(k);
+ }
+ }, throwsConcurrentModificationError);
+ });
+
+ ut.test('throwsNullThrownError', () {
+ expect(() => throw null, throwsNullThrownError);
+ });
+
+ ut.test('throwsFallThroughError', () {
+ expect(() {
+ var a = 0;
+ switch (a) {
+ case 0:
+ a += 1;
+ case 1:
+ return;
+ }
+ }, throwsFallThroughError);
+ });
+ });
+}
+
+class Bicycle {
+ static var foo = bar();
+
+ static bar() {
+ return foo + 1;
+ }
+
+ X() {
+ print(foo);
+ }
+}
+
+abstract class Abstraction {
+ void norealization();
+}
+
diff --git a/test/matchers_unminified_test.dart b/test/matchers_unminified_test.dart
new file mode 100644
index 0000000..e0629f0
--- /dev/null
+++ b/test/matchers_unminified_test.dart
@@ -0,0 +1,131 @@
+// 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.
+
+// This file is for matcher tests that rely on the names of various Dart types.
+// These tests will fail when run in minified dart2js, since the names will be
+// mangled. A version of this file that works in minified dart2js is in
+// matchers_minified_test.dart.
+
+import 'package:matcher/matcher.dart';
+import 'package:unittest/unittest.dart' as ut;
+
+import 'test_common.dart';
+import 'test_utils.dart';
+
+void main() {
+ initUtils();
+
+ ut.group('Core matchers', () {
+ ut.test('throwsFormatException', () {
+ shouldPass(() { throw new FormatException(''); },
+ throwsFormatException);
+ shouldFail(() { throw new Exception(); },
+ throwsFormatException,
+ matches(
+ r"Expected: throws FormatException +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+
+ });
+
+ ut.test('throwsArgumentError', () {
+ shouldPass(() { throw new ArgumentError(''); },
+ throwsArgumentError);
+ shouldFail(() { throw new Exception(); },
+ throwsArgumentError,
+ matches(
+ r"Expected: throws ArgumentError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+ });
+
+ ut.test('throwsRangeError', () {
+ shouldPass(() { throw new RangeError(0); },
+ throwsRangeError);
+ shouldFail(() { throw new Exception(); },
+ throwsRangeError,
+ matches(
+ r"Expected: throws RangeError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+ });
+
+ ut.test('throwsNoSuchMethodError', () {
+ shouldPass(() {
+ throw new NoSuchMethodError(null, const Symbol(''), null, null);
+ }, throwsNoSuchMethodError);
+ shouldFail(() { throw new Exception(); },
+ throwsNoSuchMethodError,
+ matches(
+ r"Expected: throws NoSuchMethodError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+ });
+
+ ut.test('throwsUnimplementedError', () {
+ shouldPass(() { throw new UnimplementedError(''); },
+ throwsUnimplementedError);
+ shouldFail(() { throw new Exception(); },
+ throwsUnimplementedError,
+ matches(
+ r"Expected: throws UnimplementedError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+ });
+
+ ut.test('throwsUnsupportedError', () {
+ shouldPass(() { throw new UnsupportedError(''); },
+ throwsUnsupportedError);
+ shouldFail(() { throw new Exception(); },
+ throwsUnsupportedError,
+ matches(
+ r"Expected: throws UnsupportedError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+ });
+
+ ut.test('throwsStateError', () {
+ shouldPass(() { throw new StateError(''); },
+ throwsStateError);
+ shouldFail(() { throw new Exception(); },
+ throwsStateError,
+ matches(
+ r"Expected: throws StateError +"
+ r"Actual: <Closure(: \(\) => dynamic)?> +"
+ r"Which: threw \?:<Exception>"));
+ });
+ });
+
+ ut.group('Iterable Matchers', () {
+ ut.test('isEmpty', () {
+ var d = new SimpleIterable(0);
+ var e = new SimpleIterable(1);
+ shouldPass(d, isEmpty);
+ shouldFail(e, isEmpty, "Expected: empty "
+ "Actual: SimpleIterable:[1]");
+ });
+
+ ut.test('contains', () {
+ var d = new SimpleIterable(3);
+ shouldPass(d, contains(2));
+ shouldFail(d, contains(5),
+ "Expected: contains <5> "
+ "Actual: SimpleIterable:[3, 2, 1]");
+ });
+ });
+
+ ut.group('Feature Matchers', () {
+ ut.test("Feature Matcher", () {
+ var w = new Widget();
+ w.price = 10;
+ shouldPass(w, new HasPrice(10));
+ shouldPass(w, new HasPrice(greaterThan(0)));
+ shouldFail(w, new HasPrice(greaterThan(10)),
+ "Expected: Widget with a price that is a value greater than <10> "
+ "Actual: <Instance of 'Widget'> "
+ "Which: has price with value <10> which is not "
+ "a value greater than <10>");
+ });
+ });
+}
diff --git a/test/mirror_matchers_test.dart b/test/mirror_matchers_test.dart
new file mode 100644
index 0000000..6def3f1
--- /dev/null
+++ b/test/mirror_matchers_test.dart
@@ -0,0 +1,45 @@
+// 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.
+
+import 'package:matcher/mirror_matchers.dart';
+import 'package:unittest/unittest.dart' as ut;
+
+import 'test_utils.dart';
+
+class C {
+ var instanceField = 1;
+ get instanceGetter => 2;
+ static var staticField = 3;
+ static get staticGetter => 4;
+}
+
+void main() {
+
+ initUtils();
+
+ ut.test('hasProperty', () {
+ var foo = [3];
+ shouldPass(foo, hasProperty('length', 1));
+ shouldFail(foo, hasProperty('foo'), 'Expected: has property "foo" '
+ 'Actual: [3] '
+ 'Which: has no property named "foo"');
+ shouldFail(foo, hasProperty('length', 2),
+ 'Expected: has property "length" which matches <2> '
+ 'Actual: [3] '
+ 'Which: has property "length" with value <1>');
+ var c = new C();
+ shouldPass(c, hasProperty('instanceField', 1));
+ shouldPass(c, hasProperty('instanceGetter', 2));
+ shouldFail(c, hasProperty('staticField'),
+ 'Expected: has property "staticField" '
+ 'Actual: <Instance of \'C\'> '
+ 'Which: has a member named "staticField",'
+ ' but it is not an instance property');
+ shouldFail(c, hasProperty('staticGetter'),
+ 'Expected: has property "staticGetter" '
+ 'Actual: <Instance of \'C\'> '
+ 'Which: has a member named "staticGetter",'
+ ' but it is not an instance property');
+ });
+}
diff --git a/test/pretty_print_minified_test.dart b/test/pretty_print_minified_test.dart
new file mode 100644
index 0000000..0758359
--- /dev/null
+++ b/test/pretty_print_minified_test.dart
@@ -0,0 +1,63 @@
+// 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.
+
+// This file is for pretty-print tests that rely on the names of various Dart
+// types. These tests normally fail when run in minified dart2js, since the
+// names will be mangled. This version of the file is modified to expect
+// minified names.
+
+import 'dart:collection';
+
+import 'package:unittest/unittest.dart' as ut;
+
+import 'package:matcher/matcher.dart';
+import 'package:matcher/src/pretty_print.dart';
+
+class DefaultToString {}
+
+class CustomToString {
+ String toString() => "string representation";
+}
+
+class _PrivateName {
+ String toString() => "string representation";
+}
+
+class _PrivateNameIterable extends IterableMixin {
+ Iterator get iterator => [1, 2, 3].iterator;
+}
+
+// A regexp fragment matching a minified name.
+final _minifiedName = r"[A-Za-z0-9]{1,3}";
+
+void main() {
+ ut.group('with an object', () {
+ ut.test('with a default [toString]', () {
+ expect(prettyPrint(new DefaultToString()),
+ matches(r"<Instance of '" + _minifiedName + r"'>"));
+ });
+
+ ut.test('with a custom [toString]', () {
+ expect(prettyPrint(new CustomToString()),
+ matches(_minifiedName + r':<string representation>'));
+ });
+
+ ut.test('with a custom [toString] and a private name', () {
+ expect(prettyPrint(new _PrivateName()),
+ matches(_minifiedName + r':<string representation>'));
+ });
+ });
+
+ ut.group('with an iterable', () {
+ ut.test("that's not a list", () {
+ expect(prettyPrint([1, 2, 3, 4].map((n) => n * 2)),
+ matches(_minifiedName + r":\[2, 4, 6, 8\]"));
+ });
+
+ ut.test("that's not a list and has a private name", () {
+ expect(prettyPrint(new _PrivateNameIterable()),
+ matches(_minifiedName + r":\[1, 2, 3\]"));
+ });
+ });
+}
diff --git a/test/pretty_print_test.dart b/test/pretty_print_test.dart
new file mode 100644
index 0000000..919bba9
--- /dev/null
+++ b/test/pretty_print_test.dart
@@ -0,0 +1,198 @@
+// Copyright (c) 2013, 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.
+
+import 'package:unittest/unittest.dart' as ut;
+
+import 'package:matcher/matcher.dart';
+import 'package:matcher/src/pretty_print.dart';
+
+void main() {
+ ut.test('with primitive objects', () {
+ expect(prettyPrint(12), equals('<12>'));
+ expect(prettyPrint(12.13), equals('<12.13>'));
+ expect(prettyPrint(true), equals('<true>'));
+ expect(prettyPrint(null), equals('<null>'));
+ expect(prettyPrint(() => 12),
+ matches(r'<Closure(: \(\) => dynamic)?>'));
+ });
+
+ ut.group('with a string', () {
+ ut.test('containing simple characters', () {
+ expect(prettyPrint('foo'), equals("'foo'"));
+ });
+
+ ut.test('containing newlines', () {
+ expect(prettyPrint('foo\nbar\nbaz'), equals(
+ "'foo\\n'\n"
+ " 'bar\\n'\n"
+ " 'baz'"));
+ });
+
+ ut.test('containing escapable characters', () {
+ expect(prettyPrint("foo\rbar\tbaz'qux"),
+ equals("'foo\\rbar\\tbaz\\'qux'"));
+ });
+ });
+
+ ut.group('with an iterable', () {
+ ut.test('containing primitive objects', () {
+ expect(prettyPrint([1, true, 'foo']), equals("[1, true, 'foo']"));
+ });
+
+ ut.test('containing a multiline string', () {
+ expect(prettyPrint(['foo', 'bar\nbaz\nbip', 'qux']), equals("[\n"
+ " 'foo',\n"
+ " 'bar\\n'\n"
+ " 'baz\\n'\n"
+ " 'bip',\n"
+ " 'qux'\n"
+ "]"));
+ });
+
+ ut.test('containing a matcher', () {
+ expect(prettyPrint(['foo', endsWith('qux')]),
+ equals("['foo', <a string ending with 'qux'>]"));
+ });
+
+ ut.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]"));
+ });
+
+ ut.test("that's over maxLineLength", () {
+ expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 29),
+ equals("[\n"
+ " 0,\n"
+ " 1,\n"
+ " 2,\n"
+ " 3,\n"
+ " 4,\n"
+ " 5,\n"
+ " 6,\n"
+ " 7,\n"
+ " 8,\n"
+ " 9\n"
+ "]"));
+ });
+
+ ut.test("factors indentation into maxLineLength", () {
+ expect(prettyPrint([
+ "foo\nbar",
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ ], maxLineLength: 30), equals("[\n"
+ " 'foo\\n'\n"
+ " 'bar',\n"
+ " [\n"
+ " 0,\n"
+ " 1,\n"
+ " 2,\n"
+ " 3,\n"
+ " 4,\n"
+ " 5,\n"
+ " 6,\n"
+ " 7,\n"
+ " 8,\n"
+ " 9\n"
+ " ]\n"
+ "]"));
+ });
+
+ ut.test("that's under maxItems", () {
+ expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 10),
+ equals("[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"));
+ });
+
+ ut.test("that's over maxItems", () {
+ expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 9),
+ equals("[0, 1, 2, 3, 4, 5, 6, 7, ...]"));
+ });
+
+ ut.test("that's recursive", () {
+ var list = [1, 2, 3];
+ list.add(list);
+ expect(prettyPrint(list), equals("[1, 2, 3, (recursive)]"));
+ });
+ });
+
+ ut.group("with a map", () {
+ ut.test('containing primitive objects', () {
+ expect(prettyPrint({'foo': 1, 'bar': true}),
+ equals("{'foo': 1, 'bar': true}"));
+ });
+
+ ut.test('containing a multiline string key', () {
+ expect(prettyPrint({'foo\nbar': 1, 'bar': true}), equals("{\n"
+ " 'foo\\n'\n"
+ " 'bar': 1,\n"
+ " 'bar': true\n"
+ "}"));
+ });
+
+ ut.test('containing a multiline string value', () {
+ expect(prettyPrint({'foo': 'bar\nbaz', 'qux': true}), equals("{\n"
+ " 'foo': 'bar\\n'\n"
+ " 'baz',\n"
+ " 'qux': true\n"
+ "}"));
+ });
+
+ ut.test('containing a multiline string key/value pair', () {
+ expect(prettyPrint({'foo\nbar': 'baz\nqux'}), equals("{\n"
+ " 'foo\\n'\n"
+ " 'bar': 'baz\\n'\n"
+ " 'qux'\n"
+ "}"));
+ });
+
+ ut.test('containing a matcher key', () {
+ expect(prettyPrint({endsWith('bar'): 'qux'}),
+ equals("{<a string ending with 'bar'>: 'qux'}"));
+ });
+
+ ut.test('containing a matcher value', () {
+ expect(prettyPrint({'foo': endsWith('qux')}),
+ equals("{'foo': <a string ending with 'qux'>}"));
+ });
+
+ ut.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}"));
+ });
+
+ ut.test("that's over maxLineLength", () {
+ expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 31),
+ equals("{\n"
+ " '0': 1,\n"
+ " '2': 3,\n"
+ " '4': 5,\n"
+ " '6': 7\n"
+ "}"));
+ });
+
+ ut.test("factors indentation into maxLineLength", () {
+ expect(prettyPrint(["foo\nbar", {'0': 1, '2': 3, '4': 5, '6': 7}],
+ maxLineLength: 32),
+ equals("[\n"
+ " 'foo\\n'\n"
+ " 'bar',\n"
+ " {\n"
+ " '0': 1,\n"
+ " '2': 3,\n"
+ " '4': 5,\n"
+ " '6': 7\n"
+ " }\n"
+ "]"));
+ });
+
+ ut.test("that's under maxItems", () {
+ expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 4),
+ equals("{'0': 1, '2': 3, '4': 5, '6': 7}"));
+ });
+
+ ut.test("that's over maxItems", () {
+ expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 3),
+ equals("{'0': 1, '2': 3, ...}"));
+ });
+ });
+}
diff --git a/test/pretty_print_unminified_test.dart b/test/pretty_print_unminified_test.dart
new file mode 100644
index 0000000..5f46fd7
--- /dev/null
+++ b/test/pretty_print_unminified_test.dart
@@ -0,0 +1,59 @@
+// 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.
+
+// This file is for pretty-print tests that rely on the names of various Dart
+// types. These tests will fail when run in minified dart2js, since the names
+// will be mangled. A version of this file that works in minified dart2js is in
+// pretty_print_minified_test.dart.
+
+import 'dart:collection';
+import 'package:unittest/unittest.dart' as ut;
+
+import 'package:matcher/matcher.dart';
+import 'package:matcher/src/pretty_print.dart';
+
+class DefaultToString {}
+
+class CustomToString {
+ String toString() => "string representation";
+}
+
+class _PrivateName {
+ String toString() => "string representation";
+}
+
+class _PrivateNameIterable extends IterableMixin {
+ Iterator get iterator => [1, 2, 3].iterator;
+}
+
+void main() {
+ ut.group('with an object', () {
+ ut.test('with a default [toString]', () {
+ expect(prettyPrint(new DefaultToString()),
+ equals("<Instance of 'DefaultToString'>"));
+ });
+
+ ut.test('with a custom [toString]', () {
+ expect(prettyPrint(new CustomToString()),
+ equals('CustomToString:<string representation>'));
+ });
+
+ ut.test('with a custom [toString] and a private name', () {
+ expect(prettyPrint(new _PrivateName()),
+ equals('?:<string representation>'));
+ });
+ });
+
+ ut.group('with an iterable', () {
+ ut.test("that's not a list", () {
+ expect(prettyPrint([1, 2, 3, 4].map((n) => n * 2)),
+ equals("MappedListIterable:[2, 4, 6, 8]"));
+ });
+
+ ut.test("that's not a list and has a private name", () {
+ expect(prettyPrint(new _PrivateNameIterable()),
+ equals("?:[1, 2, 3]"));
+ });
+ });
+}
diff --git a/test/test_common.dart b/test/test_common.dart
new file mode 100644
index 0000000..291033c
--- /dev/null
+++ b/test/test_common.dart
@@ -0,0 +1,59 @@
+// 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.
+
+library test_common;
+
+import 'dart:collection';
+
+import 'package:matcher/matcher.dart';
+
+class Widget {
+ int price;
+}
+
+class HasPrice extends CustomMatcher {
+ HasPrice(matcher) :
+ super("Widget with a price that is", "price", matcher);
+ featureValueOf(actual) => actual.price;
+}
+
+class SimpleIterable extends IterableBase {
+ int count;
+ SimpleIterable(this.count);
+
+ bool contains(int val) => count < val ? false : true;
+
+ bool any(bool f(element)) {
+ for (var i = 0; i <= count; i++) {
+ if (f(i)) return true;
+ }
+ return false;
+ }
+
+ String toString() => "<[$count]>";
+
+ Iterator get iterator {
+ return new SimpleIterator(count);
+ }
+}
+
+class SimpleIterator implements Iterator {
+ int _count;
+ int _current;
+
+ SimpleIterator(this._count);
+
+ bool moveNext() {
+ if (_count > 0) {
+ _current = _count;
+ _count--;
+ return true;
+ }
+ _current = null;
+ return false;
+ }
+
+ get current => _current;
+}
+
diff --git a/test/test_utils.dart b/test/test_utils.dart
new file mode 100644
index 0000000..fba527a
--- /dev/null
+++ b/test/test_utils.dart
@@ -0,0 +1,82 @@
+// 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.
+
+library test_utils;
+
+import 'package:matcher/matcher.dart';
+import 'package:unittest/unittest.dart' as ut;
+
+import 'dart:async';
+
+int errorCount;
+String errorString;
+var _testHandler = null;
+
+class MyFailureHandler extends DefaultFailureHandler {
+ void fail(String reason) {
+ ++errorCount;
+ errorString = reason;
+ }
+}
+
+void initUtils() {
+ if (_testHandler == null) {
+ _testHandler = new MyFailureHandler();
+ }
+}
+
+void shouldFail(value, Matcher matcher, expected, {bool isAsync: false}) {
+ configureExpectFailureHandler(_testHandler);
+ errorCount = 0;
+ errorString = '';
+ expect(value, matcher);
+ afterTest() {
+ configureExpectFailureHandler(null);
+ expect(errorCount, equals(1));
+ if (expected is String) {
+ expect(errorString, equalsIgnoringWhitespace(expected));
+ } else {
+ expect(errorString.replaceAll('\n', ''), expected);
+ }
+ }
+
+ if (isAsync) {
+ Timer.run(ut.expectAsync(afterTest));
+ } else {
+ afterTest();
+ }
+}
+
+void shouldPass(value, Matcher matcher, {bool isAsync: false}) {
+ configureExpectFailureHandler(_testHandler);
+ errorCount = 0;
+ errorString = '';
+ expect(value, matcher);
+ afterTest() {
+ configureExpectFailureHandler(null);
+ expect(errorCount, equals(0));
+ }
+ if (isAsync) {
+ Timer.run(ut.expectAsync(afterTest));
+ } else {
+ afterTest();
+ }
+}
+
+doesNotThrow() {}
+doesThrow() { throw 'X'; }
+
+class PrefixMatcher extends Matcher {
+ final String _prefix;
+ const PrefixMatcher(this._prefix);
+ bool matches(item, Map matchState) {
+ return item is String &&
+ (collapseWhitespace(item)).startsWith(collapseWhitespace(_prefix));
+ }
+
+ Description describe(Description description) =>
+ description.add('a string starting with ').
+ addDescriptionOf(collapseWhitespace(_prefix)).
+ add(' ignoring whitespace');
+}