Skip to content

Commit 2c08bc1

Browse files
authored
Deeply encode objects in encode() (#23)
This changes `encode()` to deeply encode the specified object rather than performing a shallow encode. Previously, it relied on `JSONEncoder` to do the work of walking the object graph and calling `toEncodable` whenever it found a non-JSON type. Now, callers can call `encode()` on the top-level object and get back a deeply JSON-typed object that can be converted to JSON without the need for a `toEncodable` callback. This will be used during replay, when we'll compare an invocation to the list of recorded invocations, each of which exists as a JSON-type map. Since we now control the encoding process entirely ourselves, this also changes `encode()` to return `Future<dynamic>` rather than `dynamic`, which in turn allows both `InvocationEvent.serialize()` and `ResultReference.serializedValue` to return futures as well. Doing so allows for more technical correctness when serializing result references. Part of flutter#11
1 parent 53c69df commit 2c08bc1

File tree

6 files changed

+165
-63
lines changed

6 files changed

+165
-63
lines changed

lib/src/backends/record_replay/encoding.dart

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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+
import 'dart:async';
56
import 'dart:convert';
67

78
import 'package:file/file.dart';
@@ -18,6 +19,8 @@ import 'recording_random_access_file.dart';
1819
import 'result_reference.dart';
1920

2021
/// Encodes an object into a JSON-ready representation.
22+
///
23+
/// It is legal for an encoder to return a future value.
2124
typedef dynamic _Encoder(dynamic object);
2225

2326
/// Known encoders. Types not covered here will be encoded using
@@ -32,9 +35,8 @@ const Map<TypeMatcher<dynamic>, _Encoder> _kEncoders =
3235
const TypeMatcher<bool>(): _encodeRaw,
3336
const TypeMatcher<String>(): _encodeRaw,
3437
const TypeMatcher<Null>(): _encodeRaw,
35-
const TypeMatcher<List<dynamic>>(): _encodeRaw,
36-
const TypeMatcher<Map<dynamic, dynamic>>(): _encodeMap,
37-
const TypeMatcher<Iterable<dynamic>>(): _encodeIterable,
38+
const TypeMatcher<Iterable<dynamic>>(): encodeIterable,
39+
const TypeMatcher<Map<dynamic, dynamic>>(): encodeMap,
3840
const TypeMatcher<Symbol>(): getSymbolName,
3941
const TypeMatcher<DateTime>(): _encodeDateTime,
4042
const TypeMatcher<Uri>(): _encodeUri,
@@ -54,22 +56,20 @@ const Map<TypeMatcher<dynamic>, _Encoder> _kEncoders =
5456
const TypeMatcher<FileSystemEvent>(): _encodeFileSystemEvent,
5557
};
5658

