Skip to content

Commit

Permalink
Add ResultReference<T> to record_replay library. (flutter#18)
Browse files Browse the repository at this point in the history
This introduces a level of indirection that will allow
recording objects to differentiate between the invocation
result return value, the recorded result value, and the
serialized result value.

This is used to provide "special handling" of certain
invocation results (such as byte arrays) to make recordings
more human-readable & editable.

This design also yields more technical correctness over
the previous design. This is because we used to delay
recording an invocation whose result was a Future until
that future completed. Now, we record the invocation
immediately and late-record future results (with support
for awaiting those futures when serializing a recoridng).

Another step in flutter#11
  • Loading branch information
tvolkert authored Feb 4, 2017
1 parent e0aca53 commit 42bf782
Show file tree
Hide file tree
Showing 9 changed files with 726 additions and 123 deletions.
10 changes: 10 additions & 0 deletions lib/src/backends/record_replay/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ String getSymbolName(Symbol symbol) {
int offset = str.indexOf('"') + 1;
return str.substring(offset, str.indexOf('"', offset));
}

/// This class is a work-around for the "is" operator not accepting a variable
/// value as its right operand (https://github.com/dart-lang/sdk/issues/27680).
class TypeMatcher<T> {
/// Creates a type matcher for the given type parameter.
const TypeMatcher();

/// Returns `true` if the given object is of type `T`.
bool matches(dynamic object) => object is T;
}
72 changes: 34 additions & 38 deletions lib/src/backends/record_replay/encoding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,43 @@ import 'recording_file_system_entity.dart';
import 'recording_io_sink.dart';
import 'recording_link.dart';
import 'recording_random_access_file.dart';
import 'result_reference.dart';

/// Encodes an object into a JSON-ready representation.
typedef dynamic _Encoder(dynamic object);

/// This class is a work-around for the "is" operator not accepting a variable
/// value as its right operand (https://github.com/dart-lang/sdk/issues/27680).
class _TypeMatcher<T> {
/// Creates a type matcher for the given type parameter.
const _TypeMatcher();

/// Returns `true` if the given object is of type `T`.
bool check(dynamic object) => object is T;
}

/// Known encoders. Types not covered here will be encoded using
/// [_encodeDefault].
///
/// When encoding an object, we will walk this map in iteration order looking
/// for a matching encoder. Thus, when there are two encoders that match an
// object, the first one will win.
const Map<_TypeMatcher<dynamic>, _Encoder> _kEncoders =
const <_TypeMatcher<dynamic>, _Encoder>{
const _TypeMatcher<num>(): _encodeRaw,
const _TypeMatcher<bool>(): _encodeRaw,
const _TypeMatcher<String>(): _encodeRaw,
const _TypeMatcher<Null>(): _encodeRaw,
const _TypeMatcher<List<dynamic>>(): _encodeRaw,
const _TypeMatcher<Map<dynamic, dynamic>>(): _encodeMap,
const _TypeMatcher<Iterable<dynamic>>(): _encodeIterable,
const _TypeMatcher<Symbol>(): getSymbolName,
const _TypeMatcher<DateTime>(): _encodeDateTime,
const _TypeMatcher<Uri>(): _encodeUri,
const _TypeMatcher<p.Context>(): _encodePathContext,
const _TypeMatcher<EventImpl<dynamic>>(): _encodeEvent,
const _TypeMatcher<FileSystem>(): _encodeFileSystem,
const _TypeMatcher<RecordingDirectory>(): _encodeFileSystemEntity,
const _TypeMatcher<RecordingFile>(): _encodeFileSystemEntity,
const _TypeMatcher<RecordingLink>(): _encodeFileSystemEntity,
const _TypeMatcher<RecordingIOSink>(): _encodeIOSink,
const _TypeMatcher<RecordingRandomAccessFile>(): _encodeRandomAccessFile,
const _TypeMatcher<Encoding>(): _encodeEncoding,
const _TypeMatcher<FileMode>(): _encodeFileMode,
const _TypeMatcher<FileStat>(): _encodeFileStat,
const _TypeMatcher<FileSystemEntityType>(): _encodeFileSystemEntityType,
const _TypeMatcher<FileSystemEvent>(): _encodeFileSystemEvent,
const Map<TypeMatcher<dynamic>, _Encoder> _kEncoders =
const <TypeMatcher<dynamic>, _Encoder>{
const TypeMatcher<num>(): _encodeRaw,
const TypeMatcher<bool>(): _encodeRaw,
const TypeMatcher<String>(): _encodeRaw,
const TypeMatcher<Null>(): _encodeRaw,
const TypeMatcher<List<dynamic>>(): _encodeRaw,
const TypeMatcher<Map<dynamic, dynamic>>(): _encodeMap,
const TypeMatcher<Iterable<dynamic>>(): _encodeIterable,
const TypeMatcher<Symbol>(): getSymbolName,
const TypeMatcher<DateTime>(): _encodeDateTime,
const TypeMatcher<Uri>(): _encodeUri,
const TypeMatcher<p.Context>(): _encodePathContext,
const TypeMatcher<ResultReference<dynamic>>(): _encodeResultReference,
const TypeMatcher<LiveInvocationEvent<dynamic>>(): _encodeEvent,
const TypeMatcher<FileSystem>(): _encodeFileSystem,
const TypeMatcher<RecordingDirectory>(): _encodeFileSystemEntity,
const TypeMatcher<RecordingFile>(): _encodeFileSystemEntity,
const TypeMatcher<RecordingLink>(): _encodeFileSystemEntity,
const TypeMatcher<RecordingIOSink>(): _encodeIOSink,
const TypeMatcher<RecordingRandomAccessFile>(): _encodeRandomAccessFile,
const TypeMatcher<Encoding>(): _encodeEncoding,
const TypeMatcher<FileMode>(): _encodeFileMode,
const TypeMatcher<FileStat>(): _encodeFileStat,
const TypeMatcher<FileSystemEntityType>(): _encodeFileSystemEntityType,
const TypeMatcher<FileSystemEvent>(): _encodeFileSystemEvent,
};

/// Encodes [object] into a JSON-ready representation.
Expand All @@ -72,8 +64,8 @@ const Map<_TypeMatcher<dynamic>, _Encoder> _kEncoders =
/// - [JsonEncoder.withIndent]
dynamic encode(dynamic object) {
_Encoder encoder = _encodeDefault;
for (_TypeMatcher<dynamic> matcher in _kEncoders.keys) {
if (matcher.check(object)) {
for (TypeMatcher<dynamic> matcher in _kEncoders.keys) {
if (matcher.matches(object)) {
encoder = _kEncoders[matcher];
break;
}
Expand Down Expand Up @@ -114,7 +106,11 @@ Map<String, String> _encodePathContext(p.Context context) => <String, String>{
'cwd': context.current,
};

Map<String, dynamic> _encodeEvent(EventImpl<dynamic> event) => event.encode();
dynamic _encodeResultReference(ResultReference<dynamic> reference) =>
reference.serializedValue;

Map<String, dynamic> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
event.serialize();

String _encodeFileSystem(FileSystem fs) => kFileSystemEncodedValue;

Expand Down
88 changes: 61 additions & 27 deletions lib/src/backends/record_replay/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// 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 'recording.dart';
import 'result_reference.dart';

/// Base class for recordable file system invocation events.
///
Expand Down Expand Up @@ -53,53 +56,83 @@ abstract class MethodEvent<T> extends InvocationEvent<T> {
Map<Symbol, dynamic> get namedArguments;
}

/// Non-exported implementation of [InvocationEvent].
abstract class EventImpl<T> implements InvocationEvent<T> {
/// Creates a new `EventImpl`.
EventImpl(this.object, this.result, this.timestamp);
/// An [InvocationEvent] that's in the process of being recorded.
abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
/// Creates a new `LiveInvocationEvent`.
LiveInvocationEvent(this.object, this._result, this.timestamp);

final dynamic _result;

@override
final Object object;

@override
final T result;
T get result {
dynamic result = _result;
while (result is ResultReference) {
ResultReference<dynamic> reference = result;
result = reference.recordedValue;
}
return result;
}

@override
final int timestamp;

/// Encodes this event into a JSON-ready format.
Map<String, dynamic> encode() => <String, dynamic>{
/// A [Future] that completes once [result] is ready for serialization.
///
/// If [result] is a [Future], this future completes when [result] completes.
/// If [result] is a [Stream], this future completes when the stream sends a
/// "done" event. If [result] is neither a future nor a stream, this future
/// completes immediately.
///
/// It is legal for [serialize] to be called before this future completes,
/// but doing so will cause incomplete results to be serialized. Results that
/// are unfinished futures will be serialized as `null`, and results that are
/// unfinished streams will be serialized as the data that has been received
/// thus far.
Future<Null> get done async {
dynamic result = _result;
while (result is ResultReference) {
ResultReference<dynamic> reference = result;
await reference.complete;
result = reference.recordedValue;
}
}

/// Returns this event as a JSON-serializable object.
Map<String, dynamic> serialize() => <String, dynamic>{
'object': object,
'result': result,
'result': _result,
'timestamp': timestamp,
};

@override
String toString() => encode().toString();
String toString() => serialize().toString();
}

/// Non-exported implementation of [PropertyGetEvent].
class PropertyGetEventImpl<T> extends EventImpl<T>
/// A [PropertyGetEvent] that's in the process of being recorded.
class LivePropertyGetEvent<T> extends LiveInvocationEvent<T>
implements PropertyGetEvent<T> {
/// Create a new `PropertyGetEventImpl`.
PropertyGetEventImpl(Object object, this.property, T result, int timestamp)
/// Creates a new `LivePropertyGetEvent`.
LivePropertyGetEvent(Object object, this.property, T result, int timestamp)
: super(object, result, timestamp);

@override
final Symbol property;

@override
Map<String, dynamic> encode() => <String, dynamic>{
Map<String, dynamic> serialize() => <String, dynamic>{
'type': 'get',
'property': property,
}..addAll(super.encode());
}..addAll(super.serialize());
}

/// Non-exported implementation of [PropertySetEvent].
class PropertySetEventImpl<T> extends EventImpl<Null>
/// A [PropertySetEvent] that's in the process of being recorded.
class LivePropertySetEvent<T> extends LiveInvocationEvent<Null>
implements PropertySetEvent<T> {
/// Create a new `PropertySetEventImpl`.
PropertySetEventImpl(Object object, this.property, this.value, int timestamp)
/// Creates a new `LivePropertySetEvent`.
LivePropertySetEvent(Object object, this.property, this.value, int timestamp)
: super(object, null, timestamp);

@override
Expand All @@ -109,17 +142,18 @@ class PropertySetEventImpl<T> extends EventImpl<Null>
final T value;

@override
Map<String, dynamic> encode() => <String, dynamic>{
Map<String, dynamic> serialize() => <String, dynamic>{
'type': 'set',
'property': property,
'value': value,
}..addAll(super.encode());
}..addAll(super.serialize());
}

/// Non-exported implementation of [MethodEvent].
class MethodEventImpl<T> extends EventImpl<T> implements MethodEvent<T> {
/// Create a new `MethodEventImpl`.
MethodEventImpl(
/// A [MethodEvent] that's in the process of being recorded.
class LiveMethodEvent<T> extends LiveInvocationEvent<T>
implements MethodEvent<T> {
/// Creates a new `LiveMethodEvent`.
LiveMethodEvent(
Object object,
this.method,
List<dynamic> positionalArguments,
Expand All @@ -143,10 +177,10 @@ class MethodEventImpl<T> extends EventImpl<T> implements MethodEvent<T> {
final Map<Symbol, dynamic> namedArguments;

@override
Map<String, dynamic> encode() => <String, dynamic>{
Map<String, dynamic> serialize() => <String, dynamic>{
'type': 'invoke',
'method': method,
'positionalArguments': positionalArguments,
'namedArguments': namedArguments,
}..addAll(super.encode());
}..addAll(super.serialize());
}
39 changes: 28 additions & 11 deletions lib/src/backends/record_replay/mutable_recording.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,45 @@ import 'recording.dart';

/// A mutable live recording.
class MutableRecording implements LiveRecording {
final List<InvocationEvent<dynamic>> _events = <InvocationEvent<dynamic>>[];

/// Creates a new `MutableRecording` that will serialize its data to the
/// specified [destination].
MutableRecording(this.destination);

final List<LiveInvocationEvent<dynamic>> _events =
<LiveInvocationEvent<dynamic>>[];

bool _flushing = false;

@override
final Directory destination;

@override
List<InvocationEvent<dynamic>> get events =>
new List<InvocationEvent<dynamic>>.unmodifiable(_events);
List<LiveInvocationEvent<dynamic>> get events =>
new List<LiveInvocationEvent<dynamic>>.unmodifiable(_events);

// TODO(tvolkert): Add ability to wait for all Future and Stream results
@override
Future<Null> flush() async {
Directory dir = destination;
String json = new JsonEncoder.withIndent(' ', encode).convert(_events);
String filename = dir.fileSystem.path.join(dir.path, kManifestName);
await dir.fileSystem.file(filename).writeAsString(json, flush: true);
Future<Null> flush({Duration awaitPendingResults}) async {
if (_flushing) {
throw new StateError('Recording is already flushing');
}
_flushing = true;
try {
if (awaitPendingResults != null) {
Iterable<Future<Null>> futures =
_events.map((LiveInvocationEvent<dynamic> event) => event.done);
await Future
.wait<String>(futures)
.timeout(awaitPendingResults, onTimeout: () {});
}
Directory dir = destination;
String json = new JsonEncoder.withIndent(' ', encode).convert(_events);
String filename = dir.fileSystem.path.join(dir.path, kManifestName);
await dir.fileSystem.file(filename).writeAsString(json, flush: true);
} finally {
_flushing = false;
}
}

/// Adds the specified [event] to this recording.
void add(InvocationEvent<dynamic> event) => _events.add(event);
void add(LiveInvocationEvent<dynamic> event) => _events.add(event);
}
15 changes: 13 additions & 2 deletions lib/src/backends/record_replay/recording.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,20 @@ abstract class LiveRecording extends Recording {
/// Writes this recording to disk.
///
/// Live recordings will *not* call `flush` on themselves, so it is up to
/// callers to call this method when they wish to write the recording to disk.
/// callers to call this method when they wish to write the recording to
/// disk.
///
/// If [awaitPendingResults] is specified, this will wait the specified
/// duration for any results that are `Future`s or `Stream`s to complete
/// before serializing the recording to disk. Futures that don't complete
/// within the specified duration will have their results recorded as `null`,
/// and streams that don't send a "done" event within the specified duration
/// will have their results recorded as the list of events the stream has
/// fired thus far.
///
/// Throws a [StateError] if a flush is already in progress.
///
/// Returns a future that completes once the recording has been fully written
/// to disk.
Future<Null> flush();
Future<Null> flush({Duration awaitPendingResults});
}
Loading

0 comments on commit 42bf782

Please sign in to comment.