blob: e476cfb3a866b9b976ba7aa3161fdbb8a339e0da [file]
// 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:mockito/mockito.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>();
/// A mock that can be used to fake a process manager that runs commands
/// and returns results.
///
/// Call [setResults] to provide a list of results that will return from
/// each command line (with arguments).
///
/// Call [verifyCalls] to verify that each desired call occurred.
class FakeProcessManager extends Mock implements ProcessManager {
FakeProcessManager(this.stdinResults) {
_setupMock();
}
/// The callback that will be called each time stdin input is supplied to
/// a call.
final StringReceivedCallback stdinResults;
/// 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<List<String>, List<ProcessResult>> _fakeResults = <List<String>, List<ProcessResult>>{};
Map<List<String>, List<ProcessResult>> get fakeResults => _fakeResults;
set fakeResults(Map<List<String>, List<ProcessResult>> value) {
_fakeResults = <List<String>, List<ProcessResult>>{};
for (final List<String> 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<Invocation> invocations = <Invocation>[];
/// Verify that the given command lines were called, in the given order, and
/// that the parameters were in the same order.
void verifyCalls(Iterable<List<String>> calls) {
int index = 0;
expect(invocations.length, equals(calls.length));
for (final List<String> call in calls) {
expect(call, orderedEquals(invocations[index].positionalArguments[0] as Iterable<dynamic>));
index++;
}
}
ProcessResult _popResult(List<String> command) {
expect(fakeResults, isNotEmpty);
List<ProcessResult> foundResult;
List<String> foundCommand;
for (final List<String> fakeCommand in fakeResults.keys) {
if (fakeCommand.length != command.length) {
continue;
}
bool listsIdentical = true;
for (int i = 0; i < fakeCommand.length; ++i) {
if (fakeCommand[i] != command[i]) {
listsIdentical = false;
break;
}
}
if (listsIdentical) {
foundResult = fakeResults[fakeCommand];
foundCommand = fakeCommand;
break;
}
}
expect(foundResult, isNotNull, reason: '$command not found in expected results.');
expect(foundResult, isNotEmpty);
return fakeResults[foundCommand]?.removeAt(0);
}
FakeProcess _popProcess(List<String> command) => FakeProcess(_popResult(command), stdinResults);
Future<Process> _nextProcess(Invocation invocation) async {
invocations.add(invocation);
return Future<Process>.value(_popProcess(invocation.positionalArguments[0] as List<String>));
}
ProcessResult _nextResultSync(Invocation invocation) {
invocations.add(invocation);
return _popResult(invocation.positionalArguments[0] as List<String>);
}
Future<ProcessResult> _nextResult(Invocation invocation) async {
invocations.add(invocation);
return Future<ProcessResult>.value(
_popResult(invocation.positionalArguments[0] as List<String>));
}
void _setupMock() {
// Not all possible types of invocations are covered here, just the ones
// expected to be called by ProcessManager.
// TODO(gspencergoog): make this more general so that any call will be captured.
when(start(
any,
// ignore: dead_code
environment: anyNamed('environment'),
// ignore: dead_code
workingDirectory: anyNamed('workingDirectory'),
// ignore: dead_code
runInShell: anyNamed('runInShell'),
)).thenAnswer(_nextProcess);
when(start(any)).thenAnswer(_nextProcess);
when(run(
any,
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenAnswer(_nextResult);
when(run(any)).thenAnswer(_nextResult);
when(runSync(
any,
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenAnswer(_nextResultSync);
when(runSync(any)).thenAnswer(_nextResultSync);
when(killPid(any, any)).thenReturn(true);
when(canRun(any, workingDirectory: anyNamed('workingDirectory'))).thenReturn(true);
}
}
typedef StdinResults = void Function(String input);
/// A fake process that can be used to interact with a process "started" by the
/// FakeProcessManager.
class FakeProcess extends Mock 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)) {
_setupMock();
}
final IOSink stdinSink;
final Stream<List<int>> stdoutStream;
final Stream<List<int>> stderrStream;
final int desiredExitCode;
void _setupMock() {
// ignore: dead_code
when(kill(any)).thenReturn(true);
}
@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;
}
/// 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<dynamic> completer in completers) {
await completer.future;
}
completers.clear();
streams.clear();
subscriptions.clear();
return Future<dynamic>.value(null);
}
}