Cleanup
diff --git a/lib/src/record_replay/manifest.dart b/lib/src/record_replay/manifest.dart
index e5b5476..8c192e7 100644
--- a/lib/src/record_replay/manifest.dart
+++ b/lib/src/record_replay/manifest.dart
@@ -24,9 +24,11 @@
dynamic testValue, [
bool isEqual(dynamic value1, dynamic value2),
]) {
- if (testValue == null)
+ if (testValue == null) {
return true;
- else if (isEqual != null) return isEqual(entryValue, testValue);
+ } else if (isEqual != null) {
+ return isEqual(entryValue, testValue);
+ }
return entryValue == testValue;
}
@@ -38,7 +40,7 @@
/// Creates a new manifest.
Manifest();
- /// Creates a new manifest, populated with the specified [json] data.
+ /// Creates a new manifest populated with the specified [json] data.
///
/// If [json] does not represent a valid JSON string (matching the format of
/// [toJson]), a [FormatException] will be thrown.
diff --git a/lib/src/record_replay/manifest_entry.dart b/lib/src/record_replay/manifest_entry.dart
index 6e9f403..64fd687 100644
--- a/lib/src/record_replay/manifest_entry.dart
+++ b/lib/src/record_replay/manifest_entry.dart
@@ -5,18 +5,49 @@
import 'dart:convert';
import 'dart:io' show ProcessStartMode, SYSTEM_ENCODING;
+import 'manifest.dart';
import 'replay_process_manager.dart';
+/// Throws a [FormatException] if [data] does not contain [key].
+void _checkRequiredField(Map<String, dynamic> data, String key) {
+ if (!data.containsKey(key))
+ throw new FormatException('Required field missing: $key');
+}
+
+/// Gets a `ProcessStartMode` value by its string name.
+ProcessStartMode _getProcessStartMode(String value) {
+ if (value != null) {
+ for (ProcessStartMode mode in ProcessStartMode.values) {
+ if (mode.toString() == value) {
+ return mode;
+ }
+ }
+ throw new FormatException('Invalid value for mode: $value');
+ }
+ return null;
+}
+
+/// Gets an `Encoding` instance by the encoding name.
+Encoding _getEncoding(String encoding) {
+ if (encoding == 'system') {
+ return SYSTEM_ENCODING;
+ } else if (encoding != null) {
+ return Encoding.getByName(encoding);
+ }
+ return null;
+}
+
/// An entry in the process invocation manifest.
///
-/// Each entry represents a single recorded process invocation.
+/// Each entry in the [Manifest] represents a single recorded process
+/// invocation.
class ManifestEntry {
/// The process id.
final int pid;
/// The base file name for this entry. `stdout` and `stderr` files for this
- /// process will be serialized as `$basename.stdout` and `$basename.stderr`,
- /// respectively.
+ /// process will be serialized in the recording directory as
+ /// `$basename.stdout` and `$basename.stderr`, respectively.
final String basename;
/// The name of the executable that spawned the process.
@@ -40,16 +71,16 @@
/// The mode with which the process was spawned.
final ProcessStartMode mode;
- /// The encoding used for decoding `stdout` of the process.
+ /// The encoding used for the `stdout` of the process.
final Encoding stdoutEncoding;
- /// The encoding used for decoding `stderr` of the process.
+ /// The encoding used for the `stderr` of the process.
final Encoding stderrEncoding;
/// The exit code of the process.
int exitCode;
- /// Constructs a new manifest entry with the given properties.
+ /// Creates a new manifest entry with the given properties.
ManifestEntry({
this.pid,
this.basename,
@@ -65,7 +96,10 @@
this.exitCode,
});
- /// Creates a new manifest entry, populated with the specified JSON [data].
+ /// Creates a new manifest entry populated with the specified JSON [data].
+ ///
+ /// If any required fields are missing from the JSON data, this will throw
+ /// a [FormatException].
factory ManifestEntry.fromJson(Map<String, dynamic> data) {
_checkRequiredField(data, 'pid');
_checkRequiredField(data, 'basename');
@@ -90,28 +124,6 @@
return entry;
}
- static void _checkRequiredField(Map<String, dynamic> data, String key) {
- if (!data.containsKey(key))
- throw new FormatException('Required field missing: $key');
- }
-
- static ProcessStartMode _getProcessStartMode(String mode) {
- if (mode != null) {
- for (ProcessStartMode iter in ProcessStartMode.values) {
- if (iter.toString() == mode) return iter;
- }
- throw new FormatException('Invalid value for mode: $mode');
- }
- return null;
- }
-
- static Encoding _getEncoding(String encoding) {
- if (encoding == 'system')
- return SYSTEM_ENCODING;
- else if (encoding != null) return Encoding.getByName(encoding);
- return null;
- }
-
/// Indicates that the process is a daemon.
bool get daemon => _daemon;
bool _daemon = false;
@@ -122,11 +134,11 @@
bool _notResponding = false;
set notResponding(bool value) => _notResponding = value ?? false;
- /// Whether this entry has been invoked by [ReplayProcessManager].
+ /// Whether this entry has been "invoked" by [ReplayProcessManager].
bool get invoked => _invoked;
bool _invoked = false;
- /// Marks this entry as having been invoked by [ReplayProcessManager].
+ /// Marks this entry as having been "invoked" by [ReplayProcessManager].
void setInvoked() {
_invoked = true;
}
diff --git a/lib/src/record_replay/recording_process_manager.dart b/lib/src/record_replay/recording_process_manager.dart
index 9468f63..ae6a32f 100644
--- a/lib/src/record_replay/recording_process_manager.dart
+++ b/lib/src/record_replay/recording_process_manager.dart
@@ -28,9 +28,10 @@
/// A `RecordingProcessManager` decorates another `ProcessManager` instance by
/// recording all process invocation activity (including the stdout and stderr
/// of the associated processes) before delegating to the underlying manager.
-/// It is the basis of "record / replay" tests, where you record the process
+///
+/// This class enables "record / replay" tests, where you record the process
/// invocation activity during a real program run, serialize the activity to
-/// disk, then fake all invication activity during tests by replaying the
+/// disk, then fake all invocation activity during tests by replaying the
/// serialized recording.
///
/// See also:
@@ -64,9 +65,13 @@
/// a [StateError] will be thrown.
///
/// [destination] should be treated as opaque. Its contents are intended to
- /// be consumed by [ReplayProcessManager] and are subject to change between
- /// versions of which package.
- RecordingProcessManager(this.delegate, this.destination);
+ /// be consumed only by [ReplayProcessManager] and are subject to change
+ /// between versions of `package:process`.
+ RecordingProcessManager(this.delegate, this.destination) {
+ if (!destination.existsSync() || destination.listSync().isNotEmpty) {
+ throw new StateError('Cannot record to ${destination.path}');
+ }
+ }
/// The file system in which this manager will create recording files.
FileSystem get fs => destination.fileSystem;
diff --git a/lib/src/record_replay/replay_process_manager.dart b/lib/src/record_replay/replay_process_manager.dart
index d31c612..f78a2d6 100644
--- a/lib/src/record_replay/replay_process_manager.dart
+++ b/lib/src/record_replay/replay_process_manager.dart
@@ -23,52 +23,23 @@
import 'manifest_entry.dart';
import 'recording_process_manager.dart';
-/// Mocks out all process invocations by replaying a previously-recorded series
+/// Fakes all process invocations by replaying a previously-recorded series
/// of invocations.
///
-/// Fopo, throwing an
-/// exception if the requested invocations substantively differ in any way
-/// from those in the recording.
-///
-/// Recordings are expected to be of the form produced by
-/// [RecordingProcessManager]. Namely, this includes:
-///
-/// - a [_kManifestName](manifest file) encoded as UTF-8 JSON that lists all
-/// invocations in order, along with the following metadata for each
-/// invocation:
-/// - `pid` (required): The process id integer.
-/// - `basename` (required): A string specifying the base filename from which
-/// the incovation's `stdout` and `stderr` files can be located.
-/// - `executable` (required): A string specifying the path to the executable
-/// command that kicked off the process.
-/// - `arguments` (required): A list of strings that were passed as arguments
-/// to the executable.
-/// - `workingDirectory` (required): The current working directory from which
-/// the process was spawned.
-/// - `environment` (required): A map from string environment variable keys
-/// to their corresponding string values.
-/// - `mode` (optional): A string specifying the [ProcessStartMode].
-/// - `stdoutEncoding` (optional): The name of the encoding scheme that was
-/// used in the `stdout` file. If unspecified, then the file was written
-/// as binary data.
-/// - `stderrEncoding` (optional): The name of the encoding scheme that was
-/// used in the `stderr` file. If unspecified, then the file was written
-/// as binary data.
-/// - `exitCode` (required): The exit code of the process, or null if the
-/// process was not responding.
-/// - `daemon` (optional): A boolean indicating that the process is to stay
-/// resident during the entire lifetime of the master Flutter tools process.
-/// - a `stdout` file for each process invocation. The location of this file
-/// can be derived from the `basename` manifest property like so:
-/// `'$basename.stdout'`.
-/// - a `stderr` file for each process invocation. The location of this file
-/// can be derived from the `basename` manifest property like so:
-/// `'$basename.stderr'`.
+/// Recordings exist as opaque directories that are produced by
+/// [RecordingProcessManager].
class ReplayProcessManager implements ProcessManager {
final Manifest _manifest;
- final Directory _dir;
- ReplayProcessManager._(this._manifest, this._dir);
+ /// The location of the serialized recording that's driving this manager.
+ final Directory location;
+
+ /// If non-null, processes spawned by this manager will delay their
+ /// `stdout` and `stderr` stream production by the this amount. See
+ /// description of the associated parameter in [create].
+ final Duration streamDelay;
+
+ ReplayProcessManager._(this._manifest, this.location, this.streamDelay);
/// Creates a new `ReplayProcessManager` capable of replaying a recording that
/// was serialized to the specified [location] by [RecordingProcessManager].
@@ -76,7 +47,22 @@
/// If [location] does not exist, or if it does not represent a valid
/// recording (as determined by [RecordingProcessManager]), an [ArgumentError]
/// will be thrown.
- static Future<ReplayProcessManager> create(Directory location) async {
+ ///
+ /// If [streamDelay] is specified, processes spawned by this manager will
+ /// delay their `stdout` and `stderr` stream production by the specified
+ /// amount. This is useful in cases where the real process invocation had
+ /// a necessary delay in stream production, and you need to mirror that
+ /// behavior. e.g. you spawn a `tail` process to tail a log file, then in a
+ /// follow-on event loop, you invoke a `startServer` process, which starts
+ /// producing log output. In this case, you may need to delay the `tail`
+ /// output to prevent its stream from flushing all its content before you
+ /// start listening.
+ static Future<ReplayProcessManager> create(
+ Directory location, {
+ Duration streamDelay: Duration.ZERO,
+ }) async {
+ assert(streamDelay != null);
+
if (!location.existsSync()) {
throw new ArgumentError.value(location.path, 'location', "Doesn't exist");
}
@@ -93,7 +79,7 @@
// We don't validate the existence of all stdout and stderr files
// referenced in the manifest.
Manifest manifest = new Manifest.fromJson(content);
- return new ReplayProcessManager._(manifest, location);
+ return new ReplayProcessManager._(manifest, location, streamDelay);
} on FormatException catch (e) {
throw new ArgumentError('$kManifestName is not a valid JSON file: $e');
}
@@ -111,7 +97,7 @@
}) async {
ManifestEntry entry = _popEntry(executable, arguments, mode: mode);
_ReplayResult result =
- await _ReplayResult.create(executable, arguments, _dir, entry);
+ await _ReplayResult.create(this, executable, arguments, entry);
return result.asProcess(entry.daemon);
}
@@ -128,7 +114,7 @@
}) async {
ManifestEntry entry = _popEntry(executable, arguments,
stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
- return await _ReplayResult.create(executable, arguments, _dir, entry);
+ return await _ReplayResult.create(this, executable, arguments, entry);
}
@override
@@ -144,7 +130,7 @@
}) {
ManifestEntry entry = _popEntry(executable, arguments,
stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
- return _ReplayResult.createSync(executable, arguments, _dir, entry);
+ return _ReplayResult.createSync(this, executable, arguments, entry);
}
/// Finds and returns the next entry in the process manifest that matches
@@ -185,6 +171,8 @@
/// A [ProcessResult] implementation that derives its data from a recording
/// fragment.
class _ReplayResult implements io.ProcessResult {
+ final ReplayProcessManager manager;
+
@override
final int pid;
@@ -197,18 +185,19 @@
@override
final dynamic stderr;
- _ReplayResult._({this.pid, this.exitCode, this.stdout, this.stderr});
+ _ReplayResult._({this.manager, this.pid, this.exitCode, this.stdout, this.stderr,});
static Future<_ReplayResult> create(
+ ReplayProcessManager manager,
String executable,
List<String> arguments,
- Directory dir,
ManifestEntry entry,
) async {
- FileSystem fs = dir.fileSystem;
- String basePath = path.join(dir.path, entry.basename);
+ FileSystem fs = manager.location.fileSystem;
+ String basePath = path.join(manager.location.path, entry.basename);
try {
return new _ReplayResult._(
+ manager: manager,
pid: entry.pid,
exitCode: entry.exitCode,
stdout: await _getData(fs, '$basePath.stdout', entry.stdoutEncoding),
@@ -228,15 +217,16 @@
}
static _ReplayResult createSync(
+ ReplayProcessManager manager,
String executable,
List<String> arguments,
- Directory dir,
ManifestEntry entry,
) {
- FileSystem fs = dir.fileSystem;
- String basePath = path.join(dir.path, entry.basename);
+ FileSystem fs = manager.location.fileSystem;
+ String basePath = path.join(manager.location.path, entry.basename);
try {
return new _ReplayResult._(
+ manager: manager,
pid: entry.pid,
exitCode: entry.exitCode,
stdout: _getDataSync(fs, '$basePath.stdout', entry.stdoutEncoding),
@@ -281,23 +271,9 @@
_stderrController = new StreamController<List<int>>(),
_exitCode = result.exitCode,
_exitCodeCompleter = new Completer<int>() {
- // Don't flush our stdio streams until we reach the outer event loop. This
- // is necessary because some of our process invocations transform the stdio
- // streams into broadcast streams (e.g. DeviceLogReader implementations),
- // and delaying our stdio stream production until we reach the outer event
- // loop allows all code running in the microtask loop to register as
- // listeners on these streams before we flush them.
- //
- // TODO(tvolkert): Once https://github.com/flutter/flutter/issues/7166 is
- // resolved, running on the outer event loop should be
- // sufficient (as described above), and we should switch to
- // Duration.ZERO. In the meantime, native file I/O
- // operations are causing a Duration.ZERO callback here to
- // run before our ProtocolDiscovery instantiation, and thus,
- // we flush our stdio streams before our protocol discovery
- // is listening on them (causing us to timeout waiting for
- // the observatory port discovery).
- new Timer(const Duration(milliseconds: 50), () {
+ // Don't flush our stdio streams until we at least reach the outer event
+ // loop. i.e. even if `streamDelay` is zero, we still want to use the timer.
+ new Timer(result.manager.streamDelay, () {
_stdoutController.add(_stdout);
_stderrController.add(_stderr);
if (!daemon) kill();