blob: 54471d9124652b671aca4e196edb4cdb09cdc7cb [file] [log] [blame]
Greg Spencerddfc3222018-01-29 23:15:57 -08001// 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
5import 'dart:async';
6import 'dart:convert';
7import 'dart:io';
8
9import 'package:process/process.dart';
10import 'package:mockito/mockito.dart';
11import '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.
20class 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 Ardhuinc02b6a82018-02-02 23:27:29 +010030 /// that will produce them. Each command line has a list of returned stdout
Greg Spencerddfc3222018-01-29 23:15:57 -080031 /// output that will be returned on each successive call.
Greg Spencer8a2df392018-02-06 15:32:19 -080032 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 Spencerddfc3222018-01-29 23:15:57 -080041
42 /// The list of invocations that occurred, in the order they occurred.
43 List<Invocation> invocations = <Invocation>[];
44
Greg Spencer8a2df392018-02-06 15:32:19 -080045 /// Verify that the given command lines were called, in the given order, and that the
46 /// parameters were in the same order.
Greg Spencerddfc3222018-01-29 23:15:57 -080047 void verifyCalls(List<String> calls) {
48 int index = 0;
Greg Spencerddfc3222018-01-29 23:15:57 -080049 for (String call in calls) {
50 expect(call.split(' '), orderedEquals(invocations[index].positionalArguments[0]));
51 index++;
52 }
Greg Spencer8a2df392018-02-06 15:32:19 -080053 expect(invocations.length, equals(calls.length));
Greg Spencerddfc3222018-01-29 23:15:57 -080054 }
55
Greg Spencer8a2df392018-02-06 15:32:19 -080056 ProcessResult _popResult(List<String> command) {
57 final String key = command.join(' ');
Greg Spencerddfc3222018-01-29 23:15:57 -080058 expect(fakeResults, isNotEmpty);
59 expect(fakeResults, contains(key));
60 expect(fakeResults[key], isNotEmpty);
61 return fakeResults[key].removeAt(0);
62 }
63
Greg Spencer8a2df392018-02-06 15:32:19 -080064 FakeProcess _popProcess(List<String> command) =>
65 new FakeProcess(_popResult(command), stdinResults: stdinResults);
Greg Spencerddfc3222018-01-29 23:15:57 -080066
67 Future<Process> _nextProcess(Invocation invocation) async {
68 invocations.add(invocation);
Greg Spencer8a2df392018-02-06 15:32:19 -080069 return new Future<Process>.value(_popProcess(invocation.positionalArguments[0]));
Greg Spencerddfc3222018-01-29 23:15:57 -080070 }
71
72 ProcessResult _nextResultSync(Invocation invocation) {
73 invocations.add(invocation);
Greg Spencer8a2df392018-02-06 15:32:19 -080074 return _popResult(invocation.positionalArguments[0]);
Greg Spencerddfc3222018-01-29 23:15:57 -080075 }
76
77 Future<ProcessResult> _nextResult(Invocation invocation) async {
78 invocations.add(invocation);
Greg Spencer8a2df392018-02-06 15:32:19 -080079 return new Future<ProcessResult>.value(_popResult(invocation.positionalArguments[0]));
Greg Spencerddfc3222018-01-29 23:15:57 -080080 }
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 Spencer8a2df392018-02-06 15:32:19 -080086 when(start(
87 typed(captureAny),
88 environment: typed(captureAny, named: 'environment'),
89 workingDirectory: typed(captureAny, named: 'workingDirectory'),
90 )).thenAnswer(_nextProcess);
Greg Spencerddfc3222018-01-29 23:15:57 -080091
Greg Spencer8a2df392018-02-06 15:32:19 -080092 when(start(typed(captureAny))).thenAnswer(_nextProcess);
Greg Spencerddfc3222018-01-29 23:15:57 -080093
Greg Spencer8a2df392018-02-06 15:32:19 -080094 when(run(
95 typed(captureAny),
96 environment: typed(captureAny, named: 'environment'),
97 workingDirectory: typed(captureAny, named: 'workingDirectory'),
98 )).thenAnswer(_nextResult);
Greg Spencerddfc3222018-01-29 23:15:57 -080099
Greg Spencer8a2df392018-02-06 15:32:19 -0800100 when(run(typed(captureAny))).thenAnswer(_nextResult);
Greg Spencerddfc3222018-01-29 23:15:57 -0800101
Greg Spencer8a2df392018-02-06 15:32:19 -0800102 when(runSync(
103 typed(captureAny),
104 environment: typed(captureAny, named: 'environment'),
105 workingDirectory: typed(captureAny, named: 'workingDirectory')
106 )).thenAnswer(_nextResultSync);
Greg Spencerddfc3222018-01-29 23:15:57 -0800107
Greg Spencer8a2df392018-02-06 15:32:19 -0800108 when(runSync(typed(captureAny))).thenAnswer(_nextResultSync);
Greg Spencerddfc3222018-01-29 23:15:57 -0800109
110 when(killPid(typed(captureAny), typed(captureAny))).thenReturn(true);
111
Greg Spencer8a2df392018-02-06 15:32:19 -0800112 when(canRun(captureAny, workingDirectory: typed(captureAny, named: 'workingDirectory')))
113 .thenReturn(true);
Greg Spencerddfc3222018-01-29 23:15:57 -0800114 }
115}
116
117/// A fake process that can be used to interact with a process "started" by the FakeProcessManager.
118class 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.
153typedef void StringReceivedCallback(String received);
154
155/// A stream consumer class that consumes UTF8 strings as lists of ints.
156class 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 Spencer8a2df392018-02-06 15:32:19 -0800170 subscriptions.add(
171 value.listen((List<int> data) {
172 sendString(utf8.decode(data));
173 }),
174 );
Greg Spencerddfc3222018-01-29 23:15:57 -0800175 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}