blob: 599fa70b418bd1c119ea40d32b2f300d3d94ff8f [file] [log] [blame]
// Copyright 2020 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';
import 'package:process/process.dart';
import 'package:test/test.dart' as test_package show TypeMatcher;
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>();
class FakeInvocationRecord {
FakeInvocationRecord(
this.invocation, {
this.workingDirectory,
this.runInShell = false,
this.includeParentEnvironment = true,
});
final List<String> invocation;
final String? workingDirectory;
final bool runInShell;
final bool includeParentEnvironment;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
if (other is! FakeInvocationRecord) {
return false;
}
if (other.workingDirectory != workingDirectory) {
return false;
}
if (other.runInShell != runInShell) {
return false;
}
if (other.includeParentEnvironment != includeParentEnvironment) {
return false;
}
if (other.invocation.length != invocation.length) {
return false;
}
for (var i = 0; i < invocation.length; ++i) {
if (other.invocation[i] != invocation[i]) {
return false;
}
}
return true;
}
@override
int get hashCode => Object.hashAll(
[workingDirectory, runInShell, includeParentEnvironment, ...invocation]);
@override
String toString() {
return 'FakeInvocationRecord(invocation: $invocation, '
'workingDirectory: $workingDirectory, runInShell: $runInShell, '
'includeParentEnvironment: $includeParentEnvironment)';
}
}
/// A mock that can be used to fake a process manager that runs commands and
/// returns results.
///
/// Call [verifyCalls] to verify that each desired call occurred.
class FakeProcessManager implements ProcessManager {
FakeProcessManager(this.stdinResults, {this.commandsThrow = false});
/// The callback that will be called each time stdin input is supplied to
/// a call.
final StringReceivedCallback stdinResults;
/// Set to true if all commands run with this process manager should throw an
/// exception.
bool commandsThrow;
/// The list of results that will be sent back, organized by the command line
/// that will produce them. Each command line has a list of returned stdout
/// output that will be returned on each successive call.
Map<FakeInvocationRecord, List<ProcessResult>> _fakeResults =
<FakeInvocationRecord, List<ProcessResult>>{};
Map<FakeInvocationRecord, List<ProcessResult>> get fakeResults =>
_fakeResults;
set fakeResults(Map<FakeInvocationRecord, List<ProcessResult>> value) {
_fakeResults = <FakeInvocationRecord, List<ProcessResult>>{};
for (final key in value.keys) {
_fakeResults[key] =
(value[key] ?? <ProcessResult>[ProcessResult(0, 0, '', '')]).toList();
}
}
/// The list of invocations that occurred, in the order they occurred.
List<FakeInvocationRecord> invocations = <FakeInvocationRecord>[];
/// Verify that the given command lines were called, in the given order, and
/// that the parameters were in the same order.
void verifyCalls(Iterable<FakeInvocationRecord> calls) {
var index = 0;
expect(invocations.length, equals(calls.length));
for (final call in calls) {
expect(call.invocation, orderedEquals(invocations[index].invocation));
expect(
call.workingDirectory, equals(invocations[index].workingDirectory));
index++;
}
}
ProcessResult _popResult(FakeInvocationRecord command) {
expect(fakeResults, isNotEmpty);
final foundResult = fakeResults[command];
expect(foundResult, isNotNull,
reason: '$command not found in expected results.');
expect(foundResult, isNotEmpty);
return fakeResults[command]!.removeAt(0);
}
FakeProcess _popProcess(FakeInvocationRecord command) =>
FakeProcess(_popResult(command), stdinResults);
Future<Process> _nextProcess(
List<String> invocation, {
String? workingDirectory,
bool runInShell = false,
bool includeParentEnvironment = true,
}) async {
final record = FakeInvocationRecord(
invocation,
workingDirectory: workingDirectory,
runInShell: runInShell,
includeParentEnvironment: includeParentEnvironment,
);
invocations.add(record);
return Future<Process>.value(_popProcess(record));
}
ProcessResult _nextResultSync(
List<String> invocation, {
String? workingDirectory,
bool runInShell = false,
bool includeParentEnvironment = true,
}) {
final record = FakeInvocationRecord(
invocation,
workingDirectory: workingDirectory,
runInShell: runInShell,
includeParentEnvironment: includeParentEnvironment,
);
invocations.add(record);
return _popResult(record);
}
Future<ProcessResult> _nextResult(
List<String> invocation, {
String? workingDirectory,
bool runInShell = false,
bool includeParentEnvironment = true,
}) async {
final record = FakeInvocationRecord(
invocation,
workingDirectory: workingDirectory,
runInShell: runInShell,
includeParentEnvironment: includeParentEnvironment,
);
invocations.add(record);
return Future<ProcessResult>.value(_popResult(record));
}
@override
bool canRun(dynamic executable, {String? workingDirectory}) {
return true;
}
@override
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) {
return true;
}
@override
Future<ProcessResult> run(
List<dynamic> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) {
if (commandsThrow) {
throw const ProcessException('failed_executable', <String>[]);
}
return _nextResult(
command as List<String>,
workingDirectory: workingDirectory,
runInShell: runInShell,
includeParentEnvironment: includeParentEnvironment,
);
}
@override
ProcessResult runSync(
List<dynamic> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) {
if (commandsThrow) {
throw const ProcessException('failed_executable', <String>[]);
}
return _nextResultSync(
command as List<String>,
workingDirectory: workingDirectory,
runInShell: runInShell,
includeParentEnvironment: includeParentEnvironment,
);
}
@override
Future<Process> start(
List<dynamic> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) {
if (commandsThrow) {
throw const ProcessException('failed_executable', <String>[]);
}
return _nextProcess(
command as List<String>,
workingDirectory: workingDirectory,
runInShell: runInShell,
includeParentEnvironment: includeParentEnvironment,
);
}
}
typedef StdinResults = void Function(String input);
/// A fake process that can be used to interact with a process "started" by the
/// FakeProcessManager.
class FakeProcess implements Process {
FakeProcess(ProcessResult result, StdinResults stdinResults)
: stdoutStream =
Stream<List<int>>.value((result.stdout as String).codeUnits),
stderrStream =
Stream<List<int>>.value((result.stderr as String).codeUnits),
desiredExitCode = result.exitCode,
stdinSink = IOSink(StringStreamConsumer(stdinResults));
final IOSink stdinSink;
final Stream<List<int>> stdoutStream;
final Stream<List<int>> stderrStream;
final int desiredExitCode;
@override
Future<int> get exitCode => Future<int>.value(desiredExitCode);
@override
int get pid => 0;
@override
IOSink get stdin => stdinSink;
@override
Stream<List<int>> get stderr => stderrStream;
@override
Stream<List<int>> get stdout => stdoutStream;
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
return true;
}
}
/// Callback used to receive stdin input when it occurs.
typedef StringReceivedCallback = void Function(String received);
/// A stream consumer class that consumes UTF8 strings as lists of ints.
class StringStreamConsumer implements StreamConsumer<List<int>> {
StringStreamConsumer(this.sendString);
List<Stream<List<int>>> streams = <Stream<List<int>>>[];
List<StreamSubscription<List<int>>> subscriptions =
<StreamSubscription<List<int>>>[];
List<Completer<dynamic>> completers = <Completer<dynamic>>[];
/// The callback called when this consumer receives input.
StringReceivedCallback sendString;
@override
Future<dynamic> addStream(Stream<List<int>> value) {
streams.add(value);
completers.add(Completer<dynamic>());
subscriptions.add(
value.listen((List<int> data) {
sendString(utf8.decode(data));
}),
);
subscriptions.last.onDone(() => completers.last.complete(null));
return Future<dynamic>.value(null);
}
@override
Future<dynamic> close() async {
for (final completer in completers) {
await completer.future;
}
completers.clear();
streams.clear();
subscriptions.clear();
return Future<dynamic>.value(null);
}
}