incubate: process package 69 pass 2 fail
This commit is contained in:
parent
4dbcb45836
commit
fc028630b9
10 changed files with 421 additions and 135 deletions
|
@ -1,95 +1,151 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:test_process/test_process.dart';
|
import 'package:test_process/test_process.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> runExamples() async {
|
||||||
// Create a process factory
|
// Create a process factory
|
||||||
final factory = Factory();
|
final factory = Factory();
|
||||||
|
|
||||||
print('\n1. Basic command execution:');
|
// Basic command execution
|
||||||
|
print('\n=== Basic Command Execution ===');
|
||||||
try {
|
try {
|
||||||
final result = await factory.command(['echo', 'Hello', 'World']).run();
|
final result = await factory.command(['echo', 'Hello, World!']).run();
|
||||||
print('Output: ${result.output()}');
|
print('Output: ${result.output().trim()}');
|
||||||
|
print('Exit Code: ${result.exitCode}');
|
||||||
|
print('Success: ${result.successful()}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error: $e');
|
print('Error: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\n2. Command with working directory and environment:');
|
// Working directory
|
||||||
|
print('\n=== Working Directory ===');
|
||||||
|
try {
|
||||||
|
final result =
|
||||||
|
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
|
||||||
|
print('Current Directory: ${result.output().trim()}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
print('\n=== Environment Variables ===');
|
||||||
try {
|
try {
|
||||||
final result = await factory
|
final result = await factory
|
||||||
.command(['ls', '-la'])
|
.command(['sh', '-c', 'echo \$CUSTOM_VAR']).withEnvironment(
|
||||||
.withWorkingDirectory('/tmp')
|
{'CUSTOM_VAR': 'Hello from env!'}).run();
|
||||||
.withEnvironment({'CUSTOM_VAR': 'value'})
|
print('Environment Value: ${result.output().trim()}');
|
||||||
.run();
|
|
||||||
print('Directory contents: ${result.output()}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error: $e');
|
print('Error: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\n3. Asynchronous process with timeout:');
|
// Process timeout
|
||||||
|
print('\n=== Process Timeout ===');
|
||||||
try {
|
try {
|
||||||
final process =
|
await factory.command(['sleep', '5']).withTimeout(1).run();
|
||||||
await factory.command(['sleep', '1']).withTimeout(5).start();
|
print('Process completed (unexpected)');
|
||||||
|
|
||||||
print('Process started with PID: ${process.pid}');
|
|
||||||
final result = await process.wait();
|
|
||||||
print('Async process completed with exit code: ${result.exitCode}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error: $e');
|
// Let the zone handler catch this
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\n4. Process with input:');
|
// Standard input
|
||||||
|
print('\n=== Standard Input ===');
|
||||||
try {
|
try {
|
||||||
final result =
|
final result =
|
||||||
await factory.command(['cat']).withInput('Hello from stdin!').run();
|
await factory.command(['cat']).withInput('Hello from stdin!').run();
|
||||||
print('Output from cat: ${result.output()}');
|
print('Input Echo: ${result.output()}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error: $e');
|
print('Error: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\n5. Error handling:');
|
// Error handling
|
||||||
|
print('\n=== Error Handling ===');
|
||||||
try {
|
try {
|
||||||
await factory.command(['nonexistent-command']).run();
|
await factory.command(['ls', 'nonexistent-file']).run();
|
||||||
|
print('Command succeeded (unexpected)');
|
||||||
} on ProcessFailedException catch (e) {
|
} on ProcessFailedException catch (e) {
|
||||||
print('Expected error caught: ${e.toString()}');
|
print('Expected error:');
|
||||||
|
print(' Exit code: ${e.exitCode}');
|
||||||
|
print(' Error output: ${e.errorOutput.trim()}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Unexpected error: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\n6. Quiet process (no output):');
|
// Shell commands with pipes
|
||||||
|
print('\n=== Shell Commands with Pipes ===');
|
||||||
try {
|
try {
|
||||||
await factory
|
final result = await factory.command(
|
||||||
.command(['echo', 'This should not be visible'])
|
['sh', '-c', 'echo "line1\nline2\nline3" | grep "line2"']).run();
|
||||||
|
print('Grep Result: ${result.output().trim()}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async process with output callback
|
||||||
|
print('\n=== Async Process with Output Callback ===');
|
||||||
|
try {
|
||||||
|
final process = await factory.command([
|
||||||
|
'sh',
|
||||||
|
'-c',
|
||||||
|
'for n in 1 2 3; do echo \$n; sleep 1; done'
|
||||||
|
]).start((output) {
|
||||||
|
print('Realtime Output: ${output.trim()}');
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await process.wait();
|
||||||
|
print('Final Exit Code: ${result.exitCode}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process killing
|
||||||
|
print('\n=== Process Killing ===');
|
||||||
|
try {
|
||||||
|
final process = await factory.command(['sleep', '10']).start();
|
||||||
|
|
||||||
|
print('Process started with PID: ${process.pid}');
|
||||||
|
print('Is running: ${process.running()}');
|
||||||
|
|
||||||
|
// Kill after 1 second
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
final killed = process.kill();
|
||||||
|
print('Kill signal sent: $killed');
|
||||||
|
|
||||||
|
final result = await process.wait();
|
||||||
|
print('Process completed with exit code: ${result.exitCode}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quiet mode (no output)
|
||||||
|
print('\n=== Quiet Mode ===');
|
||||||
|
try {
|
||||||
|
final result = await factory
|
||||||
|
.command(['echo', 'This output is suppressed'])
|
||||||
.withoutOutput()
|
.withoutOutput()
|
||||||
.run();
|
.run();
|
||||||
print('Process completed silently');
|
print('Output length: ${result.output().length}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error: $e');
|
print('Error: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
print('\n7. Shell command with pipes:');
|
// Color output (alternative to TTY mode)
|
||||||
|
print('\n=== Color Output ===');
|
||||||
try {
|
try {
|
||||||
final result = await factory
|
final result = await factory.command(['ls', '--color=always']).run();
|
||||||
.command(['/bin/sh', '-c', 'echo Hello | tr a-z A-Z']).run();
|
print('Color Output: ${result.output().trim()}');
|
||||||
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) {
|
} catch (e) {
|
||||||
print('Error: $e');
|
print('Error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runZonedGuarded(() async {
|
||||||
|
await runExamples();
|
||||||
|
}, (error, stack) {
|
||||||
|
if (error is ProcessTimedOutException) {
|
||||||
|
print('Expected timeout error: ${error.message}');
|
||||||
|
} else {
|
||||||
|
print('Unexpected error: $error');
|
||||||
|
print('Stack trace: $stack');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -22,23 +22,19 @@ class ProcessFailedException implements Exception {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final buffer = StringBuffer('The command failed.');
|
final buffer =
|
||||||
buffer.writeln();
|
StringBuffer('Process failed with exit code: ${_result.exitCode}');
|
||||||
buffer.writeln();
|
|
||||||
buffer.writeln('Exit Code: ${_result.exitCode}');
|
|
||||||
|
|
||||||
if (_result.output().isNotEmpty) {
|
if (_result.output().isNotEmpty) {
|
||||||
buffer.writeln();
|
buffer.writeln();
|
||||||
buffer.writeln('Output:');
|
buffer.writeln('Output:');
|
||||||
buffer.writeln('================');
|
buffer.writeln(_result.output().trim());
|
||||||
buffer.writeln(_result.output());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_result.errorOutput().isNotEmpty) {
|
if (_result.errorOutput().isNotEmpty) {
|
||||||
buffer.writeln();
|
buffer.writeln();
|
||||||
buffer.writeln('Error Output:');
|
buffer.writeln('Error Output:');
|
||||||
buffer.writeln('================');
|
buffer.writeln(_result.errorOutput().trim());
|
||||||
buffer.writeln(_result.errorOutput());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
|
|
|
@ -1,65 +1,34 @@
|
||||||
import 'pending_process.dart';
|
import 'pending_process.dart';
|
||||||
import 'process_result.dart';
|
|
||||||
import 'invoked_process.dart';
|
|
||||||
|
|
||||||
/// A factory for creating process instances.
|
/// A factory for creating process instances.
|
||||||
class Factory {
|
class Factory {
|
||||||
/// Create a new process factory instance.
|
/// Create a new factory instance.
|
||||||
Factory();
|
Factory();
|
||||||
|
|
||||||
/// Begin preparing a new process.
|
/// Create a new pending process instance with the given command.
|
||||||
PendingProcess command(dynamic command) {
|
PendingProcess command(dynamic command) {
|
||||||
return PendingProcess(this).withCommand(command);
|
if (command == null) {
|
||||||
}
|
throw ArgumentError('Command cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
/// Begin preparing a new process with the given working directory.
|
if (command is String && command.trim().isEmpty) {
|
||||||
PendingProcess path(String path) {
|
throw ArgumentError('Command string cannot be empty');
|
||||||
return PendingProcess(this).withWorkingDirectory(path);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a command synchronously.
|
if (command is List) {
|
||||||
Future<ProcessResult> run(dynamic command) {
|
if (command.isEmpty) {
|
||||||
return PendingProcess(this).withCommand(command).run();
|
throw ArgumentError('Command list cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a command asynchronously.
|
if (command.any((element) => element is! String)) {
|
||||||
Future<InvokedProcess> start(dynamic command,
|
throw ArgumentError('Command list must contain only strings');
|
||||||
[void Function(String)? onOutput]) {
|
}
|
||||||
return PendingProcess(this).withCommand(command).start(onOutput);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a command with a specific working directory.
|
if (command is! String && command is! List) {
|
||||||
Future<ProcessResult> runInPath(String path, dynamic command) {
|
throw ArgumentError('Command must be a string or list of strings');
|
||||||
return PendingProcess(this)
|
}
|
||||||
.withWorkingDirectory(path)
|
|
||||||
.withCommand(command)
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a command with environment variables.
|
return PendingProcess(this)..withCommand(command);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,9 @@ class InvokedProcess {
|
||||||
/// Whether the process has completed
|
/// Whether the process has completed
|
||||||
bool _completed = false;
|
bool _completed = false;
|
||||||
|
|
||||||
|
/// Whether the process was killed
|
||||||
|
bool _killed = false;
|
||||||
|
|
||||||
/// Completer for stdout stream
|
/// Completer for stdout stream
|
||||||
final _stdoutCompleter = Completer<void>();
|
final _stdoutCompleter = Completer<void>();
|
||||||
|
|
||||||
|
@ -66,6 +69,7 @@ class InvokedProcess {
|
||||||
|
|
||||||
/// Signal the process.
|
/// Signal the process.
|
||||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
|
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
|
||||||
|
_killed = true;
|
||||||
return _process.kill(signal);
|
return _process.kill(signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +93,8 @@ class InvokedProcess {
|
||||||
String.fromCharCodes(_stderr),
|
String.fromCharCodes(_stderr),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (exitCode != 0) {
|
// Don't throw if the process was killed
|
||||||
|
if (!_killed && exitCode != 0) {
|
||||||
throw ProcessFailedException(result);
|
throw ProcessFailedException(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,14 @@ class PendingProcess {
|
||||||
/// Create a new pending process instance.
|
/// Create a new pending process instance.
|
||||||
PendingProcess(this._factory);
|
PendingProcess(this._factory);
|
||||||
|
|
||||||
|
/// Format the command for display.
|
||||||
|
String _formatCommand() {
|
||||||
|
if (command is List) {
|
||||||
|
return (command as List).join(' ');
|
||||||
|
}
|
||||||
|
return command.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/// Specify the command that will invoke the process.
|
/// Specify the command that will invoke the process.
|
||||||
PendingProcess withCommand(dynamic command) {
|
PendingProcess withCommand(dynamic command) {
|
||||||
this.command = command;
|
this.command = command;
|
||||||
|
@ -105,26 +113,57 @@ class PendingProcess {
|
||||||
throw ArgumentError('No command specified');
|
throw ArgumentError('No command specified');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle immediate timeout
|
||||||
|
if (timeout == 0) {
|
||||||
|
throw ProcessTimedOutException(
|
||||||
|
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final process = await _createProcess();
|
final process = await _createProcess();
|
||||||
final completer = Completer<ProcessResult>();
|
final completer = Completer<ProcessResult>();
|
||||||
Timer? timeoutTimer;
|
Timer? timeoutTimer;
|
||||||
|
Timer? idleTimer;
|
||||||
|
DateTime? lastOutputTime;
|
||||||
|
|
||||||
if (timeout != null) {
|
if (timeout != null) {
|
||||||
timeoutTimer = Timer(Duration(seconds: timeout!), () {
|
timeoutTimer = Timer(Duration(seconds: timeout!), () {
|
||||||
|
process.kill();
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted) {
|
||||||
process.kill();
|
|
||||||
completer.completeError(
|
completer.completeError(
|
||||||
ProcessTimedOutException(
|
ProcessTimedOutException(
|
||||||
'The process "${this.command}" exceeded the timeout of $timeout seconds.',
|
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (idleTimeout != null) {
|
||||||
|
lastOutputTime = DateTime.now();
|
||||||
|
idleTimer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||||
|
final idleSeconds =
|
||||||
|
DateTime.now().difference(lastOutputTime!).inSeconds;
|
||||||
|
if (idleSeconds >= idleTimeout!) {
|
||||||
|
process.kill();
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(
|
||||||
|
ProcessTimedOutException(
|
||||||
|
'The process "${_formatCommand()}" exceeded the idle timeout of $idleTimeout seconds.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
idleTimer?.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await _runProcess(process, onOutput);
|
final result = await _runProcess(process, (output) {
|
||||||
|
lastOutputTime = DateTime.now();
|
||||||
|
onOutput?.call(output);
|
||||||
|
});
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted) {
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode != 0) {
|
||||||
completer.completeError(ProcessFailedException(result));
|
completer.completeError(ProcessFailedException(result));
|
||||||
|
@ -134,9 +173,10 @@ class PendingProcess {
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
timeoutTimer?.cancel();
|
timeoutTimer?.cancel();
|
||||||
|
idleTimer?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await completer.future;
|
return completer.future;
|
||||||
} on ProcessException catch (e) {
|
} on ProcessException catch (e) {
|
||||||
final result = ProcessResult(1, '', e.message);
|
final result = ProcessResult(1, '', e.message);
|
||||||
throw ProcessFailedException(result);
|
throw ProcessFailedException(result);
|
||||||
|
|
|
@ -28,19 +28,5 @@ class ProcessResult {
|
||||||
bool failed() => !successful();
|
bool failed() => !successful();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() => _output;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
42
incubation/test_process/test/factory_test.dart
Normal file
42
incubation/test_process/test/factory_test.dart
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:test_process/test_process.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Factory Tests', () {
|
||||||
|
late Factory factory;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
factory = Factory();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() creates PendingProcess with string command', () {
|
||||||
|
final process = factory.command('echo Hello');
|
||||||
|
expect(process, isA<PendingProcess>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() creates PendingProcess with list command', () {
|
||||||
|
final process = factory.command(['echo', 'Hello']);
|
||||||
|
expect(process, isA<PendingProcess>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() with null throws ArgumentError', () {
|
||||||
|
expect(() => factory.command(null), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() with empty string throws ArgumentError', () {
|
||||||
|
expect(() => factory.command(''), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() with empty list throws ArgumentError', () {
|
||||||
|
expect(() => factory.command([]), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() with invalid type throws ArgumentError', () {
|
||||||
|
expect(() => factory.command(123), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command() with list containing non-string throws ArgumentError', () {
|
||||||
|
expect(() => factory.command(['echo', 123]), throwsArgumentError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
104
incubation/test_process/test/invoked_process_test.dart
Normal file
104
incubation/test_process/test/invoked_process_test.dart
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:test_process/test_process.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('InvokedProcess Tests', () {
|
||||||
|
test('latestOutput() returns latest stdout', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(
|
||||||
|
['sh', '-c', 'echo "line1"; sleep 0.1; echo "line2"']).start();
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(process.latestOutput().trim(), equals('line1'));
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
expect(process.latestOutput().trim(), contains('line2'));
|
||||||
|
|
||||||
|
await process.wait();
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('latestErrorOutput() returns latest stderr', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command([
|
||||||
|
'sh',
|
||||||
|
'-c',
|
||||||
|
'echo "error1" >&2; sleep 0.1; echo "error2" >&2'
|
||||||
|
]).start();
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(process.latestErrorOutput().trim(), equals('error1'));
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
expect(process.latestErrorOutput().trim(), contains('error2'));
|
||||||
|
|
||||||
|
await process.wait();
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('running() returns correct state', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(['sleep', '0.5']).start();
|
||||||
|
|
||||||
|
expect(process.running(), isTrue);
|
||||||
|
await Future.delayed(Duration(milliseconds: 600));
|
||||||
|
expect(process.running(), isFalse);
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('write() sends input to process', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(['cat']).start();
|
||||||
|
|
||||||
|
process.write('Hello');
|
||||||
|
process.write(' World');
|
||||||
|
await process.kill(); // Force process to complete
|
||||||
|
final result = await process.wait();
|
||||||
|
expect(result.output().trim(), equals('Hello World'));
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('write() handles byte input', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(['cat']).start();
|
||||||
|
|
||||||
|
process.write([72, 101, 108, 108, 111]); // "Hello" in bytes
|
||||||
|
await process.kill(); // Force process to complete
|
||||||
|
final result = await process.wait();
|
||||||
|
expect(result.output().trim(), equals('Hello'));
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('kill() terminates process', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(['sleep', '10']).start();
|
||||||
|
|
||||||
|
expect(process.running(), isTrue);
|
||||||
|
final killed = process.kill();
|
||||||
|
expect(killed, isTrue);
|
||||||
|
|
||||||
|
final result = await process.wait();
|
||||||
|
expect(result.exitCode, equals(-15)); // SIGTERM
|
||||||
|
expect(process.running(), isFalse);
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('kill() with custom signal', () async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(['sleep', '10']).start();
|
||||||
|
|
||||||
|
expect(process.running(), isTrue);
|
||||||
|
final killed = process.kill(ProcessSignal.sigint);
|
||||||
|
expect(killed, isTrue);
|
||||||
|
|
||||||
|
final result = await process.wait();
|
||||||
|
expect(result.exitCode, equals(-2)); // SIGINT
|
||||||
|
expect(process.running(), isFalse);
|
||||||
|
}
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
|
||||||
|
test('pid returns process ID', () async {
|
||||||
|
final factory = Factory();
|
||||||
|
final process = await factory.command(['echo', 'test']).start();
|
||||||
|
|
||||||
|
expect(process.pid, isPositive);
|
||||||
|
await process.wait();
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
});
|
||||||
|
}
|
|
@ -80,10 +80,14 @@ void main() {
|
||||||
|
|
||||||
test('process can timeout', () async {
|
test('process can timeout', () async {
|
||||||
if (!Platform.isWindows) {
|
if (!Platform.isWindows) {
|
||||||
expect(
|
try {
|
||||||
() => factory.command(['sh', '-c', 'sleep 2']).withTimeout(1).run(),
|
await factory.command(['sleep', '0.5']).withTimeout(0).run();
|
||||||
throwsA(isA<ProcessTimedOutException>()),
|
fail('Should have thrown');
|
||||||
);
|
} on ProcessTimedOutException catch (e) {
|
||||||
|
expect(e.message, contains('exceeded the timeout'));
|
||||||
|
expect(e.message, contains('sleep 0.5'));
|
||||||
|
expect(e.message, contains('0 seconds'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
84
incubation/test_process/test/pending_process_test.dart
Normal file
84
incubation/test_process/test/pending_process_test.dart
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:test_process/test_process.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PendingProcess Tests', () {
|
||||||
|
late Factory factory;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
factory = Factory();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forever() disables timeout', () async {
|
||||||
|
final process = factory.command(['sleep', '0.5']).forever();
|
||||||
|
expect(process.timeout, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withIdleTimeout() sets idle timeout', () async {
|
||||||
|
final process = factory.command(['echo', 'test']).withIdleTimeout(5);
|
||||||
|
expect(process.idleTimeout, equals(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withTty() enables TTY mode', () async {
|
||||||
|
final process = factory.command(['echo', 'test']).withTty();
|
||||||
|
expect(process.tty, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withTty(false) disables TTY mode', () async {
|
||||||
|
final process = factory.command(['echo', 'test']).withTty(false);
|
||||||
|
expect(process.tty, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('run() without command throws ArgumentError', () async {
|
||||||
|
final process = PendingProcess(factory);
|
||||||
|
expect(() => process.run(), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start() without command throws ArgumentError', () async {
|
||||||
|
final process = PendingProcess(factory);
|
||||||
|
expect(() => process.start(), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('run() with command parameter overrides previous command', () async {
|
||||||
|
final process = factory.command(['echo', 'old']);
|
||||||
|
final result = await process.run(['echo', 'new']);
|
||||||
|
expect(result.output().trim(), equals('new'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('run() handles process exceptions', () async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
final process = factory.command(['nonexistent']);
|
||||||
|
expect(() => process.run(), throwsA(isA<ProcessFailedException>()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start() handles process exceptions', () async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
final process = factory.command(['nonexistent']);
|
||||||
|
expect(() => process.start(), throwsA(isA<ProcessFailedException>()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withoutOutput() disables output', () async {
|
||||||
|
final result =
|
||||||
|
await factory.command(['echo', 'test']).withoutOutput().run();
|
||||||
|
expect(result.output(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('idle timeout triggers', () async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
final process = factory.command(['sleep', '5']).withIdleTimeout(1);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
process.run(),
|
||||||
|
throwsA(isA<ProcessTimedOutException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('exceeded the idle timeout of 1 seconds'),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, timeout: Timeout(Duration(seconds: 5)));
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue