fake_process_manager.dart 15 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 Process, ProcessResult, ProcessSignal, ProcessStartMode, 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
  final List<Pattern> command;
43 44

  /// 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
  /// When true, stdout and stderr will only be emitted after the `exitCode`
  /// [Future] on [io.Process] completes.
105 106
  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 116 117 118 119 120 121
    expect(command.length, this.command.length);
    for(int i = 0; i < command.length; i++) {
      final Pattern expected = this.command[i];
      if (expected is String) {
        expect(command[i], expected);
      } else {
        expect(command[i], matches(this.command[i]));
      }
    }
122 123
    if (this.workingDirectory != null) {
      expect(this.workingDirectory, workingDirectory);
124 125
    }
    if (this.environment != null) {
126
      expect(this.environment, environment);
127
    }
128 129 130
    if (this.encoding != null) {
      expect(this.encoding, encoding);
    }
131 132 133
  }
}

134 135 136 137 138 139 140 141 142 143 144 145 146 147
/// A fake process for use with [FakeProcessManager].
///
/// The process delays exit until both [duration] (if specified) has elapsed
/// and [completer] (if specified) has completed.
///
/// When [outputFollowsExit] is specified, bytes are streamed to [stderr] and
/// [stdout] after the process exits.
@visibleForTesting
class FakeProcess implements io.Process {
  FakeProcess({
    int exitCode = 0,
    Duration duration = Duration.zero,
    this.pid = 1234,
    List<int> stderr = const <int>[],
148
    IOSink? stdin,
149 150 151 152 153 154 155 156 157 158 159 160 161 162
    List<int> stdout = const <int>[],
    Completer<void>? completer,
    bool outputFollowsExit = false,
  }) : _exitCode = exitCode,
       exitCode = Future<void>.delayed(duration).then((void value) {
         if (completer != null) {
           return completer.future.then((void _) => exitCode);
         }
         return exitCode;
       }),
      _stderr = stderr,
      stdin = stdin ?? IOSink(StreamController<List<int>>().sink),
      _stdout = stdout,
      _completer = completer
163
  {
164
    if (_stderr.isEmpty) {
165
      this.stderr = const Stream<List<int>>.empty();
166
    } else if (outputFollowsExit) {
167
      // Wait for the process to exit before emitting stderr.
168
      this.stderr = Stream<List<int>>.fromFuture(this.exitCode.then((_) {
169 170
        // Return a Future so stderr isn't immediately available to those who
        // await exitCode, but is available asynchronously later.
171
        return Future<List<int>>(() => _stderr);
172 173
      }));
    } else {
174
      this.stderr = Stream<List<int>>.value(_stderr);
175 176
    }

177
    if (_stdout.isEmpty) {
178
      this.stdout = const Stream<List<int>>.empty();
179
    } else if (outputFollowsExit) {
180
      // Wait for the process to exit before emitting stdout.
181
      this.stdout = Stream<List<int>>.fromFuture(this.exitCode.then((_) {
182 183
        // Return a Future so stdout isn't immediately available to those who
        // await exitCode, but is available asynchronously later.
184
        return Future<List<int>>(() => _stdout);
185 186
      }));
    } else {
187
      this.stdout = Stream<List<int>>.value(_stdout);
188 189
    }
  }
190

191
  /// The process exit code.
192
  final int _exitCode;
193 194

  /// When specified, blocks process exit until completed.
195
  final Completer<void>? _completer;
196 197 198 199 200 201 202

  @override
  final Future<int> exitCode;

  @override
  final int pid;

203
  /// The raw byte content of stderr.
204
  final List<int> _stderr;
205 206

  @override
207
  late final Stream<List<int>> stderr;
208 209 210 211 212

  @override
  final IOSink stdin;

  @override
213
  late final Stream<List<int>> stdout;
214

215
  /// The raw byte content of stdout.
216
  final List<int> _stdout;
217 218 219

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
220
    // Killing a fake process has no effect.
221 222 223 224
    return false;
  }
}

225 226 227 228
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;
229

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
  /// 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;
246
  factory FakeProcessManager.empty() => _SequenceProcessManager(<FakeCommand>[]);
247

248 249
  FakeProcessManager._();

250 251 252 253 254 255 256 257
  /// 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);

258 259 260 261 262
  /// Add multiple [FakeCommand] to the current process manager.
  void addCommands(Iterable<FakeCommand> commands) {
    commands.forEach(addCommand);
  }

263
  final Map<int, FakeProcess> _fakeRunningProcesses = <int, FakeProcess>{};
264

265 266 267 268 269
  /// Whether this fake has more [FakeCommand]s that are expected to run.
  ///
  /// This is always `true` for [FakeProcessManager.any].
  bool get hasRemainingExpectations;

270 271 272
  /// The expected [FakeCommand]s that have not yet run.
  List<FakeCommand> get _remainingExpectations;

273
  @protected
274 275
  FakeCommand findCommand(
    List<String> command,
276 277
    String? workingDirectory,
    Map<String, String>? environment,
278
    Encoding? encoding,
279
  );
280 281 282

  int _pid = 9999;

