incubate: adding test process package 10 pass 1 fail
This commit is contained in:
parent
bf7e607b4d
commit
4dbcb45836
21 changed files with 1796 additions and 67 deletions
159
incubation/test_process/README.md
Normal file
159
incubation/test_process/README.md
Normal file
|
@ -0,0 +1,159 @@
|
|||
# Test Process
|
||||
|
||||
A Laravel-compatible process management implementation in pure Dart. This package provides a robust way to execute and manage system processes with features like timeouts, input/output handling, and asynchronous execution.
|
||||
|
||||
## Features
|
||||
|
||||
- 💫 Fluent API for process configuration
|
||||
- ⏱️ Process timeout support
|
||||
- 🔄 Asynchronous process execution
|
||||
- 📥 Input/output handling
|
||||
- 🌍 Environment variables support
|
||||
- 📁 Working directory configuration
|
||||
- 🚦 TTY mode support
|
||||
- 🤫 Quiet mode for suppressing output
|
||||
- ⚡ Process pooling capabilities
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your package's `pubspec.yaml` file:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
test_process: ^1.0.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Command Execution
|
||||
|
||||
```dart
|
||||
import 'package:test_process/test_process.dart';
|
||||
|
||||
void main() async {
|
||||
final factory = Factory();
|
||||
|
||||
// Simple command execution
|
||||
final result = await factory.run('echo "Hello World"');
|
||||
print(result.output());
|
||||
}
|
||||
```
|
||||
|
||||
### Configuring Process Execution
|
||||
|
||||
```dart
|
||||
final result = await factory
|
||||
.command('ls -la')
|
||||
.withWorkingDirectory('/tmp')
|
||||
.withEnvironment({'CUSTOM_VAR': 'value'})
|
||||
.withTimeout(30)
|
||||
.run();
|
||||
|
||||
print('Exit code: ${result.exitCode}');
|
||||
print('Output: ${result.output()}');
|
||||
print('Error output: ${result.errorOutput()}');
|
||||
```
|
||||
|
||||
### Asynchronous Process Execution
|
||||
|
||||
```dart
|
||||
final process = await factory
|
||||
.command('long-running-command')
|
||||
.withTimeout(60)
|
||||
.start();
|
||||
|
||||
print('Process started with PID: ${process.pid}');
|
||||
|
||||
// Wait for completion
|
||||
final result = await process.wait();
|
||||
print('Process completed with exit code: ${result.exitCode}');
|
||||
```
|
||||
|
||||
### Process Input/Output
|
||||
|
||||
```dart
|
||||
// Provide input to process
|
||||
final result = await factory
|
||||
.command('cat')
|
||||
.withInput('Hello from stdin!')
|
||||
.run();
|
||||
|
||||
// Disable output
|
||||
await factory
|
||||
.command('noisy-command')
|
||||
.withoutOutput()
|
||||
.run();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```dart
|
||||
try {
|
||||
await factory.run('nonexistent-command');
|
||||
} on ProcessFailedException catch (e) {
|
||||
print('Process failed with exit code: ${e.exitCode}');
|
||||
print('Error output: ${e.errorOutput}');
|
||||
} on ProcessTimedOutException catch (e) {
|
||||
print('Process timed out: ${e.message}');
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Factory
|
||||
|
||||
The main entry point for creating and running processes.
|
||||
|
||||
- `run(command)` - Run a command synchronously
|
||||
- `command(command)` - Begin configuring a command
|
||||
- `path(directory)` - Begin configuring a command with a working directory
|
||||
|
||||
### PendingProcess
|
||||
|
||||
Configures how a process should be run.
|
||||
|
||||
- `withCommand(command)` - Set the command to run
|
||||
- `withWorkingDirectory(directory)` - Set the working directory
|
||||
- `withTimeout(seconds)` - Set the process timeout
|
||||
- `withIdleTimeout(seconds)` - Set the idle timeout
|
||||
- `withEnvironment(env)` - Set environment variables
|
||||
- `withInput(input)` - Provide input to the process
|
||||
- `withoutOutput()` - Disable process output
|
||||
- `withTty()` - Enable TTY mode
|
||||
- `forever()` - Disable timeout
|
||||
- `run()` - Run the process
|
||||
- `start()` - Start the process asynchronously
|
||||
|
||||
### ProcessResult
|
||||
|
||||
Represents the result of a completed process.
|
||||
|
||||
- `exitCode` - The process exit code
|
||||
- `output()` - The process standard output
|
||||
- `errorOutput()` - The process error output
|
||||
- `successful()` - Whether the process was successful
|
||||
- `failed()` - Whether the process failed
|
||||
|
||||
### InvokedProcess
|
||||
|
||||
Represents a running process.
|
||||
|
||||
- `pid` - The process ID
|
||||
- `write(input)` - Write to the process stdin
|
||||
- `kill([signal])` - Send a signal to the process
|
||||
- `wait()` - Wait for the process to complete
|
||||
|
||||
## Error Handling
|
||||
|
||||
The package provides two main exception types:
|
||||
|
||||
- `ProcessFailedException` - Thrown when a process exits with a non-zero code
|
||||
- `ProcessTimedOutException` - Thrown when a process exceeds its timeout
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This package is open-sourced software licensed under the MIT license.
|
95
incubation/test_process/example/example.dart
Normal file
95
incubation/test_process/example/example.dart
Normal file
|
@ -0,0 +1,95 @@
|
|||
import 'package:test_process/test_process.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
// Create a process factory
|
||||
final factory = Factory();
|
||||
|
||||
print('\n1. Basic command execution:');
|
||||
try {
|
||||
final result = await factory.command(['echo', 'Hello', 'World']).run();
|
||||
print('Output: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n2. Command with working directory and environment:');
|
||||
try {
|
||||
final result = await factory
|
||||
.command(['ls', '-la'])
|
||||
.withWorkingDirectory('/tmp')
|
||||
.withEnvironment({'CUSTOM_VAR': 'value'})
|
||||
.run();
|
||||
print('Directory contents: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n3. Asynchronous process with timeout:');
|
||||
try {
|
||||
final process =
|
||||
await factory.command(['sleep', '1']).withTimeout(5).start();
|
||||
|
||||
print('Process started with PID: ${process.pid}');
|
||||
final result = await process.wait();
|
||||
print('Async process completed with exit code: ${result.exitCode}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n4. Process with input:');
|
||||
try {
|
||||
final result =
|
||||
await factory.command(['cat']).withInput('Hello from stdin!').run();
|
||||
print('Output from cat: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n5. Error handling:');
|
||||
try {
|
||||
await factory.command(['nonexistent-command']).run();
|
||||
} on ProcessFailedException catch (e) {
|
||||
print('Expected error caught: ${e.toString()}');
|
||||
}
|
||||
|
||||
print('\n6. Quiet process (no output):');
|
||||
try {
|
||||
await factory
|
||||
.command(['echo', 'This should not be visible'])
|
||||
.withoutOutput()
|
||||
.run();
|
||||
print('Process completed silently');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n7. Shell command with pipes:');
|
||||
try {
|
||||
final result = await factory
|
||||
.command(['/bin/sh', '-c', 'echo Hello | tr a-z A-Z']).run();
|
||||
print('Output: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n8. Multiple commands with shell:');
|
||||
try {
|
||||
final result = await factory
|
||||
.command(['/bin/sh', '-c', 'echo Start && sleep 1 && echo End']).run();
|
||||
print('Output: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
print('\n9. Complex shell command:');
|
||||
try {
|
||||
final result = await factory.command([
|
||||
'/bin/sh',
|
||||
'-c',
|
||||
r'for i in 1 2 3; do echo "Count: $i"; sleep 0.1; done'
|
||||
]).run();
|
||||
print('Output: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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('The command failed.');
|
||||
buffer.writeln();
|
||||
buffer.writeln();
|
||||
buffer.writeln('Exit Code: ${_result.exitCode}');
|
||||
|
||||
if (_result.output().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln('Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(_result.output());
|
||||
}
|
||||
|
||||
if (_result.errorOutput().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln('Error Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(_result.errorOutput());
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import '../process_result.dart';
|
||||
|
||||
/// Exception thrown when a process times out.
|
||||
class ProcessTimedOutException implements Exception {
|
||||
/// The error message.
|
||||
final String message;
|
||||
|
||||
/// The process result, if available.
|
||||
final ProcessResult? result;
|
||||
|
||||
/// Create a new process timed out exception instance.
|
||||
ProcessTimedOutException(this.message, [this.result]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer(message);
|
||||
|
||||
if (result != null) {
|
||||
if (result!.output().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln();
|
||||
buffer.writeln('Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(result!.output());
|
||||
}
|
||||
|
||||
if (result!.errorOutput().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln('Error Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(result!.errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
65
incubation/test_process/lib/src/factory.dart
Normal file
65
incubation/test_process/lib/src/factory.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
import 'pending_process.dart';
|
||||
import 'process_result.dart';
|
||||
import 'invoked_process.dart';
|
||||
|
||||
/// A factory for creating process instances.
|
||||
class Factory {
|
||||
/// Create a new process factory instance.
|
||||
Factory();
|
||||
|
||||
/// Begin preparing a new process.
|
||||
PendingProcess command(dynamic command) {
|
||||
return PendingProcess(this).withCommand(command);
|
||||
}
|
||||
|
||||
/// Begin preparing a new process with the given working directory.
|
||||
PendingProcess path(String path) {
|
||||
return PendingProcess(this).withWorkingDirectory(path);
|
||||
}
|
||||
|
||||
/// Run a command synchronously.
|
||||
Future<ProcessResult> run(dynamic command) {
|
||||
return PendingProcess(this).withCommand(command).run();
|
||||
}
|
||||
|
||||
/// Start a command asynchronously.
|
||||
Future<InvokedProcess> start(dynamic command,
|
||||
[void Function(String)? onOutput]) {
|
||||
return PendingProcess(this).withCommand(command).start(onOutput);
|
||||
}
|
||||
|
||||
/// Run a command with a specific working directory.
|
||||
Future<ProcessResult> runInPath(String path, dynamic command) {
|
||||
return PendingProcess(this)
|
||||
.withWorkingDirectory(path)
|
||||
.withCommand(command)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Run a command with environment variables.
|
||||
Future<ProcessResult> runWithEnvironment(
|
||||
dynamic command,
|
||||
Map<String, String> environment,
|
||||
) {
|
||||
return PendingProcess(this)
|
||||
.withCommand(command)
|
||||
.withEnvironment(environment)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Run a command with a timeout.
|
||||
Future<ProcessResult> runWithTimeout(
|
||||
dynamic command,
|
||||
int seconds,
|
||||
) {
|
||||
return PendingProcess(this).withCommand(command).withTimeout(seconds).run();
|
||||
}
|
||||
|
||||
/// Run a command with input.
|
||||
Future<ProcessResult> runWithInput(
|
||||
dynamic command,
|
||||
dynamic input,
|
||||
) {
|
||||
return PendingProcess(this).withCommand(command).withInput(input).run();
|
||||
}
|
||||
}
|
118
incubation/test_process/lib/src/invoked_process.dart
Normal file
118
incubation/test_process/lib/src/invoked_process.dart
Normal file
|
@ -0,0 +1,118 @@
|
|||
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;
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal the process.
|
||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
|
||||
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),
|
||||
);
|
||||
|
||||
if (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);
|
||||
}
|
||||
}
|
255
incubation/test_process/lib/src/pending_process.dart
Normal file
255
incubation/test_process/lib/src/pending_process.dart
Normal file
|
@ -0,0 +1,255 @@
|
|||
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);
|
||||
|
||||
/// 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');
|
||||
}
|
||||
|
||||
try {
|
||||
final process = await _createProcess();
|
||||
final completer = Completer<ProcessResult>();
|
||||
Timer? timeoutTimer;
|
||||
|
||||
if (timeout != null) {
|
||||
timeoutTimer = Timer(Duration(seconds: timeout!), () {
|
||||
if (!completer.isCompleted) {
|
||||
process.kill();
|
||||
completer.completeError(
|
||||
ProcessTimedOutException(
|
||||
'The process "${this.command}" exceeded the timeout of $timeout seconds.',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _runProcess(process, onOutput);
|
||||
if (!completer.isCompleted) {
|
||||
if (result.exitCode != 0) {
|
||||
completer.completeError(ProcessFailedException(result));
|
||||
} else {
|
||||
completer.complete(result);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
timeoutTimer?.cancel();
|
||||
}
|
||||
|
||||
return await completer.future;
|
||||
} 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),
|
||||
);
|
||||
}
|
||||
}
|
46
incubation/test_process/lib/src/process_result.dart
Normal file
46
incubation/test_process/lib/src/process_result.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
/// 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() {
|
||||
final buffer = StringBuffer();
|
||||
if (_output.isNotEmpty) {
|
||||
buffer.writeln('Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(_output);
|
||||
}
|
||||
if (_errorOutput.isNotEmpty) {
|
||||
if (buffer.isNotEmpty) buffer.writeln();
|
||||
buffer.writeln('Error Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(_errorOutput);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
9
incubation/test_process/lib/test_process.dart
Normal file
9
incubation/test_process/lib/test_process.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
/// A Laravel-compatible process management implementation in pure Dart.
|
||||
library test_process;
|
||||
|
||||
export 'src/pending_process.dart';
|
||||
export 'src/process_result.dart';
|
||||
export 'src/invoked_process.dart';
|
||||
export 'src/factory.dart';
|
||||
export 'src/exceptions/process_failed_exception.dart';
|
||||
export 'src/exceptions/process_timed_out_exception.dart';
|
402
incubation/test_process/pubspec.lock
Normal file
402
incubation/test_process/pubspec.lock
Normal file
|
@ -0,0 +1,402 @@
|
|||
# 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"
|
17
incubation/test_process/pubspec.yaml
Normal file
17
incubation/test_process/pubspec.yaml
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
72
incubation/test_process/test/exceptions_test.dart
Normal file
72
incubation/test_process/test/exceptions_test.dart
Normal file
|
@ -0,0 +1,72 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:test_process/test_process.dart';
|
||||
|
||||
void main() {
|
||||
group('ProcessFailedException', () {
|
||||
test('contains process result', () {
|
||||
final result = ProcessResult(1, 'output', 'error');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.result, equals(result));
|
||||
});
|
||||
|
||||
test('provides access to error details', () {
|
||||
final result = ProcessResult(2, 'output', 'error message');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.exitCode, equals(2));
|
||||
expect(exception.errorOutput, equals('error message'));
|
||||
expect(exception.output, equals('output'));
|
||||
});
|
||||
|
||||
test('toString includes error details', () {
|
||||
final result = ProcessResult(1, 'output', 'error message');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.toString(), contains('error message'));
|
||||
expect(exception.toString(), contains('1'));
|
||||
});
|
||||
|
||||
test('handles empty error output', () {
|
||||
final result = ProcessResult(1, 'output', '');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(
|
||||
exception.toString(), contains('Process failed with exit code: 1'));
|
||||
});
|
||||
|
||||
test('handles empty output', () {
|
||||
final result = ProcessResult(1, '', 'error');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.output, isEmpty);
|
||||
expect(exception.errorOutput, equals('error'));
|
||||
});
|
||||
});
|
||||
|
||||
group('ProcessTimedOutException', () {
|
||||
test('contains timeout message', () {
|
||||
final exception = ProcessTimedOutException('Process timed out after 60s');
|
||||
expect(exception.message, equals('Process timed out after 60s'));
|
||||
});
|
||||
|
||||
test('optionally includes process result', () {
|
||||
final result = ProcessResult(143, 'partial output', '');
|
||||
final exception = ProcessTimedOutException('Timed out', result);
|
||||
expect(exception.result, equals(result));
|
||||
});
|
||||
|
||||
test('toString includes message', () {
|
||||
final exception = ProcessTimedOutException('Custom timeout message');
|
||||
expect(exception.toString(), contains('Custom timeout message'));
|
||||
});
|
||||
|
||||
test('toString includes result details when available', () {
|
||||
final result = ProcessResult(143, 'output', 'error');
|
||||
final exception = ProcessTimedOutException('Timed out', result);
|
||||
expect(exception.toString(), contains('Timed out'));
|
||||
expect(exception.result, equals(result));
|
||||
});
|
||||
|
||||
test('handles null result', () {
|
||||
final exception = ProcessTimedOutException('Timed out');
|
||||
expect(exception.result, isNull);
|
||||
expect(exception.toString(), contains('Timed out'));
|
||||
});
|
||||
});
|
||||
}
|
143
incubation/test_process/test/laravel_process_test.dart
Normal file
143
incubation/test_process/test/laravel_process_test.dart
Normal file
|
@ -0,0 +1,143 @@
|
|||
import 'dart:io';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_process/test_process.dart';
|
||||
|
||||
void main() {
|
||||
late Factory factory;
|
||||
|
||||
setUp(() {
|
||||
factory = Factory();
|
||||
});
|
||||
|
||||
group('Laravel Process Tests', () {
|
||||
test('successful process', () async {
|
||||
final result = await factory
|
||||
.command(['ls'])
|
||||
.withWorkingDirectory(Directory.current.path)
|
||||
.run();
|
||||
|
||||
expect(result.successful(), isTrue);
|
||||
expect(result.failed(), isFalse);
|
||||
expect(result.exitCode, equals(0));
|
||||
expect(result.output(), contains('test'));
|
||||
expect(result.errorOutput(), isEmpty);
|
||||
});
|
||||
|
||||
test('process with error output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo "Hello World" >&2; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output, isEmpty);
|
||||
expect(e.errorOutput.trim(), equals('Hello World'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can throw without output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory.command(['sh', '-c', 'exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output, isEmpty);
|
||||
expect(e.errorOutput, isEmpty);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can throw with error output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo "Hello World" >&2; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output, isEmpty);
|
||||
expect(e.errorOutput.trim(), equals('Hello World'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can throw with output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo "Hello World"; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output.trim(), equals('Hello World'));
|
||||
expect(e.errorOutput, isEmpty);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can timeout', () async {
|
||||
if (!Platform.isWindows) {
|
||||
expect(
|
||||
() => factory.command(['sh', '-c', 'sleep 2']).withTimeout(1).run(),
|
||||
throwsA(isA<ProcessTimedOutException>()),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('process can use standard input', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory.command(['cat']).withInput('foobar').run();
|
||||
|
||||
expect(result.output(), equals('foobar'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process pipe operations', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory.command([
|
||||
'sh',
|
||||
'-c',
|
||||
'echo "Hello, world\nfoo\nbar" | grep -i "foo"'
|
||||
]).run();
|
||||
|
||||
expect(result.output().trim(), equals('foo'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process with working directory', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result =
|
||||
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
|
||||
|
||||
expect(result.output().trim(), equals('/tmp'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process with environment variables', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo \$TEST_VAR']).withEnvironment(
|
||||
{'TEST_VAR': 'test_value'}).run();
|
||||
|
||||
expect(result.output().trim(), equals('test_value'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process output can be captured via callback', () async {
|
||||
final output = <String>[];
|
||||
|
||||
final process = await factory.command(['ls']).start((data) {
|
||||
output.add(data);
|
||||
});
|
||||
|
||||
await Future<void>.delayed(Duration(milliseconds: 100));
|
||||
expect(output, isNotEmpty);
|
||||
expect(output.join(), contains('test'));
|
||||
|
||||
await process.wait();
|
||||
});
|
||||
});
|
||||
}
|
70
incubation/test_process/test/process_result_test.dart
Normal file
70
incubation/test_process/test/process_result_test.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
}
|
125
incubation/test_process/test/process_test.dart
Normal file
125
incubation/test_process/test/process_test.dart
Normal file
|
@ -0,0 +1,125 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:test_process/test_process.dart';
|
||||
|
||||
void main() {
|
||||
late Factory factory;
|
||||
|
||||
setUp(() {
|
||||
factory = Factory();
|
||||
});
|
||||
|
||||
group('Basic Process Operations', () {
|
||||
test('echo command returns expected output', () async {
|
||||
final result = await factory.command(['echo', 'test']).run();
|
||||
expect(result.output().trim(), equals('test'));
|
||||
expect(result.exitCode, equals(0));
|
||||
});
|
||||
|
||||
test('nonexistent command throws ProcessFailedException', () async {
|
||||
expect(
|
||||
() => factory.command(['nonexistent-command']).run(),
|
||||
throwsA(isA<ProcessFailedException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('command with arguments works correctly', () async {
|
||||
final result = await factory.command(['echo', '-n', 'test']).run();
|
||||
expect(result.output(), equals('test'));
|
||||
});
|
||||
});
|
||||
|
||||
group('Process Configuration', () {
|
||||
test('working directory is respected', () async {
|
||||
final result =
|
||||
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
|
||||
expect(result.output().trim(), equals('/tmp'));
|
||||
});
|
||||
|
||||
test('environment variables are passed correctly', () async {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo \$TEST_VAR']).withEnvironment(
|
||||
{'TEST_VAR': 'test_value'}).run();
|
||||
expect(result.output().trim(), equals('test_value'));
|
||||
});
|
||||
|
||||
test('quiet mode suppresses output', () async {
|
||||
final result =
|
||||
await factory.command(['echo', 'test']).withoutOutput().run();
|
||||
expect(result.output(), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('Async Process Operations', () {
|
||||
test('async process completes successfully', () async {
|
||||
final process = await factory.command(['sleep', '0.1']).start();
|
||||
|
||||
expect(process.pid, greaterThan(0));
|
||||
|
||||
final result = await process.wait();
|
||||
expect(result.exitCode, equals(0));
|
||||
});
|
||||
|
||||
test('process input is handled correctly', () async {
|
||||
final result =
|
||||
await factory.command(['cat']).withInput('test input').run();
|
||||
expect(result.output(), equals('test input'));
|
||||
});
|
||||
|
||||
test('process can be killed', () async {
|
||||
final process = await factory.command(['sleep', '10']).start();
|
||||
|
||||
expect(process.kill(), isTrue);
|
||||
|
||||
final result = await process.wait();
|
||||
expect(result.exitCode, isNot(0));
|
||||
});
|
||||
});
|
||||
|
||||
group('Shell Commands', () {
|
||||
test('pipe operations work correctly', () async {
|
||||
final result =
|
||||
await factory.command(['sh', '-c', 'echo hello | tr a-z A-Z']).run();
|
||||
expect(result.output().trim(), equals('HELLO'));
|
||||
});
|
||||
|
||||
test('multiple commands execute in sequence', () async {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo start && sleep 0.1 && echo end']).run();
|
||||
expect(
|
||||
result.output().trim().split('\n'),
|
||||
equals(['start', 'end']),
|
||||
);
|
||||
});
|
||||
|
||||
test('complex shell operations work', () async {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo "Count: 1" && echo "Count: 2"']).run();
|
||||
expect(
|
||||
result.output().trim().split('\n'),
|
||||
equals(['Count: 1', 'Count: 2']),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Error Handling', () {
|
||||
test('failed process throws with correct exit code', () async {
|
||||
try {
|
||||
await factory.command(['sh', '-c', 'exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
}
|
||||
});
|
||||
|
||||
test('process failure includes error output', () async {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo error message >&2; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.errorOutput.trim(), equals('error message'));
|
||||
expect(e.exitCode, equals(1));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,38 +1,29 @@
|
|||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class AsyncGreetingPipe {
|
||||
Future<dynamic> handle(String input, Function next) async {
|
||||
dynamic handle(dynamic input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncExclamationPipe {
|
||||
Future<dynamic> handle(String input, Function next) async {
|
||||
dynamic handle(dynamic input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next('$input!');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var app = Application(reflector: MirrorsReflector());
|
||||
var http = PlatformHttp(app);
|
||||
var container = Container(MirrorsReflector());
|
||||
|
||||
app.container.registerSingleton((c) => Pipeline(c));
|
||||
var pipeline = Pipeline(container);
|
||||
var result = await pipeline
|
||||
.send('World')
|
||||
.through([AsyncGreetingPipe(), AsyncExclamationPipe()]).then(
|
||||
(result) => result.toString().toUpperCase());
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
var pipeline = app.container.make<Pipeline>();
|
||||
var result = await pipeline
|
||||
.send('World')
|
||||
.through(['AsyncGreetingPipe', 'AsyncExclamationPipe']).then(
|
||||
(result) => result.toUpperCase());
|
||||
|
||||
res.write(result); // Outputs: "HELLO, WORLD!" (after 2 seconds)
|
||||
});
|
||||
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server started on http://localhost:3000');
|
||||
print(result); // Should output: "HELLO, WORLD!" (after 2 seconds)
|
||||
}
|
||||
|
|
24
packages/pipeline/example/async_simple.dart
Normal file
24
packages/pipeline/example/async_simple.dart
Normal file
|
@ -0,0 +1,24 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class AsyncTransformPipe {
|
||||
dynamic handle(dynamic value, Function next) async {
|
||||
// Simulate async operation
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
var upperValue = (value as String).toUpperCase();
|
||||
return next(upperValue);
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
|
||||
print('Starting pipeline...');
|
||||
|
||||
var result = await Pipeline(container)
|
||||
.send('hello')
|
||||
.through([AsyncTransformPipe()]).then((value) => value as String);
|
||||
|
||||
print(result); // Should output HELLO after 1 second
|
||||
}
|
|
@ -1,34 +1,23 @@
|
|||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class ErrorPipe {
|
||||
dynamic handle(String input, Function next) {
|
||||
throw Exception('Simulated error');
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
throw Exception('Simulated error in pipeline');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var app = Application(reflector: MirrorsReflector());
|
||||
var http = PlatformHttp(app);
|
||||
var container = Container(MirrorsReflector());
|
||||
var pipeline = Pipeline(container);
|
||||
|
||||
app.container.registerSingleton((c) => Pipeline(c));
|
||||
try {
|
||||
var result = await pipeline.send('World').through([ErrorPipe()]).then(
|
||||
(result) => result.toString().toUpperCase());
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
var pipeline = app.container.make<Pipeline>();
|
||||
try {
|
||||
await pipeline
|
||||
.send('World')
|
||||
.through(['ErrorPipe']).then((result) => result.toUpperCase());
|
||||
} catch (e) {
|
||||
res.write('Error occurred: ${e.toString()}');
|
||||
return;
|
||||
}
|
||||
|
||||
res.write('This should not be reached');
|
||||
});
|
||||
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server started on http://localhost:3000');
|
||||
print('This should not be printed');
|
||||
} catch (e) {
|
||||
print('Caught error: $e');
|
||||
}
|
||||
}
|
||||
|
|
57
packages/pipeline/example/http_server.dart
Normal file
57
packages/pipeline/example/http_server.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
|
||||
class GreetingPipe {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
class ExclamationPipe {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
return next('$input!');
|
||||
}
|
||||
}
|
||||
|
||||
class UppercasePipe {
|
||||
dynamic handle(dynamic input, Function next) async {
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: 500)); // Small delay to demonstrate async
|
||||
return next(input.toString().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
// Create application with empty reflector
|
||||
var app = Application(reflector: EmptyReflector());
|
||||
|
||||
// Create HTTP server
|
||||
var http = PlatformHttp(app);
|
||||
|
||||
// Define routes
|
||||
app.get('/', (RequestContext req, ResponseContext res) {
|
||||
res.write('Try visiting /greet/world to see the pipeline in action');
|
||||
return false;
|
||||
});
|
||||
|
||||
app.get('/greet/:name', (RequestContext req, ResponseContext res) async {
|
||||
var name = req.params['name'] ?? 'guest';
|
||||
|
||||
var pipeline = Pipeline(app.container);
|
||||
var result = await pipeline.send(name).through([
|
||||
GreetingPipe(),
|
||||
ExclamationPipe(),
|
||||
UppercasePipe(),
|
||||
]).then((result) => result);
|
||||
|
||||
res.write(result);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Start server
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server running at http://localhost:3000');
|
||||
print('Visit http://localhost:3000/greet/world to see pipeline in action');
|
||||
}
|
|
@ -1,35 +1,30 @@
|
|||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class GreetingPipe {
|
||||
dynamic handle(String input, Function next) {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var app = Application(reflector: MirrorsReflector());
|
||||
var http = PlatformHttp(app);
|
||||
var container = Container(MirrorsReflector());
|
||||
var pipeline = Pipeline(container);
|
||||
|
||||
app.container.registerSingleton((c) => Pipeline(c));
|
||||
print('Starting mixed pipeline...');
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
var pipeline = app.container.make<Pipeline>();
|
||||
var result = await pipeline.send('World').through([
|
||||
'GreetingPipe',
|
||||
(String input, Function next) => next('$input!'),
|
||||
(String input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next(input.toUpperCase());
|
||||
},
|
||||
]).then((result) => 'Final result: $result');
|
||||
var result = await pipeline.send('World').through([
|
||||
GreetingPipe(),
|
||||
// Closure-based pipe
|
||||
(dynamic input, Function next) => next('$input!'),
|
||||
// Async closure-based pipe
|
||||
(dynamic input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next(input.toString().toUpperCase());
|
||||
},
|
||||
]).then((result) => 'Final result: $result');
|
||||
|
||||
res.write(
|
||||
result); // Outputs: "Final result: HELLO, WORLD!" (after 1 second)
|
||||
});
|
||||
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server started on http://localhost:3000');
|
||||
print(
|
||||
result); // Should output: "Final result: HELLO, WORLD!" (after 1 second)
|
||||
}
|
||||
|
|
14
packages/pipeline/example/simple.dart
Normal file
14
packages/pipeline/example/simple.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
|
||||
var result = await Pipeline(container).send('Hello').through([
|
||||
(value, next) => next('$value World'),
|
||||
(value, next) => next('$value!'),
|
||||
]).then((value) => value);
|
||||
|
||||
print(result); // Should output: Hello World!
|
||||
}
|
Loading…
Reference in a new issue