From 4dbcb45836edd79d04d50e1e3dc07f8236dcb24f Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Mon, 30 Dec 2024 20:39:19 -0700 Subject: [PATCH] incubate: adding test process package 10 pass 1 fail --- incubation/test_process/README.md | 159 +++++++ incubation/test_process/example/example.dart | 95 +++++ .../exceptions/process_failed_exception.dart | 46 ++ .../process_timed_out_exception.dart | 37 ++ incubation/test_process/lib/src/factory.dart | 65 +++ .../test_process/lib/src/invoked_process.dart | 118 +++++ .../test_process/lib/src/pending_process.dart | 255 +++++++++++ .../test_process/lib/src/process_result.dart | 46 ++ incubation/test_process/lib/test_process.dart | 9 + incubation/test_process/pubspec.lock | 402 ++++++++++++++++++ incubation/test_process/pubspec.yaml | 17 + .../test_process/test/exceptions_test.dart | 72 ++++ .../test/laravel_process_test.dart | 143 +++++++ .../test/process_result_test.dart | 70 +++ .../test_process/test/process_test.dart | 125 ++++++ packages/pipeline/example/async_pipeline.dart | 31 +- packages/pipeline/example/async_simple.dart | 24 ++ packages/pipeline/example/error_handling.dart | 37 +- packages/pipeline/example/http_server.dart | 57 +++ packages/pipeline/example/mixed_pipes.dart | 41 +- packages/pipeline/example/simple.dart | 14 + 21 files changed, 1796 insertions(+), 67 deletions(-) create mode 100644 incubation/test_process/README.md create mode 100644 incubation/test_process/example/example.dart create mode 100644 incubation/test_process/lib/src/exceptions/process_failed_exception.dart create mode 100644 incubation/test_process/lib/src/exceptions/process_timed_out_exception.dart create mode 100644 incubation/test_process/lib/src/factory.dart create mode 100644 incubation/test_process/lib/src/invoked_process.dart create mode 100644 incubation/test_process/lib/src/pending_process.dart create mode 100644 incubation/test_process/lib/src/process_result.dart create mode 100644 incubation/test_process/lib/test_process.dart create mode 100644 incubation/test_process/pubspec.lock create mode 100644 incubation/test_process/pubspec.yaml create mode 100644 incubation/test_process/test/exceptions_test.dart create mode 100644 incubation/test_process/test/laravel_process_test.dart create mode 100644 incubation/test_process/test/process_result_test.dart create mode 100644 incubation/test_process/test/process_test.dart create mode 100644 packages/pipeline/example/async_simple.dart create mode 100644 packages/pipeline/example/http_server.dart create mode 100644 packages/pipeline/example/simple.dart diff --git a/incubation/test_process/README.md b/incubation/test_process/README.md new file mode 100644 index 0000000..71fb95a --- /dev/null +++ b/incubation/test_process/README.md @@ -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. diff --git a/incubation/test_process/example/example.dart b/incubation/test_process/example/example.dart new file mode 100644 index 0000000..ee10c4a --- /dev/null +++ b/incubation/test_process/example/example.dart @@ -0,0 +1,95 @@ +import 'package:test_process/test_process.dart'; + +Future 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'); + } +} diff --git a/incubation/test_process/lib/src/exceptions/process_failed_exception.dart b/incubation/test_process/lib/src/exceptions/process_failed_exception.dart new file mode 100644 index 0000000..3c98606 --- /dev/null +++ b/incubation/test_process/lib/src/exceptions/process_failed_exception.dart @@ -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(); + } +} diff --git a/incubation/test_process/lib/src/exceptions/process_timed_out_exception.dart b/incubation/test_process/lib/src/exceptions/process_timed_out_exception.dart new file mode 100644 index 0000000..673d5e4 --- /dev/null +++ b/incubation/test_process/lib/src/exceptions/process_timed_out_exception.dart @@ -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(); + } +} diff --git a/incubation/test_process/lib/src/factory.dart b/incubation/test_process/lib/src/factory.dart new file mode 100644 index 0000000..ca5b9bd --- /dev/null +++ b/incubation/test_process/lib/src/factory.dart @@ -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 run(dynamic command) { + return PendingProcess(this).withCommand(command).run(); + } + + /// Start a command asynchronously. + Future start(dynamic command, + [void Function(String)? onOutput]) { + return PendingProcess(this).withCommand(command).start(onOutput); + } + + /// Run a command with a specific working directory. + Future runInPath(String path, dynamic command) { + return PendingProcess(this) + .withWorkingDirectory(path) + .withCommand(command) + .run(); + } + + /// Run a command with environment variables. + Future runWithEnvironment( + dynamic command, + Map environment, + ) { + return PendingProcess(this) + .withCommand(command) + .withEnvironment(environment) + .run(); + } + + /// Run a command with a timeout. + Future runWithTimeout( + dynamic command, + int seconds, + ) { + return PendingProcess(this).withCommand(command).withTimeout(seconds).run(); + } + + /// Run a command with input. + Future runWithInput( + dynamic command, + dynamic input, + ) { + return PendingProcess(this).withCommand(command).withInput(input).run(); + } +} diff --git a/incubation/test_process/lib/src/invoked_process.dart b/incubation/test_process/lib/src/invoked_process.dart new file mode 100644 index 0000000..8326350 --- /dev/null +++ b/incubation/test_process/lib/src/invoked_process.dart @@ -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 _stdout = []; + + /// The collected stderr data. + final List _stderr = []; + + /// Whether the process has completed + bool _completed = false; + + /// Completer for stdout stream + final _stdoutCompleter = Completer(); + + /// Completer for stderr stream + final _stderrCompleter = Completer(); + + /// 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) { + _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 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); + } +} diff --git a/incubation/test_process/lib/src/pending_process.dart b/incubation/test_process/lib/src/pending_process.dart new file mode 100644 index 0000000..0924592 --- /dev/null +++ b/incubation/test_process/lib/src/pending_process.dart @@ -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 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 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 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(); + 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 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) { + 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 _createProcess() async { + if (command is List) { + final List 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 _runProcess( + Process process, void Function(String)? onOutput) async { + final stdout = []; + final stderr = []; + final stdoutCompleter = Completer(); + final stderrCompleter = Completer(); + + 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) { + 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), + ); + } +} diff --git a/incubation/test_process/lib/src/process_result.dart b/incubation/test_process/lib/src/process_result.dart new file mode 100644 index 0000000..ff0637e --- /dev/null +++ b/incubation/test_process/lib/src/process_result.dart @@ -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(); + } +} diff --git a/incubation/test_process/lib/test_process.dart b/incubation/test_process/lib/test_process.dart new file mode 100644 index 0000000..c7db762 --- /dev/null +++ b/incubation/test_process/lib/test_process.dart @@ -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'; diff --git a/incubation/test_process/pubspec.lock b/incubation/test_process/pubspec.lock new file mode 100644 index 0000000..6caee6d --- /dev/null +++ b/incubation/test_process/pubspec.lock @@ -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" diff --git a/incubation/test_process/pubspec.yaml b/incubation/test_process/pubspec.yaml new file mode 100644 index 0000000..1387a17 --- /dev/null +++ b/incubation/test_process/pubspec.yaml @@ -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 diff --git a/incubation/test_process/test/exceptions_test.dart b/incubation/test_process/test/exceptions_test.dart new file mode 100644 index 0000000..d72e44a --- /dev/null +++ b/incubation/test_process/test/exceptions_test.dart @@ -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')); + }); + }); +} diff --git a/incubation/test_process/test/laravel_process_test.dart b/incubation/test_process/test/laravel_process_test.dart new file mode 100644 index 0000000..f5a2031 --- /dev/null +++ b/incubation/test_process/test/laravel_process_test.dart @@ -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()), + ); + } + }); + + 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 = []; + + final process = await factory.command(['ls']).start((data) { + output.add(data); + }); + + await Future.delayed(Duration(milliseconds: 100)); + expect(output, isNotEmpty); + expect(output.join(), contains('test')); + + await process.wait(); + }); + }); +} diff --git a/incubation/test_process/test/process_result_test.dart b/incubation/test_process/test/process_result_test.dart new file mode 100644 index 0000000..bb77491 --- /dev/null +++ b/incubation/test_process/test/process_result_test.dart @@ -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); + }); + }); +} diff --git a/incubation/test_process/test/process_test.dart b/incubation/test_process/test/process_test.dart new file mode 100644 index 0000000..4f8420c --- /dev/null +++ b/incubation/test_process/test/process_test.dart @@ -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()), + ); + }); + + 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)); + } + }); + }); +} diff --git a/packages/pipeline/example/async_pipeline.dart b/packages/pipeline/example/async_pipeline.dart index 4291edc..04ea003 100644 --- a/packages/pipeline/example/async_pipeline.dart +++ b/packages/pipeline/example/async_pipeline.dart @@ -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 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 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(); - 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) } diff --git a/packages/pipeline/example/async_simple.dart b/packages/pipeline/example/async_simple.dart new file mode 100644 index 0000000..d2f77d7 --- /dev/null +++ b/packages/pipeline/example/async_simple.dart @@ -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 +} diff --git a/packages/pipeline/example/error_handling.dart b/packages/pipeline/example/error_handling.dart index b141459..d8d8004 100644 --- a/packages/pipeline/example/error_handling.dart +++ b/packages/pipeline/example/error_handling.dart @@ -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(); - 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'); + } } diff --git a/packages/pipeline/example/http_server.dart b/packages/pipeline/example/http_server.dart new file mode 100644 index 0000000..546985e --- /dev/null +++ b/packages/pipeline/example/http_server.dart @@ -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'); +} diff --git a/packages/pipeline/example/mixed_pipes.dart b/packages/pipeline/example/mixed_pipes.dart index e8aabf0..834d7fb 100644 --- a/packages/pipeline/example/mixed_pipes.dart +++ b/packages/pipeline/example/mixed_pipes.dart @@ -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(); - 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) } diff --git a/packages/pipeline/example/simple.dart b/packages/pipeline/example/simple.dart new file mode 100644 index 0000000..a6e31be --- /dev/null +++ b/packages/pipeline/example/simple.dart @@ -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! +}