Record & replay errors in (Recording|Replay)FileSystem (#35)
diff --git a/lib/src/backends/record_replay/codecs.dart b/lib/src/backends/record_replay/codecs.dart
index 2ecd1f7..a974692 100644
--- a/lib/src/backends/record_replay/codecs.dart
+++ b/lib/src/backends/record_replay/codecs.dart
@@ -10,6 +10,7 @@
import 'package:path/path.dart' as path;
import 'common.dart';
+import 'errors.dart';
import 'events.dart';
import 'replay_directory.dart';
import 'replay_file.dart';
@@ -41,8 +42,8 @@
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
@@ -67,22 +68,20 @@
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();
}
}
@@ -552,3 +551,181 @@
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;
+}
diff --git a/lib/src/backends/record_replay/common.dart b/lib/src/backends/record_replay/common.dart
index 9313f90..3dbac0d 100644
--- a/lib/src/backends/record_replay/common.dart
+++ b/lib/src/backends/record_replay/common.dart
@@ -28,6 +28,14 @@
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';
@@ -118,9 +126,9 @@
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;
diff --git a/lib/src/backends/record_replay/errors.dart b/lib/src/backends/record_replay/errors.dart
index 8e199ba..d2ebdb0 100644
--- a/lib/src/backends/record_replay/errors.dart
+++ b/lib/src/backends/record_replay/errors.dart
@@ -42,3 +42,8 @@
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 {}
diff --git a/lib/src/backends/record_replay/events.dart b/lib/src/backends/record_replay/events.dart
index 65bab49..53884e3 100644
--- a/lib/src/backends/record_replay/events.dart
+++ b/lib/src/backends/record_replay/events.dart
@@ -16,10 +16,26 @@
/// 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
@@ -60,8 +76,8 @@
/// 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;
@@ -79,6 +95,9 @@
}
@override
+ final dynamic error;
+
+ @override
final int timestamp;
/// A [Future] that completes once [result] is ready for serialization.
@@ -107,6 +126,7 @@
return <String, dynamic>{
kManifestObjectKey: encode(object),
kManifestResultKey: encode(_result),
+ kManifestErrorKey: encode(error),
kManifestTimestampKey: timestamp,
};
}
@@ -118,9 +138,10 @@
/// 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;
@@ -137,9 +158,10 @@
/// 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;
@@ -160,20 +182,21 @@
/// 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;
diff --git a/lib/src/backends/record_replay/mutable_recording.dart b/lib/src/backends/record_replay/mutable_recording.dart
index 2693291..4a7681b 100644
--- a/lib/src/backends/record_replay/mutable_recording.dart
+++ b/lib/src/backends/record_replay/mutable_recording.dart
@@ -32,19 +32,19 @@
new List<LiveInvocationEvent<dynamic>>.unmodifiable(_events);
@override
- Future<Null> flush({Duration awaitPendingResults}) async {
+ Future<Null> flush({Duration pendingResultTimeout}) 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<Null>(futures)
- .timeout(awaitPendingResults, onTimeout: () {});
+ Iterable<Future<Null>> futures =
+ _events.map((LiveInvocationEvent<dynamic> event) => event.done);
+ Future<List<Null>> results = Future.wait<Null>(futures);
+ if (pendingResultTimeout != null) {
+ results = results.timeout(pendingResultTimeout, onTimeout: () {});
}
+ await results;
Directory dir = destination;
String json = new JsonEncoder.withIndent(' ').convert(encode(_events));
String filename = dir.fileSystem.path.join(dir.path, kManifestName);
diff --git a/lib/src/backends/record_replay/recording.dart b/lib/src/backends/record_replay/recording.dart
index 3932905..6676195 100644
--- a/lib/src/backends/record_replay/recording.dart
+++ b/lib/src/backends/record_replay/recording.dart
@@ -38,7 +38,7 @@
/// callers to call this method when they wish to write the recording to
/// disk.
///
- /// If [awaitPendingResults] is specified, this will wait the specified
+ /// If [pendingResultTimeout] 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`,
@@ -46,9 +46,13 @@
/// will have their results recorded as the list of events the stream has
/// fired thus far.
///
+ /// If [pendingResultTimeout] is not specified (or is `null`), this will wait
+ /// indefinitely for for any results that are `Future`s or `Stream`s to
+ /// complete before serializing the recording to disk.
+ ///
/// 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({Duration awaitPendingResults});
+ Future<Null> flush({Duration pendingResultTimeout});
}
diff --git a/lib/src/backends/record_replay/recording_proxy_mixin.dart b/lib/src/backends/record_replay/recording_proxy_mixin.dart
index ddf1f43..055b7a1 100644
--- a/lib/src/backends/record_replay/recording_proxy_mixin.dart
+++ b/lib/src/backends/record_replay/recording_proxy_mixin.dart
@@ -113,10 +113,30 @@
: super.noSuchMethod(invocation);
}
- // Invoke the configured delegate method, wrapping Future and Stream
- // results so that we record their values as they become available.
- // TODO(tvolkert): record exceptions.
- dynamic value = Function.apply(method, args, namedArgs);
+ InvocationEvent<dynamic> createEvent({dynamic result, dynamic error}) {
+ if (invocation.isGetter) {
+ return new LivePropertyGetEvent<dynamic>(
+ this, name, result, error, time);
+ } else if (invocation.isSetter) {
+ return new LivePropertySetEvent<dynamic>(
+ this, name, args[0], error, time);
+ } else {
+ return new LiveMethodEvent<dynamic>(
+ this, name, args, namedArgs, result, error, time);
+ }
+ }
+
+ // Invoke the configured delegate method, recording an error if one occurs.
+ dynamic value;
+ try {
+ value = Function.apply(method, args, namedArgs);
+ } catch (error) {
+ recording.add(createEvent(error: error));
+ rethrow;
+ }
+
+ // Wrap Future and Stream results so that we record their values as they
+ // become available.
if (value is Stream) {
value = new StreamReference<dynamic>(value);
} else if (value is Future) {
@@ -124,19 +144,7 @@
}
// Record the invocation event associated with this invocation.
- InvocationEvent<dynamic> event;
- if (invocation.isGetter) {
- event = new LivePropertyGetEvent<dynamic>(this, name, value, time);
- } else if (invocation.isSetter) {
- // TODO(tvolkert): Remove indirection once SDK 1.22 is in stable branch
- dynamic temp =
- new LivePropertySetEvent<dynamic>(this, name, args[0], time);
- event = temp;
- } else {
- event = new LiveMethodEvent<dynamic>(
- this, name, args, namedArgs, value, time);
- }
- recording.add(event);
+ recording.add(createEvent(result: value));
// Unwrap any result references before returning to the caller.
dynamic result = value;
diff --git a/lib/src/backends/record_replay/replay_proxy_mixin.dart b/lib/src/backends/record_replay/replay_proxy_mixin.dart
index 56d8258..9f37b9c 100644
--- a/lib/src/backends/record_replay/replay_proxy_mixin.dart
+++ b/lib/src/backends/record_replay/replay_proxy_mixin.dart
@@ -126,6 +126,10 @@
}
entry[kManifestOrdinalKey] = _nextOrdinal++;
+ dynamic error = entry[kManifestErrorKey];
+ if (error != null) {
+ throw const ToError().convert(error);
+ }
dynamic result = reviver.convert(entry[kManifestResultKey]);
result = onResult(invocation, result);
return result;
diff --git a/lib/src/backends/record_replay/result_reference.dart b/lib/src/backends/record_replay/result_reference.dart
index fa6e9c4..ab4378b 100644
--- a/lib/src/backends/record_replay/result_reference.dart
+++ b/lib/src/backends/record_replay/result_reference.dart
@@ -89,9 +89,9 @@
@override
T get recordedValue => _value;
- // TODO(tvolkert): remove `.then()` once Dart 1.22 is in stable
+ // TODO(tvolkert): remove `as Future<Null>` once Dart 1.22 is in stable
@override
- Future<Null> get complete => value.then<Null>((_) {});
+ Future<Null> get complete => value.catchError((_) {}) as Future<Null>;
}
/// Wraps a stream result.
@@ -159,5 +159,5 @@
List<T> get recordedValue => _data;
@override
- Future<Null> get complete => _completer.future;
+ Future<Null> get complete => _completer.future.catchError((_) {});
}
diff --git a/test/recording_test.dart b/test/recording_test.dart
index 7efcf2f..99bb54a 100644
--- a/test/recording_test.dart
+++ b/test/recording_test.dart
@@ -130,6 +130,7 @@
'value': 'foo',
'object': '_RecordingClass',
'result': null,
+ 'error': null,
'timestamp': 10,
});
expect(manifest[1], <String, dynamic>{
@@ -137,6 +138,7 @@
'property': 'basicProperty',
'object': '_RecordingClass',
'result': 'foo',
+ 'error': null,
'timestamp': 11,
});
expect(manifest[2], <String, dynamic>{
@@ -146,21 +148,21 @@
'namedArguments': <String, dynamic>{'namedArg': 'bar'},
'object': '_RecordingClass',
'result': 'foo.bar',
+ 'error': null,
'timestamp': 12
});
});
- test('doesntAwaitPendingResultsUnlessToldToDoSo', () async {
- rc.futureMethod('foo', namedArg: 'bar'); // ignore: unawaited_futures
- await recording.flush();
- List<Map<String, dynamic>> manifest = _loadManifest(recording);
- expect(manifest[0], containsPair('result', null));
+ test('awaitsPendingResultsIndefinitelyByDefault', () async {
+ rc.veryLongFutureMethod(); // ignore: unawaited_futures
+ expect(recording.flush().timeout(const Duration(milliseconds: 50)),
+ throwsTimeoutException);
});
test('succeedsIfAwaitPendingResultsThatComplete', () async {
rc.futureMethod('foo', namedArg: 'bar'); // ignore: unawaited_futures
await recording.flush(
- awaitPendingResults: const Duration(seconds: 30));
+ pendingResultTimeout: const Duration(seconds: 30));
List<Map<String, dynamic>> manifest = _loadManifest(recording);
expect(manifest[0], containsPair('result', 'future.foo.bar'));
});
@@ -169,7 +171,7 @@
rc.veryLongFutureMethod(); // ignore: unawaited_futures
DateTime before = new DateTime.now();
await recording.flush(
- awaitPendingResults: const Duration(milliseconds: 250));
+ pendingResultTimeout: const Duration(milliseconds: 250));
DateTime after = new DateTime.now();
Duration delta = after.difference(before);
List<Map<String, dynamic>> manifest = _loadManifest(recording);
@@ -200,6 +202,7 @@
'value': 'foo',
'object': '_RecordingClass',
'result': isNull,
+ 'error': null,
'timestamp': 10,
});
expect(manifest[1], <String, dynamic>{
@@ -207,6 +210,7 @@
'property': 'basicProperty',
'object': '_RecordingClass',
'result': 'foo',
+ 'error': null,
'timestamp': 11,
});
expect(manifest[2], <String, dynamic>{
@@ -216,6 +220,7 @@
'namedArguments': <String, String>{'namedArg': 'baz'},
'object': '_RecordingClass',
'result': 'bar.baz',
+ 'error': null,
'timestamp': 12,
});
expect(manifest[3], <String, dynamic>{
@@ -223,6 +228,7 @@
'property': 'futureProperty',
'object': '_RecordingClass',
'result': 'future.foo',
+ 'error': null,
'timestamp': 13,
});
expect(manifest[4], <String, dynamic>{
@@ -232,6 +238,7 @@
'namedArguments': <String, String>{'namedArg': 'quz'},
'object': '_RecordingClass',
'result': 'future.qux.quz',
+ 'error': null,
'timestamp': 14,
});
expect(manifest[5], <String, dynamic>{
@@ -241,6 +248,7 @@
'namedArguments': <String, String>{'namedArg': 'quuz'},
'object': '_RecordingClass',
'result': <String>['stream', 'quux', 'quuz'],
+ 'error': null,
'timestamp': 15,
});
});
@@ -920,3 +928,7 @@
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
+
+/// Successfully matches against a function that throws a [TimeoutException].
+const Matcher throwsTimeoutException =
+ const Throws(const isInstanceOf<TimeoutException>());
diff --git a/test/replay_test.dart b/test/replay_test.dart
index f9f7fa9..4b2ea18 100644
--- a/test/replay_test.dart
+++ b/test/replay_test.dart
@@ -39,8 +39,11 @@
() => recordingFileSystem,
replay: replay,
skip: <String>[
- // ReplayFileSystem does not yet replay exceptions
- '.*(disallows|throws|blocks).*',
+ // ReplayFileSystem does not yet support futures & streams that throw
+ 'File > openRead > throws.*',
+ 'File > openWrite > throws.*',
+ 'File > openWrite > ioSink > throwsIfAddError',
+ 'File > openWrite > ioSink > addStream > blocks.*',
'File > open', // Not yet implemented in MemoryFileSystem
],