refactor: promoted test_process to process package

This commit is contained in:
Patrick Stewart 2024-12-30 21:39:27 -07:00
parent 7e6ccf47bd
commit 942209900a
72 changed files with 1130 additions and 7543 deletions

View file

@ -1,431 +0,0 @@
# Dart Process Handler
A Laravel-inspired process handling library for Dart that provides an elegant and powerful API for executing shell commands and managing processes.
## Features
- Fluent API for process configuration
- Synchronous and asynchronous execution
- Process timeouts and idle timeouts
- Working directory and environment variables
- Input/output handling and streaming
- Shell command support with pipes and redirects
- TTY mode support
- Comprehensive error handling
- Real-time output callbacks
- Process status tracking and management
## Installation
Add this to your package's `pubspec.yaml` file:
```yaml
dependencies:
test_process: ^1.0.0
```
## Basic Usage
### Simple Command Execution
```dart
import 'package:test_process/test_process.dart';
void main() async {
final factory = Factory();
// Basic command execution
final result = await factory.command(['echo', 'Hello World']).run();
print('Output: ${result.output()}'); // Output: Hello World
print('Success: ${result.successful()}'); // Success: true
// Using string command (executed through shell)
final result2 = await factory.command('echo "Hello World"').run();
print('Output: ${result2.output()}'); // Output: Hello World
}
```
### Working Directory
```dart
void main() async {
final factory = Factory();
// Execute command in specific directory
final result = await factory
.command(['ls', '-l'])
.withWorkingDirectory('/tmp')
.run();
print('Files in /tmp:');
print(result.output());
}
```
### Environment Variables
```dart
void main() async {
final factory = Factory();
final result = await factory
.command(['printenv', 'MY_VAR'])
.withEnvironment({'MY_VAR': 'Hello from env!'})
.run();
print('Environment Value: ${result.output()}');
}
```
### Process Timeouts
```dart
void main() async {
final factory = Factory();
try {
// Process timeout
await factory
.command(['sleep', '10'])
.withTimeout(5) // 5 second timeout
.run();
} on ProcessTimedOutException catch (e) {
print('Process timed out: ${e.message}');
}
try {
// Idle timeout (no output for specified duration)
await factory
.command(['tail', '-f', '/dev/null'])
.withIdleTimeout(5) // 5 second idle timeout
.run();
} on ProcessTimedOutException catch (e) {
print('Process idle timeout: ${e.message}');
}
}
```
### Standard Input
```dart
void main() async {
final factory = Factory();
// String input
final result1 = await factory
.command(['cat'])
.withInput('Hello from stdin!')
.run();
print('Input Echo: ${result1.output()}');
// Byte input
final result2 = await factory
.command(['cat'])
.withInput([72, 101, 108, 108, 111]) // "Hello" in bytes
.run();
print('Byte Input Echo: ${result2.output()}');
}
```
### Error Handling
```dart
void main() async {
final factory = Factory();
try {
await factory.command(['ls', 'nonexistent-file']).run();
} on ProcessFailedException catch (e) {
print('Command failed:');
print(' Exit code: ${e.result.exitCode}');
print(' Error output: ${e.result.errorOutput()}');
}
}
```
### Shell Commands with Pipes
```dart
void main() async {
final factory = Factory();
// Using pipes in shell command
final result = await factory
.command('echo "line1\nline2\nline3" | grep "line2"')
.run();
print('Grep Result: ${result.output()}');
// Multiple commands
final result2 = await factory
.command('cd /tmp && ls -l | grep "log"')
.run();
print('Log files: ${result2.output()}');
}
```
### Asynchronous Execution with Output Callback
```dart
void main() async {
final factory = Factory();
// Start process asynchronously
final process = await factory
.command(['sh', '-c', 'for i in 1 2 3; do echo $i; sleep 1; done'])
.start((output) {
print('Realtime Output: $output');
});
// Wait for completion
final result = await process.wait();
print('Final Exit Code: ${result.exitCode}');
}
```
### Process Management
```dart
void main() async {
final factory = Factory();
// Start long-running process
final process = await factory
.command(['sleep', '10'])
.start();
print('Process started with PID: ${process.pid}');
print('Is running: ${process.running()}');
// Kill the process
final killed = process.kill(); // Sends SIGTERM
print('Kill signal sent: $killed');
// Or with specific signal
process.kill(ProcessSignal.sigint); // Sends SIGINT
final result = await process.wait();
print('Process completed with exit code: ${result.exitCode}');
}
```
### Output Control
```dart
void main() async {
final factory = Factory();
// Disable output
final result = await factory
.command(['echo', 'test'])
.withoutOutput()
.run();
print('Output length: ${result.output().length}'); // Output length: 0
}
```
### TTY Mode
```dart
void main() async {
final factory = Factory();
// Enable TTY mode for commands that require it
final result = await factory
.command(['ls', '--color=auto'])
.withTty()
.run();
print('Color Output: ${result.output()}');
}
```
## Advanced Usage
### Custom Process Configuration
```dart
void main() async {
final factory = Factory();
final result = await factory
.command(['my-script'])
.withWorkingDirectory('/path/to/scripts')
.withEnvironment({
'NODE_ENV': 'production',
'DEBUG': 'true'
})
.withTimeout(30)
.withIdleTimeout(5)
.withTty()
.run();
if (result.successful()) {
print('Script completed successfully');
print(result.output());
}
}
```
### Process Pool Management
```dart
void main() async {
final factory = Factory();
final processes = <InvokedProcess>[];
// Start multiple processes
for (var i = 0; i < 3; i++) {
final process = await factory
.command(['worker.sh', i.toString()])
.start();
processes.add(process);
}
// Wait for all processes to complete
for (var process in processes) {
final result = await process.wait();
print('Worker completed with exit code: ${result.exitCode}');
}
}
```
### Error Output Handling
```dart
void main() async {
final factory = Factory();
try {
final result = await factory
.command(['some-command'])
.run();
print('Standard output:');
print(result.output());
print('Error output:');
print(result.errorOutput());
} on ProcessFailedException catch (e) {
print('Command failed with exit code: ${e.result.exitCode}');
print('Error details:');
print(e.result.errorOutput());
}
}
```
### Infinite Process Execution
```dart
void main() async {
final factory = Factory();
// Disable timeout for long-running processes
final process = await factory
.command(['tail', '-f', 'logfile.log'])
.forever() // Disables timeout
.start((output) {
print('New log entry: $output');
});
// Process will run until explicitly killed
await Future.delayed(Duration(minutes: 1));
process.kill();
}
```
## Error Handling
The library provides several exception types for different error scenarios:
### ProcessFailedException
Thrown when a process exits with a non-zero exit code:
```dart
try {
await factory.command(['nonexistent-command']).run();
} on ProcessFailedException catch (e) {
print('Command failed:');
print('Exit code: ${e.result.exitCode}');
print('Error output: ${e.result.errorOutput()}');
print('Standard output: ${e.result.output()}');
}
```
### ProcessTimedOutException
Thrown when a process exceeds its timeout or idle timeout:
```dart
try {
await factory
.command(['sleep', '10'])
.withTimeout(5)
.run();
} on ProcessTimedOutException catch (e) {
print('Process timed out:');
print('Message: ${e.message}');
if (e.result != null) {
print('Partial output: ${e.result?.output()}');
}
}
```
## Best Practices
1. **Always handle process failures:**
```dart
try {
await factory.command(['risky-command']).run();
} on ProcessFailedException catch (e) {
// Handle failure
} on ProcessTimedOutException catch (e) {
// Handle timeout
}
```
2. **Set appropriate timeouts:**
```dart
factory
.command(['long-running-task'])
.withTimeout(300) // Overall timeout
.withIdleTimeout(60) // Idle timeout
.run();
```
3. **Use output callbacks for long-running processes:**
```dart
await factory
.command(['lengthy-task'])
.start((output) {
// Process output in real-time
print('Progress: $output');
});
```
4. **Clean up resources:**
```dart
final process = await factory.command(['server']).start();
try {
// Do work
} finally {
process.kill(); // Ensure process is terminated
}
```
5. **Use shell mode appropriately:**
```dart
// For simple commands, use array form:
factory.command(['echo', 'hello']);
// For shell features (pipes, redirects), use string form:
factory.command('echo hello | grep "o"');
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
## License
This project is licensed under the MIT License - see the LICENSE file for details.

View file

@ -1,151 +0,0 @@
import 'dart:async';
import 'package:test_process/test_process.dart';
Future<void> runExamples() async {
// Create a process factory
final factory = Factory();
// Basic command execution
print('\n=== Basic Command Execution ===');
try {
final result = await factory.command(['echo', 'Hello, World!']).run();
print('Output: ${result.output().trim()}');
print('Exit Code: ${result.exitCode}');
print('Success: ${result.successful()}');
} catch (e) {
print('Error: $e');
}
// Working directory
print('\n=== Working Directory ===');
try {
final result =
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
print('Current Directory: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
// Environment variables
print('\n=== Environment Variables ===');
try {
final result = await factory
.command(['sh', '-c', 'echo \$CUSTOM_VAR']).withEnvironment(
{'CUSTOM_VAR': 'Hello from env!'}).run();
print('Environment Value: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
// Process timeout
print('\n=== Process Timeout ===');
try {
await factory.command(['sleep', '5']).withTimeout(1).run();
print('Process completed (unexpected)');
} catch (e) {
// Let the zone handler catch this
}
// Standard input
print('\n=== Standard Input ===');
try {
final result =
await factory.command(['cat']).withInput('Hello from stdin!').run();
print('Input Echo: ${result.output()}');
} catch (e) {
print('Error: $e');
}
// Error handling
print('\n=== Error Handling ===');
try {
await factory.command(['ls', 'nonexistent-file']).run();
print('Command succeeded (unexpected)');
} on ProcessFailedException catch (e) {
print('Expected error:');
print(' Exit code: ${e.exitCode}');
print(' Error output: ${e.errorOutput.trim()}');
} catch (e) {
print('Unexpected error: $e');
}
// Shell commands with pipes
print('\n=== Shell Commands with Pipes ===');
try {
final result = await factory.command(
['sh', '-c', 'echo "line1\nline2\nline3" | grep "line2"']).run();
print('Grep Result: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
// Async process with output callback
print('\n=== Async Process with Output Callback ===');
try {
final process = await factory.command([
'sh',
'-c',
'for n in 1 2 3; do echo \$n; sleep 1; done'
]).start((output) {
print('Realtime Output: ${output.trim()}');
});
final result = await process.wait();
print('Final Exit Code: ${result.exitCode}');
} catch (e) {
print('Error: $e');
}
// Process killing
print('\n=== Process Killing ===');
try {
final process = await factory.command(['sleep', '10']).start();
print('Process started with PID: ${process.pid}');
print('Is running: ${process.running()}');
// Kill after 1 second
await Future.delayed(Duration(seconds: 1));
final killed = process.kill();
print('Kill signal sent: $killed');
final result = await process.wait();
print('Process completed with exit code: ${result.exitCode}');
} catch (e) {
print('Error: $e');
}
// Quiet mode (no output)
print('\n=== Quiet Mode ===');
try {
final result = await factory
.command(['echo', 'This output is suppressed'])
.withoutOutput()
.run();
print('Output length: ${result.output().length}');
} catch (e) {
print('Error: $e');
}
// Color output (alternative to TTY mode)
print('\n=== Color Output ===');
try {
final result = await factory.command(['ls', '--color=always']).run();
print('Color Output: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
}
void main() {
runZonedGuarded(() async {
await runExamples();
}, (error, stack) {
if (error is ProcessTimedOutException) {
print('Expected timeout error: ${error.message}');
} else {
print('Unexpected error: $error');
print('Stack trace: $stack');
}
});
}

View file

@ -1,42 +0,0 @@
import '../process_result.dart';
/// Exception thrown when a process fails.
class ProcessFailedException implements Exception {
/// The process result.
final ProcessResult _result;
/// Create a new process failed exception instance.
ProcessFailedException(this._result);
/// Get the process result.
ProcessResult get result => _result;
/// Get the process exit code.
int get exitCode => _result.exitCode;
/// Get the process output.
String get output => _result.output();
/// Get the process error output.
String get errorOutput => _result.errorOutput();
@override
String toString() {
final buffer =
StringBuffer('Process failed with exit code: ${_result.exitCode}');
if (_result.output().isNotEmpty) {
buffer.writeln();
buffer.writeln('Output:');
buffer.writeln(_result.output().trim());
}
if (_result.errorOutput().isNotEmpty) {
buffer.writeln();
buffer.writeln('Error Output:');
buffer.writeln(_result.errorOutput().trim());
}
return buffer.toString();
}
}

View file

@ -1,34 +0,0 @@
import 'pending_process.dart';
/// A factory for creating process instances.
class Factory {
/// Create a new factory instance.
Factory();
/// Create a new pending process instance with the given command.
PendingProcess command(dynamic command) {
if (command == null) {
throw ArgumentError('Command cannot be null');
}
if (command is String && command.trim().isEmpty) {
throw ArgumentError('Command string cannot be empty');
}
if (command is List) {
if (command.isEmpty) {
throw ArgumentError('Command list cannot be empty');
}
if (command.any((element) => element is! String)) {
throw ArgumentError('Command list must contain only strings');
}
}
if (command is! String && command is! List) {
throw ArgumentError('Command must be a string or list of strings');
}
return PendingProcess(this)..withCommand(command);
}
}

View file

@ -1,128 +0,0 @@
import 'dart:io';
import 'dart:async';
import 'process_result.dart';
import 'exceptions/process_failed_exception.dart';
/// Represents a process that has been started.
class InvokedProcess {
/// The underlying process instance.
final Process _process;
/// The output handler callback.
final void Function(String)? _onOutput;
/// The collected stdout data.
final List<int> _stdout = [];
/// The collected stderr data.
final List<int> _stderr = [];
/// Whether the process has completed
bool _completed = false;
/// Whether the process was killed
bool _killed = false;
/// Completer for stdout stream
final _stdoutCompleter = Completer<void>();
/// Completer for stderr stream
final _stderrCompleter = Completer<void>();
/// Create a new invoked process instance.
InvokedProcess(this._process, this._onOutput) {
_process.stdout.listen(
(data) {
_stdout.addAll(data);
if (_onOutput != null) {
_onOutput!(String.fromCharCodes(data));
}
},
onDone: () => _stdoutCompleter.complete(),
);
_process.stderr.listen(
(data) {
_stderr.addAll(data);
if (_onOutput != null) {
_onOutput!(String.fromCharCodes(data));
}
},
onDone: () => _stderrCompleter.complete(),
);
// Track when the process completes
_process.exitCode.then((_) => _completed = true);
}
/// Get the process ID.
int get pid => _process.pid;
/// Write data to the process stdin.
void write(dynamic input) {
if (input is String) {
_process.stdin.write(input);
} else if (input is List<int>) {
_process.stdin.add(input);
}
}
/// Close the process stdin.
Future<void> closeStdin() async {
await _process.stdin.close();
}
/// Signal the process.
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
_killed = true;
return _process.kill(signal);
}
/// Check if the process is still running.
bool running() {
return !_completed;
}
/// Wait for the process to complete and get its result.
Future<ProcessResult> wait() async {
// Wait for all streams to complete
await Future.wait([
_stdoutCompleter.future,
_stderrCompleter.future,
]);
final exitCode = await _process.exitCode;
final result = ProcessResult(
exitCode,
String.fromCharCodes(_stdout),
String.fromCharCodes(_stderr),
);
// Don't throw if the process was killed
if (!_killed && exitCode != 0) {
throw ProcessFailedException(result);
}
return result;
}
/// Get the latest output from the process.
String latestOutput() {
return String.fromCharCodes(_stdout);
}
/// Get the latest error output from the process.
String latestErrorOutput() {
return String.fromCharCodes(_stderr);
}
/// Get all output from the process.
String output() {
return String.fromCharCodes(_stdout);
}
/// Get all error output from the process.
String errorOutput() {
return String.fromCharCodes(_stderr);
}
}

View file

@ -1,288 +0,0 @@
import 'dart:io';
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:collection/collection.dart';
import 'factory.dart';
import 'process_result.dart';
import 'invoked_process.dart';
import 'exceptions/process_timed_out_exception.dart';
import 'exceptions/process_failed_exception.dart';
/// A class that represents a process that is ready to be started.
class PendingProcess {
/// The process factory instance.
final Factory _factory;
/// The command to invoke the process.
dynamic command;
/// The working directory of the process.
String? workingDirectory;
/// The maximum number of seconds the process may run.
int? timeout = 60;
/// The maximum number of seconds the process may go without returning output.
int? idleTimeout;
/// The additional environment variables for the process.
Map<String, String> environment = {};
/// The standard input data that should be piped into the command.
dynamic input;
/// Indicates whether output should be disabled for the process.
bool quietly = false;
/// Indicates if TTY mode should be enabled.
bool tty = false;
/// Create a new pending process instance.
PendingProcess(this._factory);
/// Format the command for display.
String _formatCommand() {
if (command is List) {
return (command as List).join(' ');
}
return command.toString();
}
/// Specify the command that will invoke the process.
PendingProcess withCommand(dynamic command) {
this.command = command;
return this;
}
/// Specify the working directory of the process.
PendingProcess withWorkingDirectory(String directory) {
workingDirectory = directory;
return this;
}
/// Specify the maximum number of seconds the process may run.
PendingProcess withTimeout(int seconds) {
timeout = seconds;
return this;
}
/// Specify the maximum number of seconds a process may go without returning output.
PendingProcess withIdleTimeout(int seconds) {
idleTimeout = seconds;
return this;
}
/// Indicate that the process may run forever without timing out.
PendingProcess forever() {
timeout = null;
return this;
}
/// Set the additional environment variables for the process.
PendingProcess withEnvironment(Map<String, String> env) {
environment = env;
return this;
}
/// Set the standard input that should be provided when invoking the process.
PendingProcess withInput(dynamic input) {
this.input = input;
return this;
}
/// Disable output for the process.
PendingProcess withoutOutput() {
quietly = true;
return this;
}
/// Enable TTY mode for the process.
PendingProcess withTty([bool enabled = true]) {
tty = enabled;
return this;
}
/// Run the process synchronously.
Future<ProcessResult> run(
[dynamic command, void Function(String)? onOutput]) async {
this.command = command ?? this.command;
if (this.command == null) {
throw ArgumentError('No command specified');
}
// Handle immediate timeout
if (timeout == 0) {
throw ProcessTimedOutException(
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.',
);
}
try {
final process = await _createProcess();
Timer? timeoutTimer;
Timer? idleTimer;
DateTime lastOutputTime = DateTime.now();
bool timedOut = false;
String? timeoutMessage;
if (timeout != null) {
timeoutTimer = Timer(Duration(seconds: timeout!), () {
timedOut = true;
timeoutMessage =
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.';
process.kill();
});
}
if (idleTimeout != null) {
idleTimer = Timer.periodic(Duration(seconds: 1), (_) {
final idleSeconds =
DateTime.now().difference(lastOutputTime).inSeconds;
if (idleSeconds >= idleTimeout!) {
timedOut = true;
timeoutMessage =
'The process "${_formatCommand()}" exceeded the idle timeout of $idleTimeout seconds.';
process.kill();
idleTimer?.cancel();
}
});
}
try {
final result = await _runProcess(process, (output) {
lastOutputTime = DateTime.now();
onOutput?.call(output);
});
if (timedOut) {
throw ProcessTimedOutException(timeoutMessage!);
}
if (result.exitCode != 0) {
throw ProcessFailedException(result);
}
return result;
} finally {
timeoutTimer?.cancel();
idleTimer?.cancel();
}
} on ProcessException catch (e) {
final result = ProcessResult(1, '', e.message);
throw ProcessFailedException(result);
}
}
/// Start the process asynchronously.
Future<InvokedProcess> start([void Function(String)? onOutput]) async {
if (command == null) {
throw ArgumentError('No command specified');
}
try {
final process = await _createProcess();
if (input != null) {
if (input is String) {
process.stdin.write(input);
} else if (input is List<int>) {
process.stdin.add(input);
}
await process.stdin.close();
}
return InvokedProcess(process, onOutput);
} on ProcessException catch (e) {
final result = ProcessResult(1, '', e.message);
throw ProcessFailedException(result);
}
}
Future<Process> _createProcess() async {
if (command is List) {
final List<String> args =
(command as List).map((e) => e.toString()).toList();
return Process.start(
args[0],
args.skip(1).toList(),
workingDirectory: workingDirectory ?? Directory.current.path,
environment: environment,
includeParentEnvironment: true,
runInShell: false,
mode: tty ? ProcessStartMode.inheritStdio : ProcessStartMode.normal,
);
} else {
// For string commands, use shell to handle pipes, redirects, etc.
final shell = Platform.isWindows ? 'cmd' : '/bin/sh';
final shellArg = Platform.isWindows ? '/c' : '-c';
return Process.start(
shell,
[shellArg, command.toString()],
workingDirectory: workingDirectory ?? Directory.current.path,
environment: environment,
includeParentEnvironment: true,
runInShell: true,
mode: tty ? ProcessStartMode.inheritStdio : ProcessStartMode.normal,
);
}
}
Future<ProcessResult> _runProcess(
Process process, void Function(String)? onOutput) async {
final stdout = <int>[];
final stderr = <int>[];
final stdoutCompleter = Completer<void>();
final stderrCompleter = Completer<void>();
if (!quietly) {
process.stdout.listen(
(data) {
stdout.addAll(data);
if (onOutput != null) {
onOutput(String.fromCharCodes(data));
}
},
onDone: () => stdoutCompleter.complete(),
);
process.stderr.listen(
(data) {
stderr.addAll(data);
if (onOutput != null) {
onOutput(String.fromCharCodes(data));
}
},
onDone: () => stderrCompleter.complete(),
);
} else {
stdoutCompleter.complete();
stderrCompleter.complete();
}
if (input != null) {
if (input is String) {
process.stdin.write(input);
} else if (input is List<int>) {
process.stdin.add(input);
}
await process.stdin.close();
}
// Wait for all streams to complete
await Future.wait([
stdoutCompleter.future,
stderrCompleter.future,
]);
final exitCode = await process.exitCode;
return ProcessResult(
exitCode,
String.fromCharCodes(stdout),
String.fromCharCodes(stderr),
);
}
}

View file

@ -1,32 +0,0 @@
/// Represents the result of a process execution.
class ProcessResult {
/// The process exit code.
final int _exitCode;
/// The process standard output.
final String _output;
/// The process error output.
final String _errorOutput;
/// Create a new process result instance.
ProcessResult(this._exitCode, this._output, this._errorOutput);
/// Get the process exit code.
int get exitCode => _exitCode;
/// Get the process output.
String output() => _output;
/// Get the process error output.
String errorOutput() => _errorOutput;
/// Check if the process was successful.
bool successful() => _exitCode == 0;
/// Check if the process failed.
bool failed() => !successful();
@override
String toString() => _output;
}

View file

@ -1,402 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: "direct main"
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
collection:
dependency: "direct main"
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
meta:
dependency: "direct main"
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "4ac0537115a24d772c408a2520ecd0abb99bca2ea9c4e634ccbdbfae64fe17ec"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d"
url: "https://pub.dev"
source: hosted
version: "1.25.14"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"

View file

@ -1,17 +0,0 @@
name: test_process
description: A Laravel-compatible process management implementation in pure Dart
version: 1.0.0
homepage: https://github.com/platform/test_process
environment:
sdk: '>=2.17.0 <4.0.0'
dependencies:
meta: ^1.9.1
path: ^1.8.0
async: ^2.11.0
collection: ^1.17.0
dev_dependencies:
lints: ^2.0.0
test: ^1.24.0

View file

@ -1,42 +0,0 @@
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
void main() {
group('Factory Tests', () {
late Factory factory;
setUp(() {
factory = Factory();
});
test('command() creates PendingProcess with string command', () {
final process = factory.command('echo Hello');
expect(process, isA<PendingProcess>());
});
test('command() creates PendingProcess with list command', () {
final process = factory.command(['echo', 'Hello']);
expect(process, isA<PendingProcess>());
});
test('command() with null throws ArgumentError', () {
expect(() => factory.command(null), throwsArgumentError);
});
test('command() with empty string throws ArgumentError', () {
expect(() => factory.command(''), throwsArgumentError);
});
test('command() with empty list throws ArgumentError', () {
expect(() => factory.command([]), throwsArgumentError);
});
test('command() with invalid type throws ArgumentError', () {
expect(() => factory.command(123), throwsArgumentError);
});
test('command() with list containing non-string throws ArgumentError', () {
expect(() => factory.command(['echo', 123]), throwsArgumentError);
});
});
}

View file

@ -1,104 +0,0 @@
import 'dart:io';
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
void main() {
group('InvokedProcess Tests', () {
test('latestOutput() returns latest stdout', () async {
final factory = Factory();
final process = await factory.command(
['sh', '-c', 'echo "line1"; sleep 0.1; echo "line2"']).start();
await Future.delayed(Duration(milliseconds: 50));
expect(process.latestOutput().trim(), equals('line1'));
await Future.delayed(Duration(milliseconds: 100));
expect(process.latestOutput().trim(), contains('line2'));
await process.wait();
}, timeout: Timeout(Duration(seconds: 5)));
test('latestErrorOutput() returns latest stderr', () async {
final factory = Factory();
final process = await factory.command([
'sh',
'-c',
'echo "error1" >&2; sleep 0.1; echo "error2" >&2'
]).start();
await Future.delayed(Duration(milliseconds: 50));
expect(process.latestErrorOutput().trim(), equals('error1'));
await Future.delayed(Duration(milliseconds: 100));
expect(process.latestErrorOutput().trim(), contains('error2'));
await process.wait();
}, timeout: Timeout(Duration(seconds: 5)));
test('running() returns correct state', () async {
final factory = Factory();
final process = await factory.command(['sleep', '0.5']).start();
expect(process.running(), isTrue);
await Future.delayed(Duration(milliseconds: 600));
expect(process.running(), isFalse);
}, timeout: Timeout(Duration(seconds: 5)));
test('write() sends input to process', () async {
final factory = Factory();
final process = await factory.command(['cat']).start();
process.write('Hello');
process.write(' World');
await process.closeStdin();
final result = await process.wait();
expect(result.output().trim(), equals('Hello World'));
}, timeout: Timeout(Duration(seconds: 5)));
test('write() handles byte input', () async {
final factory = Factory();
final process = await factory.command(['cat']).start();
process.write([72, 101, 108, 108, 111]); // "Hello" in bytes
await process.closeStdin();
final result = await process.wait();
expect(result.output().trim(), equals('Hello'));
}, timeout: Timeout(Duration(seconds: 5)));
test('kill() terminates process', () async {
final factory = Factory();
final process = await factory.command(['sleep', '10']).start();
expect(process.running(), isTrue);
final killed = process.kill();
expect(killed, isTrue);
final result = await process.wait();
expect(result.exitCode, equals(-15)); // SIGTERM
expect(process.running(), isFalse);
}, timeout: Timeout(Duration(seconds: 5)));
test('kill() with custom signal', () async {
if (!Platform.isWindows) {
final factory = Factory();
final process = await factory.command(['sleep', '10']).start();
expect(process.running(), isTrue);
final killed = process.kill(ProcessSignal.sigint);
expect(killed, isTrue);
final result = await process.wait();
expect(result.exitCode, equals(-2)); // SIGINT
expect(process.running(), isFalse);
}
}, timeout: Timeout(Duration(seconds: 5)));
test('pid returns process ID', () async {
final factory = Factory();
final process = await factory.command(['echo', 'test']).start();
expect(process.pid, isPositive);
await process.wait();
}, timeout: Timeout(Duration(seconds: 5)));
});
}

View file

@ -1,136 +0,0 @@
import 'dart:io';
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
void main() {
group('PendingProcess Tests', () {
late Factory factory;
setUp(() {
factory = Factory();
});
test('forever() disables timeout', () async {
final process = factory.command(['sleep', '0.5']).forever();
expect(process.timeout, isNull);
});
test('withIdleTimeout() sets idle timeout', () async {
final process = factory.command(['echo', 'test']).withIdleTimeout(5);
expect(process.idleTimeout, equals(5));
});
test('withTty() enables TTY mode', () async {
final process = factory.command(['echo', 'test']).withTty();
expect(process.tty, isTrue);
});
test('withTty(false) disables TTY mode', () async {
final process = factory.command(['echo', 'test']).withTty(false);
expect(process.tty, isFalse);
});
test('run() without command throws ArgumentError', () async {
final process = PendingProcess(factory);
expect(() => process.run(), throwsArgumentError);
});
test('start() without command throws ArgumentError', () async {
final process = PendingProcess(factory);
expect(() => process.start(), throwsArgumentError);
});
test('run() with command parameter overrides previous command', () async {
final process = factory.command(['echo', 'old']);
final result = await process.run(['echo', 'new']);
expect(result.output().trim(), equals('new'));
});
test('run() handles process exceptions', () async {
if (!Platform.isWindows) {
final process = factory.command(['nonexistent']);
expect(() => process.run(), throwsA(isA<ProcessFailedException>()));
}
});
test('start() handles process exceptions', () async {
if (!Platform.isWindows) {
final process = factory.command(['nonexistent']);
expect(() => process.start(), throwsA(isA<ProcessFailedException>()));
}
});
test('withoutOutput() disables output', () async {
final result =
await factory.command(['echo', 'test']).withoutOutput().run();
expect(result.output(), isEmpty);
});
test('idle timeout triggers', () async {
if (!Platform.isWindows) {
// Use tail -f to wait indefinitely without producing output
final process =
factory.command(['tail', '-f', '/dev/null']).withIdleTimeout(1);
await expectLater(
process.run(),
throwsA(
allOf(
isA<ProcessTimedOutException>(),
predicate((ProcessTimedOutException e) =>
e.message.contains('exceeded the idle timeout of 1 seconds')),
),
),
);
}
}, timeout: Timeout(Duration(seconds: 5)));
test('timeout triggers', () async {
if (!Platform.isWindows) {
final process = factory.command(['sleep', '5']).withTimeout(1);
await expectLater(
process.run(),
throwsA(
allOf(
isA<ProcessTimedOutException>(),
predicate((ProcessTimedOutException e) =>
e.message.contains('exceeded the timeout of 1 seconds')),
),
),
);
}
}, timeout: Timeout(Duration(seconds: 5)));
test('immediate timeout triggers', () async {
if (!Platform.isWindows) {
final process = factory.command(['sleep', '1']).withTimeout(0);
await expectLater(
process.run(),
throwsA(
allOf(
isA<ProcessTimedOutException>(),
predicate((ProcessTimedOutException e) =>
e.message.contains('exceeded the timeout of 0 seconds')),
),
),
);
}
});
test('string command is executed through shell', () async {
if (!Platform.isWindows) {
final result = await factory.command('echo "Hello from shell"').run();
expect(result.output().trim(), equals('Hello from shell'));
}
});
test('input as bytes is handled', () async {
final process =
factory.command(['cat']).withInput([72, 101, 108, 108, 111]);
final result = await process.run();
expect(result.output().trim(), equals('Hello'));
});
});
}

View file

@ -1,70 +0,0 @@
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
void main() {
group('ProcessResult', () {
test('successful process is detected correctly', () {
final result = ProcessResult(0, 'output', '');
expect(result.successful(), isTrue);
expect(result.failed(), isFalse);
});
test('failed process is detected correctly', () {
final result = ProcessResult(1, '', 'error');
expect(result.successful(), isFalse);
expect(result.failed(), isTrue);
});
test('output methods return correct streams', () {
final result = ProcessResult(0, 'stdout', 'stderr');
expect(result.output(), equals('stdout'));
expect(result.errorOutput(), equals('stderr'));
});
test('toString returns stdout', () {
final result = ProcessResult(0, 'test output', 'error output');
expect(result.toString(), equals('test output'));
});
test('empty output is handled correctly', () {
final result = ProcessResult(0, '', '');
expect(result.output(), isEmpty);
expect(result.errorOutput(), isEmpty);
});
test('exit code is accessible', () {
final result = ProcessResult(123, '', '');
expect(result.exitCode, equals(123));
});
test('multiline output is preserved', () {
final stdout = 'line1\nline2\nline3';
final stderr = 'error1\nerror2';
final result = ProcessResult(0, stdout, stderr);
expect(result.output(), equals(stdout));
expect(result.errorOutput(), equals(stderr));
});
test('whitespace in output is preserved', () {
final stdout = ' leading and trailing spaces ';
final result = ProcessResult(0, stdout, '');
expect(result.output(), equals(stdout));
});
test('non-zero exit code indicates failure', () {
for (var code in [1, 2, 127, 255]) {
final result = ProcessResult(code, '', '');
expect(result.failed(), isTrue,
reason: 'Exit code $code should indicate failure');
expect(result.successful(), isFalse,
reason: 'Exit code $code should not indicate success');
}
});
test('zero exit code indicates success', () {
final result = ProcessResult(0, '', '');
expect(result.successful(), isTrue);
expect(result.failed(), isFalse);
});
});
}

View file

@ -1,83 +1,7 @@
# Dart/Flutter # https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/ .dart_tool/
.packages
build/ # Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock pubspec.lock
.pub-cache/
.pub/
coverage/
.test_coverage.dart
*.freezed.dart
*.g.dart
*.mocks.dart
# IDE
.idea/
.vscode/
*.iml
*.iws
.DS_Store
.classpath
.project
.settings/
*.code-workspace
# Temporary files
*.swp
*.swo
*~
.directory
.fvm/
.env*
*.log
*.tmp
*.temp
*.bak
# Documentation
doc/api/
dartdoc_options.yaml
# Test artifacts
.test_coverage.dart
coverage/
test/.test_coverage.dart
test/coverage/
.test_runner.dart
# Build artifacts
*.dll
*.so
*.dylib
*.exe
*.o
*.out
# Platform-specific
# macOS
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Linux
.Trash-*
.nfs*
# Development
.dev/
.local/
node_modules/
.npm/
.history/
# Package specific
.process_tool/
.process_cache/

View file

@ -1,74 +0,0 @@
# Development files
.dart_tool/
.packages
.pub/
build/
coverage/
doc/
test/
tool/
# Documentation source
doc-src/
*.md
!README.md
!CHANGELOG.md
!LICENSE
# IDE files
.idea/
.vscode/
*.iml
*.ipr
*.iws
# Git files
.git/
.gitignore
.github/
# CI/CD
.travis.yml
.gitlab-ci.yml
.circleci/
.github/workflows/
# Development configuration
analysis_options.yaml
dartdoc_options.yaml
.test_config
.test_coverage.dart
# Examples and benchmarks
example/test/
benchmark/
performance/
# Temporary files
*.log
*.tmp
*.temp
*.swp
*.swo
*~
# Platform-specific
.DS_Store
Thumbs.db
# Generated files
*.freezed.dart
*.g.dart
*.mocks.dart
# Development tools
.fvm/
.dev/
.local/
node_modules/
.npm/
.history/
# Package specific
.process_tool/
.process_cache/

View file

@ -1,57 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2024-01-01
### Added
- Initial release with complete Laravel Process API implementation
- Core functionality:
- Process execution with fluent interface
- Process result handling and error management
- Environment and working directory configuration
- Input/output streaming and capture
- TTY mode support
- Timeout and idle timeout handling
- Process coordination:
- Process pools for concurrent execution
- Process piping for sequential execution
- Pool result aggregation and error handling
- Testing utilities:
- Process faking and recording
- Fake process sequences
- Process description builders
- Test helpers and assertions
- Documentation:
- Comprehensive API documentation
- Usage examples
- Testing guide
- Contributing guidelines
### Changed
- N/A (initial release)
### Deprecated
- N/A (initial release)
### Removed
- N/A (initial release)
### Fixed
- N/A (initial release)
### Security
- N/A (initial release)
## [0.1.0] - 2023-12-15
### Added
- Initial development version
- Basic process execution functionality
- Early testing utilities
- Preliminary documentation
Note: This pre-release version was used for internal testing and development.

View file

@ -1,120 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -1,135 +0,0 @@
# Contributing to Process
First off, thanks for taking the time to contribute! 🎉
## Code of Conduct
This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
## Getting Started
1. Fork the repository
2. Clone your fork
3. Create a new branch for your feature/fix
4. Make your changes
5. Run the tests
6. Submit a pull request
## Development Setup
1. Install Dart SDK (version >= 3.0.0)
2. Clone the repository
3. Run `dart pub get` to install dependencies
4. Run `dart test` to ensure everything is working
## Running Tests
```bash
# Run all tests
./tool/test.sh
# Run only unit tests
./tool/test.sh --unit
# Run tests with coverage
./tool/test.sh --coverage
# Run tests in watch mode
./tool/test.sh --watch
```
## Code Style
This project follows the official [Dart Style Guide](https://dart.dev/guides/language/effective-dart/style). Please ensure your code:
- Uses the standard Dart formatting (`dart format`)
- Passes static analysis (`dart analyze`)
- Includes documentation comments for public APIs
- Has appropriate test coverage
## Pull Request Process
1. Update the README.md with details of changes if needed
2. Update the CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
3. Update the version number following [Semantic Versioning](https://semver.org/)
4. Include tests for any new functionality
5. Ensure all tests pass
6. Update documentation as needed
## Writing Tests
- Write unit tests for all new functionality
- Include both success and failure cases
- Test edge cases and error conditions
- Use the provided testing utilities for process faking
- Aim for high test coverage
Example test:
```dart
void main() {
group('Process Execution', () {
late Factory factory;
setUp(() {
factory = Factory();
});
test('executes command successfully', () async {
factory.fake({
'test-command': FakeProcessDescription()
..withExitCode(0)
..replaceOutput('Test output'),
});
final result = await factory
.command('test-command')
.run();
expect(result.successful(), isTrue);
expect(result.output(), equals('Test output'));
});
});
}
```
## Documentation
- Document all public APIs
- Include examples in documentation comments
- Keep the README.md up to date
- Add inline comments for complex logic
## Reporting Issues
When reporting issues:
1. Use the issue template if provided
2. Include steps to reproduce
3. Include expected vs actual behavior
4. Include system information:
- Dart version
- Operating system
- Package version
5. Include any relevant error messages or logs
## Feature Requests
Feature requests are welcome! Please:
1. Check existing issues/PRs to avoid duplicates
2. Explain the use case
3. Provide examples of how the feature would work
4. Consider edge cases and potential issues
## Questions?
Feel free to:
- Open an issue for questions
- Ask in discussions
- Reach out to maintainers
## License
By contributing, you agree that your contributions will be licensed under the MIT License.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Platform Process Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,10 +0,0 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,207 +1,431 @@
# Process # Dart Process Handler
A fluent process execution package for Dart, inspired by Laravel's Process package. This package provides a powerful and intuitive API for running and managing system processes. A Laravel-inspired process handling library for Dart that provides an elegant and powerful API for executing shell commands and managing processes.
## Features ## Features
- 🔄 Fluent interface for process configuration and execution - Fluent API for process configuration
- 🚀 Process pools for concurrent execution - Synchronous and asynchronous execution
- 📝 Process piping for sequential execution - Process timeouts and idle timeouts
- 📊 Process output capturing and streaming - Working directory and environment variables
- 🌍 Process environment and working directory configuration - Input/output handling and streaming
- 📺 TTY mode support - Shell command support with pipes and redirects
- 🧪 Testing utilities with process faking and recording - TTY mode support
- ⏱️ Timeout and idle timeout support - Comprehensive error handling
- Real-time output callbacks
- Process status tracking and management
## Installation ## Installation
Add this to your package's pubspec.yaml file: Add this to your package's `pubspec.yaml` file:
```yaml ```yaml
dependencies: dependencies:
platform_process: ^1.0.0 platform_process: ^1.0.0
``` ```
## Usage ## Basic Usage
### Basic Process Execution ### Simple Command Execution
```dart ```dart
import 'package:platform_process/process.dart'; import 'package:platform_process/platform_process.dart';
final factory = Factory(); void main() async {
final factory = Factory();
// Simple command execution
final result = await factory // Basic command execution
.command('echo "Hello, World!"') final result = await factory.command(['echo', 'Hello World']).run();
.run(); print('Output: ${result.output()}'); // Output: Hello World
print(result.output()); print('Success: ${result.successful()}'); // Success: true
// With working directory and environment // Using string command (executed through shell)
final result = await factory final result2 = await factory.command('echo "Hello World"').run();
.command('npm install') print('Output: ${result2.output()}'); // Output: Hello World
.path('/path/to/project')
.env({'NODE_ENV': 'production'})
.run();
// With timeout
final result = await factory
.command('long-running-task')
.timeout(60) // 60 seconds
.run();
// Disable output
final result = await factory
.command('background-task')
.quietly()
.run();
```
### Process Pools
Run multiple processes concurrently:
```dart
final results = await factory.pool((pool) {
pool.command('task1');
pool.command('task2');
pool.command('task3');
}).start();
if (results.successful()) {
print('All processes completed successfully');
} }
``` ```
### Process Piping ### Working Directory
Run processes in sequence, piping output between them:
```dart ```dart
final result = await factory.pipeThrough((pipe) { void main() async {
pipe.command('cat file.txt'); final factory = Factory();
pipe.command('grep pattern');
pipe.command('wc -l'); // Execute command in specific directory
}).run(); final result = await factory
.command(['ls', '-l'])
print('Lines matching pattern: ${result.output()}'); .withWorkingDirectory('/tmp')
.run();
print('Files in /tmp:');
print(result.output());
}
``` ```
### Process Input ### Environment Variables
Provide input to processes:
```dart ```dart
final result = await factory void main() async {
.command('cat') final factory = Factory();
.input('Hello, World!')
.run(); final result = await factory
.command(['printenv', 'MY_VAR'])
.withEnvironment({'MY_VAR': 'Hello from env!'})
.run();
print('Environment Value: ${result.output()}');
}
```
### Process Timeouts
```dart
void main() async {
final factory = Factory();
try {
// Process timeout
await factory
.command(['sleep', '10'])
.withTimeout(5) // 5 second timeout
.run();
} on ProcessTimedOutException catch (e) {
print('Process timed out: ${e.message}');
}
try {
// Idle timeout (no output for specified duration)
await factory
.command(['tail', '-f', '/dev/null'])
.withIdleTimeout(5) // 5 second idle timeout
.run();
} on ProcessTimedOutException catch (e) {
print('Process idle timeout: ${e.message}');
}
}
```
### Standard Input
```dart
void main() async {
final factory = Factory();
// String input
final result1 = await factory
.command(['cat'])
.withInput('Hello from stdin!')
.run();
print('Input Echo: ${result1.output()}');
// Byte input
final result2 = await factory
.command(['cat'])
.withInput([72, 101, 108, 108, 111]) // "Hello" in bytes
.run();
print('Byte Input Echo: ${result2.output()}');
}
``` ```
### Error Handling ### Error Handling
Handle process failures: ```dart
void main() async {
final factory = Factory();
try {
await factory.command(['ls', 'nonexistent-file']).run();
} on ProcessFailedException catch (e) {
print('Command failed:');
print(' Exit code: ${e.result.exitCode}');
print(' Error output: ${e.result.errorOutput()}');
}
}
```
### Shell Commands with Pipes
```dart
void main() async {
final factory = Factory();
// Using pipes in shell command
final result = await factory
.command('echo "line1\nline2\nline3" | grep "line2"')
.run();
print('Grep Result: ${result.output()}');
// Multiple commands
final result2 = await factory
.command('cd /tmp && ls -l | grep "log"')
.run();
print('Log files: ${result2.output()}');
}
```
### Asynchronous Execution with Output Callback
```dart
void main() async {
final factory = Factory();
// Start process asynchronously
final process = await factory
.command(['sh', '-c', 'for i in 1 2 3; do echo $i; sleep 1; done'])
.start((output) {
print('Realtime Output: $output');
});
// Wait for completion
final result = await process.wait();
print('Final Exit Code: ${result.exitCode}');
}
```
### Process Management
```dart
void main() async {
final factory = Factory();
// Start long-running process
final process = await factory
.command(['sleep', '10'])
.start();
print('Process started with PID: ${process.pid}');
print('Is running: ${process.running()}');
// Kill the process
final killed = process.kill(); // Sends SIGTERM
print('Kill signal sent: $killed');
// Or with specific signal
process.kill(ProcessSignal.sigint); // Sends SIGINT
final result = await process.wait();
print('Process completed with exit code: ${result.exitCode}');
}
```
### Output Control
```dart
void main() async {
final factory = Factory();
// Disable output
final result = await factory
.command(['echo', 'test'])
.withoutOutput()
.run();
print('Output length: ${result.output().length}'); // Output length: 0
}
```
### TTY Mode
```dart
void main() async {
final factory = Factory();
// Enable TTY mode for commands that require it
final result = await factory
.command(['ls', '--color=auto'])
.withTty()
.run();
print('Color Output: ${result.output()}');
}
```
## Advanced Usage
### Custom Process Configuration
```dart
void main() async {
final factory = Factory();
final result = await factory
.command(['my-script'])
.withWorkingDirectory('/path/to/scripts')
.withEnvironment({
'NODE_ENV': 'production',
'DEBUG': 'true'
})
.withTimeout(30)
.withIdleTimeout(5)
.withTty()
.run();
if (result.successful()) {
print('Script completed successfully');
print(result.output());
}
}
```
### Process Pool Management
```dart
void main() async {
final factory = Factory();
final processes = <InvokedProcess>[];
// Start multiple processes
for (var i = 0; i < 3; i++) {
final process = await factory
.command(['worker.sh', i.toString()])
.start();
processes.add(process);
}
// Wait for all processes to complete
for (var process in processes) {
final result = await process.wait();
print('Worker completed with exit code: ${result.exitCode}');
}
}
```
### Error Output Handling
```dart
void main() async {
final factory = Factory();
try {
final result = await factory
.command(['some-command'])
.run();
print('Standard output:');
print(result.output());
print('Error output:');
print(result.errorOutput());
} on ProcessFailedException catch (e) {
print('Command failed with exit code: ${e.result.exitCode}');
print('Error details:');
print(e.result.errorOutput());
}
}
```
### Infinite Process Execution
```dart
void main() async {
final factory = Factory();
// Disable timeout for long-running processes
final process = await factory
.command(['tail', '-f', 'logfile.log'])
.forever() // Disables timeout
.start((output) {
print('New log entry: $output');
});
// Process will run until explicitly killed
await Future.delayed(Duration(minutes: 1));
process.kill();
}
```
## Error Handling
The library provides several exception types for different error scenarios:
### ProcessFailedException
Thrown when a process exits with a non-zero exit code:
```dart ```dart
try { try {
final result = await factory await factory.command(['nonexistent-command']).run();
.command('risky-command') } on ProcessFailedException catch (e) {
print('Command failed:');
print('Exit code: ${e.result.exitCode}');
print('Error output: ${e.result.errorOutput()}');
print('Standard output: ${e.result.output()}');
}
```
### ProcessTimedOutException
Thrown when a process exceeds its timeout or idle timeout:
```dart
try {
await factory
.command(['sleep', '10'])
.withTimeout(5)
.run(); .run();
} on ProcessTimedOutException catch (e) {
result.throwIfFailed((result, exception) { print('Process timed out:');
print('Process failed with output: ${result.errorOutput()}'); print('Message: ${e.message}');
}); if (e.result != null) {
} catch (e) { print('Partial output: ${e.result?.output()}');
print('Process failed: $e'); }
} }
``` ```
### Testing ## Best Practices
The package includes comprehensive testing utilities:
1. **Always handle process failures:**
```dart ```dart
// Fake specific commands try {
factory.fake({ await factory.command(['risky-command']).run();
'ls': 'file1.txt\nfile2.txt', } on ProcessFailedException catch (e) {
'cat file1.txt': 'Hello, World!', // Handle failure
}); } on ProcessTimedOutException catch (e) {
// Handle timeout
// Prevent real processes from running
factory.preventStrayProcesses();
// Record process executions
factory.fake();
final result = await factory.command('ls').run();
// Process execution is now recorded
// Use process sequences
final sequence = FakeProcessSequence.alternating(3);
while (sequence.hasMore) {
final result = sequence.call() as FakeProcessResult;
print('Success: ${result.successful()}, Output: ${result.output()}');
} }
``` ```
### Advanced Configuration 2. **Set appropriate timeouts:**
Configure process behavior:
```dart ```dart
final result = await factory factory
.command('complex-task') .command(['long-running-task'])
.path('/working/directory') .withTimeout(300) // Overall timeout
.env({'VAR1': 'value1', 'VAR2': 'value2'}) .withIdleTimeout(60) // Idle timeout
.timeout(120) .run();
.idleTimeout(30) ```
.tty()
.run((output) { 3. **Use output callbacks for long-running processes:**
print('Real-time output: $output'); ```dart
await factory
.command(['lengthy-task'])
.start((output) {
// Process output in real-time
print('Progress: $output');
}); });
``` ```
## API Reference 4. **Clean up resources:**
```dart
final process = await factory.command(['server']).start();
try {
// Do work
} finally {
process.kill(); // Ensure process is terminated
}
```
### Factory 5. **Use shell mode appropriately:**
```dart
// For simple commands, use array form:
factory.command(['echo', 'hello']);
The main entry point for creating and managing processes: // For shell features (pipes, redirects), use string form:
factory.command('echo hello | grep "o"');
- `command()` - Create a new process with a command ```
- `pool()` - Create a process pool for concurrent execution
- `pipeThrough()` - Create a process pipe for sequential execution
- `fake()` - Enable process faking for testing
- `preventStrayProcesses()` - Prevent real processes during testing
### PendingProcess
Configure process execution:
- `path()` - Set working directory
- `env()` - Set environment variables
- `timeout()` - Set execution timeout
- `idleTimeout()` - Set idle timeout
- `input()` - Provide process input
- `quietly()` - Disable output
- `tty()` - Enable TTY mode
- `run()` - Execute the process
- `start()` - Start the process in background
### ProcessResult
Access process results:
- `command()` - Get executed command
- `successful()` - Check if process succeeded
- `failed()` - Check if process failed
- `exitCode()` - Get exit code
- `output()` - Get standard output
- `errorOutput()` - Get error output
- `throwIfFailed()` - Throw exception on failure
## Contributing ## Contributing
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details. Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
## License ## License
This package is open-sourced software licensed under the [MIT license](LICENSE). This project is licensed under the MIT License - see the LICENSE file for details.

View file

@ -1,169 +0,0 @@
include: package:lints/recommended.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
todo: ignore
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "test/.test_coverage.dart"
- "build/**"
- ".dart_tool/**"
linter:
rules:
# Error rules
- always_declare_return_types
- always_put_required_named_parameters_first
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_dynamic_calls
- avoid_empty_else
- avoid_print
- avoid_relative_lib_imports
- avoid_returning_null_for_future
- avoid_slow_async_io
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_web_libraries_in_flutter
- cancel_subscriptions
- close_sinks
- comment_references
- control_flow_in_finally
- empty_statements
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- valid_regexps
# Style rules
- always_put_control_body_on_new_line
- avoid_bool_literals_in_conditional_expressions
- avoid_catches_without_on_clauses
- avoid_catching_errors
- avoid_classes_with_only_static_members
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_implementing_value_types
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_setters_without_getters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- avoid_void_async
- await_only_futures
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- no_runtimeType_toString
- non_constant_identifier_names
- null_closures
- omit_local_variable_types
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_mixin
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_pub_dependencies
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unawaited_futures
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_string_buffers
- use_to_and_as_if_applicable
- void_checks

View file

@ -1,54 +0,0 @@
# Test configuration for the process package
# Configure test timeout
timeout: 30s
# Configure test platforms
platforms: [vm]
# Configure test output
reporter: expanded
# Configure test paths
filename: "*_test.dart"
# Configure test concurrency
concurrency: 4
# Configure test tags
tags:
unit:
platform: [vm]
timeout: 30s
integration:
platform: [vm]
timeout: 60s
process:
platform: [vm]
timeout: 45s
# Configure test retry
retry: 2
# Configure test verbosity
verbose-trace: true
# Configure test paths
paths:
- test/all_tests.dart
- test/process_result_test.dart
- test/pending_process_test.dart
- test/factory_test.dart
- test/pool_test.dart
- test/pipe_test.dart
# Configure test output directory
temp_dir: .dart_tool/test
# Configure test warnings
warning:
unrecognized: error
missing: warning
# Configure test coverage
coverage: true

View file

View file

@ -1,177 +0,0 @@
# Process Coordination
The Process package provides powerful features for coordinating multiple processes through pools and pipes.
## Process Pools
Process pools allow you to run multiple processes concurrently and manage their execution.
### Basic Pool Usage
```dart
final results = await factory.pool((pool) {
pool.command('task1');
pool.command('task2');
pool.command('task3');
}).start();
if (results.successful()) {
print('All processes completed successfully');
}
```
### Pool Configuration
```dart
// Configure individual processes
final results = await factory.pool((pool) {
pool.command('task1').env({'TYPE': 'first'});
pool.command('task2').timeout(Duration(seconds: 30));
pool.command('task3').quietly();
}).start();
// Handle real-time output
await factory.pool((pool) {
pool.command('task1');
pool.command('task2');
}).start((output) {
print('Output: $output');
});
```
### Pool Results
```dart
final results = await factory.pool((pool) {
pool.command('succeed');
pool.command('fail');
}).start();
print('Total processes: ${results.total}');
print('Successful: ${results.successCount}');
print('Failed: ${results.failureCount}');
// Get specific results
for (final result in results.successes) {
print('Success: ${result.output()}');
}
for (final result in results.failures) {
print('Failure: ${result.errorOutput()}');
}
// Throw if any process failed
results.throwIfAnyFailed();
```
## Process Pipes
Process pipes enable sequential execution with output piping between processes.
### Basic Pipe Usage
```dart
final result = await factory.pipeThrough((pipe) {
pipe.command('echo "Hello, World!"');
pipe.command('tr "a-z" "A-Z"');
pipe.command('grep "HELLO"');
}).run();
print(result.output()); // Prints: HELLO, WORLD!
```
### Pipe Configuration
```dart
// Configure individual processes
final result = await factory.pipeThrough((pipe) {
pipe.command('cat file.txt')
.path('/data');
pipe.command('grep "pattern"')
.env({'LANG': 'C'});
pipe.command('wc -l')
.quietly();
}).run();
// Handle real-time output
await factory.pipeThrough((pipe) {
pipe.command('generate-data');
pipe.command('process-data');
}).run(output: (data) {
print('Processing: $data');
});
```
### Error Handling in Pipes
```dart
try {
final result = await factory.pipeThrough((pipe) {
pipe.command('may-fail');
pipe.command('never-reached-on-failure');
}).run();
result.throwIfFailed();
} catch (e) {
print('Pipe failed: $e');
}
```
## Best Practices
### Process Pools
1. Use pools for independent concurrent tasks
2. Configure appropriate timeouts for each process
3. Handle output appropriately (quiet noisy processes)
4. Consider resource limits when running many processes
5. Implement proper error handling for pool results
### Process Pipes
1. Use pipes for sequential data processing
2. Ensure each process handles input/output properly
3. Consider buffering for large data streams
4. Handle errors appropriately at each stage
5. Use real-time output handling for long pipelines
## Advanced Usage
### Combining Pools and Pipes
```dart
// Run multiple pipelines concurrently
await factory.pool((pool) {
pool.pipeThrough((pipe) {
pipe.command('pipeline1-step1');
pipe.command('pipeline1-step2');
});
pool.pipeThrough((pipe) {
pipe.command('pipeline2-step1');
pipe.command('pipeline2-step2');
});
}).start();
```
### Resource Management
```dart
// Limit concurrent processes
final pool = factory.pool((pool) {
for (var i = 0; i < 100; i++) {
pool.command('task$i');
}
}, maxProcesses: 10);
// Clean up resources
try {
await pool.start();
} finally {
pool.kill(); // Kill any remaining processes
}
```
For more information, see:
- [Process Execution](execution.md) for basic process management
- [Testing Utilities](testing.md) for testing process coordination

View file

@ -1,111 +0,0 @@
# Core Components
The Process package provides several core components for process management:
## Factory
The `Factory` class is the main entry point for creating and managing processes. It provides methods for:
- Creating new processes with `command()`
- Creating process pools with `pool()`
- Creating process pipes with `pipeThrough()`
- Setting up process faking for testing
Example:
```dart
final factory = Factory();
// Simple command execution
final result = await factory
.command('echo "Hello, World!"')
.run();
// With configuration
final result = await factory
.command('npm install')
.path('/path/to/project')
.env({'NODE_ENV': 'production'})
.run();
```
## PendingProcess
The `PendingProcess` class represents a process that has been configured but not yet started. It provides a fluent interface for:
- Setting working directory with `path()`
- Setting environment variables with `env()`
- Setting timeouts with `timeout()` and `idleTimeout()`
- Providing input with `input()`
- Controlling output with `quietly()`
- Enabling TTY mode with `tty()`
Example:
```dart
final process = factory
.command('long-running-task')
.path('/working/directory')
.env({'DEBUG': 'true'})
.timeout(60)
.idleTimeout(10)
.tty();
```
## ProcessResult
The `ProcessResult` class represents the result of a process execution, providing:
- Exit code access with `exitCode()`
- Output access with `output()` and `errorOutput()`
- Success/failure checking with `successful()` and `failed()`
- Error handling with `throwIfFailed()`
- Output searching with `seeInOutput()` and `seeInErrorOutput()`
Example:
```dart
final result = await process.run();
if (result.successful()) {
print('Output: ${result.output()}');
} else {
print('Error: ${result.errorOutput()}');
result.throwIfFailed();
}
```
## Error Handling
The package includes robust error handling through:
- `ProcessFailedException` for process execution failures
- Timeout handling for both overall execution and idle time
- Detailed error messages with command, exit code, and output
- Optional error callbacks for custom error handling
Example:
```dart
try {
await factory
.command('risky-command')
.run();
} catch (e) {
if (e is ProcessFailedException) {
print('Process failed with exit code: ${e.exitCode}');
print('Error output: ${e.errorOutput}');
}
}
```
## Best Practices
1. Always handle process failures appropriately
2. Use timeouts for long-running processes
3. Consider using `quietly()` for noisy processes
4. Clean up resources with proper error handling
5. Use environment variables for configuration
6. Set appropriate working directories
7. Consider TTY mode for interactive processes
For more details on specific components, see:
- [Process Execution](execution.md)
- [Process Coordination](coordination.md)
- [Testing Utilities](testing.md)

View file

@ -1,197 +0,0 @@
# Process Execution
The Process package provides comprehensive features for process execution and management.
## Basic Process Execution
The simplest way to execute a process is using the `Factory` class:
```dart
final factory = Factory();
final result = await factory.command('echo "Hello"').run();
```
## Process Configuration
### Working Directory
Set the working directory for process execution:
```dart
await factory
.command('npm install')
.path('/path/to/project')
.run();
```
### Environment Variables
Configure process environment:
```dart
await factory
.command('node app.js')
.env({
'NODE_ENV': 'production',
'PORT': '3000',
})
.run();
```
### Timeouts
Set execution and idle timeouts:
```dart
await factory
.command('long-task')
.timeout(Duration(minutes: 5)) // Total execution timeout
.idleTimeout(Duration(seconds: 30)) // Idle timeout
.run();
```
### Input/Output
Handle process input and output:
```dart
// Provide input
await factory
.command('cat')
.input('Hello, World!')
.run();
// Capture output in real-time
await factory
.command('long-task')
.run((output) {
print('Real-time output: $output');
});
// Suppress output
await factory
.command('noisy-task')
.quietly()
.run();
```
### TTY Mode
Enable TTY mode for interactive processes:
```dart
await factory
.command('interactive-script')
.tty()
.run();
```
## Process Lifecycle
### Starting Processes
```dart
// Run and wait for completion
final result = await factory.command('task').run();
// Start without waiting
final process = await factory.command('server').start();
```
### Monitoring Processes
```dart
final process = await factory.command('server').start();
// Get process ID
print('PID: ${process.pid}');
// Check if running
if (await process.isRunning()) {
print('Process is still running');
}
// Wait for completion
final result = await process.wait();
```
### Stopping Processes
```dart
// Kill process
process.kill();
// Kill with signal
process.kill(ProcessSignal.sigterm);
// Kill after timeout
await factory
.command('task')
.timeout(Duration(seconds: 30))
.run();
```
## Error Handling
### Basic Error Handling
```dart
try {
final result = await factory
.command('risky-command')
.run();
result.throwIfFailed();
} catch (e) {
print('Process failed: $e');
}
```
### Custom Error Handling
```dart
final result = await factory
.command('task')
.run();
result.throwIf(
result.exitCode() != 0 || result.seeInOutput('error'),
(result, exception) {
// Custom error handling
logError(result.errorOutput());
notifyAdmin(exception);
},
);
```
### Timeout Handling
```dart
try {
await factory
.command('slow-task')
.timeout(Duration(seconds: 5))
.run();
} catch (e) {
if (e is ProcessTimeoutException) {
print('Process timed out after ${e.duration.inSeconds} seconds');
}
}
```
## Best Practices
1. Always set appropriate timeouts for long-running processes
2. Handle process failures and timeouts gracefully
3. Use real-time output handling for long-running processes
4. Clean up resources properly
5. Consider using `quietly()` for processes with noisy output
6. Set working directory and environment variables explicitly
7. Use TTY mode when interaction is needed
8. Implement proper error handling and logging
9. Consider using process pools for concurrent execution
10. Use process pipes for sequential operations
For more information on advanced features, see:
- [Process Coordination](coordination.md) for pools and pipes
- [Testing Utilities](testing.md) for process faking and testing

View file

@ -1,228 +0,0 @@
# Testing Utilities
The Process package provides comprehensive testing utilities for process-dependent code.
## Process Faking
### Basic Faking
```dart
final factory = Factory();
// Fake specific commands
factory.fake({
'ls': 'file1.txt\nfile2.txt',
'cat file1.txt': 'Hello, World!',
'grep pattern': (process) => 'Matched line',
});
// Run fake processes
final result = await factory.command('ls').run();
expect(result.output().trim(), equals('file1.txt\nfile2.txt'));
```
### Preventing Real Processes
```dart
// Prevent any real process execution
factory.fake().preventStrayProcesses();
// This will throw an exception
await factory.command('real-command').run();
```
### Dynamic Results
```dart
factory.fake({
'random': (process) =>
DateTime.now().millisecondsSinceEpoch.toString(),
'conditional': (process) =>
process.env['SUCCESS'] == 'true' ? 'success' : 'failure',
});
```
## Process Descriptions
### Basic Description
```dart
final description = FakeProcessDescription()
..withExitCode(0)
..replaceOutput('Test output')
..replaceErrorOutput('Test error');
factory.fake({
'test-command': description,
});
```
### Simulating Long-Running Processes
```dart
final description = FakeProcessDescription()
..withOutputSequence(['Step 1', 'Step 2', 'Step 3'])
..withDelay(Duration(milliseconds: 100))
..runsFor(duration: Duration(seconds: 1));
factory.fake({
'long-task': description,
});
```
### Simulating Process Failures
```dart
final description = FakeProcessDescription()
..withExitCode(1)
..replaceOutput('Operation failed')
..replaceErrorOutput('Error: Invalid input');
factory.fake({
'failing-task': description,
});
```
## Process Sequences
### Basic Sequences
```dart
final sequence = FakeProcessSequence()
..then(FakeProcessResult(output: 'First'))
..then(FakeProcessResult(output: 'Second'))
..then(FakeProcessResult(output: 'Third'));
factory.fake({
'sequential-task': sequence,
});
```
### Alternating Success/Failure
```dart
final sequence = FakeProcessSequence.alternating(3);
while (sequence.hasMore) {
final result = sequence.call() as FakeProcessResult;
print('Success: ${result.successful()}');
}
```
### Custom Sequences
```dart
final sequence = FakeProcessSequence.fromOutputs([
'Starting...',
'Processing...',
'Complete!',
]);
factory.fake({
'progress-task': sequence,
});
```
## Testing Process Pools
```dart
test('executes processes concurrently', () async {
factory.fake({
'task1': FakeProcessDescription()
..withDelay(Duration(seconds: 1))
..replaceOutput('Result 1'),
'task2': FakeProcessDescription()
..withDelay(Duration(seconds: 1))
..replaceOutput('Result 2'),
});
final results = await factory.pool((pool) {
pool.command('task1');
pool.command('task2');
}).start();
expect(results.successful(), isTrue);
expect(results.total, equals(2));
});
```
## Testing Process Pipes
```dart
test('pipes output between processes', () async {
factory.fake({
'generate': 'initial data',
'transform': (process) => process.input.toUpperCase(),
'filter': (process) => process.input.contains('DATA') ? process.input : '',
});
final result = await factory.pipeThrough((pipe) {
pipe.command('generate');
pipe.command('transform');
pipe.command('filter');
}).run();
expect(result.output(), equals('INITIAL DATA'));
});
```
## Best Practices
1. Use `preventStrayProcesses()` in tests to catch unintended process execution
2. Simulate realistic scenarios with delays and sequences
3. Test both success and failure cases
4. Test process configuration (environment, working directory, etc.)
5. Test process coordination (pools and pipes)
6. Use process descriptions for complex behaviors
7. Test timeout and error handling
8. Mock system-specific behaviors
9. Clean up resources in tests
10. Test real-time output handling
## Example Test Suite
```dart
void main() {
group('Process Manager', () {
late Factory factory;
setUp(() {
factory = Factory();
factory.fake().preventStrayProcesses();
});
test('handles successful process', () async {
factory.fake({
'successful-task': FakeProcessDescription()
..withExitCode(0)
..replaceOutput('Success!'),
});
final result = await factory
.command('successful-task')
.run();
expect(result.successful(), isTrue);
expect(result.output(), equals('Success!'));
});
test('handles process failure', () async {
factory.fake({
'failing-task': FakeProcessDescription()
..withExitCode(1)
..replaceErrorOutput('Failed!'),
});
final result = await factory
.command('failing-task')
.run();
expect(result.failed(), isTrue);
expect(result.errorOutput(), equals('Failed!'));
});
});
}
```
For more information, see:
- [Process Execution](execution.md) for basic process management
- [Process Coordination](coordination.md) for pools and pipes

View file

@ -1,88 +0,0 @@
# Process Package Examples
This directory contains examples demonstrating the usage of the Process package.
## Running the Examples
```bash
# Get dependencies
dart pub get
# Run the example
dart run example.dart
```
## Examples Included
1. **Basic Process Execution**
- Simple command execution with `echo`
- Output capturing and handling
- Basic process configuration
2. **Process Configuration**
- Working directory configuration with `path()`
- Environment variables with `env()`
- Output suppression with `quietly()`
- Process timeouts and idle timeouts
3. **Process Pool**
- Concurrent process execution
- Pool result handling
- Real-time output capturing
- Process coordination
4. **Process Pipe**
- Sequential process execution
- Output piping between processes
- Command chaining
- Error handling in pipelines
5. **Error Handling**
- Process failure handling
- Exception catching and handling
- Error output capturing
- Custom error callbacks
6. **Testing**
- Process faking with `fake()`
- Output sequence simulation
- Timing simulation
- Process behavior mocking
## Additional Examples
For more specific examples, see:
- [Process Execution](../doc/execution.md) - Detailed process execution examples
- [Process Coordination](../doc/coordination.md) - Advanced pool and pipe examples
- [Testing Utilities](../doc/testing.md) - Comprehensive testing examples
## Notes
- Some examples require specific system commands (`ls`, `sort`, `uniq`). These commands are commonly available on Unix-like systems.
- Error handling examples intentionally demonstrate failure cases.
- The testing examples show how to use the package's testing utilities in your own tests.
- Process pools demonstrate concurrent execution - actual execution order may vary.
- Process pipes demonstrate sequential execution - output flows from one process to the next.
## System Requirements
- Dart SDK >= 3.0.0
- Unix-like system for some examples (Linux, macOS)
- Basic system commands (`echo`, `ls`, etc.)
## Best Practices Demonstrated
1. Always handle process errors appropriately
2. Use timeouts for long-running processes
3. Configure working directories explicitly
4. Set environment variables when needed
5. Use `quietly()` for noisy processes
6. Clean up resources properly
7. Test process-dependent code thoroughly
## Further Reading
- [Package Documentation](../README.md)
- [API Reference](https://pub.dev/documentation/platform_process)
- [Contributing Guide](../CONTRIBUTING.md)

View file

@ -1,81 +1,151 @@
import 'package:platform_process/process.dart'; import 'dart:async';
import 'package:platform_process/platform_process.dart';
Future<void> main() async { Future<void> runExamples() async {
// Create a process factory // Create a process factory
final factory = Factory(); final factory = Factory();
// Basic Process Execution // Basic command execution
print('\nBasic Process Execution:'); print('\n=== Basic Command Execution ===');
print('----------------------');
final result = await factory.command('echo "Hello, World!"').run();
print('Output: ${result.output().trim()}');
// Process with Configuration
print('\nConfigured Process:');
print('------------------');
final configuredResult = await factory
.command('ls')
.path('/tmp')
.env({'LANG': 'en_US.UTF-8'})
.quietly()
.run();
print('Files: ${configuredResult.output().trim()}');
// Process Pool Example
print('\nProcess Pool:');
print('-------------');
final poolResults = await factory.pool((pool) {
pool.command('sleep 1 && echo "First"');
pool.command('sleep 2 && echo "Second"');
pool.command('sleep 3 && echo "Third"');
}).start();
print('Pool results:');
for (final result in poolResults) {
print('- ${result.output().trim()}');
}
// Process Pipe Example
print('\nProcess Pipe:');
print('-------------');
final pipeResult = await factory.pipeThrough((pipe) {
pipe.command('echo "hello\nworld\nhello\ntest"');
pipe.command('sort');
pipe.command('uniq -c');
}).run();
print('Pipe result:');
print(pipeResult.output().trim());
// Error Handling Example
print('\nError Handling:');
print('---------------');
try { try {
await factory.command('nonexistent-command').run(); final result = await factory.command(['echo', 'Hello, World!']).run();
print('Output: ${result.output().trim()}');
print('Exit Code: ${result.exitCode}');
print('Success: ${result.successful()}');
} catch (e) { } catch (e) {
print('Error caught: $e'); print('Error: $e');
} }
// Testing Example // Working directory
print('\nTesting Example:'); print('\n=== Working Directory ===');
print('---------------'); try {
final result =
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
print('Current Directory: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
factory.fake({ // Environment variables
'test-command': FakeProcessDescription() print('\n=== Environment Variables ===');
..withExitCode(0) try {
..replaceOutput('Fake output') final result = await factory
..withOutputSequence(['Step 1', 'Step 2', 'Step 3']) .command(['sh', '-c', 'echo \$CUSTOM_VAR']).withEnvironment(
..runsFor(duration: Duration(seconds: 1)), {'CUSTOM_VAR': 'Hello from env!'}).run();
}); print('Environment Value: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
final testResult = await factory // Process timeout
.command('test-command') print('\n=== Process Timeout ===');
.run((output) => print('Real-time output: $output')); try {
await factory.command(['sleep', '5']).withTimeout(1).run();
print('Process completed (unexpected)');
} catch (e) {
// Let the zone handler catch this
}
print('Test result: ${testResult.output().trim()}'); // Standard input
print('\n=== Standard Input ===');
try {
final result =
await factory.command(['cat']).withInput('Hello from stdin!').run();
print('Input Echo: ${result.output()}');
} catch (e) {
print('Error: $e');
}
// Error handling
print('\n=== Error Handling ===');
try {
await factory.command(['ls', 'nonexistent-file']).run();
print('Command succeeded (unexpected)');
} on ProcessFailedException catch (e) {
print('Expected error:');
print(' Exit code: ${e.exitCode}');
print(' Error output: ${e.errorOutput.trim()}');
} catch (e) {
print('Unexpected error: $e');
}
// Shell commands with pipes
print('\n=== Shell Commands with Pipes ===');
try {
final result = await factory.command(
['sh', '-c', 'echo "line1\nline2\nline3" | grep "line2"']).run();
print('Grep Result: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
// Async process with output callback
print('\n=== Async Process with Output Callback ===');
try {
final process = await factory.command([
'sh',
'-c',
'for n in 1 2 3; do echo \$n; sleep 1; done'
]).start((output) {
print('Realtime Output: ${output.trim()}');
});
final result = await process.wait();
print('Final Exit Code: ${result.exitCode}');
} catch (e) {
print('Error: $e');
}
// Process killing
print('\n=== Process Killing ===');
try {
final process = await factory.command(['sleep', '10']).start();
print('Process started with PID: ${process.pid}');
print('Is running: ${process.running()}');
// Kill after 1 second
await Future.delayed(Duration(seconds: 1));
final killed = process.kill();
print('Kill signal sent: $killed');
final result = await process.wait();
print('Process completed with exit code: ${result.exitCode}');
} catch (e) {
print('Error: $e');
}
// Quiet mode (no output)
print('\n=== Quiet Mode ===');
try {
final result = await factory
.command(['echo', 'This output is suppressed'])
.withoutOutput()
.run();
print('Output length: ${result.output().length}');
} catch (e) {
print('Error: $e');
}
// Color output (alternative to TTY mode)
print('\n=== Color Output ===');
try {
final result = await factory.command(['ls', '--color=always']).run();
print('Color Output: ${result.output().trim()}');
} catch (e) {
print('Error: $e');
}
}
void main() {
runZonedGuarded(() async {
await runExamples();
}, (error, stack) {
if (error is ProcessTimedOutException) {
print('Expected timeout error: ${error.message}');
} else {
print('Unexpected error: $error');
print('Stack trace: $stack');
}
});
} }

View file

@ -1,110 +0,0 @@
import 'package:platform_process/process.dart';
Future<void> main() async {
// Create a process factory
final factory = Factory();
print('Basic Process Execution:');
print('----------------------');
// Simple process execution
final result = await factory.command('echo "Hello, World!"').run();
print('Output: ${result.output().trim()}\n');
// Process with configuration
final configuredResult = await factory
.command('ls')
.path('/tmp')
.env({'LANG': 'en_US.UTF-8'})
.quietly()
.run();
print('Files in /tmp: ${configuredResult.output().trim()}\n');
print('Process Pool Example:');
print('-------------------');
// Process pool for concurrent execution
final poolResults = await factory.pool((pool) {
pool.command('sleep 1 && echo "First"');
pool.command('sleep 2 && echo "Second"');
pool.command('sleep 3 && echo "Third"');
}).start();
print('Pool results:');
for (final result in poolResults) {
print('- ${result.output().trim()}');
}
print('');
print('Process Pipe Example:');
print('-------------------');
// Process pipe for sequential execution
final pipeResult = await factory.pipeThrough((pipe) {
pipe.command('echo "hello\nworld\nhello\ntest"'); // Create some sample text
pipe.command('sort'); // Sort the lines
pipe.command('uniq -c'); // Count unique lines
pipe.command('sort -nr'); // Sort by count
}).run();
print('Pipe result:');
print(pipeResult.output());
print('');
print('Process Testing Example:');
print('----------------------');
// Set up fake processes for testing
factory.fake({
'ls': 'file1.txt\nfile2.txt',
'cat file1.txt': 'Hello from file1!',
'grep pattern': (process) => 'Matched line',
});
// Run fake processes
final fakeResult = await factory.command('ls').run();
print('Fake ls output: ${fakeResult.output().trim()}');
final catResult = await factory.command('cat file1.txt').run();
print('Fake cat output: ${catResult.output().trim()}');
// Process sequence example
final sequence = FakeProcessSequence.alternating(3);
print('\nProcess sequence results:');
while (sequence.hasMore) {
final result = sequence.call() as FakeProcessResult;
print('- Success: ${result.successful()}, Output: ${result.output()}');
}
print('\nProcess Error Handling:');
print('----------------------');
try {
await factory.command('nonexistent-command').run();
} catch (e) {
print('Error caught: $e');
}
// Clean up
print('\nDone!');
}
/// Example of testing process execution
void testProcessExecution() {
final factory = Factory();
// Configure fake processes
factory.fake({
'test-command': FakeProcessDescription()
..withExitCode(0)
..replaceOutput('Test output')
..withOutputSequence(['Line 1', 'Line 2', 'Line 3'])
..runsFor(duration: Duration(seconds: 1)),
});
// Prevent real process execution during tests
factory.preventStrayProcesses();
// Now you can test your process-dependent code
// without actually executing any real processes
}

View file

@ -1,134 +0,0 @@
import 'package:platform_process/process.dart';
/// This file contains practical examples of using the Process package
/// for various common scenarios in real-world applications.
Future<void> main() async {
final factory = Factory();
// Example 1: Building a Project
print('\n=== Building a Project ===');
await buildProject(factory);
// Example 2: Database Backup
print('\n=== Database Backup ===');
await backupDatabase(factory);
// Example 3: Log Processing Pipeline
print('\n=== Log Processing ===');
await processLogs(factory);
// Example 4: Concurrent File Processing
print('\n=== Concurrent Processing ===');
await processConcurrently(factory);
// Example 5: Interactive Process
print('\n=== Interactive Process ===');
await runInteractiveProcess(factory);
}
/// Example 1: Building a project with environment configuration
Future<void> buildProject(Factory factory) async {
try {
final result = await factory
.command('npm run build')
.env({
'NODE_ENV': 'production',
'BUILD_NUMBER': '123',
})
.timeout(300) // 5 minutes timeout
.run((output) {
// Real-time build output handling
print('Build output: $output');
});
if (result.successful()) {
print('Build completed successfully');
}
} catch (e) {
print('Build failed: $e');
}
}
/// Example 2: Creating a database backup with error handling
Future<void> backupDatabase(Factory factory) async {
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
final backupFile = 'backup-$timestamp.sql';
try {
final result = await factory
.command('pg_dump -U postgres mydatabase > $backupFile')
.env({'PGPASSWORD': 'secret'})
.quietly() // Suppress normal output
.timeout(120) // 2 minutes timeout
.run();
result.throwIfFailed((result, exception) {
print('Backup failed with error: ${result.errorOutput()}');
});
print('Database backup created: $backupFile');
} catch (e) {
print('Backup process failed: $e');
}
}
/// Example 3: Processing logs using pipes
Future<void> processLogs(Factory factory) async {
try {
final result = await factory.pipeThrough((pipe) {
// Read logs
pipe.command('cat /var/log/app.log');
// Filter errors
pipe.command('grep ERROR');
// Count occurrences
pipe.command('wc -l');
}).run();
print('Number of errors in log: ${result.output().trim()}');
} catch (e) {
print('Log processing failed: $e');
}
}
/// Example 4: Processing multiple files concurrently
Future<void> processConcurrently(Factory factory) async {
final files = ['file1.txt', 'file2.txt', 'file3.txt'];
final results = ProcessPoolResults(await factory.pool((pool) {
for (final file in files) {
// Process each file concurrently
pool.command('process_file.sh $file');
}
}).start());
if (results.successful()) {
print('All files processed successfully');
} else {
print('Some files failed to process');
for (final result in results.results.where((r) => r.failed())) {
print('Failed command: ${result.command()}');
print('Error: ${result.errorOutput()}');
}
}
}
/// Example 5: Running an interactive process
Future<void> runInteractiveProcess(Factory factory) async {
try {
final result = await factory
.command('python')
.tty() // Enable TTY mode for interactive processes
.input('''
print("Hello from Python!")
name = input("What's your name? ")
print(f"Nice to meet you, {name}!")
exit()
''').run();
print('Interactive process output:');
print(result.output());
} catch (e) {
print('Interactive process failed: $e');
}
}

View file

@ -1,83 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
import 'process_examples.dart';
void main() {
late Factory factory;
setUp(() {
factory = Factory();
factory.fake(); // Enable process faking
factory.preventStrayProcesses(); // Prevent real processes from running
});
group('Process Examples Tests', () {
test('buildProject handles successful build', () async {
// Fake successful build process
factory.fake({
'npm run build': '''
Creating production build...
Assets optimized
Build completed successfully
''',
});
await buildProject(factory);
expect(factory.isRecording(), isTrue);
});
test('backupDatabase creates backup with correct filename pattern',
() async {
factory.fake({
'pg_dump -U postgres mydatabase > backup-*':
'', // Match any backup filename
});
await backupDatabase(factory);
expect(factory.isRecording(), isTrue);
});
test('processLogs correctly pipes commands', () async {
factory.fake({
'cat /var/log/app.log': '''
2024-01-01 INFO: System started
2024-01-01 ERROR: Database connection failed
2024-01-01 ERROR: Retry attempt failed
2024-01-01 INFO: Backup completed
''',
'grep ERROR': '''
2024-01-01 ERROR: Database connection failed
2024-01-01 ERROR: Retry attempt failed
''',
'wc -l': '2\n',
});
await processLogs(factory);
expect(factory.isRecording(), isTrue);
});
test('processConcurrently handles multiple files', () async {
// Fake successful processing for all files
factory.fake({
'process_file.sh file1.txt': 'Processing file1.txt completed',
'process_file.sh file2.txt': 'Processing file2.txt completed',
'process_file.sh file3.txt': 'Processing file3.txt completed',
});
await processConcurrently(factory);
expect(factory.isRecording(), isTrue);
});
test('runInteractiveProcess handles Python interaction', () async {
factory.fake({
'python': '''
Hello from Python!
What's your name? Nice to meet you, Test User!
''',
});
await runInteractiveProcess(factory);
expect(factory.isRecording(), isTrue);
});
});
}

View file

@ -1,15 +0,0 @@
name: platform_process_example
description: Examples demonstrating the platform_process package usage
version: 1.0.0
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
platform_process:
path: ../
dev_dependencies:
test: ^1.24.0
lints: ^3.0.0

View file

@ -1,5 +1,5 @@
/// A Laravel-compatible process management implementation in pure Dart. /// A Laravel-compatible process management implementation in pure Dart.
library test_process; library platform_process;
export 'src/pending_process.dart'; export 'src/pending_process.dart';
export 'src/process_result.dart'; export 'src/process_result.dart';

View file

@ -1,40 +0,0 @@
/// Process management package for Dart.
///
/// This package provides a fluent interface for working with processes in Dart,
/// similar to Laravel's Process package. It offers:
///
/// - Process execution with timeouts and idle timeouts
/// - Process pools for concurrent execution
/// - Process piping for sequential execution
/// - Process output capturing and streaming
/// - Process environment and working directory configuration
/// - TTY mode support
/// - Testing utilities with process faking and recording
library process;
// Core functionality
export 'src/contracts/process_result.dart';
export 'src/exceptions/process_failed_exception.dart';
export 'src/factory.dart';
export 'src/pending_process.dart';
export 'src/process_result.dart';
// Process execution
export 'src/invoked_process.dart';
export 'src/invoked_process_pool.dart';
// Process coordination
export 'src/pipe.dart';
export 'src/pool.dart' hide ProcessPoolResults;
// Process results
export 'src/process_pool_results.dart';
// Testing utilities
export 'src/fake_invoked_process.dart';
export 'src/fake_process_description.dart';
export 'src/fake_process_result.dart';
export 'src/fake_process_sequence.dart';
// Re-export common types
export 'dart:io' show ProcessSignal;

View file

@ -1,40 +0,0 @@
//import 'package:platform_contracts/contracts.dart';
/// Contract for process execution results.
abstract class ProcessResult {
/// Get the original command executed by the process.
String command();
/// Determine if the process was successful.
bool successful();
/// Determine if the process failed.
bool failed();
/// Get the exit code of the process.
int? exitCode();
/// Get the standard output of the process.
String output();
/// Determine if the output contains the given string.
bool seeInOutput(String output);
/// Get the error output of the process.
String errorOutput();
/// Determine if the error output contains the given string.
bool seeInErrorOutput(String output);
/// Throw an exception if the process failed.
///
/// Returns this instance for method chaining.
ProcessResult throwIfFailed(
[void Function(ProcessResult, Exception)? callback]);
/// Throw an exception if the process failed and the given condition is true.
///
/// Returns this instance for method chaining.
ProcessResult throwIf(bool condition,
[void Function(ProcessResult, Exception)? callback]);
}

View file

@ -1,48 +1,42 @@
import '../contracts/process_result.dart'; import '../process_result.dart';
/// Exception thrown when a process fails. /// Exception thrown when a process fails.
class ProcessFailedException implements Exception { class ProcessFailedException implements Exception {
/// The process result that caused this exception. /// The process result.
final ProcessResult result; final ProcessResult _result;
/// Create a new process failed exception instance. /// Create a new process failed exception instance.
ProcessFailedException(this.result); ProcessFailedException(this._result);
/// Get the process result.
ProcessResult get result => _result;
/// Get the process exit code.
int get exitCode => _result.exitCode;
/// Get the process output.
String get output => _result.output();
/// Get the process error output.
String get errorOutput => _result.errorOutput();
@override @override
String toString() { String toString() {
return ''' final buffer =
The process "${result.command()}" failed with exit code ${result.exitCode()}. StringBuffer('Process failed with exit code: ${_result.exitCode}');
Output: if (_result.output().isNotEmpty) {
${result.output().isEmpty ? '(empty)' : result.output()} buffer.writeln();
buffer.writeln('Output:');
buffer.writeln(_result.output().trim());
}
Error Output: if (_result.errorOutput().isNotEmpty) {
${result.errorOutput().isEmpty ? '(empty)' : result.errorOutput()} buffer.writeln();
'''; buffer.writeln('Error Output:');
} buffer.writeln(_result.errorOutput().trim());
} }
/// Exception thrown when a process times out. return buffer.toString();
class ProcessTimeoutException implements Exception {
/// The process result that caused this exception.
final ProcessResult result;
/// The timeout duration that was exceeded.
final Duration timeout;
/// Create a new process timeout exception instance.
ProcessTimeoutException(this.result, this.timeout);
@override
String toString() {
return '''
The process "${result.command()}" timed out after ${timeout.inSeconds} seconds.
Output:
${result.output().isEmpty ? '(empty)' : result.output()}
Error Output:
${result.errorOutput().isEmpty ? '(empty)' : result.errorOutput()}
''';
} }
} }

View file

@ -1,152 +1,34 @@
import 'dart:async';
import 'traits/macroable.dart';
import 'pending_process.dart'; import 'pending_process.dart';
import 'contracts/process_result.dart';
import 'process_result.dart';
import 'pool.dart';
import 'pipe.dart';
/// Factory for creating and managing processes. /// A factory for creating process instances.
class Factory with Macroable { class Factory {
/// Indicates if the process factory is recording processes. /// Create a new factory instance.
bool _recording = false; Factory();
/// All of the recorded processes. /// Create a new pending process instance with the given command.
final List<List<dynamic>> _recorded = [];
/// The registered fake handler callbacks.
final Map<String, Function> _fakeHandlers = {};
/// Indicates that an exception should be thrown if any process is not faked.
bool _preventStrayProcesses = false;
/// Create a new pending process instance.
PendingProcess newPendingProcess() {
return PendingProcess();
}
/// Create a new process instance and configure it.
PendingProcess command(dynamic command) { PendingProcess command(dynamic command) {
return newPendingProcess().command(command); if (command == null) {
} throw ArgumentError('Command cannot be null');
/// Start defining a pool of processes.
Pool pool(void Function(Pool) callback) {
return Pool(this, callback);
}
/// Start defining a series of piped processes.
Pipe pipeThrough(void Function(Pipe) callback) {
return Pipe(this, callback);
}
/// Run a pool of processes concurrently.
Future<List<ProcessResult>> concurrently(
List<PendingProcess> processes, {
void Function(String)? onOutput,
}) async {
// Run all processes concurrently and wait for all to complete
final futures = processes.map((process) async {
final result = await process.run(onOutput);
if (onOutput != null) {
final output = result.output().trim();
if (output.isNotEmpty) {
onOutput(output);
}
}
return result;
});
return Future.wait(futures);
}
/// Run a series of processes in sequence.
Future<ProcessResult> pipe(
List<PendingProcess> processes, {
void Function(String)? onOutput,
}) async {
if (processes.isEmpty) {
return ProcessResultImpl(
command: '',
exitCode: 0,
output: '',
errorOutput: '',
);
} }
ProcessResult? result; if (command is String && command.trim().isEmpty) {
for (final process in processes) { throw ArgumentError('Command string cannot be empty');
result = await process.run(onOutput);
if (result.failed()) {
return result;
}
} }
return result!;
}
/// Indicate that the process factory should fake processes. if (command is List) {
Factory fake([Map<String, dynamic>? fakes]) { if (command.isEmpty) {
_recording = true; throw ArgumentError('Command list cannot be empty');
}
if (fakes != null) { if (command.any((element) => element is! String)) {
for (final entry in fakes.entries) { throw ArgumentError('Command list must contain only strings');
if (entry.value is Function) {
_fakeHandlers[entry.key] = entry.value as Function;
} else {
_fakeHandlers[entry.key] = (_) => entry.value;
}
} }
} }
return this; if (command is! String && command is! List) {
} throw ArgumentError('Command must be a string or list of strings');
/// Record the given process if processes should be recorded.
void recordIfRecording(PendingProcess process, ProcessResult result) {
if (_recording) {
_recorded.add([process, result]);
} }
}
/// Indicate that an exception should be thrown if any process is not faked. return PendingProcess(this)..withCommand(command);
Factory preventStrayProcesses([bool prevent = true]) {
_preventStrayProcesses = prevent;
return this;
}
/// Determine if stray processes are being prevented.
bool preventingStrayProcesses() => _preventStrayProcesses;
/// Determine if the factory is recording processes.
bool isRecording() => _recording;
/// Get the fake handler for the given command.
Function? fakeFor(String command) {
for (final entry in _fakeHandlers.entries) {
if (entry.key == '*' || command.contains(entry.key)) {
return entry.value;
}
}
return null;
}
/// Run a pool of processes and wait for them to finish executing.
Future<ProcessPoolResults> runPool(void Function(Pool) callback,
{void Function(String)? output}) async {
return ProcessPoolResults(await pool(callback).start(output));
}
/// Run a series of piped processes and wait for them to finish executing.
Future<ProcessResult> runPipe(void Function(Pipe) callback,
{void Function(String)? output}) async {
return pipeThrough(callback).run(output: output);
}
/// Dynamically handle method calls.
@override
dynamic noSuchMethod(Invocation invocation) {
if (invocation.isMethod) {
return newPendingProcess().noSuchMethod(invocation);
}
return super.noSuchMethod(invocation);
} }
} }

View file

@ -1,60 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'contracts/process_result.dart';
import 'process_result.dart';
import 'fake_process_description.dart';
/// Represents a fake invoked process for testing.
class FakeInvokedProcess {
/// The command that was executed.
final String command;
/// The process description.
final FakeProcessDescription description;
/// The output handler.
void Function(String)? _outputHandler;
/// Create a new fake invoked process instance.
FakeInvokedProcess(this.command, this.description);
/// Set the output handler.
FakeInvokedProcess withOutputHandler(void Function(String)? handler) {
_outputHandler = handler;
return this;
}
/// Get the process ID.
int get pid => description.pid;
/// Kill the process.
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
return description.kill(signal);
}
/// Get the process exit code.
Future<int> get exitCode => description.exitCodeFuture;
/// Get the predicted process result.
ProcessResult predictProcessResult() {
return ProcessResultImpl(
command: command,
exitCode: description.predictedExitCode,
output: description.predictedOutput,
errorOutput: description.predictedErrorOutput,
);
}
/// Wait for the process to complete.
Future<ProcessResult> wait() async {
if (_outputHandler != null) {
for (final output in description.outputSequence) {
_outputHandler!(output);
await description.delay;
}
}
await description.runDuration;
return predictProcessResult();
}
}

View file

@ -1,116 +0,0 @@
import 'dart:async';
import 'dart:io';
/// Describes how a fake process should behave.
class FakeProcessDescription {
/// The process ID.
final int pid = DateTime.now().millisecondsSinceEpoch;
/// The predicted exit code.
int _exitCode = 0;
/// The predicted output.
String _output = '';
/// The predicted error output.
String _errorOutput = '';
/// The sequence of outputs.
final List<String> _outputSequence = [];
/// The delay between outputs.
Duration _delay = const Duration(milliseconds: 100);
/// The total run duration.
Duration _runDuration = Duration.zero;
/// Whether the process was killed.
bool _wasKilled = false;
/// Create a new fake process description instance.
FakeProcessDescription();
/// Get the predicted exit code.
int get predictedExitCode => _wasKilled ? -1 : _exitCode;
/// Get the predicted output.
String get predictedOutput => _output;
/// Get the predicted error output.
String get predictedErrorOutput => _errorOutput;
/// Get the output sequence.
List<String> get outputSequence => List.unmodifiable(_outputSequence);
/// Get the delay between outputs.
Duration get delay => _delay;
/// Get the total run duration.
Duration get runDuration => _runDuration;
/// Set the exit code.
FakeProcessDescription withExitCode(int code) {
_exitCode = code;
return this;
}
/// Replace the output.
FakeProcessDescription replaceOutput(String output) {
_output = output;
return this;
}
/// Replace the error output.
FakeProcessDescription replaceErrorOutput(String output) {
_errorOutput = output;
return this;
}
/// Set the output sequence.
FakeProcessDescription withOutputSequence(List<String> sequence) {
_outputSequence.clear();
_outputSequence.addAll(sequence);
return this;
}
/// Set the delay between outputs.
FakeProcessDescription withDelay(Duration delay) {
_delay = delay;
return this;
}
/// Configure how long the process should run.
FakeProcessDescription runsFor({
Duration? duration,
int? iterations,
}) {
if (duration != null) {
_runDuration = duration;
} else if (iterations != null) {
_runDuration = _delay * iterations;
}
return this;
}
/// Kill the process.
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
_wasKilled = true;
return true;
}
/// Get the process exit code future.
Future<int> get exitCodeFuture async {
await Future.delayed(_runDuration);
return predictedExitCode;
}
/// Create a process result from this description.
ProcessResult toProcessResult(String command) {
return ProcessResult(
pid,
predictedExitCode,
predictedOutput,
predictedErrorOutput,
);
}
}

View file

@ -1,118 +0,0 @@
import 'contracts/process_result.dart';
import 'exceptions/process_failed_exception.dart';
/// Represents a fake process result for testing.
class FakeProcessResult implements ProcessResult {
/// The command that was executed.
final String _command;
/// The exit code of the process.
final int _exitCode;
/// The output of the process.
final String _output;
/// The error output of the process.
final String _errorOutput;
/// Create a new fake process result instance.
FakeProcessResult({
String command = '',
int exitCode = 0,
String output = '',
String errorOutput = '',
}) : _command = command,
_exitCode = exitCode,
_output = output,
_errorOutput = errorOutput;
@override
String command() => _command;
@override
bool successful() => _exitCode == 0;
@override
bool failed() => !successful();
@override
int? exitCode() => _exitCode;
@override
String output() => _output;
@override
bool seeInOutput(String output) => _output.contains(output);
@override
String errorOutput() => _errorOutput;
@override
bool seeInErrorOutput(String output) => _errorOutput.contains(output);
@override
ProcessResult throwIfFailed(
[void Function(ProcessResult, Exception)? callback]) {
if (successful()) {
return this;
}
final exception = ProcessFailedException(this);
if (callback != null) {
callback(this, exception);
}
throw exception;
}
@override
ProcessResult throwIf(bool condition,
[void Function(ProcessResult, Exception)? callback]) {
if (condition) {
return throwIfFailed(callback);
}
return this;
}
/// Create a copy of this result with a different command.
FakeProcessResult withCommand(String command) {
return FakeProcessResult(
command: command,
exitCode: _exitCode,
output: _output,
errorOutput: _errorOutput,
);
}
/// Create a copy of this result with a different exit code.
FakeProcessResult withExitCode(int exitCode) {
return FakeProcessResult(
command: _command,
exitCode: exitCode,
output: _output,
errorOutput: _errorOutput,
);
}
/// Create a copy of this result with different output.
FakeProcessResult withOutput(String output) {
return FakeProcessResult(
command: _command,
exitCode: _exitCode,
output: output,
errorOutput: _errorOutput,
);
}
/// Create a copy of this result with different error output.
FakeProcessResult withErrorOutput(String errorOutput) {
return FakeProcessResult(
command: _command,
exitCode: _exitCode,
output: _output,
errorOutput: errorOutput,
);
}
}

View file

@ -1,69 +0,0 @@
import 'dart:collection';
import 'contracts/process_result.dart';
import 'fake_process_description.dart';
import 'fake_process_result.dart';
/// Represents a sequence of fake process results for testing.
class FakeProcessSequence {
/// The sequence of results.
final Queue<dynamic> _sequence;
/// Create a new fake process sequence instance.
FakeProcessSequence([List<dynamic> sequence = const []])
: _sequence = Queue.from(sequence);
/// Add a result to the sequence.
FakeProcessSequence then(dynamic result) {
_sequence.add(result);
return this;
}
/// Get the next result in the sequence.
dynamic call() {
if (_sequence.isEmpty) {
throw StateError('No more results in sequence.');
}
return _sequence.removeFirst();
}
/// Create a sequence from a list of results.
static FakeProcessSequence fromResults(List<ProcessResult> results) {
return FakeProcessSequence(results);
}
/// Create a sequence from a list of descriptions.
static FakeProcessSequence fromDescriptions(
List<FakeProcessDescription> descriptions) {
return FakeProcessSequence(descriptions);
}
/// Create a sequence from a list of outputs.
static FakeProcessSequence fromOutputs(List<String> outputs) {
return FakeProcessSequence(
outputs.map((output) => FakeProcessResult(output: output)).toList(),
);
}
/// Create a sequence that alternates between success and failure.
static FakeProcessSequence alternating(int count) {
return FakeProcessSequence(
List.generate(
count,
(i) => FakeProcessResult(
exitCode: i.isEven ? 0 : 1,
output: 'Output ${i + 1}',
errorOutput: i.isEven ? '' : 'Error ${i + 1}',
),
),
);
}
/// Check if there are more results in the sequence.
bool get hasMore => _sequence.isNotEmpty;
/// Get the number of remaining results.
int get remaining => _sequence.length;
/// Clear the sequence.
void clear() => _sequence.clear();
}

View file

@ -1,157 +1,128 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:async';
import 'contracts/process_result.dart';
import 'process_result.dart'; import 'process_result.dart';
import 'exceptions/process_failed_exception.dart';
/// Represents a running process. /// Represents a process that has been started.
class InvokedProcess { class InvokedProcess {
/// The underlying process instance. /// The underlying process instance.
final Process _process; final Process _process;
/// The command that was executed. /// The output handler callback.
final String _command; final void Function(String)? _onOutput;
/// The output handler. /// The collected stdout data.
final void Function(String)? _outputHandler; final List<int> _stdout = [];
/// The output buffer. /// The collected stderr data.
final StringBuffer _outputBuffer = StringBuffer(); final List<int> _stderr = [];
/// The error output buffer. /// Whether the process has completed
final StringBuffer _errorBuffer = StringBuffer(); bool _completed = false;
/// The stdout stream controller. /// Whether the process was killed
final StreamController<List<int>> _stdoutController; bool _killed = false;
/// The stderr stream controller. /// Completer for stdout stream
final StreamController<List<int>> _stderrController; final _stdoutCompleter = Completer<void>();
/// The stdout subscription. /// Completer for stderr stream
late final StreamSubscription<List<int>> _stdoutSubscription; final _stderrCompleter = Completer<void>();
/// The stderr subscription.
late final StreamSubscription<List<int>> _stderrSubscription;
/// Create a new invoked process instance. /// Create a new invoked process instance.
InvokedProcess(Process process, this._command, [this._outputHandler]) InvokedProcess(this._process, this._onOutput) {
: _process = process, _process.stdout.listen(
_stdoutController = StreamController<List<int>>.broadcast(),
_stderrController = StreamController<List<int>>.broadcast() {
// Set up output handling
_stdoutSubscription = _process.stdout.listen(
(data) { (data) {
_stdoutController.add(data); _stdout.addAll(data);
_handleOutput(data, _outputBuffer); if (_onOutput != null) {
}, _onOutput!(String.fromCharCodes(data));
onDone: _stdoutController.close,
cancelOnError: false,
);
_stderrSubscription = _process.stderr.listen(
(data) {
_stderrController.add(data);
_handleOutput(data, _errorBuffer);
},
onDone: _stderrController.close,
cancelOnError: false,
);
}
/// Handle output data.
void _handleOutput(List<int> data, StringBuffer buffer) {
final text = utf8.decode(data);
buffer.write(text);
if (_outputHandler != null) {
final lines = text.split('\n');
for (var line in lines) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) {
_outputHandler!(trimmed);
} }
} },
} onDone: () => _stdoutCompleter.complete(),
);
_process.stderr.listen(
(data) {
_stderr.addAll(data);
if (_onOutput != null) {
_onOutput!(String.fromCharCodes(data));
}
},
onDone: () => _stderrCompleter.complete(),
);
// Track when the process completes
_process.exitCode.then((_) => _completed = true);
} }
/// Get the process ID. /// Get the process ID.
int get pid => _process.pid; int get pid => _process.pid;
/// Kill the process. /// Write data to the process stdin.
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) { void write(dynamic input) {
closeStdin(); if (input is String) {
_process.kill(signal); _process.stdin.write(input);
return true; } else if (input is List<int>) {
} _process.stdin.add(input);
/// Get the process exit code.
Future<int> get exitCode => _process.exitCode;
/// Wait for the process to complete.
Future<ProcessResult> wait() async {
try {
// Wait for process to complete first
final exitCode = await _process.exitCode;
// Give streams a chance to complete
await Future.delayed(Duration(milliseconds: 10));
// Cancel stream subscriptions
await _stdoutSubscription.cancel();
await _stderrSubscription.cancel();
return ProcessResultImpl(
command: _command,
exitCode: exitCode,
output: _outputBuffer.toString(),
errorOutput: _errorBuffer.toString(),
);
} finally {
// Ensure stdin is closed
try {
_process.stdin.close();
} catch (_) {}
} }
} }
/// Get the process stdout stream. /// Close the process stdin.
Stream<List<int>> get stdout => _stdoutController.stream;
/// Get the process stderr stream.
Stream<List<int>> get stderr => _stderrController.stream;
/// Get the process stdin sink.
IOSink get stdin => _process.stdin;
/// Write data to the process stdin.
Future<void> write(String input) async {
try {
_process.stdin.write(input);
await _process.stdin.flush();
if (input.endsWith('\n')) {
await _process.stdin.close();
await Future.delayed(Duration(milliseconds: 10));
}
} catch (_) {}
}
/// Write lines to the process stdin.
Future<void> writeLines(List<String> lines) async {
try {
for (final line in lines) {
_process.stdin.write('$line\n');
await _process.stdin.flush();
}
await _process.stdin.close();
await Future.delayed(Duration(milliseconds: 10));
} catch (_) {}
}
/// Close stdin.
Future<void> closeStdin() async { Future<void> closeStdin() async {
try { await _process.stdin.close();
await _process.stdin.close(); }
await Future.delayed(Duration(milliseconds: 10));
} catch (_) {} /// Signal the process.
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
_killed = true;
return _process.kill(signal);
}
/// Check if the process is still running.
bool running() {
return !_completed;
}
/// Wait for the process to complete and get its result.
Future<ProcessResult> wait() async {
// Wait for all streams to complete
await Future.wait([
_stdoutCompleter.future,
_stderrCompleter.future,
]);
final exitCode = await _process.exitCode;
final result = ProcessResult(
exitCode,
String.fromCharCodes(_stdout),
String.fromCharCodes(_stderr),
);
// Don't throw if the process was killed
if (!_killed && exitCode != 0) {
throw ProcessFailedException(result);
}
return result;
}
/// Get the latest output from the process.
String latestOutput() {
return String.fromCharCodes(_stdout);
}
/// Get the latest error output from the process.
String latestErrorOutput() {
return String.fromCharCodes(_stderr);
}
/// Get all output from the process.
String output() {
return String.fromCharCodes(_stdout);
}
/// Get all error output from the process.
String errorOutput() {
return String.fromCharCodes(_stderr);
} }
} }

View file

@ -1,71 +0,0 @@
import 'dart:async';
import 'contracts/process_result.dart';
import 'invoked_process.dart';
import 'process_pool_results.dart';
/// Represents a pool of running processes.
class InvokedProcessPool {
/// The processes in the pool.
final List<InvokedProcess> _processes;
/// Create a new invoked process pool instance.
InvokedProcessPool(this._processes);
/// Get the list of processes.
List<InvokedProcess> get processes => List.unmodifiable(_processes);
/// Wait for all processes to complete.
Future<ProcessPoolResults> wait() async {
final results = <ProcessResult>[];
for (final process in _processes) {
results.add(await process.wait());
}
return ProcessPoolResults(results);
}
/// Kill all processes.
void kill() {
for (final process in _processes) {
process.kill();
}
}
/// Get the process IDs.
List<int> get pids => _processes.map((p) => p.pid).toList();
/// Get the number of processes.
int get length => _processes.length;
/// Check if the pool is empty.
bool get isEmpty => _processes.isEmpty;
/// Check if the pool is not empty.
bool get isNotEmpty => _processes.isNotEmpty;
/// Get a process by index.
InvokedProcess operator [](int index) => _processes[index];
/// Iterate over the processes.
Iterator<InvokedProcess> get iterator => _processes.iterator;
/// Get the first process.
InvokedProcess get first => _processes.first;
/// Get the last process.
InvokedProcess get last => _processes.last;
/// Add a process to the pool.
void add(InvokedProcess process) {
_processes.add(process);
}
/// Remove a process from the pool.
bool remove(InvokedProcess process) {
return _processes.remove(process);
}
/// Clear all processes from the pool.
void clear() {
_processes.clear();
}
}

View file

@ -1,351 +1,288 @@
import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'dart:io' as io; import 'package:meta/meta.dart';
import 'dart:convert'; import 'package:path/path.dart' as path;
import 'traits/macroable.dart'; import 'package:collection/collection.dart';
import 'contracts/process_result.dart';
import 'factory.dart';
import 'process_result.dart'; import 'process_result.dart';
import 'invoked_process.dart';
import 'exceptions/process_timed_out_exception.dart';
import 'exceptions/process_failed_exception.dart'; import 'exceptions/process_failed_exception.dart';
/// Represents a pending process that can be configured and then executed. /// A class that represents a process that is ready to be started.
class PendingProcess with Macroable { class PendingProcess {
/// The process factory instance.
final Factory _factory;
/// The command to invoke the process. /// The command to invoke the process.
dynamic _command; dynamic command;
/// The working directory of the process. /// The working directory of the process.
String? _workingDirectory; String? workingDirectory;
/// The maximum number of seconds the process may run. /// The maximum number of seconds the process may run.
int? _timeout = 60; int? timeout = 60;
/// The maximum number of seconds the process may go without returning output. /// The maximum number of seconds the process may go without returning output.
int? _idleTimeout; int? idleTimeout;
/// The additional environment variables for the process. /// The additional environment variables for the process.
final Map<String, String> _environment = {}; Map<String, String> environment = {};
/// The standard input data that should be piped into the command. /// The standard input data that should be piped into the command.
dynamic _input; dynamic input;
/// Indicates whether output should be disabled for the process. /// Indicates whether output should be disabled for the process.
bool _quietly = false; bool quietly = false;
/// Indicates if TTY mode should be enabled. /// Indicates if TTY mode should be enabled.
bool _tty = true; bool tty = false;
/// Create a new pending process instance. /// Create a new pending process instance.
PendingProcess(); PendingProcess(this._factory);
/// Format the command for display.
String _formatCommand() {
if (command is List) {
return (command as List).join(' ');
}
return command.toString();
}
/// Specify the command that will invoke the process. /// Specify the command that will invoke the process.
PendingProcess command(dynamic command) { PendingProcess withCommand(dynamic command) {
_command = command; this.command = command;
return this; return this;
} }
/// Specify the working directory of the process. /// Specify the working directory of the process.
PendingProcess path(String path) { PendingProcess withWorkingDirectory(String directory) {
_workingDirectory = path; workingDirectory = directory;
return this; return this;
} }
/// Specify the maximum number of seconds the process may run. /// Specify the maximum number of seconds the process may run.
PendingProcess timeout(int seconds) { PendingProcess withTimeout(int seconds) {
_timeout = seconds; timeout = seconds;
return this; return this;
} }
/// Specify the maximum number of seconds a process may go without returning output. /// Specify the maximum number of seconds a process may go without returning output.
PendingProcess idleTimeout(int seconds) { PendingProcess withIdleTimeout(int seconds) {
_idleTimeout = seconds; idleTimeout = seconds;
return this; return this;
} }
/// Indicate that the process may run forever without timing out. /// Indicate that the process may run forever without timing out.
PendingProcess forever() { PendingProcess forever() {
_timeout = null; timeout = null;
return this; return this;
} }
/// Set the additional environment variables for the process. /// Set the additional environment variables for the process.
PendingProcess env(Map<String, String> environment) { PendingProcess withEnvironment(Map<String, String> env) {
_environment.addAll(environment); environment = env;
return this; return this;
} }
/// Set the standard input that should be provided when invoking the process. /// Set the standard input that should be provided when invoking the process.
PendingProcess input(dynamic input) { PendingProcess withInput(dynamic input) {
_input = input; this.input = input;
return this; return this;
} }
/// Disable output for the process. /// Disable output for the process.
PendingProcess quietly() { PendingProcess withoutOutput() {
_quietly = true; quietly = true;
return this; return this;
} }
/// Enable TTY mode for the process. /// Enable TTY mode for the process.
PendingProcess tty([bool enabled = true]) { PendingProcess withTty([bool enabled = true]) {
_tty = enabled; tty = enabled;
return this; return this;
} }
/// Parse the command into executable and arguments /// Run the process synchronously.
(String, List<String>, bool) _parseCommand(dynamic command) { Future<ProcessResult> run(
if (command is List<String>) { [dynamic command, void Function(String)? onOutput]) async {
// For list commands, use directly without shell this.command = command ?? this.command;
if (command[0] == 'echo') {
// Special handling for echo command if (this.command == null) {
if (io.Platform.isWindows) { throw ArgumentError('No command specified');
return (
'cmd.exe',
['/c', 'echo', command.sublist(1).join(' ')],
true
);
}
// On Unix, pass arguments directly to echo
return ('echo', command.sublist(1), false);
} else if (command[0] == 'test' && command[1] == '-t') {
// Special handling for TTY test command
if (io.Platform.isWindows) {
return ('cmd.exe', ['/c', 'exit', '0'], true);
} else {
return ('sh', ['-c', 'exit 0'], true);
}
}
return (command[0], command.sublist(1), false);
} }
if (command is! String) { // Handle immediate timeout
throw ArgumentError('Command must be a string or list of strings'); if (timeout == 0) {
throw ProcessTimedOutException(
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.',
);
} }
final commandStr = command.toString(); try {
final process = await _createProcess();
Timer? timeoutTimer;
Timer? idleTimer;
DateTime lastOutputTime = DateTime.now();
bool timedOut = false;
String? timeoutMessage;
// Handle platform-specific shell commands if (timeout != null) {
if (io.Platform.isWindows) { timeoutTimer = Timer(Duration(seconds: timeout!), () {
if (commandStr.startsWith('cmd /c')) { timedOut = true;
// Already properly formatted for Windows, pass through directly timeoutMessage =
return ('cmd.exe', ['/c', commandStr.substring(6)], true); 'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.';
process.kill();
});
} }
// All other commands need cmd.exe shell
return ('cmd.exe', ['/c', commandStr], true); if (idleTimeout != null) {
} else { idleTimer = Timer.periodic(Duration(seconds: 1), (_) {
if (commandStr == 'test -t 0') { final idleSeconds =
// Special handling for TTY test command DateTime.now().difference(lastOutputTime).inSeconds;
return ('sh', ['-c', 'exit 0'], true); if (idleSeconds >= idleTimeout!) {
} else if (commandStr == 'pwd') { timedOut = true;
// Special handling for pwd command timeoutMessage =
return ('pwd', [], false); 'The process "${_formatCommand()}" exceeded the idle timeout of $idleTimeout seconds.';
process.kill();
idleTimer?.cancel();
}
});
} }
// All other commands need sh shell
return ('sh', ['-c', commandStr], true); try {
final result = await _runProcess(process, (output) {
lastOutputTime = DateTime.now();
onOutput?.call(output);
});
if (timedOut) {
throw ProcessTimedOutException(timeoutMessage!);
}
if (result.exitCode != 0) {
throw ProcessFailedException(result);
}
return result;
} finally {
timeoutTimer?.cancel();
idleTimer?.cancel();
}
} on ProcessException catch (e) {
final result = ProcessResult(1, '', e.message);
throw ProcessFailedException(result);
} }
} }
/// Run the process. /// Start the process asynchronously.
Future<ProcessResult> run( Future<InvokedProcess> start([void Function(String)? onOutput]) async {
[dynamic commandOrCallback, dynamic callback]) async { if (command == null) {
// Handle overloaded parameters throw ArgumentError('No command specified');
dynamic actualCommand = _command; }
void Function(String)? outputCallback;
if (commandOrCallback != null) { try {
if (commandOrCallback is void Function(String)) { final process = await _createProcess();
outputCallback = commandOrCallback;
} else { if (input != null) {
actualCommand = commandOrCallback; if (input is String) {
if (callback != null && callback is void Function(String)) { process.stdin.write(input);
outputCallback = callback; } else if (input is List<int>) {
process.stdin.add(input);
} }
await process.stdin.close();
} }
return InvokedProcess(process, onOutput);
} on ProcessException catch (e) {
final result = ProcessResult(1, '', e.message);
throw ProcessFailedException(result);
} }
}
if (actualCommand == null) { Future<Process> _createProcess() async {
throw ArgumentError('No command has been specified.'); if (command is List) {
} final List<String> args =
(command as List).map((e) => e.toString()).toList();
final (executable, args, useShell) = _parseCommand(actualCommand); return Process.start(
args[0],
// Merge current environment with custom environment args.skip(1).toList(),
final env = Map<String, String>.from(io.Platform.environment); workingDirectory: workingDirectory ?? Directory.current.path,
env.addAll(_environment); environment: environment,
includeParentEnvironment: true,
// Set TTY environment variables runInShell: false,
if (_tty) { mode: tty ? ProcessStartMode.inheritStdio : ProcessStartMode.normal,
env['TERM'] = 'xterm'; );
env['FORCE_TTY'] = '1'; } else {
if (!io.Platform.isWindows) { // For string commands, use shell to handle pipes, redirects, etc.
env['POSIXLY_CORRECT'] = '1'; final shell = Platform.isWindows ? 'cmd' : '/bin/sh';
} final shellArg = Platform.isWindows ? '/c' : '-c';
} return Process.start(
shell,
final process = await io.Process.start( [shellArg, command.toString()],
executable, workingDirectory: workingDirectory ?? Directory.current.path,
args, environment: environment,
workingDirectory: _workingDirectory ?? io.Directory.current.path, includeParentEnvironment: true,
environment: env, runInShell: true,
runInShell: useShell || _tty, mode: tty ? ProcessStartMode.inheritStdio : ProcessStartMode.normal,
includeParentEnvironment: true, );
);
final stdoutBuffer = StringBuffer();
final stderrBuffer = StringBuffer();
void handleOutput(String data) {
stdoutBuffer.write(data);
if (!_quietly && outputCallback != null) {
final lines = data.split('\n');
for (var line in lines) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) {
outputCallback(trimmed);
}
}
}
}
void handleError(String data) {
stderrBuffer.write(data);
if (!_quietly && outputCallback != null) {
final lines = data.split('\n');
for (var line in lines) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) {
outputCallback(trimmed);
}
}
}
} }
}
Future<ProcessResult> _runProcess(
Process process, void Function(String)? onOutput) async {
final stdout = <int>[];
final stderr = <int>[];
final stdoutCompleter = Completer<void>(); final stdoutCompleter = Completer<void>();
final stderrCompleter = Completer<void>(); final stderrCompleter = Completer<void>();
final stdoutSubscription = process.stdout if (!quietly) {
.transform(utf8.decoder) process.stdout.listen(
.listen(handleOutput, onDone: stdoutCompleter.complete); (data) {
stdout.addAll(data);
if (onOutput != null) {
onOutput(String.fromCharCodes(data));
}
},
onDone: () => stdoutCompleter.complete(),
);
final stderrSubscription = process.stderr process.stderr.listen(
.transform(utf8.decoder) (data) {
.listen(handleError, onDone: stderrCompleter.complete); stderr.addAll(data);
if (onOutput != null) {
onOutput(String.fromCharCodes(data));
}
},
onDone: () => stderrCompleter.complete(),
);
} else {
stdoutCompleter.complete();
stderrCompleter.complete();
}
if (_input != null) { if (input != null) {
if (_input is String) { if (input is String) {
process.stdin.write(_input); process.stdin.write(input);
} else if (_input is List<int>) { } else if (input is List<int>) {
process.stdin.add(_input as List<int>); process.stdin.add(input);
} }
await process.stdin.close(); await process.stdin.close();
} }
int? exitCode; // Wait for all streams to complete
if (_timeout != null) {
try {
exitCode = await process.exitCode.timeout(Duration(seconds: _timeout!));
} on TimeoutException {
process.kill();
throw ProcessTimeoutException(
ProcessResultImpl(
command: executable,
exitCode: null,
output: stdoutBuffer.toString(),
errorOutput: stderrBuffer.toString(),
),
Duration(seconds: _timeout!),
);
}
} else {
exitCode = await process.exitCode;
}
// Wait for output streams to complete
await Future.wait([ await Future.wait([
stdoutCompleter.future, stdoutCompleter.future,
stderrCompleter.future, stderrCompleter.future,
]); ]);
await stdoutSubscription.cancel(); final exitCode = await process.exitCode;
await stderrSubscription.cancel();
return ProcessResultImpl( return ProcessResult(
command: executable, exitCode,
exitCode: exitCode, String.fromCharCodes(stdout),
output: stdoutBuffer.toString(), String.fromCharCodes(stderr),
errorOutput: stderrBuffer.toString(),
); );
} }
/// Start the process in the background.
Future<io.Process> start(
[dynamic commandOrCallback, dynamic callback]) async {
// Handle overloaded parameters
dynamic actualCommand = _command;
void Function(String)? outputCallback;
if (commandOrCallback != null) {
if (commandOrCallback is void Function(String)) {
outputCallback = commandOrCallback;
} else {
actualCommand = commandOrCallback;
if (callback != null && callback is void Function(String)) {
outputCallback = callback;
}
}
}
if (actualCommand == null) {
throw ArgumentError('No command has been specified.');
}
final (executable, args, useShell) = _parseCommand(actualCommand);
// Merge current environment with custom environment
final env = Map<String, String>.from(io.Platform.environment);
env.addAll(_environment);
// Set TTY environment variables
if (_tty) {
env['TERM'] = 'xterm';
env['FORCE_TTY'] = '1';
if (!io.Platform.isWindows) {
env['POSIXLY_CORRECT'] = '1';
}
}
final process = await io.Process.start(
executable,
args,
workingDirectory: _workingDirectory ?? io.Directory.current.path,
environment: env,
runInShell: useShell || _tty,
includeParentEnvironment: true,
);
if (!_quietly && outputCallback != null) {
void handleOutput(String data) {
final lines = data.split('\n');
for (var line in lines) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) {
outputCallback?.call(trimmed);
}
}
}
process.stdout.transform(utf8.decoder).listen(handleOutput);
process.stderr.transform(utf8.decoder).listen(handleOutput);
}
if (_input != null) {
if (_input is String) {
process.stdin.write(_input);
} else if (_input is List<int>) {
process.stdin.add(_input as List<int>);
}
await process.stdin.close();
}
return process;
}
} }

View file

@ -1,217 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'factory.dart';
import 'pending_process.dart';
import 'contracts/process_result.dart';
import 'process_result.dart';
import 'exceptions/process_failed_exception.dart';
/// Represents a series of piped processes.
class Pipe {
/// The process factory instance.
final Factory _factory;
/// The callback that configures the pipe.
final void Function(Pipe) _callback;
/// The processes in the pipe.
final List<PendingProcess> _commands = [];
/// Create a new process pipe instance.
Pipe(this._factory, this._callback) {
// Call the callback immediately to configure the pipe
_callback(this);
}
/// Add a process to the pipe.
Pipe command(dynamic command) {
if (command == null) {
throw ArgumentError('Command cannot be null');
}
// If it's a method reference from PendingProcess, get the instance
if (command is Function && command.toString().contains('PendingProcess')) {
final pendingProcess = _factory.newPendingProcess();
command(pendingProcess);
_commands.add(pendingProcess);
} else if (command is PendingProcess) {
// If it's a PendingProcess instance
_commands.add(command);
} else if (command is PendingProcess Function()) {
// If it's a method that returns a PendingProcess
_commands.add(command());
} else if (command is Function && command.toString().contains('command')) {
// If it's the command method from PendingProcess
final pendingProcess = _factory.newPendingProcess();
_commands.add(pendingProcess);
} else {
// If it's a string command, create a PendingProcess for it
final pendingProcess = _factory.newPendingProcess();
if (command is String) {
if (command.startsWith('printf "\\x')) {
// Handle binary data
final hexString = command.substring(8, command.length - 1);
pendingProcess.command(['printf', '-e', hexString]);
} else if (command.startsWith('echo ')) {
// Handle echo command
final content = command.substring(5).trim();
final unquoted = content.startsWith('"') && content.endsWith('"')
? content.substring(1, content.length - 1)
: content;
pendingProcess.command(['printf', '%s', unquoted]);
} else {
pendingProcess.command(command);
}
} else {
pendingProcess.command(command);
}
_commands.add(pendingProcess);
}
return this;
}
/// Run the processes in the pipe.
Future<ProcessResult> run({void Function(String)? output}) async {
if (_commands.isEmpty) {
return ProcessResultImpl(
command: '',
exitCode: 0,
output: '',
errorOutput: '',
);
}
String processOutput = '';
var lastErrorOutput = StringBuffer();
Process? currentProcess;
int? lastExitCode;
String? lastCommand;
bool failed = false;
try {
// Run each process in sequence
for (var i = 0; i < _commands.length && !failed; i++) {
final command = _commands[i];
try {
// Start process
currentProcess = await command.start();
lastCommand = command.toString();
// Feed previous output to this process if not first
if (i > 0 && processOutput.isNotEmpty) {
final lines = LineSplitter.split(processOutput);
for (var line in lines) {
if (line.isNotEmpty) {
currentProcess.stdin.writeln(line);
await currentProcess.stdin.flush();
}
}
}
await currentProcess.stdin.close();
// Collect output from this process
final result = await collectOutput(currentProcess, lastErrorOutput);
processOutput = result;
print(
'After process ${command}: ${processOutput.split('\n').map((s) => s.trim()).where((s) => s.isNotEmpty).join(', ')}');
// Handle real-time output
if (output != null) {
final lines = LineSplitter.split(processOutput);
for (var line in lines) {
if (line.trim().isNotEmpty) {
output(line.trim());
}
}
}
} catch (e) {
if (e is ProcessFailedException) {
lastExitCode = e.result.exitCode();
failed = true;
break;
}
rethrow;
}
}
// Return the final result
return ProcessResultImpl(
command: lastCommand ?? '',
exitCode: lastExitCode ?? (failed ? 1 : 0),
output: processOutput,
errorOutput: lastErrorOutput.toString(),
);
} catch (e) {
if (e is ProcessFailedException) {
return ProcessResultImpl(
command: lastCommand ?? '',
exitCode: e.result.exitCode() ?? 1,
output: processOutput,
errorOutput: lastErrorOutput.toString(),
);
}
rethrow;
} finally {
if (currentProcess != null && failed) {
try {
currentProcess.kill(ProcessSignal.sigterm);
} catch (_) {}
}
}
}
/// Collect output from a process and wait for it to complete.
Future<String> collectOutput(
Process process, StringBuffer errorOutput) async {
final outputBuffer = StringBuffer();
final outputDone = Completer<void>();
final errorDone = Completer<void>();
// Collect stdout
process.stdout.transform(utf8.decoder).listen(
(data) {
outputBuffer.write(data);
},
onDone: outputDone.complete,
cancelOnError: true,
);
// Collect stderr
process.stderr.transform(utf8.decoder).listen(
(data) {
errorOutput.write(data);
},
onDone: errorDone.complete,
cancelOnError: true,
);
// Wait for process to complete and streams to finish
final exitCode = await process.exitCode;
await Future.wait([outputDone.future, errorDone.future]);
final output = outputBuffer.toString();
if (exitCode != 0) {
throw ProcessFailedException(ProcessResultImpl(
command: process.toString(),
exitCode: exitCode,
output: output,
errorOutput: errorOutput.toString(),
));
}
return output;
}
/// Run the processes in the pipe and return the final output.
Future<String> output() async {
return (await run()).output();
}
/// Run the processes in the pipe and return the final error output.
Future<String> errorOutput() async {
return (await run()).errorOutput();
}
}

View file

@ -1,66 +0,0 @@
import 'dart:async';
import 'factory.dart';
import 'pending_process.dart';
import 'contracts/process_result.dart';
/// Represents a pool of processes that can be executed concurrently.
class Pool {
/// The process factory instance.
final Factory _factory;
/// The callback that configures the pool.
final void Function(Pool) _callback;
/// The processes in the pool.
final List<PendingProcess> _processes = [];
/// Create a new process pool instance.
Pool(this._factory, this._callback) {
// Call the callback immediately to configure the pool
_callback(this);
}
/// Add a process to the pool.
Pool command(dynamic command) {
if (command is PendingProcess) {
_processes.add(command);
} else {
_processes.add(_factory.command(command));
}
return this;
}
/// Start the processes in the pool.
Future<List<ProcessResult>> start([void Function(String)? output]) async {
if (_processes.isEmpty) {
return [];
}
return _factory.concurrently(_processes, onOutput: output);
}
}
/// Represents the results of a process pool execution.
class ProcessPoolResults {
/// The results of the processes.
final List<ProcessResult> _results;
/// Create a new process pool results instance.
ProcessPoolResults(this._results);
/// Get all of the process results.
List<ProcessResult> get results => List.unmodifiable(_results);
/// Determine if all the processes succeeded.
bool successful() => _results.every((result) => result.successful());
/// Determine if any of the processes failed.
bool failed() => _results.any((result) => result.failed());
/// Throw an exception if any of the processes failed.
ProcessPoolResults throwIfAnyFailed() {
if (failed()) {
throw Exception('One or more processes failed.');
}
return this;
}
}

View file

@ -1,84 +0,0 @@
import 'contracts/process_result.dart';
/// Represents the results of a process pool execution.
class ProcessPoolResults {
/// The results of the processes.
final List<ProcessResult> _results;
/// Create a new process pool results instance.
ProcessPoolResults(this._results);
/// Get all process results.
List<ProcessResult> get results => List.unmodifiable(_results);
/// Determine if all processes succeeded.
bool successful() => _results.every((result) => result.successful());
/// Determine if any process failed.
bool failed() => _results.any((result) => result.failed());
/// Get the number of successful processes.
int get successCount =>
_results.where((result) => result.successful()).length;
/// Get the number of failed processes.
int get failureCount => _results.where((result) => result.failed()).length;
/// Get the total number of processes.
int get total => _results.length;
/// Get all successful results.
List<ProcessResult> get successes =>
_results.where((result) => result.successful()).toList();
/// Get all failed results.
List<ProcessResult> get failures =>
_results.where((result) => result.failed()).toList();
/// Throw if any process failed.
ProcessPoolResults throwIfAnyFailed() {
if (failed()) {
throw Exception(
'One or more processes in the pool failed:\n${_formatFailures()}');
}
return this;
}
/// Format failure messages for error reporting.
String _formatFailures() {
final buffer = StringBuffer();
for (final result in failures) {
buffer.writeln('- Command: ${result.command()}');
buffer.writeln(' Exit Code: ${result.exitCode()}');
if (result.output().isNotEmpty) {
buffer.writeln(' Output: ${result.output()}');
}
if (result.errorOutput().isNotEmpty) {
buffer.writeln(' Error Output: ${result.errorOutput()}');
}
buffer.writeln();
}
return buffer.toString();
}
/// Get a process result by index.
ProcessResult operator [](int index) => _results[index];
/// Get the number of results.
int get length => _results.length;
/// Check if there are no results.
bool get isEmpty => _results.isEmpty;
/// Check if there are any results.
bool get isNotEmpty => _results.isNotEmpty;
/// Get the first result.
ProcessResult get first => _results.first;
/// Get the last result.
ProcessResult get last => _results.last;
/// Iterate over the results.
Iterator<ProcessResult> get iterator => _results.iterator;
}

View file

@ -1,89 +1,32 @@
import 'contracts/process_result.dart';
import 'exceptions/process_failed_exception.dart';
/// Represents the result of a process execution. /// Represents the result of a process execution.
class ProcessResultImpl implements ProcessResult { class ProcessResult {
/// The original command executed by the process. /// The process exit code.
final String _command; final int _exitCode;
/// The exit code of the process. /// The process standard output.
final int? _exitCode;
/// The standard output of the process.
final String _output; final String _output;
/// The error output of the process. /// The process error output.
final String _errorOutput; final String _errorOutput;
/// Create a new process result instance. /// Create a new process result instance.
ProcessResultImpl({ ProcessResult(this._exitCode, this._output, this._errorOutput);
required String command,
required int? exitCode,
required String output,
required String errorOutput,
}) : _command = command,
_exitCode = exitCode,
_output = output,
_errorOutput = errorOutput;
@override /// Get the process exit code.
String command() => _command; int get exitCode => _exitCode;
@override /// Get the process output.
String output() => _output;
/// Get the process error output.
String errorOutput() => _errorOutput;
/// Check if the process was successful.
bool successful() => _exitCode == 0; bool successful() => _exitCode == 0;
@override /// Check if the process failed.
bool failed() => !successful(); bool failed() => !successful();
@override @override
int? exitCode() => _exitCode; String toString() => _output;
@override
String output() => _output;
@override
bool seeInOutput(String output) => _output.contains(output);
@override
String errorOutput() => _errorOutput;
@override
bool seeInErrorOutput(String output) => _errorOutput.contains(output);
@override
ProcessResult throwIfFailed(
[void Function(ProcessResult, Exception)? callback]) {
if (successful()) {
return this;
}
final exception = ProcessFailedException(this);
if (callback != null) {
callback(this, exception);
}
throw exception;
}
@override
ProcessResult throwIf(bool condition,
[void Function(ProcessResult, Exception)? callback]) {
if (condition) {
return throwIfFailed(callback);
}
return this;
}
@override
String toString() {
return '''
ProcessResult:
Command: $_command
Exit Code: $_exitCode
Output: ${_output.isEmpty ? '(empty)' : '\n$_output'}
Error Output: ${_errorOutput.isEmpty ? '(empty)' : '\n$_errorOutput'}
''';
}
} }

View file

@ -1,35 +0,0 @@
import 'dart:async';
/// A mixin that provides macro functionality to classes.
mixin Macroable {
/// The registered string macros.
static final Map<String, Function> _macros = {};
/// Register a custom macro.
static void macro(String name, Function macro) {
_macros[name] = macro;
}
/// Handle dynamic method calls into the class.
@override
dynamic noSuchMethod(Invocation invocation) {
if (invocation.isMethod) {
final name = invocation.memberName.toString().split('"')[1];
if (_macros.containsKey(name)) {
final result = Function.apply(
_macros[name]!,
invocation.positionalArguments,
invocation.namedArguments,
);
if (result is Future) {
return result.then((value) => value ?? this);
}
return result ?? this;
}
}
return super.noSuchMethod(invocation);
}
}

View file

@ -1,45 +1,17 @@
name: platform_process name: platform_process
description: A fluent process execution package for Dart, inspired by Laravel's Process package. Provides process pools, piping, testing utilities, and more. description: A Laravel-compatible process management implementation in pure Dart
version: 1.0.0 version: 1.0.0
homepage: https://github.com/platform-platform/process homepage: https://github.com/platform/platform_process
repository: https://github.com/platform-platform/process
issue_tracker: https://github.com/platform-platform/process/issues
documentation: https://github.com/platform-platform/process/blob/main/README.md
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=2.17.0 <4.0.0'
dependencies: dependencies:
meta: ^1.9.0 meta: ^1.9.1
collection: ^1.18.0
async: ^2.11.0
path: ^1.8.0 path: ^1.8.0
async: ^2.11.0
collection: ^1.17.0
dev_dependencies: dev_dependencies:
lints: ^2.0.0
test: ^1.24.0 test: ^1.24.0
lints: ^3.0.0
coverage: ^1.7.0
mockito: ^5.4.0
build_runner: ^2.4.0
executables:
process: process
topics:
- process
- shell
- command
- execution
- laravel
platforms:
linux:
macos:
windows:
funding:
- https://github.com/sponsors/platform-platform
false_secrets:
- /example/**
- /test/**

View file

@ -1,43 +0,0 @@
// import 'package:test/test.dart';
// import 'test_config.dart';
// import 'process_result_test.dart' as process_result_test;
// import 'pending_process_test.dart' as pending_process_test;
// import 'factory_test.dart' as factory_test;
// import 'pool_test.dart' as pool_test;
// import 'pipe_test.dart' as pipe_test;
// void main() {
// TestConfig.configure();
// group('Process Package Tests', () {
// group('Core Components', () {
// group('ProcessResult', () {
// process_result_test.main();
// });
// group('PendingProcess', () {
// pending_process_test.main();
// });
// group('Factory', () {
// factory_test.main();
// });
// });
// group('Advanced Features', () {
// group('Process Pool', () {
// pool_test.main();
// });
// group('Process Pipe', () {
// pipe_test.main();
// });
// });
// });
// // Run cleanup after all tests
// tearDownAll(() async {
// // Additional cleanup if needed
// });
// }

View file

@ -1,5 +1,5 @@
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:test_process/test_process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
group('ProcessFailedException', () { group('ProcessFailedException', () {

View file

@ -1,174 +1,42 @@
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:platform_process/process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
group('Factory', () { group('Factory Tests', () {
late Factory factory; late Factory factory;
setUp(() { setUp(() {
factory = Factory(); factory = Factory();
}); });
test('creates new pending process', () { test('command() creates PendingProcess with string command', () {
expect(factory.newPendingProcess(), isA<PendingProcess>()); final process = factory.command('echo Hello');
expect(process, isA<PendingProcess>());
}); });
test('creates process with command', () async { test('command() creates PendingProcess with list command', () {
final result = await factory.command('echo test').run(); final process = factory.command(['echo', 'Hello']);
expect(result.output().trim(), equals('test')); expect(process, isA<PendingProcess>());
}); });
test('creates process pool', () async { test('command() with null throws ArgumentError', () {
final results = await factory.pool((pool) { expect(() => factory.command(null), throwsArgumentError);
pool.command('echo 1');
pool.command('echo 2');
pool.command('echo 3');
}).start();
expect(results.length, equals(3));
expect(
results.map((r) => r.output().trim()),
containsAll(['1', '2', '3']),
);
}); });
test('creates process pipe', () async { test('command() with empty string throws ArgumentError', () {
final result = await factory.pipeThrough((pipe) { expect(() => factory.command(''), throwsArgumentError);
pipe.command('echo "hello world"');
pipe.command('tr "a-z" "A-Z"');
}).run();
expect(result.output().trim(), equals('HELLO WORLD'));
}); });
group('Process Faking', () { test('command() with empty list throws ArgumentError', () {
test('fakes specific commands', () async { expect(() => factory.command([]), throwsArgumentError);
factory.fake({
'ls': 'file1.txt\nfile2.txt',
'cat file1.txt': 'Hello, World!',
'grep pattern': (process) => 'Matched line',
});
final ls = await factory.command('ls').run();
expect(ls.output().trim(), equals('file1.txt\nfile2.txt'));
final cat = await factory.command('cat file1.txt').run();
expect(cat.output().trim(), equals('Hello, World!'));
final grep = await factory.command('grep pattern').run();
expect(grep.output().trim(), equals('Matched line'));
});
test('prevents stray processes', () {
factory.fake().preventStrayProcesses();
expect(
() => factory.command('unfaked-command').run(),
throwsA(isA<Exception>()),
);
});
test('records process executions', () async {
factory.fake();
await factory.command('ls').run();
await factory.command('pwd').run();
expect(factory.isRecording(), isTrue);
});
test('supports dynamic fake results', () async {
var counter = 0;
factory.fake({
'counter': (process) => (++counter).toString(),
});
final result1 = await factory.command('counter').run();
final result2 = await factory.command('counter').run();
expect(result1.output(), equals('1'));
expect(result2.output(), equals('2'));
});
test('fakes process descriptions', () async {
factory.fake({
'test-command': FakeProcessDescription()
..withExitCode(1)
..replaceOutput('test output')
..replaceErrorOutput('test error'),
});
final result = await factory.command('test-command').run();
expect(result.failed(), isTrue);
expect(result.output(), equals('test output'));
expect(result.errorOutput(), equals('test error'));
});
test('fakes process sequences', () async {
factory.fake({
'sequence': FakeProcessSequence()
..then('first')
..then('second')
..then('third'),
});
final result1 = await factory.command('sequence').run();
final result2 = await factory.command('sequence').run();
final result3 = await factory.command('sequence').run();
expect(result1.output(), equals('first'));
expect(result2.output(), equals('second'));
expect(result3.output(), equals('third'));
});
test('handles process configuration in fakes', () async {
factory.fake({
'env-test': (process) => process.env['TEST_VAR'] ?? 'not set',
});
final result = await factory
.command('env-test')
.env({'TEST_VAR': 'test value'}).run();
expect(result.output(), equals('test value'));
});
test('supports mixed fake types', () async {
factory.fake({
'string': 'simple output',
'function': (process) => 'dynamic output',
'description': FakeProcessDescription()..replaceOutput('desc output'),
'sequence': FakeProcessSequence()..then('seq output'),
});
expect((await factory.command('string').run()).output(),
equals('simple output'));
expect((await factory.command('function').run()).output(),
equals('dynamic output'));
expect((await factory.command('description').run()).output(),
equals('desc output'));
expect((await factory.command('sequence').run()).output(),
equals('seq output'));
});
}); });
group('Error Handling', () { test('command() with invalid type throws ArgumentError', () {
test('handles command failures', () async { expect(() => factory.command(123), throwsArgumentError);
final result = await factory.command('false').run(); });
expect(result.failed(), isTrue);
});
test('handles invalid commands', () { test('command() with list containing non-string throws ArgumentError', () {
expect( expect(() => factory.command(['echo', 123]), throwsArgumentError);
() => factory.command('nonexistent-command').run(),
throwsA(anything),
);
});
test('handles process timeouts', () async {
final result = await factory.command('sleep 5').timeout(1).run();
expect(result.failed(), isTrue);
});
}); });
}); });
} }

View file

@ -1,100 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('FakeProcessDescription', () {
late FakeProcessDescription description;
setUp(() {
description = FakeProcessDescription();
});
test('provides default values', () {
expect(description.predictedExitCode, equals(0));
expect(description.predictedOutput, isEmpty);
expect(description.predictedErrorOutput, isEmpty);
expect(description.outputSequence, isEmpty);
expect(description.delay, equals(Duration(milliseconds: 100)));
expect(description.runDuration, equals(Duration.zero));
});
test('configures exit code', () {
description.withExitCode(1);
expect(description.predictedExitCode, equals(1));
});
test('configures output', () {
description.replaceOutput('test output');
expect(description.predictedOutput, equals('test output'));
});
test('configures error output', () {
description.replaceErrorOutput('test error');
expect(description.predictedErrorOutput, equals('test error'));
});
test('configures output sequence', () {
description.withOutputSequence(['one', 'two', 'three']);
expect(description.outputSequence, equals(['one', 'two', 'three']));
});
test('configures delay', () {
description.withDelay(Duration(seconds: 1));
expect(description.delay, equals(Duration(seconds: 1)));
});
test('configures run duration with duration', () {
description.runsFor(duration: Duration(seconds: 2));
expect(description.runDuration, equals(Duration(seconds: 2)));
});
test('configures run duration with iterations', () {
description.withDelay(Duration(seconds: 1));
description.runsFor(iterations: 3);
expect(description.runDuration, equals(Duration(seconds: 3)));
});
test('handles kill signal', () {
expect(description.kill(), isTrue);
expect(description.predictedExitCode, equals(-1));
});
test('provides process result', () {
description
..withExitCode(1)
..replaceOutput('test output')
..replaceErrorOutput('test error');
final result = description.toProcessResult('test command');
expect(result.pid, isPositive);
expect(result.exitCode, equals(1));
expect(result.stdout, equals('test output'));
expect(result.stderr, equals('test error'));
});
test('provides exit code future', () async {
description
..withExitCode(1)
..runsFor(duration: Duration(milliseconds: 100));
final startTime = DateTime.now();
final exitCode = await description.exitCodeFuture;
final duration = DateTime.now().difference(startTime);
expect(exitCode, equals(1));
expect(duration.inMilliseconds, greaterThanOrEqualTo(100));
});
test('supports method chaining', () {
final result = description
.withExitCode(1)
.replaceOutput('output')
.replaceErrorOutput('error')
.withOutputSequence(['one', 'two'])
.withDelay(Duration(seconds: 1))
.runsFor(duration: Duration(seconds: 2));
expect(result, equals(description));
});
});
}

View file

@ -1,149 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('FakeProcessResult', () {
late FakeProcessResult result;
setUp(() {
result = FakeProcessResult(
command: 'test command',
exitCode: 0,
output: 'test output',
errorOutput: 'test error',
);
});
test('provides command', () {
expect(result.command(), equals('test command'));
});
test('indicates success', () {
expect(result.successful(), isTrue);
expect(result.failed(), isFalse);
});
test('indicates failure', () {
result = FakeProcessResult(exitCode: 1);
expect(result.successful(), isFalse);
expect(result.failed(), isTrue);
});
test('provides exit code', () {
expect(result.exitCode(), equals(0));
});
test('provides output', () {
expect(result.output(), equals('test output'));
});
test('provides error output', () {
expect(result.errorOutput(), equals('test error'));
});
test('checks output content', () {
expect(result.seeInOutput('test'), isTrue);
expect(result.seeInOutput('missing'), isFalse);
});
test('checks error output content', () {
expect(result.seeInErrorOutput('error'), isTrue);
expect(result.seeInErrorOutput('missing'), isFalse);
});
test('throws on failure', () {
result = FakeProcessResult(
command: 'failing command',
exitCode: 1,
errorOutput: 'error message',
);
expect(
() => result.throwIfFailed(),
throwsA(predicate((e) {
if (e is! ProcessFailedException) return false;
expect(e.result.command(), equals('failing command'));
expect(e.result.exitCode(), equals(1));
expect(e.result.errorOutput(), equals('error message'));
return true;
})),
);
});
test('handles callback on failure', () {
result = FakeProcessResult(exitCode: 1);
var callbackCalled = false;
expect(
() => result.throwIfFailed((result, exception) {
callbackCalled = true;
expect(exception, isA<ProcessFailedException>());
if (exception is ProcessFailedException) {
expect(exception.result.exitCode(), equals(1));
}
}),
throwsA(isA<ProcessFailedException>()),
);
expect(callbackCalled, isTrue);
});
test('returns self on success', () {
expect(result.throwIfFailed(), equals(result));
});
test('throws conditionally', () {
result = FakeProcessResult(exitCode: 1);
expect(
() => result.throwIf(true),
throwsA(isA<ProcessFailedException>()),
);
expect(
() => result.throwIf(false),
returnsNormally,
);
});
test('creates copy with different command', () {
final copy = result.withCommand('new command');
expect(copy.command(), equals('new command'));
expect(copy.exitCode(), equals(result.exitCode()));
expect(copy.output(), equals(result.output()));
expect(copy.errorOutput(), equals(result.errorOutput()));
});
test('creates copy with different exit code', () {
final copy = result.withExitCode(1);
expect(copy.command(), equals(result.command()));
expect(copy.exitCode(), equals(1));
expect(copy.output(), equals(result.output()));
expect(copy.errorOutput(), equals(result.errorOutput()));
});
test('creates copy with different output', () {
final copy = result.withOutput('new output');
expect(copy.command(), equals(result.command()));
expect(copy.exitCode(), equals(result.exitCode()));
expect(copy.output(), equals('new output'));
expect(copy.errorOutput(), equals(result.errorOutput()));
});
test('creates copy with different error output', () {
final copy = result.withErrorOutput('new error');
expect(copy.command(), equals(result.command()));
expect(copy.exitCode(), equals(result.exitCode()));
expect(copy.output(), equals(result.output()));
expect(copy.errorOutput(), equals('new error'));
});
test('provides default values', () {
final defaultResult = FakeProcessResult();
expect(defaultResult.command(), isEmpty);
expect(defaultResult.exitCode(), equals(0));
expect(defaultResult.output(), isEmpty);
expect(defaultResult.errorOutput(), isEmpty);
});
});
}

View file

@ -1,145 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('FakeProcessSequence', () {
test('creates empty sequence', () {
final sequence = FakeProcessSequence();
expect(sequence.hasMore, isFalse);
expect(sequence.remaining, equals(0));
});
test('adds results to sequence', () {
final sequence = FakeProcessSequence()
..then('first')
..then('second')
..then('third');
expect(sequence.hasMore, isTrue);
expect(sequence.remaining, equals(3));
});
test('retrieves results in order', () {
final sequence = FakeProcessSequence()
..then('first')
..then('second');
expect(sequence.call(), equals('first'));
expect(sequence.call(), equals('second'));
expect(sequence.hasMore, isFalse);
});
test('throws when empty', () {
final sequence = FakeProcessSequence();
expect(() => sequence.call(), throwsStateError);
});
test('creates from results list', () {
final results = [
FakeProcessResult(output: 'one'),
FakeProcessResult(output: 'two'),
];
final sequence = FakeProcessSequence.fromResults(results);
expect(sequence.remaining, equals(2));
final first = sequence.call() as FakeProcessResult;
expect(first.output(), equals('one'));
final second = sequence.call() as FakeProcessResult;
expect(second.output(), equals('two'));
});
test('creates from descriptions list', () {
final descriptions = [
FakeProcessDescription()..replaceOutput('first'),
FakeProcessDescription()..replaceOutput('second'),
];
final sequence = FakeProcessSequence.fromDescriptions(descriptions);
expect(sequence.remaining, equals(2));
final first = sequence.call() as FakeProcessDescription;
expect(first.predictedOutput, equals('first'));
final second = sequence.call() as FakeProcessDescription;
expect(second.predictedOutput, equals('second'));
});
test('creates from outputs list', () {
final outputs = ['one', 'two', 'three'];
final sequence = FakeProcessSequence.fromOutputs(outputs);
expect(sequence.remaining, equals(3));
for (final expected in outputs) {
final result = sequence.call() as FakeProcessResult;
expect(result.output(), equals(expected));
expect(result.successful(), isTrue);
}
});
test('creates alternating success/failure sequence', () {
final sequence = FakeProcessSequence.alternating(4);
expect(sequence.remaining, equals(4));
// First result (success)
var result = sequence.call() as FakeProcessResult;
expect(result.successful(), isTrue);
expect(result.output(), equals('Output 1'));
expect(result.errorOutput(), isEmpty);
// Second result (failure)
result = sequence.call() as FakeProcessResult;
expect(result.failed(), isTrue);
expect(result.output(), equals('Output 2'));
expect(result.errorOutput(), equals('Error 2'));
// Third result (success)
result = sequence.call() as FakeProcessResult;
expect(result.successful(), isTrue);
expect(result.output(), equals('Output 3'));
expect(result.errorOutput(), isEmpty);
// Fourth result (failure)
result = sequence.call() as FakeProcessResult;
expect(result.failed(), isTrue);
expect(result.output(), equals('Output 4'));
expect(result.errorOutput(), equals('Error 4'));
});
test('supports method chaining', () {
final sequence =
FakeProcessSequence().then('first').then('second').then('third');
expect(sequence.remaining, equals(3));
});
test('clears sequence', () {
final sequence = FakeProcessSequence()
..then('first')
..then('second');
expect(sequence.remaining, equals(2));
sequence.clear();
expect(sequence.remaining, equals(0));
expect(sequence.hasMore, isFalse);
});
test('handles mixed result types', () {
final sequence = FakeProcessSequence()
..then('string result')
..then(FakeProcessResult(output: 'result output'))
..then(FakeProcessDescription()..replaceOutput('description output'));
expect(sequence.call(), equals('string result'));
final result = sequence.call() as FakeProcessResult;
expect(result.output(), equals('result output'));
final description = sequence.call() as FakeProcessDescription;
expect(description.predictedOutput, equals('description output'));
});
});
}

View file

@ -1,115 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
/// Creates a temporary file with the given content.
Future<File> createTempFile(String content) async {
final file = File(
'${Directory.systemTemp.path}/test_${DateTime.now().millisecondsSinceEpoch}');
await file.writeAsString(content);
return file;
}
/// Creates a temporary directory.
Future<Directory> createTempDir() async {
return Directory.systemTemp.createTemp('test_');
}
/// Cleans up temporary test files.
Future<void> cleanupTempFiles(List<FileSystemEntity> entities) async {
for (final entity in entities) {
if (await entity.exists()) {
await entity.delete(recursive: true);
}
}
}
/// Creates a process factory with common fake commands.
Factory createTestFactory() {
final factory = Factory();
factory.fake({
'echo': (process) => process.toString(),
'cat': (process) => 'cat output',
'ls': 'file1\nfile2\nfile3',
'pwd': Directory.current.path,
'grep': (process) => 'grep output',
'wc': (process) => '1',
'sort': (process) => 'sorted output',
'head': (process) => 'head output',
'printenv': (process) => 'environment output',
'tr': (process) => 'transformed output',
'sleep': (process) => '',
'false': (process) => '',
});
return factory;
}
/// Waits for a condition to be true with timeout.
Future<bool> waitFor(
Future<bool> Function() condition, {
Duration timeout = const Duration(seconds: 5),
Duration interval = const Duration(milliseconds: 100),
}) async {
final stopwatch = Stopwatch()..start();
while (stopwatch.elapsed < timeout) {
if (await condition()) {
return true;
}
await Future.delayed(interval);
}
return false;
}
/// Creates a test file with given name and content in a temporary directory.
Future<File> createTestFile(String name, String content) async {
final dir = await createTempDir();
final file = File('${dir.path}/$name');
await file.writeAsString(content);
return file;
}
/// Creates a test directory structure.
Future<Directory> createTestDirectoryStructure(
Map<String, String> files) async {
final dir = await createTempDir();
for (final entry in files.entries) {
final file = File('${dir.path}/${entry.key}');
await file.create(recursive: true);
await file.writeAsString(entry.value);
}
return dir;
}
/// Runs a test with temporary directory that gets cleaned up.
Future<T> withTempDir<T>(Future<T> Function(Directory dir) test) async {
final dir = await createTempDir();
try {
return await test(dir);
} finally {
await dir.delete(recursive: true);
}
}
/// Creates a factory with custom fake handlers.
Factory createCustomFactory(Map<String, dynamic> fakes) {
final factory = Factory();
factory.fake(fakes);
return factory;
}
/// Asserts that a process completed within the expected duration.
Future<void> assertCompletesWithin(
Future<void> Function() action,
Duration duration,
) async {
final stopwatch = Stopwatch()..start();
await action();
stopwatch.stop();
if (stopwatch.elapsed > duration) {
fail(
'Expected to complete within ${duration.inMilliseconds}ms '
'but took ${stopwatch.elapsedMilliseconds}ms',
);
}
}

View file

@ -1,151 +0,0 @@
import 'dart:io';
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('InvokedProcessPool', () {
late Factory factory;
late List<InvokedProcess> processes;
late InvokedProcessPool pool;
setUp(() async {
factory = Factory();
processes = [];
for (var i = 1; i <= 3; i++) {
final proc = await Process.start('echo', ['Process $i']);
final process = InvokedProcess(proc, 'echo Process $i');
processes.add(process);
}
pool = InvokedProcessPool(processes);
});
tearDown(() {
pool.kill();
});
test('provides access to processes', () {
expect(pool.processes, equals(processes));
expect(pool.length, equals(3));
expect(pool.isEmpty, isFalse);
expect(pool.isNotEmpty, isTrue);
});
test('waits for all processes', () async {
final results = await pool.wait();
expect(results.results.length, equals(3));
for (var i = 0; i < 3; i++) {
expect(results.results[i].output().trim(), equals('Process ${i + 1}'));
}
});
test('kills all processes', () async {
// Start long-running processes
processes = [];
for (var i = 1; i <= 3; i++) {
final proc = await Process.start('sleep', ['10']);
final process = InvokedProcess(proc, 'sleep 10');
processes.add(process);
}
pool = InvokedProcessPool(processes);
// Kill all processes
pool.kill();
// Wait for all processes and verify they were killed
final results = await pool.wait();
for (final result in results.results) {
expect(result.failed(), isTrue);
}
});
test('provides process access by index', () {
expect(pool[0], equals(processes[0]));
expect(pool[1], equals(processes[1]));
expect(pool[2], equals(processes[2]));
});
test('provides first and last process access', () {
expect(pool.first, equals(processes.first));
expect(pool.last, equals(processes.last));
});
test('supports process list operations', () {
expect(pool.processes, equals(processes));
expect(pool.processes.length, equals(processes.length));
});
test('adds process to pool', () async {
final proc = await Process.start('echo', ['New Process']);
final newProcess = InvokedProcess(proc, 'echo New Process');
pool.add(newProcess);
expect(pool.length, equals(4));
expect(pool.last, equals(newProcess));
});
test('removes process from pool', () async {
final processToRemove = processes[1];
expect(pool.remove(processToRemove), isTrue);
expect(pool.length, equals(2));
expect(pool.processes, isNot(contains(processToRemove)));
});
test('clears all processes', () {
pool.clear();
expect(pool.isEmpty, isTrue);
expect(pool.length, equals(0));
});
test('handles mixed process results', () async {
processes = [];
// Success process
final successProc1 = await Process.start('echo', ['success']);
processes.add(InvokedProcess(successProc1, 'echo success'));
// Failure process
final failureProc = await Process.start('false', []);
processes.add(InvokedProcess(failureProc, 'false'));
// Another success process
final successProc2 = await Process.start('echo', ['another success']);
processes.add(InvokedProcess(successProc2, 'echo another success'));
pool = InvokedProcessPool(processes);
final results = await pool.wait();
expect(results.results[0].successful(), isTrue);
expect(results.results[1].failed(), isTrue);
expect(results.results[2].successful(), isTrue);
});
test('handles concurrent output', () async {
processes = [];
// Create processes with different delays
final proc1 =
await Process.start('sh', ['-c', 'sleep 0.2 && echo First']);
processes.add(InvokedProcess(proc1, 'sleep 0.2 && echo First'));
final proc2 =
await Process.start('sh', ['-c', 'sleep 0.1 && echo Second']);
processes.add(InvokedProcess(proc2, 'sleep 0.1 && echo Second'));
final proc3 = await Process.start('echo', ['Third']);
processes.add(InvokedProcess(proc3, 'echo Third'));
pool = InvokedProcessPool(processes);
final results = await pool.wait();
final outputs = results.results.map((r) => r.output().trim()).toList();
expect(outputs, containsAll(['First', 'Second', 'Third']));
});
test('provides process IDs', () {
final pids = pool.pids;
expect(pids.length, equals(3));
for (var i = 0; i < 3; i++) {
expect(pids[i], equals(processes[i].pid));
}
});
});
}

View file

@ -1,129 +1,104 @@
import 'dart:io'; import 'dart:io';
import 'dart:convert';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:platform_process/process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
group('InvokedProcess', () { group('InvokedProcess Tests', () {
late Process process; test('latestOutput() returns latest stdout', () async {
late InvokedProcess invokedProcess; final factory = Factory();
final process = await factory.command(
['sh', '-c', 'echo "line1"; sleep 0.1; echo "line2"']).start();
setUp(() async { await Future.delayed(Duration(milliseconds: 50));
process = await Process.start('echo', ['test']); expect(process.latestOutput().trim(), equals('line1'));
invokedProcess = InvokedProcess(process, 'echo test');
});
test('provides process ID', () { await Future.delayed(Duration(milliseconds: 100));
expect(invokedProcess.pid, equals(process.pid)); expect(process.latestOutput().trim(), contains('line2'));
});
test('captures output', () async { await process.wait();
final result = await invokedProcess.wait(); }, timeout: Timeout(Duration(seconds: 5)));
expect(result.output().trim(), equals('test'));
});
test('handles error output', () async { test('latestErrorOutput() returns latest stderr', () async {
process = await Process.start('sh', ['-c', 'echo error >&2']); final factory = Factory();
invokedProcess = InvokedProcess(process, 'echo error >&2'); final process = await factory.command([
'sh',
'-c',
'echo "error1" >&2; sleep 0.1; echo "error2" >&2'
]).start();
final result = await invokedProcess.wait(); await Future.delayed(Duration(milliseconds: 50));
expect(result.errorOutput().trim(), equals('error')); expect(process.latestErrorOutput().trim(), equals('error1'));
});
test('provides exit code', () async { await Future.delayed(Duration(milliseconds: 100));
final exitCode = await invokedProcess.exitCode; expect(process.latestErrorOutput().trim(), contains('error2'));
expect(exitCode, equals(0));
});
test('handles process kill', () async { await process.wait();
process = await Process.start('sleep', ['10']); }, timeout: Timeout(Duration(seconds: 5)));
invokedProcess = InvokedProcess(process, 'sleep 10');
expect(invokedProcess.kill(), isTrue); test('running() returns correct state', () async {
final exitCode = await invokedProcess.exitCode; final factory = Factory();
expect(exitCode, isNot(0)); final process = await factory.command(['sleep', '0.5']).start();
});
test('provides access to stdout stream', () async { expect(process.running(), isTrue);
final output = await invokedProcess.stdout.transform(utf8.decoder).join(); await Future.delayed(Duration(milliseconds: 600));
expect(output.trim(), equals('test')); expect(process.running(), isFalse);
}); }, timeout: Timeout(Duration(seconds: 5)));
test('provides access to stderr stream', () async { test('write() sends input to process', () async {
process = await Process.start('sh', ['-c', 'echo error >&2']); final factory = Factory();
invokedProcess = InvokedProcess(process, 'echo error >&2'); final process = await factory.command(['cat']).start();
final error = await invokedProcess.stderr.transform(utf8.decoder).join(); process.write('Hello');
expect(error.trim(), equals('error')); process.write(' World');
}); await process.closeStdin();
final result = await process.wait();
expect(result.output().trim(), equals('Hello World'));
}, timeout: Timeout(Duration(seconds: 5)));
test('provides access to stdin', () async { test('write() handles byte input', () async {
process = await Process.start('cat', []); final factory = Factory();
invokedProcess = InvokedProcess(process, 'cat'); final process = await factory.command(['cat']).start();
await invokedProcess.write('test input\n'); process.write([72, 101, 108, 108, 111]); // "Hello" in bytes
final result = await invokedProcess.wait(); await process.closeStdin();
expect(result.output().trim(), equals('test input')); final result = await process.wait();
}); expect(result.output().trim(), equals('Hello'));
}, timeout: Timeout(Duration(seconds: 5)));
test('writes multiple lines to stdin', () async { test('kill() terminates process', () async {
process = await Process.start('cat', []); final factory = Factory();
invokedProcess = InvokedProcess(process, 'cat'); final process = await factory.command(['sleep', '10']).start();
await invokedProcess.writeLines(['line 1', 'line 2', 'line 3']); expect(process.running(), isTrue);
final result = await invokedProcess.wait(); final killed = process.kill();
expect(result.output().trim().split('\n'), expect(killed, isTrue);
equals(['line 1', 'line 2', 'line 3']));
});
test('captures real-time output', () async { final result = await process.wait();
final outputs = <String>[]; expect(result.exitCode, equals(-15)); // SIGTERM
process = await Process.start( expect(process.running(), isFalse);
'sh', ['-c', 'echo line1; sleep 0.1; echo line2']); }, timeout: Timeout(Duration(seconds: 5)));
invokedProcess = InvokedProcess(process, 'echo lines', (data) {
outputs.add(data.trim());
});
await invokedProcess.wait(); test('kill() with custom signal', () async {
expect(outputs, equals(['line1', 'line2'])); if (!Platform.isWindows) {
}); final factory = Factory();
final process = await factory.command(['sleep', '10']).start();
test('handles process failure', () async { expect(process.running(), isTrue);
process = await Process.start('false', []); final killed = process.kill(ProcessSignal.sigint);
invokedProcess = InvokedProcess(process, 'false'); expect(killed, isTrue);
final result = await invokedProcess.wait(); final result = await process.wait();
expect(result.failed(), isTrue); expect(result.exitCode, equals(-2)); // SIGINT
expect(result.exitCode(), equals(1)); expect(process.running(), isFalse);
}); }
}, timeout: Timeout(Duration(seconds: 5)));
test('handles process with arguments', () async { test('pid returns process ID', () async {
process = await Process.start('echo', ['arg1', 'arg2']); final factory = Factory();
invokedProcess = InvokedProcess(process, 'echo arg1 arg2'); final process = await factory.command(['echo', 'test']).start();
final result = await invokedProcess.wait(); expect(process.pid, isPositive);
expect(result.output().trim(), equals('arg1 arg2')); await process.wait();
}); }, timeout: Timeout(Duration(seconds: 5)));
test('handles binary output', () async {
process =
await Process.start('printf', [r'\x48\x45\x4C\x4C\x4F']); // "HELLO"
invokedProcess = InvokedProcess(process, 'printf HELLO');
final result = await invokedProcess.wait();
expect(result.output(), equals('HELLO'));
});
test('handles process cleanup', () async {
process = await Process.start('sleep', ['10']);
invokedProcess = InvokedProcess(process, 'sleep 10');
// Kill process and ensure resources are cleaned up
expect(invokedProcess.kill(), isTrue);
await invokedProcess.wait();
// Verify process is terminated
expect(await invokedProcess.exitCode, isNot(0));
});
}); });
} }

View file

@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:test_process/test_process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
late Factory factory; late Factory factory;

View file

@ -1,158 +1,136 @@
import 'dart:io'; import 'dart:io';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:platform_process/process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
group('PendingProcess', () { group('PendingProcess Tests', () {
late PendingProcess process; late Factory factory;
setUp(() { setUp(() {
process = PendingProcess(); factory = Factory();
}); });
test('configures command', () { test('forever() disables timeout', () async {
process.command('echo test'); final process = factory.command(['sleep', '0.5']).forever();
expect(process.run(), completes); expect(process.timeout, isNull);
}); });
test('configures working directory', () async { test('withIdleTimeout() sets idle timeout', () async {
final result = final process = factory.command(['echo', 'test']).withIdleTimeout(5);
await process.command('pwd').path(Directory.current.path).run(); expect(process.idleTimeout, equals(5));
expect(result.output().trim(), equals(Directory.current.path));
}); });
test('configures environment variables', () async { test('withTty() enables TTY mode', () async {
final result = await process final process = factory.command(['echo', 'test']).withTty();
.command('printenv TEST_VAR') expect(process.tty, isTrue);
.env({'TEST_VAR': 'test_value'}).run();
expect(result.output().trim(), equals('test_value'));
}); });
test('handles string input', () async { test('withTty(false) disables TTY mode', () async {
final result = await process.command('cat').input('test input\n').run(); final process = factory.command(['echo', 'test']).withTty(false);
expect(result.output().trim(), equals('test input')); expect(process.tty, isFalse);
}); });
test('handles list input', () async { test('run() without command throws ArgumentError', () async {
final result = await process final process = PendingProcess(factory);
.command('cat') expect(() => process.run(), throwsArgumentError);
.input([116, 101, 115, 116]) // "test" in bytes
.run();
expect(result.output(), equals('test'));
}); });
test('respects timeout', () async { test('start() without command throws ArgumentError', () async {
// Use a longer timeout to avoid flakiness final process = PendingProcess(factory);
process.command('sleep 5').timeout(1); expect(() => process.start(), throwsArgumentError);
});
final startTime = DateTime.now(); test('run() with command parameter overrides previous command', () async {
try { final process = factory.command(['echo', 'old']);
await process.run(); final result = await process.run(['echo', 'new']);
fail('Expected ProcessTimeoutException'); expect(result.output().trim(), equals('new'));
} catch (e) { });
expect(e, isA<ProcessTimeoutException>());
final duration = DateTime.now().difference(startTime); test('run() handles process exceptions', () async {
expect(duration.inSeconds, lessThanOrEqualTo(2)); // Allow some buffer if (!Platform.isWindows) {
final process = factory.command(['nonexistent']);
expect(() => process.run(), throwsA(isA<ProcessFailedException>()));
} }
}); });
test('runs forever when timeout disabled', () async { test('start() handles process exceptions', () async {
final result = await process.command('echo test').forever().run(); if (!Platform.isWindows) {
expect(result.successful(), isTrue); final process = factory.command(['nonexistent']);
expect(() => process.start(), throwsA(isA<ProcessFailedException>()));
}
}); });
test('captures output in real time', () async { test('withoutOutput() disables output', () async {
final output = <String>[]; final result =
await factory.command(['echo', 'test']).withoutOutput().run();
// Create a platform-independent way to generate sequential output expect(result.output(), isEmpty);
final command = Platform.isWindows
? 'cmd /c "(echo 1 && timeout /T 1 > nul) && (echo 2 && timeout /T 1 > nul) && echo 3"'
: 'sh -c "echo 1; sleep 0.1; echo 2; sleep 0.1; echo 3"';
final result = await process
.command(command)
.run((String data) => output.add(data.trim()));
final numbers = output
.where((s) => s.isNotEmpty)
.map((s) => s.trim())
.where((s) => s.contains(RegExp(r'^[123]$')))
.toList();
expect(numbers, equals(['1', '2', '3']));
expect(result.successful(), isTrue);
}); });
test('disables output when quiet', () async { test('idle timeout triggers', () async {
final output = <String>[]; if (!Platform.isWindows) {
final result = await process // Use tail -f to wait indefinitely without producing output
.command('echo test') final process =
.quietly() factory.command(['tail', '-f', '/dev/null']).withIdleTimeout(1);
.run((String data) => output.add(data));
expect(output, isEmpty); await expectLater(
expect(result.output(), isNotEmpty); process.run(),
throwsA(
allOf(
isA<ProcessTimedOutException>(),
predicate((ProcessTimedOutException e) =>
e.message.contains('exceeded the idle timeout of 1 seconds')),
),
),
);
}
}, timeout: Timeout(Duration(seconds: 5)));
test('timeout triggers', () async {
if (!Platform.isWindows) {
final process = factory.command(['sleep', '5']).withTimeout(1);
await expectLater(
process.run(),
throwsA(
allOf(
isA<ProcessTimedOutException>(),
predicate((ProcessTimedOutException e) =>
e.message.contains('exceeded the timeout of 1 seconds')),
),
),
);
}
}, timeout: Timeout(Duration(seconds: 5)));
test('immediate timeout triggers', () async {
if (!Platform.isWindows) {
final process = factory.command(['sleep', '1']).withTimeout(0);
await expectLater(
process.run(),
throwsA(
allOf(
isA<ProcessTimedOutException>(),
predicate((ProcessTimedOutException e) =>
e.message.contains('exceeded the timeout of 0 seconds')),
),
),
);
}
}); });
test('enables TTY mode', () async { test('string command is executed through shell', () async {
final result = await process.command('test -t 0').tty().run(); if (!Platform.isWindows) {
expect(result.successful(), isTrue); final result = await factory.command('echo "Hello from shell"').run();
expect(result.output().trim(), equals('Hello from shell'));
}
}); });
test('starts process in background', () async { test('input as bytes is handled', () async {
final proc = await process.command('sleep 1').start(); final process =
expect(proc.pid, isPositive); factory.command(['cat']).withInput([72, 101, 108, 108, 111]);
await proc.kill(); final result = await process.run();
}); expect(result.output().trim(), equals('Hello'));
test('throws on invalid command', () {
expect(
() => process.run(),
throwsA(isA<ArgumentError>().having(
(e) => e.message,
'message',
'No command has been specified.',
)),
);
});
test('handles command as list', () async {
final result = await process.command(['echo', 'test']).run();
expect(result.output().trim(), equals('test'));
});
test('preserves environment isolation', () async {
// First process
final result1 = await process
.command('printenv TEST_VAR')
.env({'TEST_VAR': 'value1'}).run();
// Second process with different environment
final result2 = await PendingProcess()
.command('printenv TEST_VAR')
.env({'TEST_VAR': 'value2'}).run();
expect(result1.output().trim(), equals('value1'));
expect(result2.output().trim(), equals('value2'));
});
test('handles process termination', () async {
final proc = await process.command('sleep 10').start();
expect(proc.kill(), isTrue);
expect(await proc.exitCode, isNot(0));
});
test('supports chained configuration', () async {
final result = await process
.command('echo test')
.path(Directory.current.path)
.env({'TEST': 'value'})
.timeout(5)
.quietly()
.run();
expect(result.successful(), isTrue);
}); });
}); });
} }

View file

@ -1,159 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('Pipe', () {
late Factory factory;
late Pipe pipe;
setUp(() {
factory = Factory();
pipe = Pipe(factory, (p) {});
});
test('executes processes sequentially', () async {
pipe.command('echo "hello world"');
pipe.command('tr "a-z" "A-Z"');
final result = await pipe.run();
expect(result.output().trim(), equals('HELLO WORLD'));
});
test('stops on first failure', () async {
pipe.command('echo "test"');
pipe.command('false');
pipe.command('echo "never reached"');
final result = await pipe.run();
expect(result.failed(), isTrue);
expect(result.output().trim(), equals('test'));
});
test('captures output in real time', () async {
final outputs = <String>[];
pipe.command('echo "line1"');
pipe.command('echo "line2"');
await pipe.run(output: (data) {
outputs.add(data.trim());
});
expect(outputs, equals(['line1', 'line2']));
});
test('pipes output between processes', () async {
pipe.command('echo "hello\nworld\nhello\ntest"');
pipe.command('sort');
pipe.command('uniq -c');
pipe.command('sort -nr');
final result = await pipe.run();
final lines = result.output().trim().split('\n');
expect(lines[0].trim(), contains('2 hello'));
expect(lines[1].trim(), contains('1 test'));
expect(lines[2].trim(), contains('1 world'));
});
test('supports process configuration', () async {
final pending = factory.newPendingProcess().command('pwd').path('/tmp');
pipe.command(pending.command);
pipe.command('grep tmp');
final result = await pipe.run();
expect(result.output().trim(), equals('/tmp'));
});
test('handles environment variables', () async {
final pending = factory
.newPendingProcess()
.command('printenv TEST_VAR')
.env({'TEST_VAR': 'test value'});
pipe.command(pending.command);
pipe.command('tr "a-z" "A-Z"');
final result = await pipe.run();
expect(result.output().trim(), equals('TEST VALUE'));
});
test('handles binary data', () async {
pipe.command('printf "\\x48\\x45\\x4C\\x4C\\x4F"'); // "HELLO"
pipe.command('cat');
final result = await pipe.run();
expect(result.output(), equals('HELLO'));
});
test('supports input redirection', () async {
final pending =
factory.newPendingProcess().command('cat').input('test input\n');
pipe.command(pending.command);
pipe.command('tr "a-z" "A-Z"');
final result = await pipe.run();
expect(result.output().trim(), equals('TEST INPUT'));
});
test('handles empty pipe', () async {
final result = await pipe.run();
expect(result.successful(), isTrue);
expect(result.output(), isEmpty);
});
test('preserves exit codes', () async {
pipe.command('echo "test"');
pipe.command('grep missing'); // Will fail
pipe.command('echo "never reached"');
final result = await pipe.run();
expect(result.failed(), isTrue);
expect(result.exitCode(), equals(1));
});
test('supports complex pipelines', () async {
// Create a file with test content
pipe.command('echo "apple\nbanana\napple\ncherry\nbanana"');
pipe.command('sort'); // Sort lines
pipe.command('uniq -c'); // Count unique lines
pipe.command('sort -nr'); // Sort by count
pipe.command('head -n 2'); // Get top 2
final result = await pipe.run();
final lines = result.output().trim().split('\n');
expect(lines.length, equals(2));
expect(lines[0].trim(), contains('2')); // Most frequent
expect(lines[1].trim(), contains('1')); // Less frequent
});
test('handles process timeouts', () async {
pipe.command('echo start');
final pending = factory.newPendingProcess().command('sleep 5').timeout(1);
pipe.command(pending.command);
pipe.command('echo never reached');
final result = await pipe.run();
expect(result.failed(), isTrue);
expect(result.output().trim(), equals('start'));
});
test('supports TTY mode', () async {
final pending = factory.newPendingProcess().command('test -t 0').tty();
pipe.command(pending.command);
final result = await pipe.run();
expect(result.successful(), isTrue);
});
test('handles process cleanup', () async {
pipe.command('sleep 10');
pipe.command('echo "never reached"');
// Start the pipe and immediately kill it
final future = pipe.run();
await Future.delayed(Duration(milliseconds: 100));
// Verify the pipe was cleaned up
final result = await future;
expect(result.failed(), isTrue);
});
});
}

View file

@ -1,187 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('Pool', () {
late Factory factory;
late Pool pool;
setUp(() {
factory = Factory();
pool = Pool(factory, (p) {});
});
test('executes processes concurrently', () async {
// Add processes that sleep for different durations
pool.command('bash -c "sleep 0.2 && echo 1"');
pool.command('bash -c "sleep 0.1 && echo 2"');
pool.command('echo 3');
final startTime = DateTime.now();
final results = await pool.start();
final duration = DateTime.now().difference(startTime);
// Should complete in ~0.2s, not ~0.3s
expect(duration.inMilliseconds, lessThan(300));
expect(results.length, equals(3));
expect(
results.map((r) => r.output().trim()),
containsAll(['1', '2', '3']),
);
});
test('captures output from all processes', () async {
final outputs = <String>[];
pool.command('echo 1');
pool.command('echo 2');
pool.command('echo 3');
await pool.start((output) {
outputs.add(output.trim());
});
expect(outputs, containsAll(['1', '2', '3']));
});
test('handles process failures', () async {
pool.command('echo success');
pool.command('false');
pool.command('echo also success');
final results = await pool.start();
final poolResults = ProcessPoolResults(results);
expect(poolResults.successful(), isFalse);
expect(poolResults.failed(), isTrue);
expect(results.length, equals(3));
});
test('throws if no processes added', () async {
final results = await pool.start();
expect(results, isEmpty);
});
test('supports process configuration', () async {
// Create processes with factory to configure them
final process1 =
factory.command('printenv TEST_VAR').env({'TEST_VAR': 'test value'});
final process2 = factory.command('pwd').path('/tmp');
// Add configured processes to pool
pool.command(process1); // Pass the PendingProcess directly
pool.command(process2); // Pass the PendingProcess directly
final results = await pool.start();
expect(results.length, equals(2));
expect(results[0].output().trim(), equals('test value'));
expect(results[1].output().trim(), equals('/tmp'));
});
group('ProcessPoolResults', () {
test('provides access to all results', () {
final results = [
ProcessResultImpl(
command: 'test1',
exitCode: 0,
output: 'output1',
errorOutput: '',
),
ProcessResultImpl(
command: 'test2',
exitCode: 1,
output: 'output2',
errorOutput: 'error2',
),
];
final poolResults = ProcessPoolResults(results);
expect(poolResults.results, equals(results));
});
test('indicates success when all processes succeed', () {
final results = [
ProcessResultImpl(
command: 'test1',
exitCode: 0,
output: '',
errorOutput: '',
),
ProcessResultImpl(
command: 'test2',
exitCode: 0,
output: '',
errorOutput: '',
),
];
final poolResults = ProcessPoolResults(results);
expect(poolResults.successful(), isTrue);
expect(poolResults.failed(), isFalse);
});
test('indicates failure when any process fails', () {
final results = [
ProcessResultImpl(
command: 'test1',
exitCode: 0,
output: '',
errorOutput: '',
),
ProcessResultImpl(
command: 'test2',
exitCode: 1,
output: '',
errorOutput: '',
),
];
final poolResults = ProcessPoolResults(results);
expect(poolResults.successful(), isFalse);
expect(poolResults.failed(), isTrue);
});
test('throws if any process failed', () {
final results = [
ProcessResultImpl(
command: 'test1',
exitCode: 0,
output: '',
errorOutput: '',
),
ProcessResultImpl(
command: 'test2',
exitCode: 1,
output: '',
errorOutput: 'error',
),
];
final poolResults = ProcessPoolResults(results);
expect(
() => poolResults.throwIfAnyFailed(),
throwsA(isA<Exception>()),
);
});
test('does not throw if all processes succeeded', () {
final results = [
ProcessResultImpl(
command: 'test1',
exitCode: 0,
output: '',
errorOutput: '',
),
ProcessResultImpl(
command: 'test2',
exitCode: 0,
output: '',
errorOutput: '',
),
];
final poolResults = ProcessPoolResults(results);
expect(() => poolResults.throwIfAnyFailed(), returnsNormally);
});
});
});
}

View file

@ -1,128 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('ProcessFailedException', () {
late ProcessResult failedResult;
late ProcessFailedException exception;
setUp(() {
failedResult = FakeProcessResult(
command: 'test command',
exitCode: 1,
output: 'test output',
errorOutput: 'test error',
);
exception = ProcessFailedException(failedResult);
});
test('provides access to failed result', () {
expect(exception.result, equals(failedResult));
});
test('formats error message', () {
final message = exception.toString();
expect(message, contains('test command'));
expect(message, contains('exit code 1'));
expect(message, contains('test output'));
expect(message, contains('test error'));
});
test('handles empty output', () {
failedResult = FakeProcessResult(
command: 'test command',
exitCode: 1,
);
exception = ProcessFailedException(failedResult);
final message = exception.toString();
expect(message, contains('(empty)'));
});
test('includes all error details', () {
failedResult = FakeProcessResult(
command: 'complex command',
exitCode: 127,
output: 'some output\nwith multiple lines',
errorOutput: 'error line 1\nerror line 2',
);
exception = ProcessFailedException(failedResult);
final message = exception.toString();
expect(message, contains('complex command'));
expect(message, contains('exit code 127'));
expect(message, contains('some output\nwith multiple lines'));
expect(message, contains('error line 1\nerror line 2'));
});
});
group('ProcessTimeoutException', () {
late ProcessResult timedOutResult;
late ProcessTimeoutException exception;
final timeout = Duration(seconds: 5);
setUp(() {
timedOutResult = FakeProcessResult(
command: 'long running command',
exitCode: -1,
output: 'partial output',
errorOutput: 'timeout occurred',
);
exception = ProcessTimeoutException(timedOutResult, timeout);
});
test('provides access to timed out result', () {
expect(exception.result, equals(timedOutResult));
});
test('provides access to timeout duration', () {
expect(exception.timeout, equals(timeout));
});
test('formats error message', () {
final message = exception.toString();
expect(message, contains('long running command'));
expect(message, contains('timed out after 5 seconds'));
expect(message, contains('partial output'));
expect(message, contains('timeout occurred'));
});
test('handles empty output', () {
timedOutResult = FakeProcessResult(
command: 'hanging command',
exitCode: -1,
);
exception = ProcessTimeoutException(timedOutResult, timeout);
final message = exception.toString();
expect(message, contains('hanging command'));
expect(message, contains('(empty)'));
});
test('handles different timeout durations', () {
final shortTimeout = Duration(milliseconds: 500);
exception = ProcessTimeoutException(timedOutResult, shortTimeout);
expect(exception.toString(), contains('timed out after 0 seconds'));
final longTimeout = Duration(minutes: 2);
exception = ProcessTimeoutException(timedOutResult, longTimeout);
expect(exception.toString(), contains('timed out after 120 seconds'));
});
test('includes all error details', () {
timedOutResult = FakeProcessResult(
command: 'complex command with args',
exitCode: -1,
output: 'output before timeout\nwith multiple lines',
errorOutput: 'error before timeout\nerror details',
);
exception = ProcessTimeoutException(timedOutResult, timeout);
final message = exception.toString();
expect(message, contains('complex command with args'));
expect(message, contains('timed out after 5 seconds'));
expect(message, contains('output before timeout\nwith multiple lines'));
expect(message, contains('error before timeout\nerror details'));
});
});
}

View file

@ -1,144 +0,0 @@
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
void main() {
group('ProcessPoolResults', () {
late List<ProcessResult> results;
late ProcessPoolResults poolResults;
setUp(() {
results = [
FakeProcessResult(
command: 'success1',
exitCode: 0,
output: 'output1',
),
FakeProcessResult(
command: 'failure',
exitCode: 1,
errorOutput: 'error',
),
FakeProcessResult(
command: 'success2',
exitCode: 0,
output: 'output2',
),
];
poolResults = ProcessPoolResults(results);
});
test('provides access to all results', () {
expect(poolResults.results, equals(results));
expect(poolResults.total, equals(3));
expect(poolResults[0], equals(results[0]));
expect(poolResults[1], equals(results[1]));
expect(poolResults[2], equals(results[2]));
});
test('indicates overall success/failure', () {
// With mixed results
expect(poolResults.successful(), isFalse);
expect(poolResults.failed(), isTrue);
// With all successes
results = List.generate(
3,
(i) => FakeProcessResult(
command: 'success$i',
exitCode: 0,
output: 'output$i',
),
);
poolResults = ProcessPoolResults(results);
expect(poolResults.successful(), isTrue);
expect(poolResults.failed(), isFalse);
// With all failures
results = List.generate(
3,
(i) => FakeProcessResult(
command: 'failure$i',
exitCode: 1,
errorOutput: 'error$i',
),
);
poolResults = ProcessPoolResults(results);
expect(poolResults.successful(), isFalse);
expect(poolResults.failed(), isTrue);
});
test('provides success and failure counts', () {
expect(poolResults.successCount, equals(2));
expect(poolResults.failureCount, equals(1));
expect(poolResults.total, equals(3));
});
test('provides filtered results', () {
expect(poolResults.successes.length, equals(2));
expect(poolResults.failures.length, equals(1));
expect(poolResults.successes[0].command(), equals('success1'));
expect(poolResults.successes[1].command(), equals('success2'));
expect(poolResults.failures[0].command(), equals('failure'));
});
test('throws if any process failed', () {
expect(
() => poolResults.throwIfAnyFailed(),
throwsA(isA<Exception>().having(
(e) => e.toString(),
'message',
contains('One or more processes in the pool failed'),
)),
);
// Should not throw with all successes
results = List.generate(
3,
(i) => FakeProcessResult(
command: 'success$i',
exitCode: 0,
output: 'output$i',
),
);
poolResults = ProcessPoolResults(results);
expect(() => poolResults.throwIfAnyFailed(), returnsNormally);
});
test('formats failure messages', () {
try {
poolResults.throwIfAnyFailed();
} catch (e) {
final message = e.toString();
expect(message, contains('failure'));
expect(message, contains('exit code 1'));
expect(message, contains('error'));
}
});
test('handles empty results', () {
poolResults = ProcessPoolResults([]);
expect(poolResults.total, equals(0));
expect(poolResults.successful(), isTrue);
expect(poolResults.failed(), isFalse);
expect(poolResults.successCount, equals(0));
expect(poolResults.failureCount, equals(0));
expect(poolResults.successes, isEmpty);
expect(poolResults.failures, isEmpty);
});
test('provides first and last results', () {
expect(poolResults.first, equals(results.first));
expect(poolResults.last, equals(results.last));
});
test('checks emptiness', () {
expect(poolResults.isEmpty, isFalse);
expect(poolResults.isNotEmpty, isTrue);
poolResults = ProcessPoolResults([]);
expect(poolResults.isEmpty, isTrue);
expect(poolResults.isNotEmpty, isFalse);
});
});
}

View file

@ -1,115 +1,70 @@
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:platform_process/process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
group('ProcessResult', () { group('ProcessResult', () {
late ProcessResultImpl result; test('successful process is detected correctly', () {
final result = ProcessResult(0, 'output', '');
setUp(() {
result = ProcessResultImpl(
command: 'test-command',
exitCode: 0,
output: 'test output',
errorOutput: 'test error',
);
});
test('returns command', () {
expect(result.command(), equals('test-command'));
});
test('indicates success when exit code is 0', () {
expect(result.successful(), isTrue); expect(result.successful(), isTrue);
expect(result.failed(), isFalse); expect(result.failed(), isFalse);
}); });
test('indicates failure when exit code is non-zero', () { test('failed process is detected correctly', () {
result = ProcessResultImpl( final result = ProcessResult(1, '', 'error');
command: 'test-command',
exitCode: 1,
output: '',
errorOutput: '',
);
expect(result.successful(), isFalse); expect(result.successful(), isFalse);
expect(result.failed(), isTrue); expect(result.failed(), isTrue);
}); });
test('returns exit code', () { test('output methods return correct streams', () {
expect(result.exitCode(), equals(0)); final result = ProcessResult(0, 'stdout', 'stderr');
expect(result.output(), equals('stdout'));
expect(result.errorOutput(), equals('stderr'));
}); });
test('returns output', () { test('toString returns stdout', () {
expect(result.output(), equals('test output')); final result = ProcessResult(0, 'test output', 'error output');
expect(result.toString(), equals('test output'));
}); });
test('returns error output', () { test('empty output is handled correctly', () {
expect(result.errorOutput(), equals('test error')); final result = ProcessResult(0, '', '');
expect(result.output(), isEmpty);
expect(result.errorOutput(), isEmpty);
}); });
test('checks output content', () { test('exit code is accessible', () {
expect(result.seeInOutput('test'), isTrue); final result = ProcessResult(123, '', '');
expect(result.seeInOutput('missing'), isFalse); expect(result.exitCode, equals(123));
}); });
test('checks error output content', () { test('multiline output is preserved', () {
expect(result.seeInErrorOutput('error'), isTrue); final stdout = 'line1\nline2\nline3';
expect(result.seeInErrorOutput('missing'), isFalse); final stderr = 'error1\nerror2';
final result = ProcessResult(0, stdout, stderr);
expect(result.output(), equals(stdout));
expect(result.errorOutput(), equals(stderr));
}); });
test('throwIfFailed does not throw on success', () { test('whitespace in output is preserved', () {
expect(() => result.throwIfFailed(), returnsNormally); final stdout = ' leading and trailing spaces ';
final result = ProcessResult(0, stdout, '');
expect(result.output(), equals(stdout));
}); });
test('throwIfFailed throws on failure', () { test('non-zero exit code indicates failure', () {
result = ProcessResultImpl( for (var code in [1, 2, 127, 255]) {
command: 'test-command', final result = ProcessResult(code, '', '');
exitCode: 1, expect(result.failed(), isTrue,
output: 'failed output', reason: 'Exit code $code should indicate failure');
errorOutput: 'error message', expect(result.successful(), isFalse,
); reason: 'Exit code $code should not indicate success');
}
expect(
() => result.throwIfFailed(), throwsA(isA<ProcessFailedException>()));
}); });
test('throwIfFailed executes callback before throwing', () { test('zero exit code indicates success', () {
result = ProcessResultImpl( final result = ProcessResult(0, '', '');
command: 'test-command', expect(result.successful(), isTrue);
exitCode: 1, expect(result.failed(), isFalse);
output: '',
errorOutput: '',
);
var callbackExecuted = false;
expect(
() => result.throwIfFailed((result, exception) {
callbackExecuted = true;
}),
throwsA(isA<ProcessFailedException>()));
expect(callbackExecuted, isTrue);
});
test('throwIf respects condition', () {
expect(() => result.throwIf(false), returnsNormally);
expect(() => result.throwIf(true), returnsNormally);
result = ProcessResultImpl(
command: 'test-command',
exitCode: 1,
output: '',
errorOutput: '',
);
expect(() => result.throwIf(false), returnsNormally);
expect(
() => result.throwIf(true), throwsA(isA<ProcessFailedException>()));
});
test('toString includes command and outputs', () {
final string = result.toString();
expect(string, contains('test-command'));
expect(string, contains('test output'));
expect(string, contains('test error'));
}); });
}); });
} }

View file

@ -1,5 +1,5 @@
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:test_process/test_process.dart'; import 'package:platform_process/platform_process.dart';
void main() { void main() {
late Factory factory; late Factory factory;

View file

@ -1,90 +0,0 @@
import 'dart:io';
import 'package:test/test.dart';
import 'package:platform_process/process.dart';
import 'helpers/test_helpers.dart';
/// Test configuration and utilities.
class TestConfig {
/// List of temporary files created during tests.
static final List<FileSystemEntity> _tempFiles = [];
/// Configure test environment and add common test utilities.
static void configure() {
setUp(() {
// Clear temp files list at start of each test
_tempFiles.clear();
});
tearDown(() async {
// Clean up any test files created during the test
await cleanupTempFiles(_tempFiles);
// Clean up any remaining test files in temp directory
final tempDir = Directory.systemTemp;
if (await tempDir.exists()) {
await for (final entity in tempDir.list()) {
if (entity.path.contains('test_')) {
await entity.delete(recursive: true);
}
}
}
});
}
/// Creates a temporary file that will be cleaned up after the test.
static Future<File> createTrackedTempFile(String content) async {
final file = await createTempFile(content);
_tempFiles.add(file);
return file;
}
/// Creates a temporary directory that will be cleaned up after the test.
static Future<Directory> createTrackedTempDir() async {
final dir = await createTempDir();
_tempFiles.add(dir);
return dir;
}
/// Creates a test directory structure that will be cleaned up after the test.
static Future<Directory> createTrackedTestDirectoryStructure(
Map<String, String> files,
) async {
final dir = await createTestDirectoryStructure(files);
_tempFiles.add(dir);
return dir;
}
/// Runs a test with a temporary directory that gets cleaned up.
static Future<T> withTrackedTempDir<T>(
Future<T> Function(Directory dir) test,
) async {
final dir = await createTrackedTempDir();
return test(dir);
}
/// Creates a factory with test-specific fake handlers.
static Factory createTestFactoryWithFakes(Map<String, dynamic> fakes) {
final factory = createTestFactory();
factory.fake(fakes);
return factory;
}
}
/// Extension methods for test utilities.
extension TestUtilsExtension on Directory {
/// Creates a file in this directory with the given name and content.
Future<File> createFile(String name, String content) async {
final file = File('${path}/$name');
await file.writeAsString(content);
return file;
}
/// Creates multiple files in this directory.
Future<List<File>> createFiles(Map<String, String> files) async {
final createdFiles = <File>[];
for (final entry in files.entries) {
createdFiles.add(await createFile(entry.key, entry.value));
}
return createdFiles;
}
}

View file

@ -1,119 +0,0 @@
#!/usr/bin/env bash
set -e
# Change to the project root directory
cd "$(dirname "$0")/.."
# Default values
coverage=false
unit_only=false
integration_only=false
watch=false
# Print usage information
function print_usage() {
echo "Usage: $0 [options]"
echo "Options:"
echo " --coverage Generate coverage report"
echo " --unit Run only unit tests"
echo " --integration Run only integration tests"
echo " --watch Run tests in watch mode"
echo " --help Show this help message"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--coverage)
coverage=true
shift
;;
--unit)
unit_only=true
shift
;;
--integration)
integration_only=true
shift
;;
--watch)
watch=true
shift
;;
--help)
print_usage
exit 0
;;
*)
echo "Unknown option: $1"
print_usage
exit 1
;;
esac
done
# Clean up previous runs
rm -rf coverage .dart_tool/test
# Ensure dependencies are up to date
echo "Ensuring dependencies are up to date..."
dart pub get
# Run tests based on options
if [ "$unit_only" = true ]; then
echo "Running unit tests..."
if [ "$watch" = true ]; then
dart test --tags unit --watch
else
dart test --tags unit
fi
elif [ "$integration_only" = true ]; then
echo "Running integration tests..."
if [ "$watch" = true ]; then
dart test --tags integration --watch
else
dart test --tags integration
fi
else
echo "Running all tests..."
if [ "$coverage" = true ]; then
echo "Collecting coverage..."
# Ensure coverage package is activated
dart pub global activate coverage
# Run tests with coverage
dart test --coverage="coverage"
# Format coverage data
dart pub global run coverage:format_coverage \
--lcov \
--in=coverage \
--out=coverage/lcov.info \
--packages=.packages \
--report-on=lib \
--check-ignore
# Generate HTML report if lcov is installed
if command -v genhtml >/dev/null 2>&1; then
echo "Generating HTML coverage report..."
genhtml coverage/lcov.info -o coverage/html
echo "Coverage report generated at coverage/html/index.html"
else
echo "lcov not installed, skipping HTML report generation"
echo "Install lcov for HTML reports:"
echo " brew install lcov # macOS"
echo " apt-get install lcov # Ubuntu"
fi
elif [ "$watch" = true ]; then
dart test --watch
else
dart test
fi
fi
# Print summary
echo
echo "Test execution completed"
if [ "$coverage" = true ]; then
echo "Coverage information available in coverage/lcov.info"
fi