fake_process_manager.dart 11 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7 8 9 10 11
import 'dart:convert';
import 'dart:io' as io show ProcessSignal;

import 'package:flutter_tools/src/base/io.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
12
import 'common.dart';
13
import 'context.dart';
14

15 16
export 'package:process/process.dart' show ProcessManager;

17 18
typedef VoidCallback = void Function();

19 20 21 22 23 24 25
/// A command for [FakeProcessManager].
@immutable
class FakeCommand {
  const FakeCommand({
    @required this.command,
    this.workingDirectory,
    this.environment,
26
    this.encoding,
27
    this.duration = const Duration(),
28 29 30 31
    this.onRun,
    this.exitCode = 0,
    this.stdout = '',
    this.stderr = '',
32
    this.completer,
33
    this.stdin,
34 35
  }) : assert(command != null),
       assert(duration != null),
36
       assert(exitCode != null);
37 38

  /// The exact commands that must be matched for this [FakeCommand] to be
39
  /// considered correct.
40 41 42
  final List<String> command;

  /// The exact working directory that must be matched for this [FakeCommand] to
43
  /// be considered correct.
44
  ///
45
  /// If this is null, the working directory is ignored.
46 47
  final String workingDirectory;

48
  /// The environment that must be matched for this [FakeCommand] to be considered correct.
49
  ///
50
  /// If this is null, then the environment is ignored.
51 52 53 54 55
  ///
  /// Otherwise, each key in this environment must be present and must have a
  /// value that matches the one given here for the [FakeCommand] to match.
  final Map<String, String> environment;

56 57 58 59 60 61
  /// The stdout and stderr encoding that must be matched for this [FakeCommand]
  /// to be considered correct.
  ///
  /// If this is null, then the encodings are ignored.
  final Encoding encoding;

62 63 64 65 66 67 68
  /// The time to allow to elapse before returning the [exitCode], if this command
  /// is "executed".
  ///
  /// If you set this to a non-zero time, you should use a [FakeAsync] zone,
  /// otherwise the test will be artificially slow.
  final Duration duration;

69 70 71 72
  /// A callback that is run after [duration] expires but before the [exitCode]
  /// (and output) are passed back.
  final VoidCallback onRun;

73 74
  /// The process' exit code.
  ///
75
  /// To simulate a never-ending process, set [duration] to a value greater than
76 77 78 79 80 81 82 83 84 85 86 87 88 89
  /// 15 minutes (the timeout for our tests).
  ///
  /// To simulate a crash, subtract the crash signal number from 256. For example,
  /// SIGPIPE (-13) is 243.
  final int exitCode;

  /// The output to simulate on stdout. This will be encoded as UTF-8 and
  /// returned in one go.
  final String stdout;

  /// The output to simulate on stderr. This will be encoded as UTF-8 and
  /// returned in one go.
  final String stderr;

90 91 92 93
  /// If provided, allows the command completion to be blocked until the future
  /// resolves.
  final Completer<void> completer;

94 95 96 97
  /// An optional stdin sink that will be exposed through the resulting
  /// [FakeProcess].
  final IOSink stdin;

98 99 100 101 102 103
  void _matches(
    List<String> command,
    String workingDirectory,
    Map<String, String> environment,
    Encoding encoding,
  ) {
104 105 106
    expect(command, equals(this.command));
    if (this.workingDirectory != null) {
      expect(this.workingDirectory, workingDirectory);
107 108
    }
    if (this.environment != null) {
109
      expect(this.environment, environment);
110
    }
111 112 113
    if (this.encoding != null) {
      expect(this.encoding, encoding);
    }
114 115 116 117 118 119 120
  }
}

