| // Copyright 2018 The Chromium 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 'common.dart'; |
| |
| /// 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<String, List<ProcessResult>> _fakeResults = <String, List<ProcessResult>>{}; |
| Map<String, List<ProcessResult>> get fakeResults => _fakeResults; |
| set fakeResults(Map<String, List<ProcessResult>> value) { |
| _fakeResults = <String, List<ProcessResult>>{}; |
| for (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(List<String> calls) { |
| int index = 0; |
| for (String call in calls) { |
| expect(call.split(' '), orderedEquals(invocations[index].positionalArguments[0])); |
| index++; |
| } |
| expect(invocations.length, equals(calls.length)); |
| } |
| |
| ProcessResult _popResult(List<String> command) { |
| final String key = command.join(' '); |
| expect(fakeResults, isNotEmpty); |
| expect(fakeResults, contains(key)); |
| expect(fakeResults[key], isNotEmpty); |
| return fakeResults[key].removeAt(0); |
| } |
| |
| FakeProcess _popProcess(List<String> command) => |
| FakeProcess(_popResult(command), stdinResults: stdinResults); |
| |
| Future<Process> _nextProcess(Invocation invocation) async { |
| invocations.add(invocation); |
| return Future<Process>.value(_popProcess(invocation.positionalArguments[0])); |
| } |
| |
| ProcessResult _nextResultSync(Invocation invocation) { |
| invocations.add(invocation); |
| return _popResult(invocation.positionalArguments[0]); |
| } |
| |
| Future<ProcessResult> _nextResult(Invocation invocation) async { |
| invocations.add(invocation); |
| return Future<ProcessResult>.value(_popResult(invocation.positionalArguments[0])); |
| } |
| |
| void _setupMock() { |
| // Not all possible types of invocations are covered here, just the ones |
| // expected to be called. |
| // TODO(gspencer): make this more general so that any call will be captured. |
| when(start( |
| any, |
| environment: anyNamed('environment'), |
| workingDirectory: anyNamed('workingDirectory'), |
| )).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); |
| } |
| } |
| |
| /// 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, {void stdinResults(String input)}) |
| : stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[result.stdout.codeUnits]), |
| stderrStream = Stream<List<int>>.fromIterable(<List<int>>[result.stderr.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() { |
| 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 (Completer<dynamic> completer in completers) { |
| await completer.future; |
| } |
| completers.clear(); |
| streams.clear(); |
| subscriptions.clear(); |
| return Future<dynamic>.value(null); |
| } |
| } |