Skip to content

Commit

Permalink
added Process [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Dec 12, 2024
1 parent dcaeb77 commit b92138e
Show file tree
Hide file tree
Showing 11 changed files with 871 additions and 0 deletions.
443 changes: 443 additions & 0 deletions src/Utils/Process.php

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/Utils/exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,19 @@ class RegexpException extends \Exception
class AssertionException extends \Exception
{
}


/**
* The process failed to run successfully.
*/
class ProcessFailedException extends \RuntimeException
{
}


/**
* The process execution exceeded its timeout limit.
*/
class ProcessTimeoutException extends \RuntimeException
{
}
154 changes: 154 additions & 0 deletions tests/Utils/Process.basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

use Nette\Utils\Helpers;
use Nette\Utils\Process;
use Nette\Utils\ProcessFailedException;
use Nette\Utils\ProcessTimeoutException;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


// Process execution - success

test('run executable successfully', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";']);
Assert::true($process->isSuccess());
Assert::same(0, $process->getExitCode());
Assert::same('hello', $process->getStdOutput());
Assert::same('', $process->getStdError());
});

test('run command successfully', function () {
$process = Process::runCommand('echo hello');
Assert::true($process->isSuccess());
Assert::same(0, $process->getExitCode());
Assert::same('hello' . PHP_EOL, $process->getStdOutput());
Assert::same('', $process->getStdError());
});


// Process execution - errors

test('run executable with error', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'exit(1);']);
Assert::false($process->isSuccess());
Assert::same(1, $process->getExitCode());
});

test('run executable ensure success throws exception on error', function () {
Assert::exception(
fn() => Process::runExecutable(PHP_BINARY, ['-r', 'exit(1);'])->ensureSuccess(),
ProcessFailedException::class,
'Process failed with non-zero exit code: 1',
);
});

test('run command with error', function () {
$process = Process::runCommand('"' . PHP_BINARY . '" -r "exit(1);"');
Assert::false($process->isSuccess());
Assert::same(1, $process->getExitCode());
});

test('run command ensure success throws exception on error', function () {
Assert::exception(
fn() => Process::runCommand('"' . PHP_BINARY . '" -r "exit(1);"')->ensureSuccess(),
ProcessFailedException::class,
'Process failed with non-zero exit code: 1',
);
});


// Process state monitoring

test('is running', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'sleep(1);']);
Assert::true($process->isRunning());
$process->wait();
Assert::false($process->isRunning());
});

test('get pid', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'sleep(1);']);
Assert::type('int', $process->getPid());
$process->wait();
Assert::null($process->getPid());
});


// Waiting for process

test('wait', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";']);
$process->wait();
$process->wait();
Assert::false($process->isRunning());
Assert::same(0, $process->getExitCode());
Assert::same('hello', $process->getStdOutput());
});

test('wait with callback', function () {
$output = '';
$error = '';
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello"; fwrite(STDERR, "error");']);
$process->wait(function ($stdOut, $stdErr) use (&$output, &$error) {
$output .= $stdOut;
$error .= $stdErr;
});
Assert::same('hello', $output);
Assert::same('error', $error);
});


// Automatically call wait()

test('getStdOutput() automatically call wait()', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";']);
Assert::same('hello', $process->getStdOutput());
Assert::false($process->isRunning());
});

test('getExitCode() automatically call wait()', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo exit(2);']);
Assert::same(2, $process->getExitCode());
Assert::false($process->isRunning());
});


// Terminating process

test('terminate', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'sleep(5);']);
$process->terminate();
Assert::false($process->isRunning());
});

test('terminate() and then wait()', function () {
$process = Process::runExecutable(PHP_BINARY, ['-f', 'sleep(5);']);
$process->terminate();
$process->wait();
Assert::false($process->isRunning());
});


// Timeout

test('timeout', function () {
Assert::exception(
fn() => Process::runExecutable(PHP_BINARY, ['-r', 'sleep(5);'], timeout: 0.1)->wait(),
ProcessTimeoutException::class,
'Process exceeded the time limit of 0.1 seconds',
);
});


// bypass_shell

if (Helpers::IsWindows) {
test('bypass_shell = false', function () {
$process = Process::runCommand('"' . PHP_BINARY . '" -r "echo 123;"', options: ['bypass_shell' => false]);
Assert::same('123', $process->getStdOutput());
});
}
35 changes: 35 additions & 0 deletions tests/Utils/Process.consume.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

use Nette\Utils\Process;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


test('incremental output consumption', function () {
$process = Process::runExecutable(PHP_BINARY, ['-f', __DIR__ . '/fixtures.process/incremental.php', 'stdout']);
usleep(50_000);
Assert::same('hel', $process->consumeStdOutput());
usleep(50_000);
Assert::same('lo', $process->consumeStdOutput());
usleep(50_000);
Assert::same('wor', $process->consumeStdOutput());
usleep(50_000);
Assert::same('ld', $process->consumeStdOutput());
$process->wait();
Assert::same('', $process->consumeStdOutput());
Assert::same('helloworld', $process->getStdOutput());
});

