fake_process_manager.dart 6.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10
// 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';
11 12

import 'common.dart';
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

/// 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
31
  /// that will produce them. Each command line has a list of returned stdout
32
  /// output that will be returned on each successive call.
33 34 35 36
  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>>{};
37
    for (final String key in value.keys) {
38
      _fakeResults[key] = (value[key] ?? <ProcessResult>[ProcessResult(0, 0, '', '')]).toList();
39 40
    }
  }
41 42 43 44

  /// The list of invocations that occurred, in the order they occurred.
  List<Invocation> invocations = <Invocation>[];

45 46
  /// Verify that the given command lines were called, in the given order, and that the
  /// parameters were in the same order.
47 48
  void verifyCalls(List<String> calls) {
    int index = 0;
49
    for (final String call in calls) {
50
      expect(call.split(' '), orderedEquals(invocations[index].positionalArguments[0] as Iterable<dynamic>));
51 52
      index++;
    }
53
    expect(invocations.length, equals(calls.length));
54 55
  }

56 57
  ProcessResult _popResult(List<String> command) {
    final String key = command.join(' ');
58 59 60 61 62 63
    expect(fakeResults, isNotEmpty);
    expect(fakeResults, contains(key));
    expect(fakeResults[key], isNotEmpty);
    return fakeResults[key].removeAt(0);
  }

64
  FakeProcess _popProcess(List<String> command) =>
65
      FakeProcess(_popResult(command), stdinResults: stdinResults);
66 67 68

  Future<Process> _nextProcess(Invocation invocation) async {
    invocations.add(invocation);
69
    return Future<Process>.value(_popProcess(invocation.positionalArguments[0] as List<String>));
70 71 72 73
  }

  ProcessResult _nextResultSync(Invocation invocation) {
    invocations.add(invocation);
74
    return _popResult(invocation.positionalArguments[0] as List<String>);
75 76 77 78
  }

  Future<ProcessResult> _nextResult(Invocation invocation) async {
    invocations.add(invocation);
79
    return Future<ProcessResult>.value(_popResult(invocation.positionalArguments[0] as List<String>));
80 81 82
  }

  void _setupMock() {
Ian Hickson's avatar
Ian Hickson committed
83
    // Not all possible types of invocations are covered here, just the ones
84 85
    // expected to be called.
    // TODO(gspencer): make this more general so that any call will be captured.
86
    when(start(
87 88 89
      any,
      environment: anyNamed('environment'),
      workingDirectory: anyNamed('workingDirectory'),
90 91
    )).thenAnswer(_nextProcess);

92
    when(start(any)).thenAnswer(_nextProcess);
93 94

    when(run(
95 96 97
      any,
      environment: anyNamed('environment'),
      workingDirectory: anyNamed('workingDirectory'),
98 99
    )).thenAnswer(_nextResult);

100
    when(run(any)).thenAnswer(_nextResult);
101 102

    when(runSync(
103 104
      any,
      environment: anyNamed('environment'),
105
      workingDirectory: anyNamed('workingDirectory'),
106 107
    )).thenAnswer(_nextResultSync);

108
    when(runSync(any)).thenAnswer(_nextResultSync);
109

110
    when(killPid(any, any)).thenReturn(true);
111

112
    when(canRun(any, workingDirectory: anyNamed('workingDirectory')))
113
        .thenReturn(true);
114 115 116 117 118 119
  }
}

/// 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)})
120 121
      : stdoutStream = Stream<List<int>>.value((result.stdout as String).codeUnits),
        stderrStream = Stream<List<int>>.value((result.stderr as String).codeUnits),
122
        desiredExitCode = result.exitCode,
123
        stdinSink = IOSink(StringStreamConsumer(stdinResults)) {
124 125 126 127 128 129 130 131 132
    _setupMock();
  }

  final IOSink stdinSink;
  final Stream<List<int>> stdoutStream;
  final Stream<List<int>> stderrStream;
  final int desiredExitCode;

  void _setupMock() {
133
    when(kill(any)).thenReturn(true);
134 135 136
  }

  @override
137
  Future<int> get exitCode => Future<int>.value(desiredExitCode);
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

  @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.
153
typedef StringReceivedCallback = void Function(String received);
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168

/// 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);
169
    completers.add(Completer<dynamic>());
170 171 172 173 174
    subscriptions.add(
      value.listen((List<int> data) {
        sendString(utf8.decode(data));
      }),
    );
175
    subscriptions.last.onDone(() => completers.last.complete(null));
176
    return Future<dynamic>.value(null);
177 178 179 180
  }

  @override
  Future<dynamic> close() async {
181
    for (final Completer<dynamic> completer in completers) {
182 183 184 185 186
      await completer.future;
    }
    completers.clear();
    streams.clear();
    subscriptions.clear();
187
    return Future<dynamic>.value(null);
188 189
  }
}