blob: 34c53874fa0afbb67e0168f1c9ffc5bfbecc63f8 [file] [log] [blame]
// Copyright (c) 2017, 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:file/record_replay.dart';
import 'package:file/src/backends/record_replay/common.dart'
show getSymbolName; // ignore: implementation_imports
import 'package:test/test.dart';
const Map<Type, String> _kTypeDescriptions = const <Type, String>{
MethodEvent: 'a method invocation',
PropertyGetEvent: 'a property retrieval',
PropertySetEvent: 'a property mutation',
};
const Map<Type, Matcher> _kTypeMatchers = const <Type, Matcher>{
MethodEvent: const isInstanceOf<MethodEvent<dynamic>>(),
PropertyGetEvent: const isInstanceOf<PropertyGetEvent<dynamic>>(),
PropertySetEvent: const isInstanceOf<PropertySetEvent<dynamic>>(),
};
/// Returns a matcher that will match against a [MethodEvent].
///
/// If [name] is specified, only method invocations of a matching method name
/// will successfully match. [name] may be a String, a predicate function,
/// or a [Matcher].
///
/// The returned [MethodInvocation] matcher can be used to further limit the
/// scope of the match (e.g. by invocation result, target object, etc).
MethodInvocation invokesMethod([dynamic name]) => new MethodInvocation._(name);
/// Returns a matcher that will match against a [PropertyGetEvent].
///
/// If [name] is specified, only property retrievals of a matching property name
/// will successfully match. [name] may be a String, a predicate function,
/// or a [Matcher].
///
/// The returned [PropertyGet] matcher can be used to further limit the
/// scope of the match (e.g. by property value, target object, etc).
PropertyGet getsProperty([dynamic name]) => new PropertyGet._(name);
/// Returns a matcher that will match against a [PropertySetEvent].
///
/// If [name] is specified, only property mutations of a matching property name
/// will successfully match. [name] may be a String, a predicate function,
/// or a [Matcher].
///
/// The returned [PropertySet] matcher can be used to further limit the
/// scope of the match (e.g. by property value, target object, etc).
PropertySet setsProperty([dynamic name]) => new PropertySet._(name);
/// A matcher that successfully matches against an instance of
/// [NoMatchingInvocationError].
const Matcher isNoMatchingInvocationError = const _NoMatchingInvocationError();
/// A matcher that successfully matches against a future or function
/// that throws a [NoMatchingInvocationError].
Matcher throwsNoMatchingInvocationError = throwsA(isNoMatchingInvocationError);
/// Base class for matchers that match against generic [InvocationEvent]
/// instances.
abstract class RecordedInvocation<T extends RecordedInvocation<T>>
extends Matcher {
final _Type _typeMatcher;
final List<Matcher> _fieldMatchers = <Matcher>[];
RecordedInvocation._(Type type) : _typeMatcher = new _Type(type);
/// Limits the scope of the match to invocations that occurred on the
/// specified target [object].
///
/// [object] may be an instance or a [Matcher]. If it is an instance, it will
/// be automatically wrapped in an equality matcher.
///
/// Returns this matcher for chaining.
T on(dynamic object) {
_fieldMatchers.add(new _Target(object));
return this;
}
/// Limits the scope of the match to invocations that produced the specified
/// [result].
///
/// For method invocations, this matches against the return value of the
/// method. For property retrievals, this matches against the value of the
/// property. Property mutations will always produce a `null` result, so
/// [PropertySet] will automatically call `withResult(null)` when it is
/// instantiated.
///
/// [result] may be an instance or a [Matcher]. If it is an instance, it will
/// be automatically wrapped in an equality matcher.
///
/// Returns this matcher for chaining.
T withResult(dynamic result) {
_fieldMatchers.add(new _Result(result));
return this;
}
/// Limits the scope of the match to invocations that were recorded with the
/// specified [timestamp].
///
/// [timestamp] may be an `int` or a [Matcher]. If it is an `int`, it will
/// be automatically wrapped in an equality matcher.
///
/// Returns this matcher for chaining.
T withTimestamp(dynamic timestamp) {
_fieldMatchers.add(new _Timestamp(timestamp));
return this;
}
/// @nodoc
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
if (!_typeMatcher.matches(item, matchState)) {
addStateInfo(matchState, <String, Matcher>{'matcher': _typeMatcher});
return false;
}
for (Matcher matcher in _fieldMatchers) {
if (!matcher.matches(item, matchState)) {
addStateInfo(matchState, <String, Matcher>{'matcher': matcher});
return false;
}
}
return true;
}
/// @nodoc
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
Matcher matcher = matchState['matcher'];
matcher.describeMismatch(item, description, matchState['state'], verbose);
return description;
}
/// @nodoc
@override
Description describe(Description description) {
String divider = '\n - ';
return _typeMatcher
.describe(description)
.add(':')
.addAll(divider, divider, '', _fieldMatchers);
}
}
/// Matchers that matches against [MethodEvent] instances.
///
/// Instances of this matcher are obtained by calling [invokesMethod]. Once
/// instantiated, callers may use this matcher to further qualify the scope
/// of their match.
class MethodInvocation extends RecordedInvocation<MethodInvocation> {
MethodInvocation._(dynamic methodName) : super._(MethodEvent) {
if (methodName != null) {
_fieldMatchers.add(new _MethodName(methodName));
}
}
/// Limits the scope of the match to method invocations that passed the
/// specified positional [arguments].
///
/// [arguments] may be a list instance or a [Matcher]. If it is a list, it
/// will be automatically wrapped in an equality matcher.
///
/// Returns this matcher for chaining.
MethodInvocation withPositionalArguments(dynamic arguments) {
_fieldMatchers.add(new _PositionalArguments(arguments));
return this;
}
/// Limits the scope of the match to method invocations that passed the
/// specified named argument.
///
/// The argument [value] may be an instance or a [Matcher]. If it is an
/// instance, it will be automatically wrapped in an equality matcher.
///
/// Returns this matcher for chaining.
MethodInvocation withNamedArgument(String name, dynamic value) {
_fieldMatchers.add(new _NamedArgument(name, value));
return this;
}
/// Limits the scope of the match to method invocations that specified no
/// named arguments.
///
/// Returns this matcher for chaining.
MethodInvocation withNoNamedArguments() {
_fieldMatchers.add(const _NoNamedArguments());
return this;
}
}
/// Matchers that matches against [PropertyGetEvent] instances.
///
/// Instances of this matcher are obtained by calling [getsProperty]. Once
/// instantiated, callers may use this matcher to further qualify the scope
/// of their match.
class PropertyGet extends RecordedInvocation<PropertyGet> {
PropertyGet._(dynamic propertyName) : super._(PropertyGetEvent) {
if (propertyName != null) {
_fieldMatchers.add(new _GetPropertyName(propertyName));
}
}
}
/// Matchers that matches against [PropertySetEvent] instances.
///
/// Instances of this matcher are obtained by calling [setsProperty]. Once
/// instantiated, callers may use this matcher to further qualify the scope
/// of their match.
class PropertySet extends RecordedInvocation<PropertySet> {
PropertySet._(dynamic propertyName) : super._(PropertySetEvent) {
withResult(null);
if (propertyName != null) {
_fieldMatchers.add(new _SetPropertyName(propertyName));
}
}
/// Limits the scope of the match to property mutations that set the property
/// to the specified [value].
///
/// [value] may be an instance or a [Matcher]. If it is an instance, it will
/// be automatically wrapped in an equality matcher.
///
/// Returns this matcher for chaining.
PropertySet toValue(dynamic value) {
_fieldMatchers.add(new _SetValue(value));
return this;
}
}
class _Target extends Matcher {
final Matcher _matcher;
_Target(dynamic target) : _matcher = wrapMatcher(target);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.object, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
description.add('was invoked on: ${item.object}');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(item.object, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description desc) {
desc.add('on object: ');
return _matcher.describe(desc);
}
}
class _Result extends Matcher {
final Matcher _matcher;
_Result(dynamic result) : _matcher = wrapMatcher(result);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.result, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
description.add('returned: ${item.result}');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(item.result, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description desc) {
desc.add('with result: ');
return _matcher.describe(desc);
}
}
class _Timestamp extends Matcher {
final Matcher _matcher;
_Timestamp(dynamic timestamp) : _matcher = wrapMatcher(timestamp);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.timestamp, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
description.add('has timestamp: ${item.timestamp}');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(item.timestamp, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description desc) {
desc.add('with timestamp: ');
return _matcher.describe(desc);
}
}
class _Type extends Matcher {
final Type type;
const _Type(this.type);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_kTypeMatchers[type].matches(item, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
Type type;
for (Type matchType in _kTypeMatchers.keys) {
Matcher matcher = _kTypeMatchers[matchType];
if (matcher.matches(item, <dynamic, dynamic>{})) {
type = matchType;
break;
}
}
if (type != null) {
description.add('is ').add(_kTypeDescriptions[type]);
} else {
description.add('is a ${item.runtimeType}');
}
return description;
}
@override
Description describe(Description desc) => desc.add(_kTypeDescriptions[type]);
}
class _MethodName extends Matcher {
final Matcher _matcher;
_MethodName(dynamic name) : _matcher = wrapMatcher(name);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(getSymbolName(item.method), matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
String methodName = getSymbolName(item.method);
description.add('invoked method: \'$methodName\'');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(methodName, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description desc) {
desc.add('method: ');
return _matcher.describe(desc);
}
}
class _PositionalArguments extends Matcher {
final Matcher _matcher;
_PositionalArguments(dynamic value) : _matcher = wrapMatcher(value);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.positionalArguments, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return _matcher.describeMismatch(
item.positionalArguments, description, matchState, verbose);
}
@override
Description describe(Description desc) {
desc.add('with positional arguments: ');
return _matcher.describe(desc);
}
}
class _NamedArgument extends Matcher {
final String name;
final dynamic value;
final Matcher _matcher;
_NamedArgument(String name, this.value)
: this.name = name,
_matcher = containsPair(new Symbol(name), value);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.namedArguments, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return _matcher.describeMismatch(
item.namedArguments, description, matchState, verbose);
}
@override
Description describe(Description description) =>
description.add('with named argument "$name" = $value');
}
class _NoNamedArguments extends Matcher {
final Matcher _matcher = isEmpty;
const _NoNamedArguments();
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.namedArguments, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return _matcher.describeMismatch(
item.namedArguments, description, matchState, verbose);
}
@override
Description describe(Description description) =>
description.add('with no named arguments');
}
class _GetPropertyName extends Matcher {
final Matcher _matcher;
_GetPropertyName(dynamic _name) : _matcher = wrapMatcher(_name);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(getSymbolName(item.property), matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
String propertyName = getSymbolName(item.property);
description.add('got property: \'$propertyName\'');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(propertyName, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description description) {
description.add('gets property: ');
return _matcher.describe(description);
}
}
class _SetPropertyName extends Matcher {
final Matcher _matcher;
_SetPropertyName(dynamic _name) : _matcher = wrapMatcher(_name);
/// Strips the trailing `=` off the symbol name to get the property name.
String _getPropertyName(dynamic item) {
String symbolName = getSymbolName(item.property);
return symbolName.substring(0, symbolName.length - 1);
}
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
return _matcher.matches(_getPropertyName(item), matchState);
}
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
String propertyName = _getPropertyName(item);
description.add('set property: \'$propertyName\'');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(propertyName, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description description) {
description.add('of property: ');
return _matcher.describe(description);
}
}
class _SetValue extends Matcher {
final Matcher _matcher;
_SetValue(dynamic value) : _matcher = wrapMatcher(value);
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
_matcher.matches(item.value, matchState);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
description.add('set value: ${item.value}');
Description matcherDesc = new StringDescription();
_matcher.describeMismatch(item.value, matcherDesc, matchState, verbose);
if (matcherDesc.length > 0) {
description.add('\n Which: ').add(matcherDesc.toString());
}
return description;
}
@override
Description describe(Description description) {
description.add('to value: ');
return _matcher.describe(description);
}
}
class _NoMatchingInvocationError extends TypeMatcher<dynamic> {
const _NoMatchingInvocationError() : super("NoMatchingInvocationError");
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
item is NoMatchingInvocationError;
}