// Copyright 2014 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 '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> _fakeResults = >{}; Map> get fakeResults => _fakeResults; set fakeResults(Map> value) { _fakeResults = >{}; for (final String key in value.keys) { _fakeResults[key] = (value[key] ?? [ProcessResult(0, 0, '', '')]).toList(); } } /// The list of invocations that occurred, in the order they occurred. List invocations = []; /// Verify that the given command lines were called, in the given order, and that the /// parameters were in the same order. void verifyCalls(List calls) { int index = 0; for (final String call in calls) { expect(call.split(' '), orderedEquals(invocations[index].positionalArguments[0] as Iterable)); index++; } expect(invocations.length, equals(calls.length)); } ProcessResult _popResult(List 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 command) => FakeProcess(_popResult(command), stdinResults: stdinResults); Future _nextProcess(Invocation invocation) async { invocations.add(invocation); return Future.value(_popProcess(invocation.positionalArguments[0] as List)); } ProcessResult _nextResultSync(Invocation invocation) { invocations.add(invocation); return _popResult(invocation.positionalArguments[0] as List); } Future _nextResult(Invocation invocation) async { invocations.add(invocation); return Future.value(_popResult(invocation.positionalArguments[0] as List)); } 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>.value((result.stdout as String).codeUnits), stderrStream = Stream>.value((result.stderr as String).codeUnits), desiredExitCode = result.exitCode, stdinSink = IOSink(StringStreamConsumer(stdinResults)) { _setupMock(); } final IOSink stdinSink; final Stream> stdoutStream; final Stream> stderrStream; final int desiredExitCode; void _setupMock() { when(kill(any)).thenReturn(true); } @override Future get exitCode => Future.value(desiredExitCode); @override int get pid => 0; @override IOSink get stdin => stdinSink; @override Stream> get stderr => stderrStream; @override Stream> 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> { StringStreamConsumer(this.sendString); List>> streams = >>[]; List>> subscriptions = >>[]; List> completers = >[]; /// The callback called when this consumer receives input. StringReceivedCallback sendString; @override Future addStream(Stream> value) { streams.add(value); completers.add(Completer()); subscriptions.add( value.listen((List data) { sendString(utf8.decode(data)); }), ); subscriptions.last.onDone(() => completers.last.complete(null)); return Future.value(null); } @override Future close() async { for (final Completer completer in completers) { await completer.future; } completers.clear(); streams.clear(); subscriptions.clear(); return Future.value(null); } }