// 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<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] as Iterable<dynamic>)); 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] 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. // 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>>.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() { 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); } }