blob: da8d07444a5351728401fb84ef7faedfec611837 [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 'dart:convert';
import 'package:meta/meta.dart';
import 'codecs.dart';
import 'common.dart';
import 'errors.dart';
import 'proxy.dart';
typedef bool _InvocationMatcher(Map<String, dynamic> entry);
/// Used to record the order in which invocations were replayed.
/// Tests can later check expectations about the order in which invocations
/// were replayed vis-a-vis the order in which they were recorded.
int _nextOrdinal = 0;
/// Mixin that enables replaying of property accesses, property mutations, and
/// method invocations from a prior recording.
/// This class uses `noSuchMethod` to replay a well-defined set of invocations
/// (including property gets and sets) on an object. Subclasses wire this up by
/// doing the following:
/// - Populate the list of method invocations to replay in the [methods] map.
/// - Populate the list of property invocations to replay in the [properties]
/// map. The symbol name for getters should be the property name, and the
/// symbol name for setters should be the property name immediately
/// followed by an equals sign (e.g. `propertyName=`).
/// - Do not implement a concrete getter, setter, or method that you wish to
/// replay, as doing so will circumvent the machinery that this mixin uses
/// (`noSuchMethod`) to replay invocations.
/// **Example use**:
/// abstract class Foo {
/// ComplexObject sampleMethod();
/// Foo sampleParent;
/// }
/// class ReplayFoo extends Object with ReplayProxyMixin implements Foo {
/// final List<Map<String, dynamic>> manifest;
/// final String identifier;
/// ReplayFoo(this.manifest, this.identifier) {
/// methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
/// #sampleMethod: complexObjectReviver,
/// });
/// properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
/// #sampleParent: fooReviver,
/// const Symbol('sampleParent='): passthroughReviver,
/// });
/// }
/// }
mixin ReplayProxyMixin on Object implements ProxyObject, ReplayAware {
/// Maps method names to [Converter]s that will revive result values.
/// Invocations of methods listed in this map will be replayed by looking for
/// matching invocations in the [manifest] and reviving the invocation return
/// value using the [Converter] found in this map.
final Map<Symbol, Converter<dynamic, dynamic>> methods =
<Symbol, Converter<dynamic, dynamic>>{};
/// Maps property getter and setter names to [Converter]s that will revive
/// result values.
/// Access and mutation of properties listed in this map will be replayed
/// by looking for matching property accesses in the [manifest] and reviving
/// the invocation return value using the [Converter] found in this map.
/// The keys for property getters are the simple property names, whereas the
/// keys for property setters are the property names followed by an equals
/// sign (e.g. `propertyName=`).
final Map<Symbol, Converter<dynamic, dynamic>> properties =
<Symbol, Converter<dynamic, dynamic>>{};
/// The manifest of recorded invocation events.
/// When invocations are received on this object, we will attempt find a
/// matching invocation in this manifest to perform the replay. If no such
/// invocation is found (or if it has already been replayed), the caller will
/// receive a [NoMatchingInvocationError].
/// This manifest exists as `MANIFEST.txt` in a recording directory.
List<Map<String, dynamic>> get manifest;
/// Protected method for subclasses to be notified when an invocation has
/// been successfully replayed, and the result is about to be returned to
/// the caller.
/// Returns the value that is to be returned to the caller. The default
/// implementation returns [result] (replayed from the recording); subclasses
/// may override this method to alter the result that's returned to the
/// caller.
dynamic onResult(Invocation invocation, dynamic result) => result;
dynamic noSuchMethod(Invocation invocation) {
Symbol name = invocation.memberName;
Converter<dynamic, dynamic> reviver =
invocation.isAccessor ? properties[name] : methods[name];
if (reviver == null) {
// No reviver generally means that there truly is no such method on
// this object. The exception is when the invocation represents a getter
// on a method, in which case we return a method proxy that, when
// invoked, will replay the desired invocation.
return invocation.isGetter && methods[name] != null
? new MethodProxy(this, name)
: super.noSuchMethod(invocation);
Map<String, dynamic> entry = _nextEvent(invocation);
if (entry == null) {
throw new NoMatchingInvocationError(invocation);
entry[kManifestOrdinalKey] = _nextOrdinal++;
dynamic error = entry[kManifestErrorKey];
if (error != null) {
throw const ToError().convert(error);
dynamic result = reviver.convert(entry[kManifestResultKey]);
result = onResult(invocation, result);
return result;
/// Finds the next available invocation event in the [manifest] that matches
/// the specified [invocation].
Map<String, dynamic> _nextEvent(Invocation invocation) {
_InvocationMatcher matches = _getMatcher(invocation);
return manifest.firstWhere((Map<String, dynamic> entry) {
return entry[kManifestOrdinalKey] == null && matches(entry);
}, orElse: () => null);
_InvocationMatcher _getMatcher(Invocation invocation) {
String name = getSymbolName(invocation.memberName);
List<dynamic> args = encode(invocation.positionalArguments);
Map<String, dynamic> namedArgs = encode(invocation.namedArguments);
if (invocation.isGetter) {
return (Map<String, dynamic> entry) =>
entry[kManifestTypeKey] == kGetType &&
entry[kManifestPropertyKey] == name &&
entry[kManifestObjectKey] == identifier;
} else if (invocation.isSetter) {
return (Map<String, dynamic> entry) =>
entry[kManifestTypeKey] == kSetType &&
entry[kManifestPropertyKey] == name &&
deeplyEqual(entry[kManifestValueKey], args[0]) &&
entry[kManifestObjectKey] == identifier;
} else {
return (Map<String, dynamic> entry) {
return entry[kManifestTypeKey] == kInvokeType &&
entry[kManifestMethodKey] == name &&
deeplyEqual(entry[kManifestPositionalArgumentsKey], args) &&
namedArgs) &&
entry[kManifestObjectKey] == identifier;
static Map<String, dynamic> _asNamedArgsType(Map<dynamic, dynamic> map) {
return new Map<String, dynamic>.from(map);