| // Copyright 2016 Dart Mockito authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import 'package:collection/collection.dart'; |
| import 'package:matcher/matcher.dart'; |
| import 'package:mockito/src/mock.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<dynamic> positionalArguments = const [], |
| Map<Symbol, dynamic> namedArguments = const {}, |
| bool isGetter = false, |
| bool isSetter = false, |
| }) { |
| if (isGetter && isSetter) { |
| throw ArgumentError('Cannot set isGetter and iSetter'); |
| } |
| return _InvocationMatcher(_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) => _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((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); |
| |
| @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<dynamic /* Matcher | E */ >(_MatcherEquality()) |
| .equals(_invocation.positionalArguments, item.positionalArguments) && |
| const MapEquality<dynamic, dynamic /* Matcher | E */ >( |
| values: _MatcherEquality()) |
| .equals(_invocation.namedArguments, item.namedArguments); |
| } |
| |
| // Uses both DeepCollectionEquality and custom matching for invocation matchers. |
| class _MatcherEquality extends DeepCollectionEquality /* <Matcher | E> */ { |
| const _MatcherEquality(); |
| |
| @override |
| bool equals(e1, e2) { |
| // All argument matches are wrapped in `ArgMatcher`, so we have to unwrap |
| // them into the raw `Matcher` type in order to finish our equality checks. |
| if (e1 is ArgMatcher) { |
| e1 = e1.matcher; |
| } |
| if (e2 is ArgMatcher) { |
| e2 = e2.matcher; |
| } |
| if (e1 is Matcher && e2 is! Matcher) { |
| return e1.matches(e2, {}); |
| } |
| if (e2 is Matcher && e1 is! Matcher) { |
| return e2.matches(e1, {}); |
| } |
| return super.equals(e1, e2); |
| } |
| |
| // We force collisions on every value so equals() is called. |
| @override |
| int hash(_) => 0; |
| } |