blob: 75e6c27ff3882d663b8d54dcaabbbed58a36cf2a [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io show ProcessSignal;
import 'package:flutter_tools/src/base/io.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'common.dart';
import 'context.dart';
export 'package:process/process.dart' show ProcessManager;
typedef VoidCallback = void Function();
/// A command for [FakeProcessManager].
@immutable
class FakeCommand {
const FakeCommand({
@required this.command,
this.workingDirectory,
this.environment,
this.encoding,
this.duration = const Duration(),
this.onRun,
this.exitCode = 0,
this.stdout = '',
this.stderr = '',
this.completer,
this.stdin,
}) : assert(command != null),
assert(duration != null),
assert(exitCode != null);
/// The exact commands that must be matched for this [FakeCommand] to be
/// considered correct.
final List<String> command;
/// The exact working directory that must be matched for this [FakeCommand] to
/// be considered correct.
///
/// If this is null, the working directory is ignored.
final String workingDirectory;
/// The environment that must be matched for this [FakeCommand] to be considered correct.
///
/// If this is null, then the environment is ignored.
///
/// Otherwise, each key in this environment must be present and must have a
/// value that matches the one given here for the [FakeCommand] to match.
final Map<String, String> environment;
/// The stdout and stderr encoding that must be matched for this [FakeCommand]
/// to be considered correct.
///
/// If this is null, then the encodings are ignored.
final Encoding encoding;
/// The time to allow to elapse before returning the [exitCode], if this command
/// is "executed".
///
/// If you set this to a non-zero time, you should use a [FakeAsync] zone,
/// otherwise the test will be artificially slow.
final Duration duration;
/// A callback that is run after [duration] expires but before the [exitCode]
/// (and output) are passed back.
final VoidCallback onRun;
/// The process' exit code.
///
/// To simulate a never-ending process, set [duration] to a value greater than
/// 15 minutes (the timeout for our tests).
///
/// To simulate a crash, subtract the crash signal number from 256. For example,
/// SIGPIPE (-13) is 243.
final int exitCode;
/// The output to simulate on stdout. This will be encoded as UTF-8 and
/// returned in one go.
final String stdout;
/// The output to simulate on stderr. This will be encoded as UTF-8 and
/// returned in one go.
final String stderr;
/// If provided, allows the command completion to be blocked until the future
/// resolves.
final Completer<void> completer;
/// An optional stdin sink that will be exposed through the resulting
/// [FakeProcess].
final IOSink stdin;
void _matches(
List<String> command,
String workingDirectory,
Map<String, String> environment,
Encoding encoding,
) {
expect(command, equals(this.command));
if (this.workingDirectory != null) {
expect(this.workingDirectory, workingDirectory);
}
if (this.environment != null) {
expect(this.environment, environment);
}
if (this.encoding != null) {
expect(this.encoding, encoding);
}
}
}
class _FakeProcess implements Process {
_FakeProcess(
this._exitCode,
Duration duration,
this.pid,
this._stderr,
this.stdin,
this._stdout,
this._completer,
) : exitCode = Future<void>.delayed(duration).then((void value) {
if (_completer != null) {
return _completer.future.then((void _) => _exitCode);
}
return _exitCode;
}),
stderr = _stderr == null
? const Stream<List<int>>.empty()
: Stream<List<int>>.value(utf8.encode(_stderr)),
stdout = _stdout == null
? const Stream<List<int>>.empty()
: Stream<List<int>>.value(utf8.encode(_stdout));
final int _exitCode;
final Completer<void> _completer;
@override
final Future<int> exitCode;
@override
final int pid;
final String _stderr;
@override
final Stream<List<int>> stderr;
@override
final IOSink stdin;
@override
final Stream<List<int>> stdout;
final String _stdout;
@override
bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
// Killing a fake process has no effect.
return false;
}
}
abstract class FakeProcessManager implements ProcessManager {
/// A fake [ProcessManager] which responds to all commands as if they had run
/// instantaneously with an exit code of 0 and no output.
factory FakeProcessManager.any() = _FakeAnyProcessManager;
/// A fake [ProcessManager] which responds to particular commands with
/// particular results.
///
/// On creation, pass in a list of [FakeCommand] objects. When the
/// [ProcessManager] methods such as [start] are invoked, the next
/// [FakeCommand] must match (otherwise the test fails); its settings are used
/// to simulate the result of running that command.
///
/// If no command is found, then one is implied which immediately returns exit
/// code 0 with no output.
///
/// There is no logic to ensure that all the listed commands are run. Use
/// [FakeCommand.onRun] to set a flag, or specify a sentinel command as your
/// last command and verify its execution is successful, to ensure that all
/// the specified commands are actually called.
factory FakeProcessManager.list(List<FakeCommand> commands) = _SequenceProcessManager;
FakeProcessManager._();
/// Adds a new [FakeCommand] to the current process manager.
///
/// This can be used to configure test expectations after the [ProcessManager] has been
/// provided to another interface.
///
/// This is a no-op on [FakeProcessManager.any].
void addCommand(FakeCommand command);
/// Add multiple [FakeCommand] to the current process manager.
void addCommands(Iterable<FakeCommand> commands) {
commands.forEach(addCommand);
}
final Map<int, _FakeProcess> _fakeRunningProcesses = <int, _FakeProcess>{};
/// Whether this fake has more [FakeCommand]s that are expected to run.
///
/// This is always `true` for [FakeProcessManager.any].
bool get hasRemainingExpectations;
@protected
FakeCommand findCommand(
List<String> command,
String workingDirectory,
Map<String, String> environment,
Encoding encoding,
);
int _pid = 9999;
_FakeProcess _runCommand(
List<String> command,
String workingDirectory,
Map<String, String> environment,
Encoding encoding,
) {
_pid += 1;
final FakeCommand fakeCommand = findCommand(command, workingDirectory, environment, encoding);
if (fakeCommand.onRun != null) {
fakeCommand.onRun();
}
return _FakeProcess(
fakeCommand.exitCode,
fakeCommand.duration,
_pid,
fakeCommand.stderr,
fakeCommand.stdin,
fakeCommand.stdout,
fakeCommand.completer,
);
}
@override
Future<Process> start(
List<dynamic> command, {
String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment = true, // ignored
bool runInShell = false, // ignored
ProcessStartMode mode = ProcessStartMode.normal, // ignored
}) {
final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, systemEncoding);
if (process._completer != null) {
_fakeRunningProcesses[process.pid] = process;
process.exitCode.whenComplete(() {
_fakeRunningProcesses.remove(process.pid);
});
}
return Future<Process>.value(process);
}
@override
Future<ProcessResult> run(
List<dynamic> command, {
String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment = true, // ignored
bool runInShell = false, // ignored
Encoding stdoutEncoding = systemEncoding,
Encoding stderrEncoding = systemEncoding,
}) async {
final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
await process.exitCode;
return ProcessResult(
process.pid,
process._exitCode,
stdoutEncoding == null ? process.stdout : await stdoutEncoding.decodeStream(process.stdout),
stderrEncoding == null ? process.stderr : await stderrEncoding.decodeStream(process.stderr),
);
}
@override
ProcessResult runSync(
List<dynamic> command, {
String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment = true, // ignored
bool runInShell = false, // ignored
Encoding stdoutEncoding = systemEncoding, // actual encoder is ignored
Encoding stderrEncoding = systemEncoding, // actual encoder is ignored
}) {
final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
return ProcessResult(
process.pid,
process._exitCode,
stdoutEncoding == null ? utf8.encode(process._stdout) : process._stdout,
stderrEncoding == null ? utf8.encode(process._stderr) : process._stderr,
);
}
@override
bool canRun(dynamic executable, {String workingDirectory}) => true;
@override
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
// Killing a fake process has no effect unless it has an attached completer.
final _FakeProcess fakeProcess = _fakeRunningProcesses[pid];
if (fakeProcess == null) {
return false;
}
fakeProcess._completer.complete();
return true;
}
}
class _FakeAnyProcessManager extends FakeProcessManager {
_FakeAnyProcessManager() : super._();
@override
FakeCommand findCommand(
List<String> command,
String workingDirectory,
Map<String, String> environment,
Encoding encoding,
) {
return FakeCommand(
command: command,
workingDirectory: workingDirectory,
environment: environment,
encoding: encoding,
duration: const Duration(),
exitCode: 0,
stdout: '',
stderr: '',
);
}
@override
void addCommand(FakeCommand command) { }
@override
bool get hasRemainingExpectations => true;
}
class _SequenceProcessManager extends FakeProcessManager {
_SequenceProcessManager(this._commands) : super._();
final List<FakeCommand> _commands;
@override
FakeCommand findCommand(
List<String> command,
String workingDirectory,
Map<String, String> environment,
Encoding encoding,
) {
expect(_commands, isNotEmpty,
reason: 'ProcessManager was told to execute $command (in $workingDirectory) '
'but the FakeProcessManager.list expected no more processes.'
);
_commands.first._matches(command, workingDirectory, environment, encoding);
return _commands.removeAt(0);
}
@override
void addCommand(FakeCommand command) {
_commands.add(command);
}
@override
bool get hasRemainingExpectations => _commands.isNotEmpty;
}