Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 187 additions & 10 deletions lib/src/backends/record_replay/codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:file/file.dart';
import 'package:path/path.dart' as path;

import 'common.dart';
import 'errors.dart';
import 'events.dart';
import 'replay_directory.dart';
import 'replay_file.dart';
Expand Down Expand Up @@ -41,8 +42,8 @@ class _ForwardingConverter<S, T> extends Converter<S, T> {
class _GenericEncoder extends Converter<dynamic, dynamic> {
const _GenericEncoder();

/// Known encoders. Types not covered here will be encoded using
/// [_encodeDefault].
/// Known encoders. Types not covered here will be encoded as a [String]
/// whose value is the runtime type of the object being encoded.
///
/// When encoding an object, we will walk this map in insertion order looking
/// for a matching encoder. Thus, when there are two encoders that match an
Expand All @@ -67,22 +68,20 @@ class _GenericEncoder extends Converter<dynamic, dynamic> {
const TypeMatcher<FileStat>(): FileStatCodec.serialize,
const TypeMatcher<FileSystemEntityType>(): EntityTypeCodec.serialize,
const TypeMatcher<FileSystemEvent>(): FileSystemEventCodec.serialize,
const TypeMatcher<FileSystemException>(): _FSExceptionCodec.serialize,
const TypeMatcher<OSError>(): _OSErrorCodec.serialize,
const TypeMatcher<ArgumentError>(): _ArgumentErrorCodec.serialize,
const TypeMatcher<NoSuchMethodError>(): _NoSuchMethodErrorCodec.serialize,
};

/// Default encoder (used for types not covered in [_encoders]).
static String _encodeDefault(dynamic object) => object.runtimeType.toString();

@override
dynamic convert(dynamic input) {
Converter<dynamic, dynamic> encoder =
const _ForwardingConverter<dynamic, String>(_encodeDefault);
for (TypeMatcher<dynamic> matcher in _encoders.keys) {
if (matcher.matches(input)) {
encoder = _encoders[matcher];
break;
return _encoders[matcher].convert(input);
}
}
return encoder.convert(input);
return input.runtimeType.toString();
}
}

Expand Down Expand Up @@ -552,3 +551,181 @@ class BlobToBytes extends Converter<String, List<int>> {
return file.readAsBytesSync();
}
}

/// Converts serialized errors into throwable objects.
class ToError extends Converter<dynamic, dynamic> {
/// Creates a new [ToError].
const ToError();

/// Known decoders (keyed by `type`). Types not covered here will be decoded
/// into [InvocationException].
static const Map<String, Converter<Object, Object>> _decoders =
const <String, Converter<Object, Object>>{
_FSExceptionCodec.type: _FSExceptionCodec.deserialize,
_OSErrorCodec.type: _OSErrorCodec.deserialize,
_ArgumentErrorCodec.type: _ArgumentErrorCodec.deserialize,
_NoSuchMethodErrorCodec.type: _NoSuchMethodErrorCodec.deserialize,
};

@override
dynamic convert(dynamic input) {
if (input is Map) {
String errorType = input[kManifestErrorTypeKey];
if (_decoders.containsKey(errorType)) {
return _decoders[errorType].convert(input);
}
}
return new InvocationException();
}
}

class _FSExceptionCodec
extends Codec<FileSystemException, Map<String, Object>> {
const _FSExceptionCodec();

static const String type = 'FileSystemException';

static Map<String, Object> _encode(FileSystemException exception) {
return <String, Object>{
kManifestErrorTypeKey: type,
'message': exception.message,
'path': exception.path,
'osError': encode(exception.osError),
};
}

static FileSystemException _decode(Map<String, Object> input) {
Object osError = input['osError'];
return new FileSystemException(
input['message'],
input['path'],
osError == null ? null : const ToError().convert(osError),
);
}

static const Converter<FileSystemException, Map<String, Object>> serialize =
const _ForwardingConverter<FileSystemException, Map<String, Object>>(
_encode);

static const Converter<Map<String, Object>, FileSystemException> deserialize =
const _ForwardingConverter<Map<String, Object>, FileSystemException>(
_decode);

@override
Converter<FileSystemException, Map<String, Object>> get encoder => serialize;

@override
Converter<Map<String, Object>, FileSystemException> get decoder =>
deserialize;
}

