Skip to content

Commit

Permalink
Partial implementation of ReplayFileSystem (#28)
Browse files Browse the repository at this point in the history
This implements all the plumbing for `ReplayFileSystem` except
for the resurrectors for `FileSystemEntity`, `File`, `Directory`,
and `Link`. Those will land in a follow-on PR.

Part of flutter#11
  • Loading branch information
tvolkert authored Feb 9, 2017
1 parent 6c58340 commit 6d335f0
Show file tree
Hide file tree
Showing 18 changed files with 880 additions and 64 deletions.
3 changes: 3 additions & 0 deletions lib/record_replay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// 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.

export 'src/backends/record_replay/errors.dart';
export 'src/backends/record_replay/events.dart'
show InvocationEvent, PropertyGetEvent, PropertySetEvent, MethodEvent;
export 'src/backends/record_replay/recording.dart';
export 'src/backends/record_replay/recording_file_system.dart'
show RecordingFileSystem;
export 'src/backends/record_replay/replay_file_system.dart'
show ReplayFileSystem;
6 changes: 3 additions & 3 deletions lib/src/backends/record_replay/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ const String kManifestPositionalArgumentsKey = 'positionalArguments';
/// named arguments that were passed to the method.
const String kManifestNamedArgumentsKey = 'namedArguments';

/// The key in a serialized [InvocationEvent] map that is used to store whether
/// the invocation has been replayed already.
const String kManifestReplayedKey = 'replayed';
/// The key in a serialized [InvocationEvent] map that is used to store the
/// order in which the invocation has been replayed (if it has been replayed).
const String kManifestOrdinalKey = 'ordinal';

/// The serialized [kManifestTypeKey] for property retrievals.
const String kGetType = 'get';
Expand Down
4 changes: 4 additions & 0 deletions lib/src/backends/record_replay/encoding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'recording_file_system_entity.dart';
import 'recording_io_sink.dart';
import 'recording_link.dart';
import 'recording_random_access_file.dart';
import 'replay_proxy_mixin.dart';
import 'result_reference.dart';

/// Encodes an object into a JSON-ready representation.
Expand Down Expand Up @@ -48,6 +49,7 @@ const Map<TypeMatcher<dynamic>, _Encoder> _kEncoders =
const TypeMatcher<RecordingLink>(): _encodeFileSystemEntity,
const TypeMatcher<RecordingIOSink>(): _encodeIOSink,
const TypeMatcher<RecordingRandomAccessFile>(): _encodeRandomAccessFile,
const TypeMatcher<ReplayProxyMixin>(): _encodeReplayEntity,
const TypeMatcher<Encoding>(): _encodeEncoding,
const TypeMatcher<FileMode>(): _encodeFileMode,
const TypeMatcher<FileStat>(): _encodeFileStat,
Expand Down Expand Up @@ -132,6 +134,8 @@ String _encodeRandomAccessFile(RecordingRandomAccessFile raf) {
return '${raf.runtimeType}@${raf.uid}';
}

String _encodeReplayEntity(ReplayProxyMixin entity) => entity.identifier;

String _encodeEncoding(Encoding encoding) => encoding.name;

String _encodeFileMode(FileMode fileMode) {
Expand Down
44 changes: 44 additions & 0 deletions lib/src/backends/record_replay/errors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 'common.dart';
import 'encoding.dart';

/// Error thrown during replay when there is no matching invocation in the
/// recording.
class NoMatchingInvocationError extends Error {
/// The invocation that was unable to be replayed.
final Invocation invocation;

/// Creates a new `NoMatchingInvocationError` caused by the failure to replay
/// the specified [invocation].
NoMatchingInvocationError(this.invocation);

@override
String toString() {
StringBuffer buf = new StringBuffer();
buf.write('No matching invocation found: ');
buf.write(getSymbolName(invocation.memberName));
if (invocation.isMethod) {
buf.write('(');
int i = 0;
for (dynamic arg in invocation.positionalArguments) {
buf.write(Error.safeToString(encode(arg)));
if (i++ > 0) {
buf.write(', ');
}
}
invocation.namedArguments.forEach((Symbol name, dynamic value) {
if (i++ > 0) {
buf.write(', ');
}
buf.write('${getSymbolName(name)}: ${encode(value)}');
});
buf.write(')');
} else if (invocation.isSetter) {
buf.write(Error.safeToString(encode(invocation.positionalArguments[0])));
}
return buf.toString();
}
}
70 changes: 70 additions & 0 deletions lib/src/backends/record_replay/proxy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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.

/// An object that uses [noSuchMethod] to dynamically handle invocations
/// (property getters, property setters, and method invocations).
abstract class ProxyObject {}

/// A function reference that, when invoked, will forward the invocation back
/// to a [ProxyObject].
///
/// This is used when a caller accesses a method on a [ProxyObject] via the
/// method's getter. In these cases, the caller will receive a [MethodProxy]
/// that allows delayed invocation of the method.
class MethodProxy extends Object implements Function {
/// The object on which the method was retrieved.
///
/// This will be the target object when this method proxy is invoked.
final ProxyObject _proxyObject;

/// The name of the method in question.
final Symbol _methodName;

/// Creates a new [MethodProxy] that, when invoked, will invoke the method
/// identified by [methodName] on the specified target [object].
MethodProxy(ProxyObject object, Symbol methodName)
: _proxyObject = object,
_methodName = methodName;

@override
dynamic noSuchMethod(Invocation invocation) {
if (invocation.isMethod && invocation.memberName == #call) {
// The method is being invoked. Capture the arguments, and invoke the
// method on the proxy object. We have to synthesize an invocation, since
// our current `invocation` object represents the invocation of `call()`.
return _proxyObject.noSuchMethod(new _MethodInvocationProxy(
_methodName,
invocation.positionalArguments,
invocation.namedArguments,
));
}
return super.noSuchMethod(invocation);
}
}

class _MethodInvocationProxy extends Invocation {
_MethodInvocationProxy(
this.memberName,
this.positionalArguments,
this.namedArguments,
);

@override
final Symbol memberName;

@override
final List<dynamic> positionalArguments;

@override
final Map<Symbol, dynamic> namedArguments;

@override
final bool isMethod = true;

@override
final bool isGetter = false;

@override
final bool isSetter = false;
}
4 changes: 2 additions & 2 deletions lib/src/backends/record_replay/recording.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:async';
import 'package:file/file.dart';

import 'events.dart';
import 'replay_file_system.dart';

/// A recording of a series of invocations on a [FileSystem] and its associated
/// objects (`File`, `Directory`, `IOSink`, etc).
Expand All @@ -20,10 +21,9 @@ abstract class Recording {
}

/// An [Recording] in progress that can be serialized to disk for later use
/// in `ReplayFileSystem`.
/// in [ReplayFileSystem].
///
/// Live recordings exist only in memory until [flush] is called.
// TODO(tvolkert): Link to ReplayFileSystem in docs once it's implemented
abstract class LiveRecording extends Recording {
/// The directory in which recording files will be stored.
///
Expand Down
6 changes: 3 additions & 3 deletions lib/src/backends/record_replay/recording_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import 'recording_directory.dart';
import 'recording_file.dart';
import 'recording_link.dart';
import 'recording_proxy_mixin.dart';
import 'replay_file_system.dart';

/// File system that records invocations for later playback in tests.
///
/// This will record all invocations (methods, property getters, and property
/// setters) that occur on it, in an opaque format that can later be used in
/// `ReplayFileSystem`. All activity in the [File], [Directory], [Link],
/// [ReplayFileSystem]. All activity in the [File], [Directory], [Link],
/// [IOSink], and [RandomAccessFile] instances returned from this API will also
/// be recorded.
///
Expand All @@ -35,8 +36,7 @@ import 'recording_proxy_mixin.dart';
/// order.
///
/// See also:
/// - `ReplayFileSystem`
// TODO(tvolkert): Link to ReplayFileSystem in docs once it's implemented
/// - [ReplayFileSystem]
abstract class RecordingFileSystem extends FileSystem {
/// Creates a new `RecordingFileSystem`.
///
Expand Down
59 changes: 4 additions & 55 deletions lib/src/backends/record_replay/recording_proxy_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:meta/meta.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
Expand All @@ -34,7 +35,7 @@ import 'result_reference.dart';
/// int sampleProperty;
/// }
///
/// class RecordingFoo extends Object with _RecordingProxyMixin implements Foo {
/// class RecordingFoo extends Object with RecordingProxyMixin implements Foo {
/// final Foo delegate;
///
/// RecordingFoo(this.delegate) {
Expand All @@ -59,7 +60,7 @@ import 'result_reference.dart';
/// 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.
abstract class RecordingProxyMixin {
abstract class RecordingProxyMixin implements ProxyObject {
/// Maps method names to delegate functions.
///
/// Invocations of methods listed in this map will be recorded after
Expand Down Expand Up @@ -107,7 +108,7 @@ abstract class RecordingProxyMixin {
// 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)
? new MethodProxy(this, name)
: super.noSuchMethod(invocation);
}

Expand Down Expand Up @@ -144,55 +145,3 @@ abstract class RecordingProxyMixin {
return result;
}
}

/// A function reference that, when invoked, will record the invocation.
class _MethodProxy extends Object implements Function {
/// The object on which the method was originally invoked.
final RecordingProxyMixin object;

/// The name of the method that was originally invoked.
final Symbol methodName;

_MethodProxy(this.object, this.methodName);

@override
dynamic noSuchMethod(Invocation invocation) {
if (invocation.isMethod && invocation.memberName == #call) {
// The method is being invoked. Capture the arguments, and invoke the
// method on the object. We have to synthesize an invocation, since our
// current `invocation` object represents the invocation of `call()`.
return object.noSuchMethod(new _MethodInvocationProxy(
methodName,
invocation.positionalArguments,
invocation.namedArguments,
));
}
return super.noSuchMethod(invocation);
}
}

class _MethodInvocationProxy extends Invocation {
_MethodInvocationProxy(
this.memberName,
this.positionalArguments,
this.namedArguments,
);

@override
final Symbol memberName;

@override
final List<dynamic> positionalArguments;

@override
final Map<Symbol, dynamic> namedArguments;

@override
final bool isMethod = true;

@override
final bool isGetter = false;

@override
final bool isSetter = false;
}
27 changes: 27 additions & 0 deletions lib/src/backends/record_replay/replay_directory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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/file.dart';

import 'replay_file_system.dart';
import 'replay_file_system_entity.dart';
import 'resurrectors.dart';

/// [Directory] implementation that replays all invocation activity from a
/// prior recording.
class ReplayDirectory extends ReplayFileSystemEntity implements Directory {
/// Creates a new `ReplayDirectory`.
ReplayDirectory(ReplayFileSystemImpl fileSystem, String identifier)
: super(fileSystem, identifier) {
// TODO(tvolkert): fill in resurrectors
methods.addAll(<Symbol, Resurrector>{
#create: null,
#createSync: null,
#createTemp: null,
#createTempSync: null,
#list: null,
#listSync: null,
});
}
}
43 changes: 43 additions & 0 deletions lib/src/backends/record_replay/replay_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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/file.dart';

import 'replay_file_system.dart';
import 'replay_file_system_entity.dart';
import 'resurrectors.dart';

/// [File] implementation that replays all invocation activity from a prior
/// recording.
class ReplayFile extends ReplayFileSystemEntity implements File {
/// Creates a new `ReplayFile`.
ReplayFile(ReplayFileSystemImpl fileSystem, String identifier)
: super(fileSystem, identifier) {
// TODO(tvolkert): fill in resurrectors
methods.addAll(<Symbol, Resurrector>{
#create: null,
#createSync: null,
#copy: null,
#copySync: null,
#length: null,
#lengthSync: null,
#lastModified: null,
#lastModifiedSync: null,
#open: null,
#openSync: null,
#openRead: null,
#openWrite: null,
#readAsBytes: null,
#readAsBytesSync: null,
#readAsString: null,
#readAsStringSync: null,
#readAsLines: null,
#readAsLinesSync: null,
#writeAsBytes: null,
#writeAsBytesSync: null,
#writeAsString: null,
#writeAsStringSync: null,
});
}
}
Loading

0 comments on commit 6d335f0

Please sign in to comment.