57-
/// Encodes [object] into a JSON-ready representation.
58-
///
59-
/// This function is intended to be used as the `toEncodable` argument to the
60-
/// `JsonEncoder` constructors.
59+
/// Encodes an arbitrary [object] into a JSON-ready representation (a number,
60+
/// boolean, string, null, list, or map).
6161
///
62-
/// See also:
63-
/// - [JsonEncoder.withIndent]
64-
dynamic encode(dynamic object) {
62+
/// Returns a future that completes with a value suitable for conversion into
63+
/// JSON using [JsonEncoder] without the need for a `toEncodable` argument.
64+
Future<dynamic> encode(dynamic object) async {
6565
_Encoder encoder = _encodeDefault;
6666
for (TypeMatcher<dynamic> matcher in _kEncoders.keys) {
6767
if (matcher.matches(object)) {
6868
encoder = _kEncoders[matcher];
6969
break;
7070
}
7171
}
72-
return encoder(object);
72+
return await encoder(object);
7373
}
7474

7575
/// Default encoder (used for types not covered in [_kEncoders]).
@@ -78,20 +78,28 @@ String _encodeDefault(dynamic object) => object.runtimeType.toString();
7878
/// Pass-through encoder.
7979
dynamic _encodeRaw(dynamic object) => object;
8080

81-
List<T> _encodeIterable<T>(Iterable<T> iterable) => iterable.toList();
81+
/// Encodes the specified [iterable] into a JSON-ready list of encoded items.
82+
///
83+
/// Returns a future that completes with a list suitable for conversion into
84+
/// JSON using [JsonEncoder] without the need for a `toEncodable` argument.
85+
Future<List<dynamic>> encodeIterable(Iterable<dynamic> iterable) async {
86+
List<dynamic> encoded = <dynamic>[];
87+
for (dynamic element in iterable) {
88+
encoded.add(await encode(element));
89+
}
90+
return encoded;
91+
}
8292

83-
/// Encodes the map keys, and passes the values through.
93+
/// Encodes the specified [map] into a JSON-ready map of encoded key/value
94+
/// pairs.
8495
///
85-
/// As [JsonEncoder] encodes an object graph, it will repeatedly call
86-
/// `toEncodable` to encode unknown types, so any values in a map that need
87-
/// special encoding will already be handled by `JsonEncoder`. However, the
88-
/// encoder won't try to encode map *keys* by default, which is why we encode
89-
/// them here.
90-
Map<String, T> _encodeMap<T>(Map<dynamic, T> map) {
91-
Map<String, T> encoded = <String, T>{};
96+
/// Returns a future that completes with a map suitable for conversion into
97+
/// JSON using [JsonEncoder] without the need for a `toEncodable` argument.
98+
Future<Map<String, dynamic>> encodeMap(Map<dynamic, dynamic> map) async {
99+
Map<String, dynamic> encoded = <String, dynamic>{};
92100
for (dynamic key in map.keys) {
93-
String encodedKey = encode(key);
94-
encoded[encodedKey] = map[key];
101+
String encodedKey = await encode(key);
102+
encoded[encodedKey] = await encode(map[key]);
95103
}
96104
return encoded;
97105
}
@@ -100,15 +108,17 @@ int _encodeDateTime(DateTime dateTime) => dateTime.millisecondsSinceEpoch;
100108

101109
String _encodeUri(Uri uri) => uri.toString();
102110

103-
Map<String, String> _encodePathContext(p.Context context) => <String, String>{
104-
'style': context.style.name,
105-
'cwd': context.current,
106-
};
111+
Map<String, String> _encodePathContext(p.Context context) {
112+
return <String, String>{
113+
'style': context.style.name,
114+
'cwd': context.current,
115+
};
116+
}
107117

108-
dynamic _encodeResultReference(ResultReference<dynamic> reference) =>
118+
Future<dynamic> _encodeResultReference(ResultReference<dynamic> reference) =>
109119
reference.serializedValue;
110120

111-
Map<String, dynamic> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
121+
Future<Map<String, dynamic>> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
112122
event.serialize();
113123

114124
String _encodeFileSystem(FileSystem fs) => kFileSystemEncodedValue;
@@ -148,10 +158,10 @@ String _encodeFileMode(FileMode fileMode) {
148158
}
149159

150160
Map<String, dynamic> _encodeFileStat(FileStat stat) => <String, dynamic>{
151-
'changed': stat.changed,
152-
'modified': stat.modified,
153-
'accessed': stat.accessed,
154-
'type': stat.type,
161+
'changed': _encodeDateTime(stat.changed),
162+
'modified': _encodeDateTime(stat.modified),
163+
'accessed': _encodeDateTime(stat.accessed),
164+
'type': _encodeFileSystemEntityType(stat.type),
155165
'mode': stat.mode,
156166
'size': stat.size,
157167
'modeString': stat.modeString(),

lib/src/backends/record_replay/events.dart

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import 'dart:async';
66

7+
import 'common.dart';
8+
import 'encoding.dart';
79
import 'recording.dart';
810
import 'result_reference.dart';
911

@@ -101,11 +103,13 @@ abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
101103
}
102104

103105
/// Returns this event as a JSON-serializable object.
104-
Map<String, dynamic> serialize() => <String, dynamic>{
105-
'object': object,
106-
'result': _result,
107-
'timestamp': timestamp,
108-
};
106+
Future<Map<String, dynamic>> serialize() async {
107+
return <String, dynamic>{
108+
'object': await encode(object),
109+
'result': await encode(_result),
110+
'timestamp': timestamp,
111+
};
112+
}
109113

110114
@override
111115
String toString() => serialize().toString();
@@ -122,10 +126,12 @@ class LivePropertyGetEvent<T> extends LiveInvocationEvent<T>
122126
final Symbol property;
123127

124128
@override
125-
Map<String, dynamic> serialize() => <String, dynamic>{
126-
'type': 'get',
127-
'property': property,
128-
}..addAll(super.serialize());
129+
Future<Map<String, dynamic>> serialize() async {
130+
return <String, dynamic>{
131+
'type': 'get',
132+
'property': getSymbolName(property),
133+
}..addAll(await super.serialize());
134+
}
129135
}
130136

131137
/// A [PropertySetEvent] that's in the process of being recorded.
@@ -142,11 +148,13 @@ class LivePropertySetEvent<T> extends LiveInvocationEvent<Null>
142148
final T value;
143149

144150
@override
145-
Map<String, dynamic> serialize() => <String, dynamic>{
146-
'type': 'set',
147-
'property': property,
148-
'value': value,
149-
}..addAll(super.serialize());
151+
Future<Map<String, dynamic>> serialize() async {
152+
return <String, dynamic>{
153+
'type': 'set',
154+
'property': getSymbolName(property),
155+
'value': await encode(value),
156+
}..addAll(await super.serialize());
157+
}
150158
}
151159

