| // 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:meta/meta.dart'; |
| |
| import 'common.dart'; |
| import 'encoding.dart'; |
| import 'errors.dart'; |
| import 'proxy.dart'; |
| import 'resurrectors.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, Resurrector>{ |
| /// #sampleMethod: resurrectComplexObject, |
| /// }); |
| /// |
| /// properties.addAll(<Symbol, Resurrector>{ |
| /// #sampleParent: resurrectFoo, |
| /// const Symbol('sampleParent='): resurrectPassthrough, |
| /// }); |
| /// } |
| /// } |
| abstract class ReplayProxyMixin implements ProxyObject { |
| /// Maps method names to [Resurrector] functions. |
| /// |
| /// Invocations of methods listed in this map will be replayed by looking for |
| /// matching invocations in the [manifest] and resurrecting the invocation |
| /// return value using the [Resurrector] found in this map. |
| @protected |
| final Map<Symbol, Resurrector> methods = <Symbol, Resurrector>{}; |
| |
| /// Maps property getter and setter names to [Resurrector] functions. |
| /// |
| /// Access and mutation of properties listed in this map will be replayed |
| /// by looking for matching property accesses in the [manifest] and |
| /// resurrecting the invocation return value using the [Resurrector] 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=`). |
| @protected |
| final Map<Symbol, Resurrector> properties = <Symbol, Resurrector>{}; |
| |
| /// The unique identifier of this replay object. |
| /// |
| /// When replay-aware objects are serialized in a recording, they are done so |
| /// using only a unique String identifier. When the objects are resurrected |
| /// for the purpose of replay, their identifiers are used to match incoming |
| /// invocations against recorded invocations in the [manifest] (only |
| /// invocations whose target object matches the identifier are considered |
| /// possible matches). |
| String get identifier; |
| |
| /// 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; |
| |
| @override |
| dynamic noSuchMethod(Invocation invocation) { |
| Symbol name = invocation.memberName; |
| Resurrector resurrector = |
| invocation.isAccessor ? properties[name] : methods[name]; |
| |
| if (resurrector == null) { |
| // No resurrector 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++; |
| |
| return resurrector(entry[kManifestResultKey]); |
| } |
| |
| /// 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) && |
| deeplyEqual(_asNamedArgsType(entry[kManifestNamedArgumentsKey]), |
| namedArgs) && |
| entry[kManifestObjectKey] == identifier; |
| }; |
| } |
| } |
| |
| static Map<String, dynamic> _asNamedArgsType(Map<dynamic, dynamic> map) { |
| return new Map<String, dynamic>.from(map); |
| } |
| } |