class _FakeProcess implements Process {
  _FakeProcess(
    this._exitCode,
    Duration duration,
121
    VoidCallback onRun,
122 123 124 125
    this.pid,
    this._stderr,
    this.stdin,
    this._stdout,
126
    this._completer,
127 128 129 130
  ) : exitCode = Future<void>.delayed(duration).then((void value) {
        if (onRun != null) {
          onRun();
        }
131 132
        if (_completer != null) {
          return _completer.future.then((void _) => _exitCode);
133
        }
134 135
        return _exitCode;
      }),
136 137 138 139 140 141
      stderr = _stderr == null
        ? const Stream<List<int>>.empty()
        : Stream<List<int>>.value(utf8.encode(_stderr)),
      stdout = _stdout == null
        ? const Stream<List<int>>.empty()
        : Stream<List<int>>.value(utf8.encode(_stdout));
142

143
  final int _exitCode;
144
  final Completer<void> _completer;
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166

  @override
  final Future<int> exitCode;

  @override
  final int pid;

  final String _stderr;

  @override
  final Stream<List<int>> stderr;

  @override
  final IOSink stdin;

  @override
  final Stream<List<int>> stdout;

  final String _stdout;

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
167
    // Killing a fake process has no effect.
168 169 170 171
    return false;
  }
}

172 173 174 175
abstract class FakeProcessManager implements ProcessManager {
  /// A fake [ProcessManager] which responds to all commands as if they had run
  /// instantaneously with an exit code of 0 and no output.
  factory FakeProcessManager.any() = _FakeAnyProcessManager;
176

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
  /// A fake [ProcessManager] which responds to particular commands with
  /// particular results.
  ///
  /// On creation, pass in a list of [FakeCommand] objects. When the
  /// [ProcessManager] methods such as [start] are invoked, the next
  /// [FakeCommand] must match (otherwise the test fails); its settings are used
  /// to simulate the result of running that command.
  ///
  /// If no command is found, then one is implied which immediately returns exit
  /// code 0 with no output.
  ///
  /// There is no logic to ensure that all the listed commands are run. Use
  /// [FakeCommand.onRun] to set a flag, or specify a sentinel command as your
  /// last command and verify its execution is successful, to ensure that all
  /// the specified commands are actually called.
  factory FakeProcessManager.list(List<FakeCommand> commands) = _SequenceProcessManager;
193

194 195
  FakeProcessManager._();

196 197 198 199 200 201 202 203
  /// Adds a new [FakeCommand] to the current process manager.
  ///
  /// This can be used to configure test expectations after the [ProcessManager] has been
  /// provided to another interface.
  ///
  /// This is a no-op on [FakeProcessManager.any].
  void addCommand(FakeCommand command);

204 205 206 207 208
  /// Add multiple [FakeCommand] to the current process manager.
  void addCommands(Iterable<FakeCommand> commands) {
    commands.forEach(addCommand);
  }

209 210
  final Map<int, _FakeProcess> _fakeRunningProcesses = <int, _FakeProcess>{};

211 212 213 214 215
  /// Whether this fake has more [FakeCommand]s that are expected to run.
  ///
  /// This is always `true` for [FakeProcessManager.any].
  bool get hasRemainingExpectations;

216
  @protected
217 218 219 220 221 222
  FakeCommand findCommand(
    List<String> command,
    String workingDirectory,
    Map<String, String> environment,
    Encoding encoding,
  );
223 224 225

  int _pid = 9999;

226 227 228 229 230 231
  _FakeProcess _runCommand(
    List<String> command,
    String workingDirectory,
    Map<String, String> environment,
    Encoding encoding,
  ) {
232
    _pid += 1;
233
    final FakeCommand fakeCommand = findCommand(command, workingDirectory, environment, encoding);
234 235 236
    return _FakeProcess(
      fakeCommand.exitCode,
      fakeCommand.duration,
237
      fakeCommand.onRun,
238 239
      _pid,
      fakeCommand.stderr,
240
      fakeCommand.stdin,
241
      fakeCommand.stdout,
242
      fakeCommand.completer,
243 244 245 246 247 248 249 250 251 252 253
    );
  }

