| // 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 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, |
| /// }); |
| /// } |
| /// } |
| abstract class ReplayProxyMixin 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. |
| @protected |
| 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=`). |
| @protected |
| 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. |
| @protected |
| 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. |
| @protected |
| dynamic onResult(Invocation invocation, dynamic result) => result; |
| |
| @override |
| 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) && |
| deeplyEqual(_asNamedArgsType(entry[kManifestNamedArgumentsKey]), |
| namedArgs) && |
| entry[kManifestObjectKey] == identifier; |
| }; |
| } |
| } |
| |
| static Map<String, dynamic> _asNamedArgsType(Map<dynamic, dynamic> map) { |
| return new Map<String, dynamic>.from(map); |
| } |
| } |