test('incremental error output consumption', function () {
$process = Process::runExecutable(PHP_BINARY, ['-f', __DIR__ . '/fixtures.process/incremental.php', 'stderr']);
usleep(50_000);
Assert::same('hello' . PHP_EOL, $process->consumeStdError());
usleep(50_000);
Assert::same('world' . PHP_EOL, $process->consumeStdError());
usleep(50_000);
Assert::same('', $process->consumeStdError());
Assert::same('hello' . PHP_EOL . 'world' . PHP_EOL, $process->getStdError());
});
26 changes: 26 additions & 0 deletions tests/Utils/Process.environment.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

use Nette\Utils\Process;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


// Environment variables

test('environment variables', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo getenv("TEST_VAR");'], env: ['TEST_VAR' => '123']);
Assert::same('123', $process->getStdOutput());
});

test('no environment variables', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo !getenv("PATH") ? "ok" : "no";'], env: []);
Assert::same('ok', $process->getStdOutput());
});

test('parent environment variables', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo getenv("PATH") ? "ok" : "no";']);
Assert::same('ok', $process->getStdOutput());
});
56 changes: 56 additions & 0 deletions tests/Utils/Process.input.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

use Nette\Utils\Process;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


// Different input types

test('string as input', function () {
$input = 'Hello Input';
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: $input);
Assert::same('Hello Input', $process->getStdOutput());
});

test('stream as input', function () {
$input = fopen('php://memory', 'r+');
fwrite($input, 'Hello Input');
rewind($input);
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: $input);
Assert::same('Hello Input', $process->getStdOutput());
});


// Writing input

test('write input', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: null);
$process->writeStdInput('hello' . PHP_EOL);
$process->writeStdInput('world' . PHP_EOL);
$process->closeStdInput();
Assert::same('hello' . PHP_EOL, $process->getStdOutput());
});

test('writeStdInput() after closeStdInput() throws exception', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: null);
$process->writeStdInput('hello' . PHP_EOL);
$process->closeStdInput();
Assert::exception(
fn() => $process->writeStdInput('world' . PHP_EOL),
Nette\InvalidStateException::class,
'Cannot write to process: STDIN pipe is closed',
);
});

test('writeStdInput() throws exception when stdin is not null', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);']);
Assert::exception(
fn() => $process->writeStdInput('hello' . PHP_EOL),
Nette\InvalidStateException::class,
'Cannot write to process: STDIN pipe is closed',
);
});
67 changes: 67 additions & 0 deletions tests/Utils/Process.output.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

use Nette\Utils\Process;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


// Different output types

test('output to files', function () {
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";'], stdout: $tempFile, stderr: false);
$process->wait();
Assert::same('hello', file_get_contents($tempFile));
unlink($tempFile);
});

test('setting stderr to false prevents reading from getStdError() or consumeStdError()', function () {
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fwrite(STDERR, "hello");'], stderr: false);
$process->wait();
Assert::exception(
fn() => $process->getStdError(),
LogicException::class,
'Cannot read output: output capturing was not enabled',
);
Assert::exception(
fn() => $process->consumeStdError(),
LogicException::class,
'Cannot read output: output capturing was not enabled',
);
});

test('stream as output', function () {
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
$output = fopen($tempFile, 'w');
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";'], stdout: $output);
$process->wait();
fclose($output);
Assert::same('hello', file_get_contents($tempFile));
unlink($tempFile);
});

test('stream as error output', function () {
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
$output = fopen($tempFile, 'w');
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fwrite(STDERR, "hello");'], stderr: $output);
$process->wait();
fclose($output);
Assert::same('hello', file_get_contents($tempFile));
unlink($tempFile);
});

test('changing both stdout and stderr does not trigger callbacks in wait()', function () {
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
$output = fopen($tempFile, 'w');
$wasCalled = false;
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";'], stdout: $output, stderr: $output);
$process->wait(function () use (&$wasCalled) {
$wasCalled = true;
});
fclose($output);
Assert::false($wasCalled);
unlink($tempFile);
});
32 changes: 32 additions & 0 deletions tests/Utils/Process.piping.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

use Nette\Utils\Helpers;
use Nette\Utils\Process;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


if (Helpers::IsWindows) {
Tester\Environment::skip('Process piping is not supported on Windows.');
}

$process1 = Process::runExecutable(
PHP_BINARY,
['-f', __DIR__ . '/fixtures.process/tick.php'],
);

$process2 = Process::runExecutable(
PHP_BINARY,
['-f', __DIR__ . '/fixtures.process/rev.php'],
stdin: $process1,
);

$output = '';
$process2->wait(function ($stdOut, $stdErr) use (&$output) {
$output .= $stdOut;
});

Assert::same('kcit' . PHP_EOL . 'kcit' . PHP_EOL . 'kcit' . PHP_EOL, $output);
Loading

0 comments on commit b92138e

Please sign in to comment.