283
  FakeProcess _runCommand(
284
    List<String> command,
285 286
    String? workingDirectory,
    Map<String, String>? environment,
287
    Encoding? encoding,
288
  ) {
289
    _pid += 1;
290
    final FakeCommand fakeCommand = findCommand(command, workingDirectory, environment, encoding);
291
    if (fakeCommand.exception != null) {
292 293
      assert(fakeCommand.exception is Exception || fakeCommand.exception is Error);
      throw fakeCommand.exception!; // ignore: only_throw_errors
294
    }
295
    if (fakeCommand.onRun != null) {
296
      fakeCommand.onRun!();
297
    }
298 299 300 301 302 303 304 305 306
    return FakeProcess(
      duration: fakeCommand.duration,
      exitCode: fakeCommand.exitCode,
      pid: _pid,
      stderr: encoding?.encode(fakeCommand.stderr) ?? fakeCommand.stderr.codeUnits,
      stdin: fakeCommand.stdin,
      stdout: encoding?.encode(fakeCommand.stdout) ?? fakeCommand.stdout.codeUnits,
      completer: fakeCommand.completer,
      outputFollowsExit: fakeCommand.outputFollowsExit,
307 308 309 310
    );
  }

  @override
311
  Future<io.Process> start(
312
    List<dynamic> command, {
313 314
    String? workingDirectory,
    Map<String, String>? environment,
315 316
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
317
    io.ProcessStartMode mode = io.ProcessStartMode.normal, // ignored
318
  }) {
319
    final FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, io.systemEncoding);
320 321 322 323 324 325
    if (process._completer != null) {
      _fakeRunningProcesses[process.pid] = process;
      process.exitCode.whenComplete(() {
        _fakeRunningProcesses.remove(process.pid);
      });
    }
326
    return Future<io.Process>.value(process);
327
  }
328 329

  @override
330
  Future<io.ProcessResult> run(
331
    List<dynamic> command, {
332 333
    String? workingDirectory,
    Map<String, String>? environment,
334 335
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
336 337
    Encoding? stdoutEncoding = io.systemEncoding,
    Encoding? stderrEncoding = io.systemEncoding,
338
  }) async {
339
    final FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
340
    await process.exitCode;
341
    return io.ProcessResult(
342 343
      process.pid,
      process._exitCode,
344 345
      stdoutEncoding == null ? process._stdout : await stdoutEncoding.decodeStream(process.stdout),
      stderrEncoding == null ? process._stderr : await stderrEncoding.decodeStream(process.stderr),
346 347 348 349
    );
  }

  @override
350
  io.ProcessResult runSync(
351
    List<dynamic> command, {
352 353
    String? workingDirectory,
    Map<String, String>? environment,
354 355
    bool includeParentEnvironment = true, // ignored
    bool runInShell = false, // ignored
356 357
    Encoding? stdoutEncoding = io.systemEncoding,
    Encoding? stderrEncoding = io.systemEncoding,
358
  }) {
359
    final FakeProcess process = _runCommand(command.cast<String>(), workingDirectory, environment, stdoutEncoding);
360
    return io.ProcessResult(
361 362
      process.pid,
      process._exitCode,
363 364
      stdoutEncoding == null ? process._stdout : stdoutEncoding.decode(process._stdout),
      stderrEncoding == null ? process._stderr : stderrEncoding.decode(process._stderr),
365 366 367
    );
  }

368
  /// Returns false if executable in [excludedExecutables].
369
  @override
370
  bool canRun(dynamic executable, {String? workingDirectory}) => !excludedExecutables.contains(executable);
371 372

  Set<String> excludedExecutables = <String>{};
373 374

  @override
375
  bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
376
    // Killing a fake process has no effect unless it has an attached completer.
377
    final FakeProcess? fakeProcess = _fakeRunningProcesses[pid];
378 379 380
    if (fakeProcess == null) {
      return false;
    }
381 382 383
    if (fakeProcess._completer != null) {
      fakeProcess._completer!.complete();
    }
384
    return true;
385 386
  }
}
387 388 389 390 391

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

  @override
392 393
  FakeCommand findCommand(
    List<String> command,
394 395
    String? workingDirectory,
    Map<String, String>? environment,
396
    Encoding? encoding,
397
  ) {
398 399 400 401
    return FakeCommand(
      command: command,
      workingDirectory: workingDirectory,
      environment: environment,
402
      encoding: encoding,
403 404
    );
  }
405 406 407

  @override
  void addCommand(FakeCommand command) { }
408 409 410

  @override
  bool get hasRemainingExpectations => true;
411 412 413

  @override
  List<FakeCommand> get _remainingExpectations => <FakeCommand>[];
414 415 416 417 418 419 420 421
}

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

  final List<FakeCommand> _commands;

  @override
422 423
  FakeCommand findCommand(
    List<String> command,
424 425
    String? workingDirectory,
    Map<String, String>? environment,
426
    Encoding? encoding,
427
  ) {
428 429 430 431
    expect(_commands, isNotEmpty,
      reason: 'ProcessManager was told to execute $command (in $workingDirectory) '
              'but the FakeProcessManager.list expected no more processes.'
    );
432
    _commands.first._matches(command, workingDirectory, environment, encoding);
433 434
    return _commands.removeAt(0);
  }
435 436 437 438 439

  @override
  void addCommand(FakeCommand command) {
    _commands.add(command);
  }
440 441 442

  @override
  bool get hasRemainingExpectations => _commands.isNotEmpty;
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473

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