class _OSErrorCodec extends Codec<OSError, Map<String, Object>> {
const _OSErrorCodec();

static const String type = 'OSError';

static Map<String, Object> _encode(OSError error) {
return <String, Object>{
kManifestErrorTypeKey: type,
'message': error.message,
'errorCode': error.errorCode,
};
}

static OSError _decode(Map<String, Object> input) {
return new OSError(input['message'], input['errorCode']);
}

static const Converter<OSError, Map<String, Object>> serialize =
const _ForwardingConverter<OSError, Map<String, Object>>(_encode);

static const Converter<Map<String, Object>, OSError> deserialize =
const _ForwardingConverter<Map<String, Object>, OSError>(_decode);

@override
Converter<OSError, Map<String, Object>> get encoder => serialize;

@override
Converter<Map<String, Object>, OSError> get decoder => deserialize;
}

class _ArgumentErrorCodec extends Codec<ArgumentError, Map<String, Object>> {
const _ArgumentErrorCodec();

static const String type = 'ArgumentError';

static Map<String, Object> _encode(ArgumentError error) {
return <String, Object>{
kManifestErrorTypeKey: type,
'message': encode(error.message),
'invalidValue': encode(error.invalidValue),
'name': error.name,
};
}

static ArgumentError _decode(Map<String, Object> input) {
dynamic message = input['message'];
dynamic invalidValue = input['invalidValue'];
String name = input['name'];
if (invalidValue != null) {
return new ArgumentError.value(invalidValue, name, message);
} else if (name != null) {
return new ArgumentError.notNull(name);
} else {
return new ArgumentError(message);
}
}

static const Converter<ArgumentError, Map<String, Object>> serialize =
const _ForwardingConverter<ArgumentError, Map<String, Object>>(_encode);

static const Converter<Map<String, Object>, ArgumentError> deserialize =
const _ForwardingConverter<Map<String, Object>, ArgumentError>(_decode);

@override
Converter<ArgumentError, Map<String, Object>> get encoder => serialize;

@override
Converter<Map<String, Object>, ArgumentError> get decoder => deserialize;
}

class _NoSuchMethodErrorCodec
extends Codec<NoSuchMethodError, Map<String, Object>> {
const _NoSuchMethodErrorCodec();

static const String type = 'NoSuchMethodError';

static Map<String, Object> _encode(NoSuchMethodError error) {
return <String, Object>{
kManifestErrorTypeKey: type,
'toString': error.toString(),
};
}

static NoSuchMethodError _decode(Map<String, Object> input) {
return new _NoSuchMethodError(input['toString']);
}

static const Converter<NoSuchMethodError, Map<String, Object>> serialize =
const _ForwardingConverter<NoSuchMethodError, Map<String, Object>>(
_encode);

static const Converter<Map<String, Object>, NoSuchMethodError> deserialize =
const _ForwardingConverter<Map<String, Object>, NoSuchMethodError>(
_decode);

@override
Converter<NoSuchMethodError, Map<String, Object>> get encoder => serialize;

@override
Converter<Map<String, Object>, NoSuchMethodError> get decoder => deserialize;
}

class _NoSuchMethodError extends Error implements NoSuchMethodError {
final String _toString;

_NoSuchMethodError(this._toString);

@override
String toString() => _toString;
}
12 changes: 10 additions & 2 deletions lib/src/backends/record_replay/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ const String kManifestObjectKey = 'object';
/// result (return value) of the invocation.
const String kManifestResultKey = 'result';

/// The key in a serialized [InvocationEvent] map that is used to store the
/// error that was thrown during the invocation.
const String kManifestErrorKey = 'error';

/// The key in a serialized error that is used to store the runtime type of
/// the error that was thrown.
const String kManifestErrorTypeKey = 'type';

/// The key in a serialized [InvocationEvent] map that is used to store the
/// timestamp of the invocation.
const String kManifestTimestampKey = 'timestamp';
Expand Down Expand Up @@ -118,9 +126,9 @@ abstract class ReplayAware {
bool deeplyEqual(dynamic object1, dynamic object2) {
if (object1.runtimeType != object2.runtimeType) {
return false;
} else if (object1 is List<dynamic>) {
} else if (object1 is List) {
return _areListsEqual(object1, object2);
} else if (object1 is Map<dynamic, dynamic>) {
} else if (object1 is Map) {
return _areMapsEqual(object1, object2);
} else {
return object1 == object2;
Expand Down
5 changes: 5 additions & 0 deletions lib/src/backends/record_replay/errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ class NoMatchingInvocationError extends Error {
return buf.toString();
}
}

/// Exception thrown during replay when an invocation recorded error, but we
/// were unable to find a type-specific converter to deserialize the recorded
/// error into a more specific exception type.
class InvocationException implements Exception {}
47 changes: 35 additions & 12 deletions lib/src/backends/record_replay/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,26 @@ abstract class InvocationEvent<T> {
/// The object on which the invocation occurred. Will always be non-null.
Object get object;

/// The return value of the invocation. This may be null (and will always be
/// `null` for setters).
/// The return value of the invocation if the invocation completed
/// successfully.
///
/// This may be null (and will always be `null` for setters).
///
/// If the invocation completed with an error, this value will be `null`,
/// and [error] will be set.
T get result;

/// The error that was thrown by the invocation if the invocation completed
/// with an error.
///
/// If the invocation completed successfully, this value will be `null`, and
/// [result] will hold the result of the invocation (which may also be
/// `null`).
///
/// This field being non-null can be used as an indication that the invocation
/// completed with an error.
dynamic get error;

/// The stopwatch value (in milliseconds) when the invocation occurred.
///
/// This value is recorded when the invocation first occurs, not when the
Expand Down Expand Up @@ -60,8 +76,8 @@ abstract class MethodEvent<T> extends InvocationEvent<T> {

/// 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);
/// Creates a new [LiveInvocationEvent].
LiveInvocationEvent(this.object, this._result, this.error, this.timestamp);

final dynamic _result;

Expand All @@ -78,6 +94,9 @@ abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
return result;
}

@override
final dynamic error;

@override
final int timestamp;

Expand Down Expand Up @@ -107,6 +126,7 @@ abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
return <String, dynamic>{
kManifestObjectKey: encode(object),
kManifestResultKey: encode(_result),
kManifestErrorKey: encode(error),
kManifestTimestampKey: timestamp,
};
}
Expand All @@ -118,9 +138,10 @@ abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
/// A [PropertyGetEvent] that's in the process of being recorded.
class LivePropertyGetEvent<T> extends LiveInvocationEvent<T>
implements PropertyGetEvent<T> {
/// Creates a new `LivePropertyGetEvent`.
LivePropertyGetEvent(Object object, this.property, T result, int timestamp)
: super(object, result, timestamp);
/// Creates a new [LivePropertyGetEvent].
LivePropertyGetEvent(
Object object, this.property, T result, dynamic error, int timestamp)
: super(object, result, error, timestamp);

@override
final Symbol property;
Expand All @@ -137,9 +158,10 @@ class LivePropertyGetEvent<T> extends LiveInvocationEvent<T>
/// A [PropertySetEvent] that's in the process of being recorded.
class LivePropertySetEvent<T> extends LiveInvocationEvent<Null>
implements PropertySetEvent<T> {
/// Creates a new `LivePropertySetEvent`.
LivePropertySetEvent(Object object, this.property, this.value, int timestamp)
: super(object, null, timestamp);
/// Creates a new [LivePropertySetEvent].
LivePropertySetEvent(
Object object, this.property, this.value, dynamic error, int timestamp)
: super(object, null, error, timestamp);

@override
final Symbol property;
Expand All @@ -160,20 +182,21 @@ class LivePropertySetEvent<T> extends LiveInvocationEvent<Null>
/// A [MethodEvent] that's in the process of being recorded.
class LiveMethodEvent<T> extends LiveInvocationEvent<T>
implements MethodEvent<T> {
/// Creates a new `LiveMethodEvent`.
/// Creates a new [LiveMethodEvent].
LiveMethodEvent(
Object object,
this.method,
List<dynamic> positionalArguments,
Map<Symbol, dynamic> namedArguments,
T result,
dynamic error,
int timestamp,
)
: this.positionalArguments =
new List<dynamic>.unmodifiable(positionalArguments),
this.namedArguments =
new Map<Symbol, dynamic>.unmodifiable(namedArguments),
super(object, result, timestamp);
super(object, result, error, timestamp);

@override
final Symbol method;
Expand Down
Loading