refactor: promoted test_process to process package
This commit is contained in:
parent
7e6ccf47bd
commit
942209900a
72 changed files with 1130 additions and 7543 deletions
|
@ -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.
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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"
|
|
|
@ -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
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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)));
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
86
packages/process/.gitignore
vendored
86
packages/process/.gitignore
vendored
|
@ -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/
|
|
||||||
|
|
|
@ -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/
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
final factory = Factory();
|
final factory = Factory();
|
||||||
|
|
||||||
// Simple command execution
|
// Basic command execution
|
||||||
final result = await factory
|
final result = await factory.command(['echo', 'Hello World']).run();
|
||||||
.command('echo "Hello, World!"')
|
print('Output: ${result.output()}'); // Output: Hello World
|
||||||
.run();
|
print('Success: ${result.successful()}'); // Success: true
|
||||||
print(result.output());
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
}).run();
|
|
||||||
|
|
||||||
print('Lines matching pattern: ${result.output()}');
|
// Execute command in specific directory
|
||||||
|
final result = await factory
|
||||||
|
.command(['ls', '-l'])
|
||||||
|
.withWorkingDirectory('/tmp')
|
||||||
|
.run();
|
||||||
|
|
||||||
|
print('Files in /tmp:');
|
||||||
|
print(result.output());
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Process Input
|
### Environment Variables
|
||||||
|
|
||||||
Provide input to processes:
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
|
void main() async {
|
||||||
|
final factory = Factory();
|
||||||
|
|
||||||
final result = await factory
|
final result = await factory
|
||||||
.command('cat')
|
.command(['printenv', 'MY_VAR'])
|
||||||
.input('Hello, World!')
|
.withEnvironment({'MY_VAR': 'Hello from env!'})
|
||||||
.run();
|
.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.
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
0
packages/process/doc/.gitkeep
Normal file
0
packages/process/doc/.gitkeep
Normal 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
|
|
|
@ -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)
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 testResult = await factory
|
final result = await process.wait();
|
||||||
.command('test-command')
|
print('Final Exit Code: ${result.exitCode}');
|
||||||
.run((output) => print('Real-time output: $output'));
|
} catch (e) {
|
||||||
|
print('Error: $e');
|
||||||
print('Test result: ${testResult.output().trim()}');
|
}
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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';
|
|
@ -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;
|
|
|
@ -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]);
|
|
||||||
}
|
|
|
@ -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:');
|
||||||
Error Output:
|
buffer.writeln(_result.output().trim());
|
||||||
${result.errorOutput().isEmpty ? '(empty)' : result.errorOutput()}
|
|
||||||
''';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exception thrown when a process times out.
|
if (_result.errorOutput().isNotEmpty) {
|
||||||
class ProcessTimeoutException implements Exception {
|
buffer.writeln();
|
||||||
/// The process result that caused this exception.
|
buffer.writeln('Error Output:');
|
||||||
final ProcessResult result;
|
buffer.writeln(_result.errorOutput().trim());
|
||||||
|
}
|
||||||
|
|
||||||
/// The timeout duration that was exceeded.
|
return buffer.toString();
|
||||||
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()}
|
|
||||||
''';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
if (command is String && command.trim().isEmpty) {
|
||||||
Pool pool(void Function(Pool) callback) {
|
throw ArgumentError('Command string cannot be empty');
|
||||||
return Pool(this, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start defining a series of piped processes.
|
if (command is List) {
|
||||||
Pipe pipeThrough(void Function(Pipe) callback) {
|
if (command.isEmpty) {
|
||||||
return Pipe(this, callback);
|
throw ArgumentError('Command list cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a pool of processes concurrently.
|
if (command.any((element) => element is! String)) {
|
||||||
Future<List<ProcessResult>> concurrently(
|
throw ArgumentError('Command list must contain only strings');
|
||||||
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;
|
|
||||||
for (final process in processes) {
|
|
||||||
result = await process.run(onOutput);
|
|
||||||
if (result.failed()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicate that the process factory should fake processes.
|
|
||||||
Factory fake([Map<String, dynamic>? fakes]) {
|
|
||||||
_recording = true;
|
|
||||||
|
|
||||||
if (fakes != null) {
|
|
||||||
for (final entry in fakes.entries) {
|
|
||||||
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.
|
return PendingProcess(this)..withCommand(command);
|
||||||
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.
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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,
|
onDone: () => _stdoutCompleter.complete(),
|
||||||
cancelOnError: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_stderrSubscription = _process.stderr.listen(
|
_process.stderr.listen(
|
||||||
(data) {
|
(data) {
|
||||||
_stderrController.add(data);
|
_stderr.addAll(data);
|
||||||
_handleOutput(data, _errorBuffer);
|
if (_onOutput != null) {
|
||||||
|
_onOutput!(String.fromCharCodes(data));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDone: _stderrController.close,
|
onDone: () => _stderrCompleter.complete(),
|
||||||
cancelOnError: false,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle output data.
|
// Track when the process completes
|
||||||
void _handleOutput(List<int> data, StringBuffer buffer) {
|
_process.exitCode.then((_) => _completed = true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the process ID.
|
/// Get the process ID.
|
||||||
int get pid => _process.pid;
|
int get pid => _process.pid;
|
||||||
|
|
||||||
/// Kill the process.
|
|
||||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
|
|
||||||
closeStdin();
|
|
||||||
_process.kill(signal);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
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.
|
/// Write data to the process stdin.
|
||||||
Future<void> write(String input) async {
|
void write(dynamic input) {
|
||||||
try {
|
if (input is String) {
|
||||||
_process.stdin.write(input);
|
_process.stdin.write(input);
|
||||||
await _process.stdin.flush();
|
} else if (input is List<int>) {
|
||||||
if (input.endsWith('\n')) {
|
_process.stdin.add(input);
|
||||||
await _process.stdin.close();
|
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write lines to the process stdin.
|
/// Close 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) {
|
|
||||||
if (command is List<String>) {
|
|
||||||
// For list commands, use directly without shell
|
|
||||||
if (command[0] == 'echo') {
|
|
||||||
// Special handling for echo command
|
|
||||||
if (io.Platform.isWindows) {
|
|
||||||
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) {
|
|
||||||
throw ArgumentError('Command must be a string or list of strings');
|
|
||||||
}
|
|
||||||
|
|
||||||
final commandStr = command.toString();
|
|
||||||
|
|
||||||
// Handle platform-specific shell commands
|
|
||||||
if (io.Platform.isWindows) {
|
|
||||||
if (commandStr.startsWith('cmd /c')) {
|
|
||||||
// Already properly formatted for Windows, pass through directly
|
|
||||||
return ('cmd.exe', ['/c', commandStr.substring(6)], true);
|
|
||||||
}
|
|
||||||
// All other commands need cmd.exe shell
|
|
||||||
return ('cmd.exe', ['/c', commandStr], true);
|
|
||||||
} else {
|
|
||||||
if (commandStr == 'test -t 0') {
|
|
||||||
// Special handling for TTY test command
|
|
||||||
return ('sh', ['-c', 'exit 0'], true);
|
|
||||||
} else if (commandStr == 'pwd') {
|
|
||||||
// Special handling for pwd command
|
|
||||||
return ('pwd', [], false);
|
|
||||||
}
|
|
||||||
// All other commands need sh shell
|
|
||||||
return ('sh', ['-c', commandStr], true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run the process.
|
|
||||||
Future<ProcessResult> run(
|
Future<ProcessResult> run(
|
||||||
[dynamic commandOrCallback, dynamic callback]) async {
|
[dynamic command, void Function(String)? onOutput]) async {
|
||||||
// Handle overloaded parameters
|
this.command = command ?? this.command;
|
||||||
dynamic actualCommand = _command;
|
|
||||||
void Function(String)? outputCallback;
|
|
||||||
|
|
||||||
if (commandOrCallback != null) {
|
if (this.command == null) {
|
||||||
if (commandOrCallback is void Function(String)) {
|
throw ArgumentError('No command specified');
|
||||||
outputCallback = commandOrCallback;
|
|
||||||
} else {
|
|
||||||
actualCommand = commandOrCallback;
|
|
||||||
if (callback != null && callback is void Function(String)) {
|
|
||||||
outputCallback = callback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actualCommand == null) {
|
// Handle immediate timeout
|
||||||
throw ArgumentError('No command has been specified.');
|
if (timeout == 0) {
|
||||||
}
|
throw ProcessTimedOutException(
|
||||||
|
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.',
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleError(String data) {
|
/// Start the process asynchronously.
|
||||||
stderrBuffer.write(data);
|
Future<InvokedProcess> start([void Function(String)? onOutput]) async {
|
||||||
|
if (command == null) {
|
||||||
if (!_quietly && outputCallback != null) {
|
throw ArgumentError('No command specified');
|
||||||
final lines = data.split('\n');
|
|
||||||
for (var line in lines) {
|
|
||||||
final trimmed = line.trim();
|
|
||||||
if (trimmed.isNotEmpty) {
|
|
||||||
outputCallback(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final stdoutCompleter = Completer<void>();
|
try {
|
||||||
final stderrCompleter = Completer<void>();
|
final process = await _createProcess();
|
||||||
|
|
||||||
final stdoutSubscription = process.stdout
|
if (input != null) {
|
||||||
.transform(utf8.decoder)
|
if (input is String) {
|
||||||
.listen(handleOutput, onDone: stdoutCompleter.complete);
|
process.stdin.write(input);
|
||||||
|
} else if (input is List<int>) {
|
||||||
final stderrSubscription = process.stderr
|
process.stdin.add(input);
|
||||||
.transform(utf8.decoder)
|
|
||||||
.listen(handleError, onDone: stderrCompleter.complete);
|
|
||||||
|
|
||||||
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();
|
await process.stdin.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
int? exitCode;
|
return InvokedProcess(process, onOutput);
|
||||||
if (_timeout != null) {
|
} on ProcessException catch (e) {
|
||||||
try {
|
final result = ProcessResult(1, '', e.message);
|
||||||
exitCode = await process.exitCode.timeout(Duration(seconds: _timeout!));
|
throw ProcessFailedException(result);
|
||||||
} 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
|
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([
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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'}
|
|
||||||
''';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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/**
|
|
||||||
|
|
|
@ -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
|
|
||||||
// });
|
|
||||||
// }
|
|
|
@ -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', () {
|
|
@ -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();
|
test('command() with invalid type throws ArgumentError', () {
|
||||||
expect(ls.output().trim(), equals('file1.txt\nfile2.txt'));
|
expect(() => factory.command(123), throwsArgumentError);
|
||||||
|
|
||||||
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', () {
|
test('command() with list containing non-string throws ArgumentError', () {
|
||||||
factory.fake().preventStrayProcesses();
|
expect(() => factory.command(['echo', 123]), throwsArgumentError);
|
||||||
|
|
||||||
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('handles command failures', () async {
|
|
||||||
final result = await factory.command('false').run();
|
|
||||||
expect(result.failed(), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles invalid commands', () {
|
|
||||||
expect(
|
|
||||||
() => 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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 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 =
|
final result =
|
||||||
await process.command('pwd').path(Directory.current.path).run();
|
await factory.command(['echo', 'test']).withoutOutput().run();
|
||||||
expect(result.output().trim(), equals(Directory.current.path));
|
expect(result.output(), isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('configures environment variables', () async {
|
test('idle timeout triggers', () async {
|
||||||
final result = await process
|
if (!Platform.isWindows) {
|
||||||
.command('printenv TEST_VAR')
|
// Use tail -f to wait indefinitely without producing output
|
||||||
.env({'TEST_VAR': 'test_value'}).run();
|
final process =
|
||||||
expect(result.output().trim(), equals('test_value'));
|
factory.command(['tail', '-f', '/dev/null']).withIdleTimeout(1);
|
||||||
});
|
|
||||||
|
|
||||||
test('handles string input', () async {
|
await expectLater(
|
||||||
final result = await process.command('cat').input('test input\n').run();
|
process.run(),
|
||||||
expect(result.output().trim(), equals('test input'));
|
throwsA(
|
||||||
});
|
allOf(
|
||||||
|
isA<ProcessTimedOutException>(),
|
||||||
test('handles list input', () async {
|
predicate((ProcessTimedOutException e) =>
|
||||||
final result = await process
|
e.message.contains('exceeded the idle timeout of 1 seconds')),
|
||||||
.command('cat')
|
),
|
||||||
.input([116, 101, 115, 116]) // "test" in bytes
|
),
|
||||||
.run();
|
|
||||||
expect(result.output(), equals('test'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('respects timeout', () async {
|
|
||||||
// Use a longer timeout to avoid flakiness
|
|
||||||
process.command('sleep 5').timeout(1);
|
|
||||||
|
|
||||||
final startTime = DateTime.now();
|
|
||||||
try {
|
|
||||||
await process.run();
|
|
||||||
fail('Expected ProcessTimeoutException');
|
|
||||||
} catch (e) {
|
|
||||||
expect(e, isA<ProcessTimeoutException>());
|
|
||||||
final duration = DateTime.now().difference(startTime);
|
|
||||||
expect(duration.inSeconds, lessThanOrEqualTo(2)); // Allow some buffer
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('runs forever when timeout disabled', () async {
|
|
||||||
final result = await process.command('echo test').forever().run();
|
|
||||||
expect(result.successful(), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('captures output in real time', () async {
|
|
||||||
final output = <String>[];
|
|
||||||
|
|
||||||
// Create a platform-independent way to generate sequential output
|
|
||||||
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 {
|
|
||||||
final output = <String>[];
|
|
||||||
final result = await process
|
|
||||||
.command('echo test')
|
|
||||||
.quietly()
|
|
||||||
.run((String data) => output.add(data));
|
|
||||||
|
|
||||||
expect(output, isEmpty);
|
|
||||||
expect(result.output(), isNotEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('enables TTY mode', () async {
|
|
||||||
final result = await process.command('test -t 0').tty().run();
|
|
||||||
expect(result.successful(), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('starts process in background', () async {
|
|
||||||
final proc = await process.command('sleep 1').start();
|
|
||||||
expect(proc.pid, isPositive);
|
|
||||||
await proc.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws on invalid command', () {
|
|
||||||
expect(
|
|
||||||
() => process.run(),
|
|
||||||
throwsA(isA<ArgumentError>().having(
|
|
||||||
(e) => e.message,
|
|
||||||
'message',
|
|
||||||
'No command has been specified.',
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}, 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('handles command as list', () async {
|
test('string command is executed through shell', () async {
|
||||||
final result = await process.command(['echo', 'test']).run();
|
if (!Platform.isWindows) {
|
||||||
expect(result.output().trim(), equals('test'));
|
final result = await factory.command('echo "Hello from shell"').run();
|
||||||
|
expect(result.output().trim(), equals('Hello from shell'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('preserves environment isolation', () async {
|
test('input as bytes is handled', () async {
|
||||||
// First process
|
final process =
|
||||||
final result1 = await process
|
factory.command(['cat']).withInput([72, 101, 108, 108, 111]);
|
||||||
.command('printenv TEST_VAR')
|
final result = await process.run();
|
||||||
.env({'TEST_VAR': 'value1'}).run();
|
expect(result.output().trim(), equals('Hello'));
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
Loading…
Reference in a new issue