blob: 56e78330251149e55bbf639d519b2b4f2e03895e [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:async';
import 'package:file/file.dart';
import 'package:file/src/backends/memory/node.dart';
import 'package:meta/meta.dart';
import 'common.dart';
import 'events.dart';
import 'mutable_recording.dart';
import 'proxy.dart';
import 'result_reference.dart';
/// Mixin that enables recording of property accesses, property mutations, and
/// method invocations.
/// This class uses `noSuchMethod` to record a well-defined set of invocations
/// (including property gets and sets) on an object before passing the
/// invocation on to a delegate. Subclasses wire this up by doing the following:
/// - Populate the list of method invocations to record in the [methods] map.
/// - Populate the list of property invocations to record 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
/// record, as doing so will circumvent the machinery that this mixin uses
/// (`noSuchMethod`) to record invocations.
/// **Example use**:
/// abstract class Foo {
/// void sampleMethod();
/// int sampleProperty;
/// }
/// class RecordingFoo extends RecordingProxyMixin implements Foo {
/// final Foo delegate;
/// RecordingFoo(this.delegate) {
/// methods.addAll(<Symbol, Function>{
/// #sampleMethod: delegate.sampleMethod,
/// });
/// properties.addAll(<Symbol, Function>{
/// #sampleProperty: () => delegate.sampleProperty,
/// const Symbol('sampleProperty='): (int value) {
/// delegate.sampleProperty = value;
/// },
/// });
/// }
/// }
/// **Behavioral notes**:
/// Methods that return [Future]s will not be recorded until the future
/// completes.
/// Methods that return [Stream]s will be recorded immediately, but their
/// return values will be recorded as a [List] that will grow as the stream
/// produces data.
mixin RecordingProxyMixin on Object implements ProxyObject, ReplayAware {
/// Maps method names to delegate functions.
/// Invocations of methods listed in this map will be recorded after
/// invoking the underlying delegate function.
final Map<Symbol, Function> methods = <Symbol, Function>{};
/// Maps property getter and setter names to delegate functions.
/// Access and mutation of properties listed in this map will be recorded
/// after invoking the underlying delegate function.
/// 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, Function> properties = <Symbol, Function>{};
/// The object to which invocation events will be recorded.
MutableRecording get recording;
/// The stopwatch used to record invocation timestamps.
Stopwatch get stopwatch;
// This check is used in noSuchMethod to detect if this code is running in a
// Dart 1 runtime, or Dart 2.
// TODO(srawlins): Remove this after the minimum SDK constraint is such that
// there is no "Dart 1" runtime mode. 2.0.0 or something.
bool get _runningDart1Runtime => <dynamic>[] is List<String>;
/// Handles invocations for which there is no concrete implementation
/// function.
/// For invocations that have matching entries in [methods] (for method
/// invocations) or [properties] (for property access and mutation), this
/// will record the invocation in [recording] after invoking the underlying
/// delegate method. All other invocations will throw a [NoSuchMethodError].
dynamic noSuchMethod(Invocation invocation) {
Symbol name = invocation.memberName;
List<dynamic> args = invocation.positionalArguments;
Map<Symbol, dynamic> namedArgs = invocation.namedArguments;
Function method = invocation.isAccessor ? properties[name] : methods[name];
int time = stopwatch.elapsedMilliseconds;
if (method == null) {
// No delegate function 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 perform the desired recording.
return invocation.isGetter && methods[name] != null
? new MethodProxy(this, name)
: super.noSuchMethod(invocation);
InvocationEvent<dynamic> createEvent({dynamic result, dynamic error}) {
if (invocation.isGetter) {
return new LivePropertyGetEvent<dynamic>(
this, name, result, error, time);
} else if (invocation.isSetter) {
return new LivePropertySetEvent<dynamic>(
this, name, args[0], error, time);
} else {
return new LiveMethodEvent<dynamic>(
this, name, args, namedArgs, result, error, time);
// Invoke the configured delegate method, recording an error if one occurs.
dynamic value;
try {
value = Function.apply(method, args, namedArgs);
} catch (error) {
recording.add(createEvent(error: error));
// Wrap Future and Stream results so that we record their values as they
// become available.
// We have to instantiate the correct type of StreamReference or
// FutureReference, so that types are not lost when we unwrap the references
// afterward.
if (_runningDart1Runtime && value is Stream<dynamic>) {
// This one is here for Dart 1 runtime mode.
value = new StreamReference<dynamic>(value);
} else if (value is Stream<FileSystemEntity>) {
value = new StreamReference<FileSystemEntity>(value);
} else if (value is Stream<String>) {
value = new StreamReference<String>(value);
} else if (value is Stream) {
throw new UnimplementedError(
'Cannot record method with return type ${value.runtimeType}');
} else if (_runningDart1Runtime && value is Future<dynamic>) {
// This one is here for Dart 1 runtime mode.
value = new FutureReference<dynamic>(value);
} else if (value is Future<bool>) {
value = new FutureReference<bool>(value);
} else if (value is Future<Directory>) {
value = new FutureReference<Directory>(value);
} else if (value is Future<File>) {
value = new FutureReference<File>(value);
} else if (value is Future<FileNode>) {
value = new FutureReference<FileNode>(value);
} else if (value is Future<FileStat>) {
value = new FutureReference<FileStat>(value);
} else if (value is Future<Link>) {
value = new FutureReference<Link>(value);
} else if (value is Future<FileSystemEntity>) {
value = new FutureReference<FileSystemEntity>(value);
} else if (value is Future<FileSystemEntityType>) {
value = new FutureReference<FileSystemEntityType>(value);
} else if (value is Future<String>) {
value = new FutureReference<String>(value);
} else if (value is Future<RandomAccessFile>) {
value = new FutureReference<RandomAccessFile>(value);
} else if (value is Future) {
throw new UnimplementedError(
'Cannot record method with return type ${value.runtimeType}');
// Record the invocation event associated with this invocation.
recording.add(createEvent(result: value));
// Unwrap any result references before returning to the caller.
dynamic result = value;
while (result is ResultReference) {
result = result.value;
return result;