Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 1 | // Copyright 2018 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:async'; |
| 6 | import 'dart:convert'; |
| 7 | import 'dart:io'; |
| 8 | |
| 9 | import 'package:process/process.dart'; |
| 10 | import 'package:mockito/mockito.dart'; |
| 11 | import 'package:test/test.dart'; |
| 12 | |
| 13 | /// A mock that can be used to fake a process manager that runs commands |
| 14 | /// and returns results. |
| 15 | /// |
| 16 | /// Call [setResults] to provide a list of results that will return from |
| 17 | /// each command line (with arguments). |
| 18 | /// |
| 19 | /// Call [verifyCalls] to verify that each desired call occurred. |
| 20 | class FakeProcessManager extends Mock implements ProcessManager { |
| 21 | FakeProcessManager({this.stdinResults}) { |
| 22 | _setupMock(); |
| 23 | } |
| 24 | |
| 25 | /// The callback that will be called each time stdin input is supplied to |
| 26 | /// a call. |
| 27 | final StringReceivedCallback stdinResults; |
| 28 | |
| 29 | /// The list of results that will be sent back, organized by the command line |
Alexandre Ardhuin | c02b6a8 | 2018-02-02 23:27:29 +0100 | [diff] [blame] | 30 | /// that will produce them. Each command line has a list of returned stdout |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 31 | /// output that will be returned on each successive call. |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 32 | Map<String, List<ProcessResult>> _fakeResults = <String, List<ProcessResult>>{}; |
| 33 | Map<String, List<ProcessResult>> get fakeResults => _fakeResults; |
| 34 | set fakeResults(Map<String, List<ProcessResult>> value) { |
| 35 | _fakeResults = <String, List<ProcessResult>>{}; |
| 36 | for (String key in value.keys) { |
| 37 | _fakeResults[key] = <ProcessResult>[] |
| 38 | ..addAll(value[key] ?? <ProcessResult>[new ProcessResult(0, 0, '', '')]); |
| 39 | } |
| 40 | } |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 41 | |
| 42 | /// The list of invocations that occurred, in the order they occurred. |
| 43 | List<Invocation> invocations = <Invocation>[]; |
| 44 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 45 | /// Verify that the given command lines were called, in the given order, and that the |
| 46 | /// parameters were in the same order. |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 47 | void verifyCalls(List<String> calls) { |
| 48 | int index = 0; |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 49 | for (String call in calls) { |
| 50 | expect(call.split(' '), orderedEquals(invocations[index].positionalArguments[0])); |
| 51 | index++; |
| 52 | } |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 53 | expect(invocations.length, equals(calls.length)); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 54 | } |
| 55 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 56 | ProcessResult _popResult(List<String> command) { |
| 57 | final String key = command.join(' '); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 58 | expect(fakeResults, isNotEmpty); |
| 59 | expect(fakeResults, contains(key)); |
| 60 | expect(fakeResults[key], isNotEmpty); |
| 61 | return fakeResults[key].removeAt(0); |
| 62 | } |
| 63 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 64 | FakeProcess _popProcess(List<String> command) => |
| 65 | new FakeProcess(_popResult(command), stdinResults: stdinResults); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 66 | |
| 67 | Future<Process> _nextProcess(Invocation invocation) async { |
| 68 | invocations.add(invocation); |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 69 | return new Future<Process>.value(_popProcess(invocation.positionalArguments[0])); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 70 | } |
| 71 | |
| 72 | ProcessResult _nextResultSync(Invocation invocation) { |
| 73 | invocations.add(invocation); |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 74 | return _popResult(invocation.positionalArguments[0]); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 75 | } |
| 76 | |
| 77 | Future<ProcessResult> _nextResult(Invocation invocation) async { |
| 78 | invocations.add(invocation); |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 79 | return new Future<ProcessResult>.value(_popResult(invocation.positionalArguments[0])); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 80 | } |
| 81 | |
| 82 | void _setupMock() { |
| 83 | // Note that not all possible types of invocations are covered here, just the ones |
| 84 | // expected to be called. |
| 85 | // TODO(gspencer): make this more general so that any call will be captured. |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 86 | when(start( |
| 87 | typed(captureAny), |
| 88 | environment: typed(captureAny, named: 'environment'), |
| 89 | workingDirectory: typed(captureAny, named: 'workingDirectory'), |
| 90 | )).thenAnswer(_nextProcess); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 91 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 92 | when(start(typed(captureAny))).thenAnswer(_nextProcess); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 93 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 94 | when(run( |
| 95 | typed(captureAny), |
| 96 | environment: typed(captureAny, named: 'environment'), |
| 97 | workingDirectory: typed(captureAny, named: 'workingDirectory'), |
| 98 | )).thenAnswer(_nextResult); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 99 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 100 | when(run(typed(captureAny))).thenAnswer(_nextResult); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 101 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 102 | when(runSync( |
| 103 | typed(captureAny), |
| 104 | environment: typed(captureAny, named: 'environment'), |
| 105 | workingDirectory: typed(captureAny, named: 'workingDirectory') |
| 106 | )).thenAnswer(_nextResultSync); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 107 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 108 | when(runSync(typed(captureAny))).thenAnswer(_nextResultSync); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 109 | |
| 110 | when(killPid(typed(captureAny), typed(captureAny))).thenReturn(true); |
| 111 | |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 112 | when(canRun(captureAny, workingDirectory: typed(captureAny, named: 'workingDirectory'))) |
| 113 | .thenReturn(true); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 114 | } |
| 115 | } |
| 116 | |
| 117 | /// A fake process that can be used to interact with a process "started" by the FakeProcessManager. |
| 118 | class FakeProcess extends Mock implements Process { |
| 119 | FakeProcess(ProcessResult result, {void stdinResults(String input)}) |
| 120 | : stdoutStream = new Stream<List<int>>.fromIterable(<List<int>>[result.stdout.codeUnits]), |
| 121 | stderrStream = new Stream<List<int>>.fromIterable(<List<int>>[result.stderr.codeUnits]), |
| 122 | desiredExitCode = result.exitCode, |
| 123 | stdinSink = new IOSink(new StringStreamConsumer(stdinResults)) { |
| 124 | _setupMock(); |
| 125 | } |
| 126 | |
| 127 | final IOSink stdinSink; |
| 128 | final Stream<List<int>> stdoutStream; |
| 129 | final Stream<List<int>> stderrStream; |
| 130 | final int desiredExitCode; |
| 131 | |
| 132 | void _setupMock() { |
| 133 | when(kill(typed(captureAny))).thenReturn(true); |
| 134 | } |
| 135 | |
| 136 | @override |
| 137 | Future<int> get exitCode => new Future<int>.value(desiredExitCode); |
| 138 | |
| 139 | @override |
| 140 | int get pid => 0; |
| 141 | |
| 142 | @override |
| 143 | IOSink get stdin => stdinSink; |
| 144 | |
| 145 | @override |
| 146 | Stream<List<int>> get stderr => stderrStream; |
| 147 | |
| 148 | @override |
| 149 | Stream<List<int>> get stdout => stdoutStream; |
| 150 | } |
| 151 | |
| 152 | /// Callback used to receive stdin input when it occurs. |
| 153 | typedef void StringReceivedCallback(String received); |
| 154 | |
| 155 | /// A stream consumer class that consumes UTF8 strings as lists of ints. |
| 156 | class StringStreamConsumer implements StreamConsumer<List<int>> { |
| 157 | StringStreamConsumer(this.sendString); |
| 158 | |
| 159 | List<Stream<List<int>>> streams = <Stream<List<int>>>[]; |
| 160 | List<StreamSubscription<List<int>>> subscriptions = <StreamSubscription<List<int>>>[]; |
| 161 | List<Completer<dynamic>> completers = <Completer<dynamic>>[]; |
| 162 | |
| 163 | /// The callback called when this consumer receives input. |
| 164 | StringReceivedCallback sendString; |
| 165 | |
| 166 | @override |
| 167 | Future<dynamic> addStream(Stream<List<int>> value) { |
| 168 | streams.add(value); |
| 169 | completers.add(new Completer<dynamic>()); |
Greg Spencer | 8a2df39 | 2018-02-06 15:32:19 -0800 | [diff] [blame] | 170 | subscriptions.add( |
| 171 | value.listen((List<int> data) { |
| 172 | sendString(utf8.decode(data)); |
| 173 | }), |
| 174 | ); |
Greg Spencer | ddfc322 | 2018-01-29 23:15:57 -0800 | [diff] [blame] | 175 | subscriptions.last.onDone(() => completers.last.complete(null)); |
| 176 | return new Future<dynamic>.value(null); |
| 177 | } |
| 178 | |
| 179 | @override |
| 180 | Future<dynamic> close() async { |
| 181 | for (Completer<dynamic> completer in completers) { |
| 182 | await completer.future; |
| 183 | } |
| 184 | completers.clear(); |
| 185 | streams.clear(); |
| 186 | subscriptions.clear(); |
| 187 | return new Future<dynamic>.value(null); |
| 188 | } |
| 189 | } |