Commit 7536404b authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Add --replay-from argument to Flutter tools (#7146)

This argument will enable mocking of os-layer process invocations,
where the mock behavior will come from replaying a previously-
recorded set of invocations. At the point of process invocation,
the key metadata for the invocation will be looked up in the
recording's manifest, and iff a matching record exists in the
manifest, the process will be mocked out with data derived from
the corresponding recorded process (e.g. stdout, stderr, exit code).
parent 93e662ab
......@@ -11,21 +11,33 @@ import 'package:intl/intl.dart';
import 'package:path/path.dart' as path;
import 'context.dart';
import 'os.dart';
import 'process.dart';
ProcessManager get processManager => context[ProcessManager];
const String _kManifestName = 'MANIFEST.txt';
bool _areListsEqual/*<T>*/(List<dynamic/*=T*/> list1, List<dynamic/*=T*/> list2) {
int i = 0;
return list1 != null
&& list2 != null
&& list1.length == list2.length
&& list1.every((dynamic element) => element == list2[i++]);
}
/// A class that manages the creation of operating system processes. This
/// provides a lightweight wrapper around the underlying [Process] static
/// methods to allow the implementation of these methods to be mocked out or
/// decorated for testing or debugging purposes.
class ProcessManager {
Future<Process> start(
String executable,
List<String> arguments,
{String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode: ProcessStartMode.NORMAL}) {
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode: ProcessStartMode.NORMAL,
}) {
return Process.start(
executable,
arguments,
......@@ -36,12 +48,13 @@ class ProcessManager {
}
Future<ProcessResult> run(
String executable,
List<String> arguments,
{String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING}) {
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) {
return Process.run(
executable,
arguments,
......@@ -53,12 +66,13 @@ class ProcessManager {
}
ProcessResult runSync(
String executable,
List<String> arguments,
{String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING}) {
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) {
return Process.runSync(
executable,
arguments,
......@@ -85,7 +99,7 @@ class RecordingProcessManager implements ProcessManager {
'xcrun',
];
final FileSystemEntity _recordTo;
final String _recordTo;
final ProcessManager _delegate = new ProcessManager();
final Directory _tmpDir = Directory.systemTemp.createTempSync('flutter_tools_');
final List<Map<String, dynamic>> _manifest = <Map<String, dynamic>>[];
......@@ -102,18 +116,18 @@ class RecordingProcessManager implements ProcessManager {
/// If [recordTo] is a file (or doesn't exist), it is taken to be the name
/// of the ZIP file that will be created, and the containing folder will be
/// created as needed.
RecordingProcessManager({FileSystemEntity recordTo})
: _recordTo = recordTo ?? Directory.current {
RecordingProcessManager(this._recordTo) {
addShutdownHook(_onShutdown);
}
@override
Future<Process> start(
String executable,
List<String> arguments,
{String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode: ProcessStartMode.NORMAL}) async {
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode: ProcessStartMode.NORMAL,
}) async {
Process process = await _delegate.start(
executable,
arguments,
......@@ -122,8 +136,10 @@ class RecordingProcessManager implements ProcessManager {
mode: mode,
);
String basename = _getBasename(process.pid, executable, arguments);
Map<String, dynamic> manifestEntry = _createManifestEntry(
pid: process.pid,
basename: basename,
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
......@@ -134,7 +150,7 @@ class RecordingProcessManager implements ProcessManager {
_RecordingProcess result = new _RecordingProcess(
manager: this,
basename: _getBasename(process.pid, executable, arguments),
basename: basename,
delegate: process,
);
await result.startRecording();
......@@ -148,12 +164,13 @@ class RecordingProcessManager implements ProcessManager {
@override
Future<ProcessResult> run(
String executable,
List<String> arguments,
{String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING}) async {
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) async {
ProcessResult result = await _delegate.run(
executable,
arguments,
......@@ -163,8 +180,10 @@ class RecordingProcessManager implements ProcessManager {
stderrEncoding: stderrEncoding,
);
String basename = _getBasename(result.pid, executable, arguments);
_manifest.add(_createManifestEntry(
pid: result.pid,
basename: basename,
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
......@@ -174,7 +193,6 @@ class RecordingProcessManager implements ProcessManager {
exitCode: result.exitCode,
));
String basename = _getBasename(result.pid, executable, arguments);
await _recordData(result.stdout, stdoutEncoding, '$basename.stdout');
await _recordData(result.stderr, stderrEncoding, '$basename.stderr');
......@@ -198,12 +216,13 @@ class RecordingProcessManager implements ProcessManager {
@override
ProcessResult runSync(
String executable,
List<String> arguments,
{String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING}) {
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) {
ProcessResult result = _delegate.runSync(
executable,
arguments,
......@@ -213,8 +232,10 @@ class RecordingProcessManager implements ProcessManager {
stderrEncoding: stderrEncoding,
);
String basename = _getBasename(result.pid, executable, arguments);
_manifest.add(_createManifestEntry(
pid: result.pid,
basename: basename,
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
......@@ -224,7 +245,6 @@ class RecordingProcessManager implements ProcessManager {
exitCode: result.exitCode,
));
String basename = _getBasename(result.pid, executable, arguments);
_recordDataSync(result.stdout, stdoutEncoding, '$basename.stdout');
_recordDataSync(result.stderr, stderrEncoding, '$basename.stderr');
......@@ -254,27 +274,29 @@ class RecordingProcessManager implements ProcessManager {
/// Creates a JSON-encodable manifest entry representing the specified
/// process invocation.
Map<String, dynamic> _createManifestEntry({
int pid,
String executable,
List<String> arguments,
String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode,
Encoding stdoutEncoding,
Encoding stderrEncoding,
int exitCode,
int pid,
String basename,
String executable,
List<String> arguments,
String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode,
Encoding stdoutEncoding,
Encoding stderrEncoding,
int exitCode,
}) {
Map<String, dynamic> entry = <String, dynamic>{};
if (pid != null) entry['pid'] = pid;
if (executable != null) entry['executable'] = executable;
if (arguments != null) entry['arguments'] = arguments;
if (workingDirectory != null) entry['workingDirectory'] = workingDirectory;
if (environment != null) entry['environment'] = environment;
if (mode != null) entry['mode'] = mode.toString();
if (stdoutEncoding != null) entry['stdoutEncoding'] = stdoutEncoding.name;
if (stderrEncoding != null) entry['stderrEncoding'] = stderrEncoding.name;
if (exitCode != null) entry['exitCode'] = exitCode;
return entry;
return new _ManifestEntryBuilder()
.add('pid', pid)
.add('basename', basename)
.add('executable', executable)
.add('arguments', arguments)
.add('workingDirectory', workingDirectory)
.add('environment', environment)
.add('mode', mode, () => mode.toString())
.add('stdoutEncoding', stdoutEncoding, () => stdoutEncoding.name)
.add('stderrEncoding', stderrEncoding, () => stderrEncoding.name)
.add('exitCode', exitCode)
.entry;
}
/// Returns a human-readable identifier for the specified executable.
......@@ -323,7 +345,7 @@ class RecordingProcessManager implements ProcessManager {
void onTimeout(int pid, Map<String, dynamic> manifestEntry),
}) async {
await Future.wait(new List<Future<int>>.from(_runningProcesses.values))
.timeout(new Duration(milliseconds: 20), onTimeout: () {
.timeout(const Duration(milliseconds: 20), onTimeout: () {
_runningProcesses.forEach((int pid, Future<int> future) {
Map<String, dynamic> manifestEntry = _manifest
.firstWhere((Map<String, dynamic> entry) => entry['pid'] == pid);
......@@ -336,7 +358,7 @@ class RecordingProcessManager implements ProcessManager {
Future<Null> _writeManifestToDisk() async {
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
String encodedManifest = encoder.convert(_manifest);
File manifestFile = await new File('${_tmpDir.path}/process-manifest.txt').create();
File manifestFile = await new File('${_tmpDir.path}/$_kManifestName').create();
await manifestFile.writeAsString(encodedManifest, flush: true);
}
......@@ -351,17 +373,18 @@ class RecordingProcessManager implements ProcessManager {
/// in the [new RecordingProcessManager] constructor.
Future<File> _createZipFile() async {
File zipFile;
if (await FileSystemEntity.type(_recordTo.path) == FileSystemEntityType.DIRECTORY) {
zipFile = new File('${_recordTo.path}/$kDefaultRecordTo');
String recordTo = _recordTo ?? Directory.current.path;
if (FileSystemEntity.typeSync(recordTo) == FileSystemEntityType.DIRECTORY) {
zipFile = new File('$recordTo/$kDefaultRecordTo');
} else {
zipFile = new File(_recordTo.path);
zipFile = new File(recordTo);
await new Directory(path.dirname(zipFile.path)).create(recursive: true);
}
// Resolve collisions.
String basename = path.basename(zipFile.path);
for (int i = 1; await zipFile.exists(); i++) {
assert(await FileSystemEntity.isFile(zipFile.path));
for (int i = 1; zipFile.existsSync(); i++) {
assert(FileSystemEntity.isFileSync(zipFile.path));
String disambiguator = new NumberFormat('00').format(i);
String newBasename = basename;
if (basename.contains('.')) {
......@@ -397,6 +420,21 @@ class RecordingProcessManager implements ProcessManager {
}
}
/// A lightweight class that provides a builder pattern for building a
/// manifest entry.
class _ManifestEntryBuilder {
Map<String, dynamic> entry;
/// Adds the specified key/value pair to the manifest entry iff the value
/// is non-null. If [jsonValue] is specified, its value will be used instead
/// of the raw value.
_ManifestEntryBuilder add(String name, dynamic value, [dynamic jsonValue()]) {
if (value != null)
entry[name] = jsonValue == null ? value : jsonValue();
return this;
}
}
/// A [Process] implementation that records `stdout` and `stderr` stream events
/// to disk before forwarding them on to the underlying stream listener.
class _RecordingProcess implements Process {
......@@ -479,3 +517,327 @@ class _RecordingProcess implements Process {
@override
bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => delegate.kill(signal);
}
/// A [ProcessManager] implementation that mocks out all process invocations
/// by replaying a previously-recorded series of invocations, 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'`.
class ReplayProcessManager implements ProcessManager {
final List<Map<String, dynamic>> _manifest;
final Directory _dir;
ReplayProcessManager._(this._manifest, this._dir);
/// Creates a new `ReplayProcessManager` capable of replaying a recording at
/// the specified location.
///
/// If [location] represents a file, it will be treated like a recording
/// ZIP file. If it points to a directory, it will be treated like an
/// unzipped recording. If [location] points to a non-existent file or
/// directory, an [ArgumentError] will be thrown.
static Future<ReplayProcessManager> create(String location) async {
Directory dir;
switch (FileSystemEntity.typeSync(location)) {
case FileSystemEntityType.FILE:
dir = await Directory.systemTemp.createTemp('flutter_tools_');
os.unzip(new File(location), dir);
addShutdownHook(() async {
await dir.delete(recursive: true);
});
break;
case FileSystemEntityType.DIRECTORY:
dir = new Directory(location);
break;
case FileSystemEntityType.NOT_FOUND:
throw new ArgumentError.value(location, 'location', 'Does not exist');
}
File manifestFile = new File(path.join(dir.path, _kManifestName));
if (!manifestFile.existsSync()) {
// We use the existence of the manifest as a proxy for this being a
// valid replay directory. Namely, we don't validate the structure of the
// JSON within the manifest, and we don't validate the existence of
// all stdout and stderr files referenced in the manifest.
throw new ArgumentError.value(location, 'location',
'Does not represent a valid recording (it does not '
'contain $_kManifestName).');
}
String content = await manifestFile.readAsString();
try {
List<Map<String, dynamic>> manifest = new JsonDecoder().convert(content);
return new ReplayProcessManager._(manifest, dir);
} on FormatException {
throw new ArgumentError('$_kManifestName is not a valid JSON file.');
}
}
@override
Future<Process> start(
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
ProcessStartMode mode: ProcessStartMode.NORMAL,
}) async {
Map<String, dynamic> entry = _popEntry(executable, arguments, mode: mode);
_ReplayProcessResult result = await _ReplayProcessResult.create(
executable, arguments, _dir, entry);
return result.asProcess(entry['daemon'] ?? false);
}
@override
Future<ProcessResult> run(
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) async {
Map<String, dynamic> entry = _popEntry(executable, arguments,
stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
return await _ReplayProcessResult.create(
executable, arguments, _dir, entry);
}
@override
ProcessResult runSync(
String executable,
List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) {
Map<String, dynamic> entry = _popEntry(executable, arguments,
stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
return _ReplayProcessResult.createSync(
executable, arguments, _dir, entry);
}
/// Finds and returns the next entry in the process manifest that matches
/// the specified process arguments. Once found, it marks the manifest entry
/// as having been invoked and thus not eligible for invocation again.
Map<String, dynamic> _popEntry(String executable, List<String> arguments, {
ProcessStartMode mode,
Encoding stdoutEncoding,
Encoding stderrEncoding,
}) {
Map<String, dynamic> entry = _manifest.firstWhere(
(Map<String, dynamic> entry) {
// Ignore workingDirectory & environment, as they could
// yield false negatives.
return entry['executable'] == executable
&& _areListsEqual(entry['arguments'], arguments)
&& entry['mode'] == mode?.toString()
&& entry['stdoutEncoding'] == stdoutEncoding?.name
&& entry['stderrEncoding'] == stderrEncoding?.name
&& !(entry['invoked'] ?? false);
},
orElse: () => null,
);
if (entry == null)
throw new StateError('No matching invocation found for $executable');
entry['invoked'] = true;
return entry;
}
@override
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) {
throw new UnsupportedError(
"$runtimeType.killPid() has not been implemented because at the time "
"of its writing, it wasn't needed. If you're hitting this error, you "
"should implement it.");
}
}
/// A [ProcessResult] implementation that derives its data from a recording
/// fragment.
class _ReplayProcessResult implements ProcessResult {
@override
final int pid;
@override
final int exitCode;
@override
final dynamic stdout;
@override
final dynamic stderr;
_ReplayProcessResult._({this.pid, this.exitCode, this.stdout, this.stderr});
static Future<_ReplayProcessResult> create(
String executable,
List<String> arguments,
Directory dir,
Map<String, dynamic> entry,
) async {
String basePath = path.join(dir.path, entry['basename']);
try {
return new _ReplayProcessResult._(
pid: entry['pid'],
exitCode: entry['exitCode'],
stdout: await _getData('$basePath.stdout', entry['stdoutEncoding']),
stderr: await _getData('$basePath.stderr', entry['stderrEncoding']),
);
} catch (e) {
throw new ProcessException(executable, arguments, e.toString());
}
}
static Future<dynamic> _getData(String path, String encoding) async {
File file = new File(path);
return encoding == null
? await file.readAsBytes()
: await file.readAsString(encoding: _getEncodingByName(encoding));
}
static _ReplayProcessResult createSync(
String executable,
List<String> arguments,
Directory dir,
Map<String, dynamic> entry,
) {
String basePath = path.join(dir.path, entry['basename']);
try {
return new _ReplayProcessResult._(
pid: entry['pid'],
exitCode: entry['exitCode'],
stdout: _getDataSync('$basePath.stdout', entry['stdoutEncoding']),
stderr: _getDataSync('$basePath.stderr', entry['stderrEncoding']),
);
} catch (e) {
throw new ProcessException(executable, arguments, e.toString());
}
}
static dynamic _getDataSync(String path, String encoding) {
File file = new File(path);
return encoding == null
? file.readAsBytesSync()
: file.readAsStringSync(encoding: _getEncodingByName(encoding));
}
static Encoding _getEncodingByName(String encoding) {
if (encoding == 'system')
return const SystemEncoding();
else if (encoding != null)
return Encoding.getByName(encoding);
return null;
}
Process asProcess(bool daemon) {
assert(stdout is List<int>);
assert(stderr is List<int>);
return new _ReplayProcess(this, daemon);
}
}
/// A [Process] implementation derives its data from a recording fragment.
class _ReplayProcess implements Process {
@override
final int pid;
final List<int> _stdout;
final List<int> _stderr;
final StreamController<List<int>> _stdoutController;
final StreamController<List<int>> _stderrController;
final int _exitCode;
final Completer<int> _exitCodeCompleter;
_ReplayProcess(_ReplayProcessResult result, bool daemon)
: pid = result.pid,
_stdout = result.stdout,
_stderr = result.stderr,
_stdoutController = new StreamController<List<int>>(),
_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), () {
_stdoutController.add(_stdout);
_stderrController.add(_stderr);
if (!daemon)
kill();
});
}
@override
Stream<List<int>> get stdout => _stdoutController.stream;
@override
Stream<List<int>> get stderr => _stderrController.stream;
@override
Future<int> get exitCode => _exitCodeCompleter.future;
@override
set exitCode(Future<int> exitCode) => throw new UnsupportedError('set exitCode');
@override
IOSink get stdin => throw new UnimplementedError();
@override
bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) {
if (!_exitCodeCompleter.isCompleted) {
_exitCodeCompleter.complete(_exitCode);
return true;
}
return false;
}
}
......@@ -422,7 +422,8 @@ class IOSSimulator extends Device {
try {
await _setupUpdatedApplicationBundle(app);
} on ToolExit {
} on ToolExit catch (e) {
printError(e.message);
return new LaunchResult.failed();
}
} else {
......@@ -507,7 +508,7 @@ class IOSSimulator extends Device {
// Step 2: Assert that the Xcode project was successfully built.
IOSApp iosApp = app;
Directory bundle = new Directory(iosApp.simulatorBundlePath);
bool bundleExists = await bundle.exists();
bool bundleExists = bundle.existsSync();
if (!bundleExists)
throwToolExit('Could not find the built application bundle at ${bundle.path}.');
......
......@@ -10,6 +10,7 @@ import 'package:args/command_runner.dart';
import 'package:path/path.dart' as path;
import '../android/android_sdk.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/logger.dart';
import '../base/process.dart';
......@@ -102,6 +103,15 @@ class FlutterCommandRunner extends CommandRunner<Null> {
'If the location is a directory, a ZIP file named `recording.zip` '
'will be created in that directory. Otherwise, a ZIP file will be '
'created with the path specified in this flag.');
argParser.addOption('replay-from',
help:
'Enables mocking of process invocations by replaying their stdout, '
'stderr, and exit code from the specified recording (obtained '
'via --record-to).\n'
'If the location is a file, it is assumed to be a ZIP file '
'structured according to the output of --record-to. If the '
'location is a directory, it is assumed to be an unzipped version '
'of such a ZIP file.');
}
@override
......@@ -151,17 +161,28 @@ class FlutterCommandRunner extends CommandRunner<Null> {
context.setVariable(Logger, new VerboseLogger());
}
if (globalResults['record-to'] != null &&
globalResults['replay-from'] != null)
throwToolExit('--record-to and --replay-from cannot be used together.');
if (globalResults['record-to'] != null) {
// Turn on recording
String recordToPath = globalResults['record-to'].trim();
FileSystemEntity recordTo;
if (recordToPath.isNotEmpty) {
recordTo = await FileSystemEntity.isDirectory(recordToPath)
? new Directory(recordToPath)
: new File(recordToPath);
}
// Turn on recording.
String recordTo = globalResults['record-to'].trim();
if (recordTo.isEmpty)
recordTo = null;
context.setVariable(ProcessManager,
new RecordingProcessManager(recordTo: recordTo));
new RecordingProcessManager(recordTo));
}
if (globalResults['replay-from'] != null) {
// Turn on replay-based mocking.
try {
context.setVariable(ProcessManager, await ReplayProcessManager.create(
globalResults['replay-from'].trim(),
));
} on ArgumentError {
throwToolExit('--replay-from must specify a valid file or directory.');
}
}
logger.quiet = globalResults['quiet'];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment