fake_process_manager.dart 13.6 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
    this.outputFollowsExit = false,
36 37
  }) : assert(command != null),
       assert(duration != null),
38
       assert(exitCode != null);
39 40

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

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

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

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

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

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

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

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

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

103 104 105 106
  /// Indicates that output will only be emitted after the `exitCode` [Future]
  /// on [io.Process] completes.
  final bool outputFollowsExit;

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

126
class _FakeProcess implements io.Process {
127 128 129 130 131
  _FakeProcess(
    this._exitCode,
    Duration duration,
    this.pid,
    this._stderr,
132
    IOSink? stdin,
133
    this._stdout,
134
    this._completer,
135
    bool outputFollowsExit,
136
  ) : exitCode = Future<void>.delayed(duration).then((void value) {
137 138
        if (_completer != null) {
          return _completer.future.then((void _) => _exitCode);
139
        }
140 141
        return _exitCode;
      }),
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
      stdin = stdin ?? IOSink(StreamController<List<int>>().sink)
  {
    if (_stderr == null) {
      stderr = const Stream<List<int>>.empty();
    } else if (outputFollowsExit) {
      stderr = Stream<List<int>>.fromFuture(exitCode.then((_) {
        return Future<List<int>>(() => utf8.encode(_stderr));
      }));
    } else {
      stderr = Stream<List<int>>.value(utf8.encode(_stderr));
    }

    if (_stdout == null) {
      stdout = const Stream<List<int>>.empty();
    } else if (outputFollowsExit) {
      stdout = Stream<List<int>>.fromFuture(exitCode.then((_) {
        return Future<List<int>>(() => utf8.encode(_stdout));
      }));
    } else {
      stdout = Stream<List<int>>.value(utf8.encode(_stdout));
    }
  }
164

165
  final int _exitCode;
166
  final Completer<void>? _completer;
167 168 169 170 171 172 173 174 175 176

  @override
  final Future<int> exitCode;

  @override
  final int pid;

  final String _stderr;

  @override
177
  late final Stream<List<int>> stderr;
178 179 180 181 182

  @override
  final IOSink stdin;

  @override
183
  late final Stream<List<int>> stdout;
184 185 186 187 188

  final String _stdout;

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
189
    // Killing a fake process has no effect.
190 191 192 193
    return false;
  }
}

194 195 196 197
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;
198

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
  /// 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;
215
  factory FakeProcessManager.empty() => _SequenceProcessManager(<FakeCommand>[]);
216

217 218
  FakeProcessManager._();

219 220 221 222 223 224 225 226
  /// 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);

227 228 229 230 231
  /// Add multiple [FakeCommand] to the current process manager.
  void addCommands(Iterable<FakeCommand> commands) {
    commands.forEach(addCommand);
  }

232 233
  final Map<int, _FakeProcess> _fakeRunningProcesses = <int, _FakeProcess>{};

234 235 236 237 238
  /// Whether this fake has more [FakeCommand]s that are expected to run.
  ///
  /// This is always `true` for [FakeProcessManager.any].
  bool get hasRemainingExpectations;

239 240 241
  /// The expected [FakeCommand]s that have not yet run.
  List<FakeCommand> get _remainingExpectations;

242
  @protected
243 244
  FakeCommand findCommand(
    List<String> command,
245 246
    String? workingDirectory,
    Map<String, String>? environment,
247
    Encoding? encoding,
248
  );
249 250 251

  int _pid = 9999;

252 253
  _FakeProcess _runCommand(
    List<String> command,
254 255
    String? workingDirectory,
    Map<String, String>? environment,
256
    Encoding? encoding,
257
  ) {
258
    _pid += 1;
259
    final FakeCommand fakeCommand = findCommand(command, workingDirectory, environment, encoding);
260
    if (fakeCommand.exception != null) {
261 262
      assert(fakeCommand.exception is Exception || fakeCommand.exception is Error);
      throw fakeCommand.exception!; // ignore: only_throw_errors
263
    }
264
    if (fakeCommand.onRun != null) {
265
      fakeCommand.onRun!();
266
    }
267 268 269 270 271
    return _FakeProcess(
      fakeCommand.exitCode,
      fakeCommand.duration,
      _pid,
      fakeCommand.stderr,
272
      fakeCommand.stdin,
273
      fakeCommand.stdout,
274
      fakeCommand.completer,
275
      fakeCommand.outputFollowsExit,
276 277 278 279
    );
  }

  @override
280
  Future<io.Process> start(
281
    List<dynamic> command, {
282 283
    String? workingDirectory,
    Map<String, String>? environment,
284 285
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
286
    io.ProcessStartMode mode = io.ProcessStartMode.normal, // ignored
287
  }) {
288
    final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, io.systemEncoding);
289 290 291 292 293 294
    if (process._completer != null) {
      _fakeRunningProcesses[process.pid] = process;
      process.exitCode.whenComplete(() {
        _fakeRunningProcesses.remove(process.pid);
      });
    }
295
    return Future<io.Process>.value(process);
296
  }
297 298

  @override
299
  Future<io.ProcessResult> run(
300
    List<dynamic> command, {
301 302
    String? workingDirectory,
    Map<String, String>? environment,
303 304
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
305 306
    Encoding? stdoutEncoding = io.systemEncoding,
    Encoding? stderrEncoding = io.systemEncoding,
307
  }) async {
308
    final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
309
    await process.exitCode;
310
    return io.ProcessResult(
311 312 313 314 315 316 317 318
      process.pid,
      process._exitCode,
      stdoutEncoding == null ? process.stdout : await stdoutEncoding.decodeStream(process.stdout),
      stderrEncoding == null ? process.stderr : await stderrEncoding.decodeStream(process.stderr),
    );
  }

  @override
319
  io.ProcessResult runSync(
320
    List<dynamic> command, {
321 322
    String? workingDirectory,
    Map<String, String>? environment,
323 324
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
325 326
    Encoding? stdoutEncoding = io.systemEncoding, // actual encoder is ignored
    Encoding? stderrEncoding = io.systemEncoding, // actual encoder is ignored
327
  }) {
328
    final _FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
329
    return io.ProcessResult(
330 331 332 333 334 335 336
      process.pid,
      process._exitCode,
      stdoutEncoding == null ? utf8.encode(process._stdout) : process._stdout,
      stderrEncoding == null ? utf8.encode(process._stderr) : process._stderr,
    );
  }

337
  /// Returns false if executable in [excludedExecutables].
338
  @override
339
  bool canRun(dynamic executable, {String? workingDirectory}) => !excludedExecutables.contains(executable);
340 341

  Set<String> excludedExecutables = <String>{};
342 343

  @override
344
  bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
345
    // Killing a fake process has no effect unless it has an attached completer.
346
    final _FakeProcess? fakeProcess = _fakeRunningProcesses[pid];
347 348 349
    if (fakeProcess == null) {
      return false;
    }
350 351 352
    if (fakeProcess._completer != null) {
      fakeProcess._completer!.complete();
    }
353
    return true;
354 355
  }
}
356 357 358 359 360

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

  @override
361 362
  FakeCommand findCommand(
    List<String> command,
363 364
    String? workingDirectory,
    Map<String, String>? environment,
365
    Encoding? encoding,
366
  ) {
367 368 369 370
    return FakeCommand(
      command: command,
      workingDirectory: workingDirectory,
      environment: environment,
371
      encoding: encoding,
372 373
    );
  }
374 375 376

  @override
  void addCommand(FakeCommand command) { }
377 378 379

  @override
  bool get hasRemainingExpectations => true;
380 381 382

  @override
  List<FakeCommand> get _remainingExpectations => <FakeCommand>[];
383 384 385 386 387 388 389 390
}

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

  final List<FakeCommand> _commands;

  @override
391 392
  FakeCommand findCommand(
    List<String> command,
393 394
    String? workingDirectory,
    Map<String, String>? environment,
395
    Encoding? encoding,
396
  ) {
397 398 399 400
    expect(_commands, isNotEmpty,
      reason: 'ProcessManager was told to execute $command (in $workingDirectory) '
              'but the FakeProcessManager.list expected no more processes.'
    );
401
    _commands.first._matches(command, workingDirectory, environment, encoding);
402 403
    return _commands.removeAt(0);
  }
404 405 406 407 408

  @override
  void addCommand(FakeCommand command) {
    _commands.add(command);
  }
409 410 411

  @override
  bool get hasRemainingExpectations => _commands.isNotEmpty;
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442

  @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')}');
  }
443
}