152160
/// A [MethodEvent] that's in the process of being recorded.
@@ -177,10 +185,12 @@ class LiveMethodEvent<T> extends LiveInvocationEvent<T>
177185
final Map<Symbol, dynamic> namedArguments;
178186

179187
@override
180-
Map<String, dynamic> serialize() => <String, dynamic>{
181-
'type': 'invoke',
182-
'method': method,
183-
'positionalArguments': positionalArguments,
184-
'namedArguments': namedArguments,
185-
}..addAll(super.serialize());
188+
Future<Map<String, dynamic>> serialize() async {
189+
return <String, dynamic>{
190+
'type': 'invoke',
191+
'method': getSymbolName(method),
192+
'positionalArguments': await encodeIterable(positionalArguments),
193+
'namedArguments': await encodeMap(namedArguments),
194+
}..addAll(await super.serialize());
195+
}
186196
}

lib/src/backends/record_replay/mutable_recording.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ class MutableRecording implements LiveRecording {
4646
.timeout(awaitPendingResults, onTimeout: () {});
4747
}
4848
Directory dir = destination;
49-
String json = new JsonEncoder.withIndent(' ', encode).convert(_events);
49+
List<dynamic> encodedEvents = await encode(_events);
50+
String json = new JsonEncoder.withIndent(' ').convert(encodedEvents);
5051
String filename = dir.fileSystem.path.join(dir.path, kManifestName);
5152
await dir.fileSystem.file(filename).writeAsString(json, flush: true);
5253
} finally {

lib/src/backends/record_replay/recording_file.dart

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class _BlobReference<T> extends ResultReference<T> {
209209
T get recordedValue => _value;
210210

211211
@override
212-
dynamic get serializedValue => '!${_file.basename}';
212+
Future<String> get serializedValue async => '!${_file.basename}';
213213
}
214214

215215
/// A [FutureReference] that serializes its value data to a separate file.
@@ -235,14 +235,15 @@ class _BlobFutureReference<T> extends FutureReference<T> {
235235
}
236236

237237
@override
238-
dynamic get serializedValue => '!${_file.basename}';
238+
Future<String> get serializedValue async => '!${_file.basename}';
239239
}
240240

