fake_process_manager.dart 12.9 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
import 'dart:convert';
7
import 'dart:io' as io show ProcessSignal, Process, ProcessStartMode, ProcessResult, systemEncoding;
8

9
import 'package:file/file.dart';
10
import 'package:meta/meta.dart';
11
import 'package:process/process.dart';
12 13

import 'test_wrapper.dart';
14 15

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

17 18
typedef VoidCallback = void Function();

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

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

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

49
  /// The environment that must be matched for this [FakeCommand] to be considered correct.
50
  ///
51
  /// If this is null, then the environment is ignored.
52 53 54
  ///
  /// 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.
55
  final Map<String, String>? environment;
56

57 58 59 60
  /// 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.
61
  final Encoding? encoding;
62

63 64 65 66 67 68 69
  /// 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;

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

74 75
  /// The process' exit code.
  ///
76
  /// To simulate a never-ending process, set [duration] to a value greater than
77 78 79 80 81 82 83 84 85 86 87 88 89 90
  /// 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;

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

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

99
  /// If provided, this exception will be thrown when the fake command is run.
100
  final Object? exception;
101

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

121
class _FakeProcess implements io.Process {
122 123 124 125 126
  _FakeProcess(
    this._exitCode,
    Duration duration,
    this.pid,
    this._stderr,
127
    IOSink? stdin,
128
    this._stdout,
129
    this._completer,
130
  ) : exitCode = Future<void>.delayed(duration).then((void value) {
131 132
        if (_completer != null) {
          return _completer.future.then((void _) => _exitCode);
133
        }
134 135
        return _exitCode;
      }),
136
      stdin = stdin ?? IOSink(StreamController<List<int>>().sink),
137 138 139 140 141 142
      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));
143

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

  @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]) {
168
    // Killing a fake process has no effect.
169 170 171 172
    return false;
  }
}

173 174 175 176
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;
177

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
  /// 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;
194
  factory FakeProcessManager.empty() => _SequenceProcessManager(<FakeCommand>[]);
195

196 197
  FakeProcessManager._();

198 199 200 201 202 203 204 205
  /// 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);

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

211 212
  final Map<int, _FakeProcess> _fakeRunningProcesses = <int, _FakeProcess>{};

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

218 219 220
  /// The expected [FakeCommand]s that have not yet run.
  List<FakeCommand> get _remainingExpectations;

221
  @protected
222 223
  FakeCommand findCommand(
    List<String> command,
224 225
    String? workingDirectory,
    Map<String, String>? environment,
226 227
    Encoding encoding,
  );
228 229 230

  int _pid = 9999;

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

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

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

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

314
  /// Returns false if executable in [excludedExecutables].
315
  @override
316
  bool canRun(dynamic executable, {String? workingDirectory}) => !excludedExecutables.contains(executable);
317 318

  Set<String> excludedExecutables = <String>{};
319 320

  @override
321
  bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
322
    // Killing a fake process has no effect unless it has an attached completer.
323
    final _FakeProcess? fakeProcess = _fakeRunningProcesses[pid];
324 325 326
    if (fakeProcess == null) {
      return false;
    }
327 328 329
    if (fakeProcess._completer != null) {
      fakeProcess._completer!.complete();
    }
330
    return true;
331 332
  }
}
333 334 335 336 337

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

  @override
338 339
  FakeCommand findCommand(
    List<String> command,
340 341
    String? workingDirectory,
    Map<String, String>? environment,
342 343
    Encoding encoding,
  ) {
344 345 346 347
    return FakeCommand(
      command: command,
      workingDirectory: workingDirectory,
      environment: environment,
348
      encoding: encoding,
349
      duration: Duration.zero,
350 351 352 353 354
      exitCode: 0,
      stdout: '',
      stderr: '',
    );
  }
355 356 357

  @override
  void addCommand(FakeCommand command) { }
358 359 360

  @override
  bool get hasRemainingExpectations => true;
361 362 363

  @override
  List<FakeCommand> get _remainingExpectations => <FakeCommand>[];
364 365 366 367 368 369 370 371
}

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

  final List<FakeCommand> _commands;

  @override
372 373
  FakeCommand findCommand(
    List<String> command,
374 375
    String? workingDirectory,
    Map<String, String>? environment,
376 377
    Encoding encoding,
  ) {
378 379 380 381
    expect(_commands, isNotEmpty,
      reason: 'ProcessManager was told to execute $command (in $workingDirectory) '
              'but the FakeProcessManager.list expected no more processes.'
    );
382
    _commands.first._matches(command, workingDirectory, environment, encoding);
383 384
    return _commands.removeAt(0);
  }
385 386 387 388 389

  @override
  void addCommand(FakeCommand command) {
    _commands.add(command);
  }
390 391 392

  @override
  bool get hasRemainingExpectations => _commands.isNotEmpty;
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423

  @override
  List<FakeCommand> get _remainingExpectations => _commands;
}

/// Matcher that successfully matches against a [FakeProcessManager] with
/// no remaining expectations ([item.hasRemainingExpectations] returns false).
const Matcher hasNoRemainingExpectations = _HasNoRemainingExpectations();

class _HasNoRemainingExpectations extends Matcher {
  const _HasNoRemainingExpectations();

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
      item is FakeProcessManager && !item.hasRemainingExpectations;

  @override
  Description describe(Description description) =>
      description.add('a fake process manager with no remaining expectations');

  @override
  Description describeMismatch(
      dynamic item,
      Description description,
      Map<dynamic, dynamic> matchState,
      bool verbose,
      ) {
    final FakeProcessManager fakeProcessManager = item as FakeProcessManager;
    return description.add(
        'has remaining expectations:\n${fakeProcessManager._remainingExpectations.map((FakeCommand command) => command.command).join('\n')}');
  }
424
}