Skip to content

Commit 6d335f0

Browse files
authored
Partial implementation of ReplayFileSystem (#28)
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
1 parent 6c58340 commit 6d335f0

18 files changed

+880
-64
lines changed

lib/record_replay.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
export 'src/backends/record_replay/errors.dart';
56
export 'src/backends/record_replay/events.dart'
67
show InvocationEvent, PropertyGetEvent, PropertySetEvent, MethodEvent;
78
export 'src/backends/record_replay/recording.dart';
89
export 'src/backends/record_replay/recording_file_system.dart'
910
show RecordingFileSystem;
11+
export 'src/backends/record_replay/replay_file_system.dart'
12+
show ReplayFileSystem;

lib/src/backends/record_replay/common.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ const String kManifestPositionalArgumentsKey = 'positionalArguments';
5151
/// named arguments that were passed to the method.
5252
const String kManifestNamedArgumentsKey = 'namedArguments';
5353

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

5858
/// The serialized [kManifestTypeKey] for property retrievals.
5959
const String kGetType = 'get';

lib/src/backends/record_replay/encoding.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'recording_file_system_entity.dart';
1515
import 'recording_io_sink.dart';
1616
import 'recording_link.dart';
1717
import 'recording_random_access_file.dart';
18+
import 'replay_proxy_mixin.dart';
1819
import 'result_reference.dart';
1920

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

137+
String _encodeReplayEntity(ReplayProxyMixin entity) => entity.identifier;
138+
135139
String _encodeEncoding(Encoding encoding) => encoding.name;
136140

137141
String _encodeFileMode(FileMode fileMode) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'common.dart';
6+
import 'encoding.dart';
7+
8+
/// Error thrown during replay when there is no matching invocation in the
9+
/// recording.
10+
class NoMatchingInvocationError extends Error {
11+
/// The invocation that was unable to be replayed.
12+
final Invocation invocation;
13+
14+
/// Creates a new `NoMatchingInvocationError` caused by the failure to replay
15+
/// the specified [invocation].
16+
NoMatchingInvocationError(this.invocation);
17+
18+
@override
19+
String toString() {
20+
StringBuffer buf = new StringBuffer();
21+
buf.write('No matching invocation found: ');
22+
buf.write(getSymbolName(invocation.memberName));
23+
if (invocation.isMethod) {
24+
buf.write('(');
25+
int i = 0;
26+
for (dynamic arg in invocation.positionalArguments) {
27+
buf.write(Error.safeToString(encode(arg)));
28+
if (i++ > 0) {
29+
buf.write(', ');
30+
}
31+
}
32+
invocation.namedArguments.forEach((Symbol name, dynamic value) {
33+
if (i++ > 0) {
34+
buf.write(', ');
35+
}
36+
buf.write('${getSymbolName(name)}: ${encode(value)}');
37+
});
38+
buf.write(')');
39+
} else if (invocation.isSetter) {
40+
buf.write(Error.safeToString(encode(invocation.positionalArguments[0])));
41+
}
42+
return buf.toString();
43+
}
44+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// An object that uses [noSuchMethod] to dynamically handle invocations
6+
/// (property getters, property setters, and method invocations).
7+
abstract class ProxyObject {}
8+
9+
/// A function reference that, when invoked, will forward the invocation back
10+
/// to a [ProxyObject].
11+
///
12+
/// This is used when a caller accesses a method on a [ProxyObject] via the
13+
/// method's getter. In these cases, the caller will receive a [MethodProxy]
14+
/// that allows delayed invocation of the method.
15+
class MethodProxy extends Object implements Function {
16+
/// The object on which the method was retrieved.
17+
///
18+
/// This will be the target object when this method proxy is invoked.
19+
final ProxyObject _proxyObject;
20+
21+
/// The name of the method in question.
22+
final Symbol _methodName;
23+
24+
/// Creates a new [MethodProxy] that, when invoked, will invoke the method
25+
/// identified by [methodName] on the specified target [object].
26+
MethodProxy(ProxyObject object, Symbol methodName)
27+
: _proxyObject = object,
28+
_methodName = methodName;
29+
30+
@override
31+
dynamic noSuchMethod(Invocation invocation) {
32+
if (invocation.isMethod && invocation.memberName == #call) {
33+
// The method is being invoked. Capture the arguments, and invoke the
34+
// method on the proxy object. We have to synthesize an invocation, since
35+
// our current `invocation` object represents the invocation of `call()`.
36+
return _proxyObject.noSuchMethod(new _MethodInvocationProxy(
37+
_methodName,
38+
invocation.positionalArguments,
39+
invocation.namedArguments,
40+
));
41+
}
42+
return super.noSuchMethod(invocation);
43+
}
44+
}
45+
46+
class _MethodInvocationProxy extends Invocation {
47+
_MethodInvocationProxy(
48+
this.memberName,
49+
this.positionalArguments,
50+
this.namedArguments,
51+
);
52+
53+
@override
54+
final Symbol memberName;
55+
56+
@override
57+
final List<dynamic> positionalArguments;
58+
59+
@override
60+
final Map<Symbol, dynamic> namedArguments;
61+
62+
@override
63+
final bool isMethod = true;
64+
65+
@override
66+
final bool isGetter = false;
67+
68+
@override
69+
final bool isSetter = false;
70+
}

lib/src/backends/record_replay/recording.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:async';
77
import 'package:file/file.dart';
88

99
import 'events.dart';
10+
import 'replay_file_system.dart';
1011

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

2223
/// An [Recording] in progress that can be serialized to disk for later use
23-
/// in `ReplayFileSystem`.
24+
/// in [ReplayFileSystem].
2425
///
2526
/// Live recordings exist only in memory until [flush] is called.
26-
// TODO(tvolkert): Link to ReplayFileSystem in docs once it's implemented
2727
abstract class LiveRecording extends Recording {
2828
/// The directory in which recording files will be stored.
2929
///

lib/src/backends/record_replay/recording_file_system.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import 'recording_directory.dart';
1111
import 'recording_file.dart';
1212
import 'recording_link.dart';
1313
import 'recording_proxy_mixin.dart';
14+
import 'replay_file_system.dart';
1415

1516
/// File system that records invocations for later playback in tests.
1617
///
1718
/// This will record all invocations (methods, property getters, and property
1819
/// setters) that occur on it, in an opaque format that can later be used in
19-
/// `ReplayFileSystem`. All activity in the [File], [Directory], [Link],
20+
/// [ReplayFileSystem]. All activity in the [File], [Directory], [Link],
2021
/// [IOSink], and [RandomAccessFile] instances returned from this API will also
2122
/// be recorded.
2223
///
@@ -35,8 +36,7 @@ import 'recording_proxy_mixin.dart';
3536
/// order.
3637
///
3738
/// See also:
38-
/// - `ReplayFileSystem`
39-
// TODO(tvolkert): Link to ReplayFileSystem in docs once it's implemented
39+
/// - [ReplayFileSystem]
4040
abstract class RecordingFileSystem extends FileSystem {
4141
/// Creates a new `RecordingFileSystem`.
4242
///

lib/src/backends/record_replay/recording_proxy_mixin.dart

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
88

99
import 'events.dart';
1010
import 'mutable_recording.dart';
11+
import 'proxy.dart';
1112
import 'result_reference.dart';
1213

1314
/// Mixin that enables recording of property accesses, property mutations, and
@@ -34,7 +35,7 @@ import 'result_reference.dart';
3435
/// int sampleProperty;
3536
/// }
3637
///
37-
/// class RecordingFoo extends Object with _RecordingProxyMixin implements Foo {
38+
/// class RecordingFoo extends Object with RecordingProxyMixin implements Foo {
3839
/// final Foo delegate;
3940
///
4041
/// RecordingFoo(this.delegate) {
@@ -59,7 +60,7 @@ import 'result_reference.dart';
5960
/// Methods that return [Stream]s will be recorded immediately, but their
6061
/// return values will be recorded as a [List] that will grow as the stream
6162
/// produces data.
62-
abstract class RecordingProxyMixin {
63+
abstract class RecordingProxyMixin implements ProxyObject {
6364
/// Maps method names to delegate functions.
6465
///
6566
/// Invocations of methods listed in this map will be recorded after
@@ -107,7 +108,7 @@ abstract class RecordingProxyMixin {
107108
// a getter on a method, in which case we return a method proxy that,
108109
// when invoked, will perform the desired recording.
109110
return invocation.isGetter && methods[name] != null
110-
? new _MethodProxy(this, name)
111+
? new MethodProxy(this, name)
111112
: super.noSuchMethod(invocation);
112113
}
113114

@@ -144,55 +145,3 @@ abstract class RecordingProxyMixin {
144145
return result;
145146
}
146147
}
147-
148-
/// A function reference that, when invoked, will record the invocation.
149-
class _MethodProxy extends Object implements Function {
150-
/// The object on which the method was originally invoked.
151-
final RecordingProxyMixin object;
152-
153-
/// The name of the method that was originally invoked.
154-
final Symbol methodName;
155-
156-
_MethodProxy(this.object, this.methodName);
157-
158-
@override
159-
dynamic noSuchMethod(Invocation invocation) {
160-
if (invocation.isMethod && invocation.memberName == #call) {
161-
// The method is being invoked. Capture the arguments, and invoke the
162-
// method on the object. We have to synthesize an invocation, since our
163-
// current `invocation` object represents the invocation of `call()`.
164-
return object.noSuchMethod(new _MethodInvocationProxy(
165-
methodName,
166-
invocation.positionalArguments,
167-
invocation.namedArguments,
168-
));
169-
}
170-
return super.noSuchMethod(invocation);
171-
}
172-
}
173-
174-
class _MethodInvocationProxy extends Invocation {
175-
_MethodInvocationProxy(
176-
this.memberName,
177-
this.positionalArguments,
178-
this.namedArguments,
179-
);
180-
181-
@override
182-
final Symbol memberName;
183-
184-
@override
185-
final List<dynamic> positionalArguments;
186-
187-
@override
188-
final Map<Symbol, dynamic> namedArguments;
189-
190-
@override
191-
final bool isMethod = true;
192-
193-
@override
194-
final bool isGetter = false;
195-
196-
@override
197-
final bool isSetter = false;
198-
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:file/file.dart';
6+
7+
import 'replay_file_system.dart';
8+
import 'replay_file_system_entity.dart';
9+
import 'resurrectors.dart';
10+
11+
/// [Directory] implementation that replays all invocation activity from a
12+
/// prior recording.
13+
class ReplayDirectory extends ReplayFileSystemEntity implements Directory {
14+
/// Creates a new `ReplayDirectory`.
15+
ReplayDirectory(ReplayFileSystemImpl fileSystem, String identifier)
16+
: super(fileSystem, identifier) {
17+
// TODO(tvolkert): fill in resurrectors
18+
methods.addAll(<Symbol, Resurrector>{
19+
#create: null,
20+
#createSync: null,
21+
#createTemp: null,
22+
#createTempSync: null,
23+
#list: null,
24+
#listSync: null,
25+
});
26+
}
27+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:file/file.dart';
6+
7+
import 'replay_file_system.dart';
8+
import 'replay_file_system_entity.dart';
9+
import 'resurrectors.dart';
10+
11+
/// [File] implementation that replays all invocation activity from a prior
12+
/// recording.
13+
class ReplayFile extends ReplayFileSystemEntity implements File {
14+
/// Creates a new `ReplayFile`.
15+
ReplayFile(ReplayFileSystemImpl fileSystem, String identifier)
16+
: super(fileSystem, identifier) {
17+
// TODO(tvolkert): fill in resurrectors
18+
methods.addAll(<Symbol, Resurrector>{
19+
#create: null,
20+
#createSync: null,
21+
#copy: null,
22+
#copySync: null,
23+
#length: null,
24+
#lengthSync: null,
25+
#lastModified: null,
26+
#lastModifiedSync: null,
27+
#open: null,
28+
#openSync: null,
29+
#openRead: null,
30+
#openWrite: null,
31+
#readAsBytes: null,
32+
#readAsBytesSync: null,
33+
#readAsString: null,
34+
#readAsStringSync: null,
35+
#readAsLines: null,
36+
#readAsLinesSync: null,
37+
#writeAsBytes: null,
38+
#writeAsBytesSync: null,
39+
#writeAsString: null,
40+
#writeAsStringSync: null,
41+
});
42+
}
43+
}

0 commit comments

Comments
 (0)