241241
/// A [StreamReference] that serializes its value data to a separate file.
242242
class _BlobStreamReference<T> extends StreamReference<T> {
243243
final File _file;
244244
final _BlobDataStreamWriter<T> _writer;
245245
IOSink _sink;
246+
Future<dynamic> _pendingFlush;
246247

247248
_BlobStreamReference({
248249
@required File file,
@@ -251,16 +252,20 @@ class _BlobStreamReference<T> extends StreamReference<T> {
251252
})
252253
: _file = file,
253254
_writer = writer,
254-
super(stream) {
255-
_file.createSync();
256-
}
255+
_sink = file.openWrite(),
256+
super(stream);
257257

258258
@override
259259
void onData(T event) {
260-
if (_sink == null) {
261-
_sink = _file.openWrite();
260+
if (_pendingFlush == null) {
261+
_writer(_sink, event);
262+
} else {
263+
// It's illegal to write to an IOSink while a flush is pending.
264+
// https://github.com/dart-lang/sdk/issues/28635
265+
_pendingFlush.whenComplete(() {
266+
_writer(_sink, event);
267+
});
262268
}
263-
_writer(_sink, event);
264269
}
265270

266271
@override
@@ -271,7 +276,20 @@ class _BlobStreamReference<T> extends StreamReference<T> {
271276
}
272277

273278
@override
274-
dynamic get serializedValue => '!${_file.basename}';
279+
Future<String> get serializedValue async {
280+
if (_pendingFlush != null) {
281+
await _pendingFlush;
282+
} else {
283+
_pendingFlush = _sink.flush();
284+
try {
285+
await _pendingFlush;
286+
} finally {
287+
_pendingFlush = null;
288+
}
289+
}
290+
291+
return '!${_file.basename}';
292+
}
275293

276294
// TODO(tvolkert): remove `.then()` once Dart 1.22 is in stable
277295
@override

lib/src/backends/record_replay/result_reference.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ abstract class ResultReference<T> {
4949
/// actually a byte array that was read from a file). In this case, the
5050
/// method can return a `ResultReference` to the list, and it will have a
5151
/// hook into the serialization process.
52-
dynamic get serializedValue => encode(recordedValue);
52+
Future<dynamic> get serializedValue => encode(recordedValue);
5353

5454
/// A [Future] that completes when [value] has completed.
5555
///

test/recording_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'package:file/memory.dart';
1010
import 'package:file/record_replay.dart';
1111
import 'package:file/testing.dart';
1212
import 'package:file/src/backends/record_replay/common.dart';
13+
import 'package:file/src/backends/record_replay/encoding.dart';
14+
import 'package:file/src/backends/record_replay/events.dart';
1315
import 'package:file/src/backends/record_replay/mutable_recording.dart';
1416
import 'package:file/src/backends/record_replay/recording_proxy_mixin.dart';
1517
import 'package:path/path.dart' as p;
@@ -182,6 +184,67 @@ void main() {
182184
});
183185
});
184186
});
187+
188+
group('Encode', () {
189+
test('performsDeepEncoding', () async {
190+
rc.basicProperty = 'foo';
191+
rc.basicProperty;
192+
rc.basicMethod('bar', namedArg: 'baz');
193+
await rc.futureProperty;
194+
await rc.futureMethod('qux', namedArg: 'quz');
195+
await rc.streamMethod('quux', namedArg: 'quuz').drain();
196+
List<Map<String, dynamic>> manifest = await encode(recording.events);
197+
expect(manifest[0], <String, dynamic>{
198+
'type': 'set',
199+
'property': 'basicProperty=',
200+
'value': 'foo',
201+
'object': '_RecordingClass',
202+
'result': isNull,
203+
'timestamp': 10,
204+
});
205+
expect(manifest[1], <String, dynamic>{
206+
'type': 'get',
207+
'property': 'basicProperty',
208+
'object': '_RecordingClass',
209+
'result': 'foo',
210+
'timestamp': 11,
211+
});
212+
expect(manifest[2], <String, dynamic>{
213+
'type': 'invoke',
214+
'method': 'basicMethod',
215+
'positionalArguments': <String>['bar'],
216+
'namedArguments': <String, String>{'namedArg': 'baz'},
217+
'object': '_RecordingClass',
218+
'result': 'bar.baz',
219+
'timestamp': 12,
220+
});
221+
expect(manifest[3], <String, dynamic>{
222+
'type': 'get',
223+
'property': 'futureProperty',
224+
'object': '_RecordingClass',
225+
'result': 'future.foo',
226+
'timestamp': 13,
227+
});
228+
expect(manifest[4], <String, dynamic>{
229+
'type': 'invoke',
230+
'method': 'futureMethod',
231+
'positionalArguments': <String>['qux'],
232+
'namedArguments': <String, String>{'namedArg': 'quz'},
233+
'object': '_RecordingClass',
234+
'result': 'future.qux.quz',
235+
'timestamp': 14,
236+
});
237+
expect(manifest[5], <String, dynamic>{
238+
'type': 'invoke',
239+
'method': 'streamMethod',
240+
'positionalArguments': <String>['quux'],
241+
'namedArguments': <String, String>{'namedArg': 'quuz'},
242+
'object': '_RecordingClass',
243+
'result': <String>['stream', 'quux', 'quuz'],
244+
'timestamp': 15,
245+
});
246+
});
247+
});
185248
});
186249

187250
group('RecordingFileSystem', () {

0 commit comments

Comments
 (0)