  @override
  Future<Process> start(
    List<dynamic> command, {
    String workingDirectory,
    Map<String, String> environment,
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
    ProcessStartMode mode = ProcessStartMode.normal, // ignored
254 255 256 257 258 259 260 261 262 263
  }) {
    final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, systemEncoding);
    if (process._completer != null) {
      _fakeRunningProcesses[process.pid] = process;
      process.exitCode.whenComplete(() {
        _fakeRunningProcesses.remove(process.pid);
      });
    }
    return Future<Process>.value(process);
  }
264 265 266 267 268 269 270 271 272 273 274

  @override
  Future<ProcessResult> run(
    List<dynamic> command, {
    String workingDirectory,
    Map<String, String> environment,
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
    Encoding stdoutEncoding = systemEncoding,
    Encoding stderrEncoding = systemEncoding,
  }) async {
275
    final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    await process.exitCode;
    return ProcessResult(
      process.pid,
      process._exitCode,
      stdoutEncoding == null ? process.stdout : await stdoutEncoding.decodeStream(process.stdout),
      stderrEncoding == null ? process.stderr : await stderrEncoding.decodeStream(process.stderr),
    );
  }

  @override
  ProcessResult runSync(
    List<dynamic> command, {
    String workingDirectory,
    Map<String, String> environment,
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
    Encoding stdoutEncoding = systemEncoding, // actual encoder is ignored
    Encoding stderrEncoding = systemEncoding, // actual encoder is ignored
  }) {
295
    final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
296 297 298 299 300 301 302 303 304 305 306 307 308
    return ProcessResult(
      process.pid,
      process._exitCode,
      stdoutEncoding == null ? utf8.encode(process._stdout) : process._stdout,
      stderrEncoding == null ? utf8.encode(process._stderr) : process._stderr,
    );
  }

  @override
  bool canRun(dynamic executable, {String workingDirectory}) => true;

  @override
  bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
309 310 311 312 313 314 315
    // Killing a fake process has no effect unless it has an attached completer.
    final _FakeProcess fakeProcess = _fakeRunningProcesses[pid];
    if (fakeProcess == null) {
      return false;
    }
    fakeProcess._completer.complete();
    return true;
316 317
  }
}
318 319 320 321 322

class _FakeAnyProcessManager extends FakeProcessManager {
  _FakeAnyProcessManager() : super._();

  @override
323 324 325 326 327 328
  FakeCommand findCommand(
    List<String> command,
    String workingDirectory,
    Map<String, String> environment,
    Encoding encoding,
  ) {
329 330 331 332
    return FakeCommand(
      command: command,
      workingDirectory: workingDirectory,
      environment: environment,
333
      encoding: encoding,
334 335 336 337 338 339
      duration: const Duration(),
      exitCode: 0,
      stdout: '',
      stderr: '',
    );
  }
340 341 342

  @override
  void addCommand(FakeCommand command) { }
343 344 345

  @override
  bool get hasRemainingExpectations => true;
346 347 348 349 350 351 352 353
}

class _SequenceProcessManager extends FakeProcessManager {
  _SequenceProcessManager(this._commands) : super._();

  final List<FakeCommand> _commands;

  @override
354 355 356 357 358 359
  FakeCommand findCommand(
    List<String> command,
    String workingDirectory,
    Map<String, String> environment,
    Encoding encoding,
  ) {
360 361 362 363
    expect(_commands, isNotEmpty,
      reason: 'ProcessManager was told to execute $command (in $workingDirectory) '
              'but the FakeProcessManager.list expected no more processes.'
    );
364
    _commands.first._matches(command, workingDirectory, environment, encoding);
365 366
    return _commands.removeAt(0);
  }
367 368 369 370 371

  @override
  void addCommand(FakeCommand command) {
    _commands.add(command);
  }
372 373 374

  @override
  bool get hasRemainingExpectations => _commands.isNotEmpty;
375
}