blob: 294dca77b8b46d555cbda79335c7a7bda97384b5 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. 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';
/// A single recorded call to [FakeProcessManager.start] or
/// [FakeProcessManager.run].
class RecordedInvocation {
/// The full command list as passed to the [ProcessManager], with the
/// executable as the first element.
final List<String> command;
final String? workingDirectory;
final Map<String, String>? environment;
RecordedInvocation({
required this.command,
required this.workingDirectory,
required this.environment,
});
}
/// A single expected process invocation and its scripted output.
class FakeCommand {
/// When set, the actual invocation must match this list exactly.
final List<String>? command;
final String stdout;
final String stderr;
final int exitCode;
/// Called with the actual command when this invocation is consumed.
final void Function(List<String> command)? onRun;
const FakeCommand({
this.command,
this.stdout = '',
this.stderr = '',
this.exitCode = 0,
this.onRun,
});
}
/// A [ProcessManager] that never spawns a real process.
///
/// It consumes the given [FakeCommand]s in order, one per [start] or [run],
/// records every invocation in [invocations], and hands back the scripted
/// output, so tests can assert on how processes were invoked without depending
/// on the host machine.
class FakeProcessManager implements ProcessManager {
FakeProcessManager(this._commands);
final List<FakeCommand> _commands;
int _nextCommand = 0;
final List<RecordedInvocation> invocations = [];
/// Whether every [FakeCommand] has been consumed.
bool get allCommandsConsumed => _nextCommand >= _commands.length;
/// Consumes the next [FakeCommand], checking it against [command].
FakeCommand _consume(List<String> command) {
if (_nextCommand >= _commands.length) {
throw StateError('Unexpected command: $command');
}
final fake = _commands[_nextCommand++];
if (fake.command != null && !_listEquals(fake.command!, command)) {
throw StateError(
'Expected command ${fake.command} but got $command.',
);
}
fake.onRun?.call(command);
return fake;
}
@override
Future<Process> start(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) async {
final commandList = command.map((e) => '$e').toList();
final fake = _consume(commandList);
invocations.add(
RecordedInvocation(
command: commandList,
workingDirectory: workingDirectory,
environment: environment,
),
);
return _FakeProcess(fake);
}
@override
Future<ProcessResult> run(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) async {
final commandList = command.map((e) => '$e').toList();
final fake = _consume(commandList);
invocations.add(
RecordedInvocation(
command: commandList,
workingDirectory: workingDirectory,
environment: environment,
),
);
return ProcessResult(_fakePid, fake.exitCode, fake.stdout, fake.stderr);
}
@override
ProcessResult runSync(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) => throw UnimplementedError('runSync is not used by native_toolchain_c.');
@override
bool canRun(dynamic executable, {String? workingDirectory}) => true;
@override
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) => true;
}
bool _listEquals(List<String> a, List<String> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
const _fakePid = 1234;
class _FakeProcess implements Process {
_FakeProcess(this._command);
final FakeCommand _command;
@override
int get pid => _fakePid;
@override
Future<int> get exitCode async => _command.exitCode;
@override
Stream<List<int>> get stdout =>
Stream.value(systemEncoding.encode(_command.stdout));
@override
Stream<List<int>> get stderr =>
Stream.value(systemEncoding.encode(_command.stderr));
@override
IOSink get stdin =>
throw UnimplementedError('stdin is not used by native_toolchain_c.');
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => true;
}