Merge branch 'master' into cleanup
diff --git a/lib/src/invocation_matcher.dart b/lib/src/invocation_matcher.dart
new file mode 100644
index 0000000..c176a1b
--- /dev/null
+++ b/lib/src/invocation_matcher.dart
@@ -0,0 +1,167 @@
+import 'package:collection/collection.dart';
+import 'package:matcher/matcher.dart';
+import 'package:meta/meta.dart';
+
+/// Returns a matcher that expects an invocation that matches arguments given.
+///
+/// Both [positionalArguments] and [namedArguments] can also be [Matcher]s:
+///     // Expects an invocation of "foo(String a, bool b)" where "a" must be
+///     // the value 'hello' but "b" may be any value. This would match both
+///     // foo('hello', true), foo('hello', false), and foo('hello', null).
+///     expect(fooInvocation, invokes(
+///       #foo,
+///       positionalArguments: ['hello', any]
+///     ));
+///
+/// Suitable for use in mocking libraries, where `noSuchMethod` can be used to
+/// get a handle to attempted [Invocation] objects and then compared against
+/// what a user expects to be called.
+Matcher invokes(
+  Symbol memberName, {
+  List positionalArguments: const [],
+  Map<Symbol, dynamic> namedArguments: const {},
+  bool isGetter: false,
+  bool isSetter: false,
+}) {
+  if (isGetter && isSetter) {
+    throw new ArgumentError('Cannot set isGetter and iSetter');
+  }
+  if (positionalArguments == null) {
+    throw new ArgumentError.notNull('positionalArguments');
+  }
+  if (namedArguments == null) {
+    throw new ArgumentError.notNull('namedArguments');
+  }
+  return new _InvocationMatcher(new _InvocationSignature(
+    memberName: memberName,
+    positionalArguments: positionalArguments,
+    namedArguments: namedArguments,
+    isGetter: isGetter,
+    isSetter: isSetter,
+  ));
+}
+
+/// Returns a matcher that matches the name and arguments of an [invocation].
+///
+/// To expect the same _signature_ see [invokes].
+Matcher isInvocation(Invocation invocation) =>
+    new _InvocationMatcher(invocation);
+
+class _InvocationSignature extends Invocation {
+  @override
+  final Symbol memberName;
+
+  @override
+  final List positionalArguments;
+
+  @override
+  final Map<Symbol, dynamic> namedArguments;
+
+  @override
+  final bool isGetter;
+
+  @override
+  final bool isSetter;
+
+  _InvocationSignature({
+    @required this.memberName,
+    this.positionalArguments: const [],
+    this.namedArguments: const {},
+    this.isGetter: false,
+    this.isSetter: false,
+  });
+
+  @override
+  bool get isMethod => !isAccessor;
+}
+
+class _InvocationMatcher implements Matcher {
+  static Description _describeInvocation(Description d, Invocation invocation) {
+    // For a getter or a setter, just return get <member> or set <member> <arg>.
+    if (invocation.isAccessor) {
+      d = d
+          .add(invocation.isGetter ? 'get ' : 'set ')
+          .add(_symbolToString(invocation.memberName));
+      if (invocation.isSetter) {
+        d = d.add(' ').addDescriptionOf(invocation.positionalArguments.first);
+      }
+      return d;
+    }
+    // For a method, return <member>(<args>).
+    d = d
+        .add(_symbolToString(invocation.memberName))
+        .add('(')
+        .addAll('', ', ', '', invocation.positionalArguments);
+    if (invocation.positionalArguments.isNotEmpty &&
+        invocation.namedArguments.isNotEmpty) {
+      d = d.add(', ');
+    }
+    // Also added named arguments, if any.
+    return d.addAll('', ', ', '', _namedArgsAndValues(invocation)).add(')');
+  }
+
+  // Returns named arguments as an iterable of '<name>: <value>'.
+  static Iterable<String> _namedArgsAndValues(Invocation invocation) =>
+      invocation.namedArguments.keys.map/*<String>*/((name) =>
+          '${_symbolToString(name)}: ${invocation.namedArguments[name]}');
+
+  // This will give is a mangled symbol in dart2js/aot with minification
+  // enabled, but it's safe to assume very few people will use the invocation
+  // matcher in a production test anyway due to noSuchMethod.
+  static String _symbolToString(Symbol symbol) {
+    return symbol.toString().split('"')[1];
+  }
+
+  final Invocation _invocation;
+
+  _InvocationMatcher(this._invocation) {
+    if (_invocation == null) {
+      throw new ArgumentError.notNull();
+    }
+  }
+
+  @override
+  Description describe(Description d) => _describeInvocation(d, _invocation);
+
+  // TODO(matanl): Better implement describeMismatch and use state from matches.
+  // Specifically, if a Matcher is passed as an argument, we'd like to get an
+  // error like "Expected fly(miles: > 10), Actual: fly(miles: 5)".
+  @override
+  Description describeMismatch(item, Description d, _, __) {
+    if (item is Invocation) {
+      d = d.add('Does not match ');
+      return _describeInvocation(d, item);
+    }
+    return d.add('Is not an Invocation');
+  }
+
+  @override
+  bool matches(item, _) =>
+      item is Invocation &&
+      _invocation.memberName == item.memberName &&
+      _invocation.isSetter == item.isSetter &&
+      _invocation.isGetter == item.isGetter &&
+      const ListEquality(const _MatcherEquality())
+          .equals(_invocation.positionalArguments, item.positionalArguments) &&
+      const MapEquality(values: const _MatcherEquality())
+          .equals(_invocation.namedArguments, item.namedArguments);
+}
+
+class _MatcherEquality extends DefaultEquality /* <Matcher | E> */ {
+  const _MatcherEquality();
+
+  @override
+  bool equals(e1, e2) {
+    if (e1 is Matcher && e2 is! Matcher) {
+      return e1.matches(e2, const {});
+    }
+    if (e2 is Matcher && e1 is! Matcher) {
+      return e2.matches(e1, const {});
+    }
+    return super.equals(e1, e2);
+  }
+
+  // We force collisions on every value so equals() is called.
+  @override
+  int hash(_) => 0;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 07298d5..cd508e3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: mockito
-version: 1.0.1+1
+version: 1.0.1+2
 authors:
   - Dmitriy Fibulwinter <fibulwinter@gmail.com>
   - Ted Sander <tsander@google.com>
@@ -10,5 +10,6 @@
 environment:
   sdk: '>=1.0.0 <2.0.0'
 dependencies:
-  meta: '>=1.0.4 <2.0.0'
+  matcher: '^0.12.0'
+  meta: '^1.0.4'
   test: '>=0.12.0 <0.13.0'
diff --git a/test/invocation_matcher_test.dart b/test/invocation_matcher_test.dart
new file mode 100644
index 0000000..d148196
--- /dev/null
+++ b/test/invocation_matcher_test.dart
@@ -0,0 +1,158 @@
+import 'package:mockito/src/invocation_matcher.dart';
+import 'package:test/test.dart';
+
+Invocation lastInvocation;
+
+void main() {
+  const stub = const Stub();
+
+  group('$isInvocation', () {
+    test('positional arguments', () {
+      var call1 = stub.say('Hello');
+      var call2 = stub.say('Hello');
+      var call3 = stub.say('Guten Tag');
+      shouldPass(call1, isInvocation(call2));
+      shouldFail(
+        call1,
+        isInvocation(call3),
+        "Expected: say('Guten Tag') "
+            "Actual: <Instance of '${call3.runtimeType}'> "
+            "Which: Does not match say('Hello')",
+      );
+    });
+
+    test('named arguments', () {
+      var call1 = stub.eat('Chicken', alsoDrink: true);
+      var call2 = stub.eat('Chicken', alsoDrink: true);
+      var call3 = stub.eat('Chicken', alsoDrink: false);
+      shouldPass(call1, isInvocation(call2));
+      shouldFail(
+        call1,
+        isInvocation(call3),
+        "Expected: eat('Chicken', 'alsoDrink: false') "
+            "Actual: <Instance of '${call3.runtimeType}'> "
+            "Which: Does not match eat('Chicken', 'alsoDrink: true')",
+      );
+    });
+
+    test('optional arguments', () {
+      var call1 = stub.lie(true);
+      var call2 = stub.lie(true);
+      var call3 = stub.lie(false);
+      shouldPass(call1, isInvocation(call2));
+      shouldFail(
+        call1,
+        isInvocation(call3),
+        "Expected: lie(<false>) "
+            "Actual: <Instance of '${call3.runtimeType}'> "
+            "Which: Does not match lie(<true>)",
+      );
+    });
+
+    test('getter', () {
+      var call1 = stub.value;
+      var call2 = stub.value;
+      stub.value = true;
+      var call3 = Stub.lastInvocation;
+      shouldPass(call1, isInvocation(call2));
+      shouldFail(
+        call1,
+        isInvocation(call3),
+        "Expected: set value= <true> "
+            "Actual: <Instance of '${call3.runtimeType}'> "
+            "Which: Does not match get value",
+      );
+    });
+
+    test('setter', () {
+      stub.value = true;
+      var call1 = Stub.lastInvocation;
+      stub.value = true;
+      var call2 = Stub.lastInvocation;
+      stub.value = false;
+      var call3 = Stub.lastInvocation;
+      shouldPass(call1, isInvocation(call2));
+      shouldFail(
+        call1,
+        isInvocation(call3),
+        "Expected: set value= <false> "
+            "Actual: <Instance of '${call3.runtimeType}'> "
+            "Which: Does not match set value= <true>",
+      );
+    });
+  });
+
+  group('$invokes', () {
+    test('positional arguments', () {
+      var call = stub.say('Hello');
+      shouldPass(call, invokes(#say, positionalArguments: ['Hello']));
+      shouldPass(call, invokes(#say, positionalArguments: [anything]));
+      shouldFail(
+        call,
+        invokes(#say, positionalArguments: [isNull]),
+        "Expected: say(null) "
+            "Actual: <Instance of '${call.runtimeType}'> "
+            "Which: Does not match say('Hello')",
+      );
+    });
+
+    test('named arguments', () {
+      var call = stub.fly(miles: 10);
+      shouldPass(call, invokes(#fly, namedArguments: {#miles: 10}));
+      shouldPass(call, invokes(#fly, namedArguments: {#miles: greaterThan(5)}));
+      shouldFail(
+        call,
+        invokes(#fly, namedArguments: {#miles: 11}),
+        "Expected: fly('miles: 11') "
+            "Actual: <Instance of '${call.runtimeType}'> "
+            "Which: Does not match fly('miles: 10')",
+      );
+    });
+  });
+}
+
+abstract class Interface {
+  bool get value;
+  set value(value);
+  say(String text);
+  eat(String food, {bool alsoDrink});
+  lie([bool facingDown]);
+  fly({int miles});
+}
+
+/// An example of a class that captures Invocation objects.
+///
+/// Any call always returns an [Invocation].
+class Stub implements Interface {
+  static Invocation lastInvocation;
+
+  const Stub();
+
+  @override
+  noSuchMethod(Invocation invocation) => lastInvocation = invocation;
+}
+
+// Copied from package:test, which doesn't expose it to users.
+// https://github.com/dart-lang/matcher/issues/39
+void shouldFail(value, Matcher matcher, expected) {
+  var failed = false;
+  try {
+    expect(value, matcher);
+  } on TestFailure catch (err) {
+    failed = true;
+
+    var _errorString = err.message;
+
+    if (expected is String) {
+      expect(_errorString, equalsIgnoringWhitespace(expected));
+    } else {
+      expect(_errorString.replaceAll('\n', ''), expected);
+    }
+  }
+
+  expect(failed, isTrue, reason: 'Expected to fail.');
+}
+
+void shouldPass(value, Matcher matcher) {
+  expect(value, matcher);
+}
diff --git a/test/mockito_test.dart b/test/mockito_test.dart
index 2e07913..2dd4b9b 100644
--- a/test/mockito_test.dart
+++ b/test/mockito_test.dart
@@ -13,11 +13,13 @@
   String methodWithObjArgs(RealClass x) => "Real";
   // "SpecialArgs" here means type-parameterized args. But that makes for a long
   // method name.
-  String typeParameterizedFn(
-      List<int> w, List<int> x, [List<int> y, List<int> z]) => "Real";
+  String typeParameterizedFn(List<int> w, List<int> x,
+          [List<int> y, List<int> z]) =>
+      "Real";
   // "SpecialNamedArgs" here means type-parameterized, named args. But that
   // makes for a long method name.
-  String typeParameterizedNamedFn(List<int> w, List<int> x, {List<int> y, List<int> z}) =>
+  String typeParameterizedNamedFn(List<int> w, List<int> x,
+          {List<int> y, List<int> z}) =>
       "Real";
   String get getter => "Real";
   void set setter(String arg) {
@@ -85,7 +87,6 @@
     });
   });
 
-
   group("mixin support", () {
     test("should work", () {
       var foo = new MockFoo();
@@ -143,16 +144,16 @@
       expect(mock.methodWithListArgs([42]), equals("A lot!"));
       expect(mock.methodWithListArgs([43]), equals("A lot!"));
     });
-    test("should mock method with multiple named args and matchers", (){
+    test("should mock method with multiple named args and matchers", () {
       when(mock.methodWithTwoNamedArgs(any, y: any)).thenReturn("x y");
       when(mock.methodWithTwoNamedArgs(any, z: any)).thenReturn("x z");
       expect(mock.methodWithTwoNamedArgs(42), isNull);
-      expect(mock.methodWithTwoNamedArgs(42, y:18), equals("x y"));
-      expect(mock.methodWithTwoNamedArgs(42, z:17), equals("x z"));
-      expect(mock.methodWithTwoNamedArgs(42, y:18, z:17), isNull);
+      expect(mock.methodWithTwoNamedArgs(42, y: 18), equals("x y"));
+      expect(mock.methodWithTwoNamedArgs(42, z: 17), equals("x z"));
+      expect(mock.methodWithTwoNamedArgs(42, y: 18, z: 17), isNull);
       when(mock.methodWithTwoNamedArgs(any, y: any, z: any))
           .thenReturn("x y z");
-      expect(mock.methodWithTwoNamedArgs(42, y:18, z:17), equals("x y z"));
+      expect(mock.methodWithTwoNamedArgs(42, y: 18, z: 17), equals("x y z"));
     });
     test("should mock method with mix of argument matchers and real things",
         () {
@@ -231,17 +232,19 @@
           .thenReturn("A lot!");
       expect(mock.typeParameterizedFn([42], [43], [44]), equals("A lot!"));
     });
-    test("should mock method with an optional typed arg matcher and an optional real arg", () {
+    test(
+        "should mock method with an optional typed arg matcher and an optional real arg",
+        () {
       when(mock.typeParameterizedFn(typed(any), typed(any), [44], typed(any)))
           .thenReturn("A lot!");
-      expect(mock.typeParameterizedFn([42], [43], [44], [45]), equals("A lot!"));
+      expect(
+          mock.typeParameterizedFn([42], [43], [44], [45]), equals("A lot!"));
     });
     test("should mock method with only some typed arg matchers", () {
       when(mock.typeParameterizedFn(typed(any), [43], typed(any)))
           .thenReturn("A lot!");
       expect(mock.typeParameterizedFn([42], [43], [44]), equals("A lot!"));
-      when(mock.typeParameterizedFn(typed(any), [43]))
-          .thenReturn("A bunch!");
+      when(mock.typeParameterizedFn(typed(any), [43])).thenReturn("A bunch!");
       expect(mock.typeParameterizedFn([42], [43]), equals("A bunch!"));
     });
     test("should throw when [typed] used alongside [null].", () {
@@ -252,47 +255,51 @@
     });
     test("should mock method when [typed] used alongside matched [null].", () {
       when(mock.typeParameterizedFn(
-          typed(any), argThat(equals(null)), typed(any)))
+              typed(any), argThat(equals(null)), typed(any)))
           .thenReturn("A lot!");
       expect(mock.typeParameterizedFn([42], null, [44]), equals("A lot!"));
     });
     test("should mock method with named, typed arg matcher", () {
-      when(mock.typeParameterizedNamedFn(
-          typed(any), [43], y: typed(any, named: "y")))
+      when(mock.typeParameterizedNamedFn(typed(any), [43],
+              y: typed(any, named: "y")))
           .thenReturn("A lot!");
-      expect(mock.typeParameterizedNamedFn([42], [43], y: [44]), equals("A lot!"));
+      expect(
+          mock.typeParameterizedNamedFn([42], [43], y: [44]), equals("A lot!"));
     });
-    test("should mock method with named, typed arg matcher and an arg matcher", () {
-      when(
-          mock.typeParameterizedNamedFn(
-              typed(any), [43],
+    test("should mock method with named, typed arg matcher and an arg matcher",
+        () {
+      when(mock.typeParameterizedNamedFn(typed(any), [43],
               y: typed(any, named: "y"), z: argThat(contains(45))))
           .thenReturn("A lot!");
       expect(mock.typeParameterizedNamedFn([42], [43], y: [44], z: [45]),
           equals("A lot!"));
     });
-    test("should mock method with named, typed arg matcher and a regular arg", () {
-      when(
-          mock.typeParameterizedNamedFn(
-              typed(any), [43],
-              y: typed(any, named: "y"), z: [45]))
-          .thenReturn("A lot!");
+    test("should mock method with named, typed arg matcher and a regular arg",
+        () {
+      when(mock.typeParameterizedNamedFn(typed(any), [43],
+          y: typed(any, named: "y"), z: [45])).thenReturn("A lot!");
       expect(mock.typeParameterizedNamedFn([42], [43], y: [44], z: [45]),
           equals("A lot!"));
     });
     test("should throw when [typed] used as a named arg, without `named:`", () {
-      expect(() => when(mock.typeParameterizedNamedFn(
-          typed(any), [43], y: typed(any))),
+      expect(
+          () => when(
+              mock.typeParameterizedNamedFn(typed(any), [43], y: typed(any))),
           throwsArgumentError);
     });
-    test("should throw when [typed] used as a positional arg, with `named:`", () {
-      expect(() => when(mock.typeParameterizedNamedFn(
-          typed(any), typed(any, named: "y"))),
+    test("should throw when [typed] used as a positional arg, with `named:`",
+        () {
+      expect(
+          () => when(mock.typeParameterizedNamedFn(
+              typed(any), typed(any, named: "y"))),
           throwsArgumentError);
     });
-    test("should throw when [typed] used as a named arg, with the wrong `named:`", () {
-      expect(() => when(mock.typeParameterizedNamedFn(
-          typed(any), [43], y: typed(any, named: "z"))),
+    test(
+        "should throw when [typed] used as a named arg, with the wrong `named:`",
+        () {
+      expect(
+          () => when(mock.typeParameterizedNamedFn(typed(any), [43],
+              y: typed(any, named: "z"))),
           throwsArgumentError);
     });
   });
@@ -344,9 +351,9 @@
       mock.methodWithObjArgs(m1);
       expectFail(
           "No matching calls. All calls: MockedClass.methodWithObjArgs(m1)",
-              () {
-            verify(mock.methodWithObjArgs(new MockedClass()));
-          });
+          () {
+        verify(mock.methodWithObjArgs(new MockedClass()));
+      });
       verify(mock.methodWithObjArgs(m1));
     });
     test("should mock method with list args", () {
@@ -376,10 +383,15 @@
     test("should mock method with argument matcher and capturer", () {
       mock.methodWithNormalArgs(50);
       mock.methodWithNormalArgs(100);
-      expect(verify(mock.methodWithNormalArgs(
-          captureThat(greaterThan(75)))).captured.single, equals(100));
-      expect(verify(mock
-              .methodWithNormalArgs(captureThat(lessThan(75)))).captured.single,
+      expect(
+          verify(mock.methodWithNormalArgs(captureThat(greaterThan(75))))
+              .captured
+              .single,
+          equals(100));
+      expect(
+          verify(mock.methodWithNormalArgs(captureThat(lessThan(75))))
+              .captured
+              .single,
           equals(50));
     });
     test("should mock method with mix of argument matchers and real things",
@@ -417,9 +429,12 @@
     test("should verify method with argument capturer", () {
       mock.typeParameterizedFn([50], [17]);
       mock.typeParameterizedFn([100], [17]);
-      expect(verify(mock.typeParameterizedFn(
-          typed(captureAny), [17])).captured,
-          equals([[50], [100]]));
+      expect(
+          verify(mock.typeParameterizedFn(typed(captureAny), [17])).captured,
+          equals([
+            [50],
+            [100]
+          ]));
     });
   });
   group("verify() qualifies", () {
@@ -580,21 +595,22 @@
     });
     test("should captureOut list arguments", () {
       mock.methodWithListArgs([42]);
-      expect(verify(
-          mock.methodWithListArgs(captureAny)).captured.single,
+      expect(verify(mock.methodWithListArgs(captureAny)).captured.single,
           equals([42]));
     });
     test("should captureOut multiple arguments", () {
       mock.methodWithPositionalArgs(1, 2);
-      expect(verify(
-              mock.methodWithPositionalArgs(captureAny, captureAny)).captured,
+      expect(
+          verify(mock.methodWithPositionalArgs(captureAny, captureAny))
+              .captured,
           equals([1, 2]));
     });
     test("should captureOut with matching arguments", () {
       mock.methodWithPositionalArgs(1);
       mock.methodWithPositionalArgs(2, 3);
-      expect(verify(
-              mock.methodWithPositionalArgs(captureAny, captureAny)).captured,
+      expect(
+          verify(mock.methodWithPositionalArgs(captureAny, captureAny))
+              .captured,
           equals([2, 3]));
     });
     test("should captureOut multiple invocations", () {
diff --git a/tool/travis.sh b/tool/travis.sh
index 5387de5..e22fa98 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -6,8 +6,10 @@
 # Verify that the libraries are error free.
 dartanalyzer --fatal-warnings \
   lib/mockito.dart \
+  lib/mockito_no_mirrors.dart \
+  lib/src/invocation_matcher.dart \
   test/mockito_test.dart
 
 # Run the tests.
-dart test/mockito_test.dart
-
+dart -c test/invocation_matcher_test.dart
+dart -c test/mockito_test.dart