Skip to content

Commit 2b48b24

Browse files
committedDec 12, 2024
added Process [WIP]
1 parent dcaeb77 commit 2b48b24

11 files changed

+873
-0
lines changed
 

‎src/Utils/Process.php

+445
Large diffs are not rendered by default.

‎src/Utils/exceptions.php

+16
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,19 @@ class RegexpException extends \Exception
4848
class AssertionException extends \Exception
4949
{
5050
}
51+
52+
53+
/**
54+
* The process failed to run successfully.
55+
*/
56+
class ProcessFailedException extends \RuntimeException
57+
{
58+
}
59+
60+
61+
/**
62+
* The process execution exceeded its timeout limit.
63+
*/
64+
class ProcessTimeoutException extends \RuntimeException
65+
{
66+
}

‎tests/Utils/Process.basic.phpt

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Helpers;
6+
use Nette\Utils\Process;
7+
use Nette\Utils\ProcessFailedException;
8+
use Nette\Utils\ProcessTimeoutException;
9+
use Tester\Assert;
10+
11+
require __DIR__ . '/../bootstrap.php';
12+
13+
14+
// Process execution - success
15+
16+
test('run executable successfully', function () {
17+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";']);
18+
Assert::true($process->isSuccess());
19+
Assert::same(0, $process->getExitCode());
20+
Assert::same('hello', $process->getStdOutput());
21+
Assert::same('', $process->getStdError());
22+
});
23+
24+
test('run command successfully', function () {
25+
$process = Process::runCommand('echo hello');
26+
Assert::true($process->isSuccess());
27+
Assert::same(0, $process->getExitCode());
28+
Assert::same('hello' . PHP_EOL, $process->getStdOutput());
29+
Assert::same('', $process->getStdError());
30+
});
31+
32+
33+
// Process execution - errors
34+
35+
test('run executable with error', function () {
36+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'exit(1);']);
37+
Assert::false($process->isSuccess());
38+
Assert::same(1, $process->getExitCode());
39+
});
40+
41+
test('run executable ensure success throws exception on error', function () {
42+
Assert::exception(
43+
fn() => Process::runExecutable(PHP_BINARY, ['-r', 'exit(1);'])->ensureSuccess(),
44+
ProcessFailedException::class,
45+
'Process failed with non-zero exit code: 1',
46+
);
47+
});
48+
49+
test('run command with error', function () {
50+
$process = Process::runCommand('"' . PHP_BINARY . '" -r "exit(1);"');
51+
Assert::false($process->isSuccess());
52+
Assert::same(1, $process->getExitCode());
53+
});
54+
55+
test('run command ensure success throws exception on error', function () {
56+
Assert::exception(
57+
fn() => Process::runCommand('"' . PHP_BINARY . '" -r "exit(1);"')->ensureSuccess(),
58+
ProcessFailedException::class,
59+
'Process failed with non-zero exit code: 1',
60+
);
61+
});
62+
63+
64+
// Process state monitoring
65+
66+
test('is running', function () {
67+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'sleep(1);']);
68+
Assert::true($process->isRunning());
69+
$process->wait();
70+
Assert::false($process->isRunning());
71+
});
72+
73+
test('get pid', function () {
74+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'sleep(1);']);
75+
Assert::type('int', $process->getPid());
76+
$process->wait();
77+
Assert::null($process->getPid());
78+
});
79+
80+
81+
// Waiting for process
82+
83+
test('wait', function () {
84+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";']);
85+
$process->wait();
86+
$process->wait();
87+
Assert::false($process->isRunning());
88+
Assert::same(0, $process->getExitCode());
89+
Assert::same('hello', $process->getStdOutput());
90+
});
91+
92+
test('wait with callback', function () {
93+
$output = '';
94+
$error = '';
95+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello"; fwrite(STDERR, "error");']);
96+
$process->wait(function ($stdOut, $stdErr) use (&$output, &$error) {
97+
$output .= $stdOut;
98+
$error .= $stdErr;
99+
});
100+
Assert::same('hello', $output);
101+
Assert::same('error', $error);
102+
});
103+
104+
105+
// Automatically call wait()
106+
107+
test('getStdOutput() automatically call wait()', function () {
108+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";']);
109+
Assert::same('hello', $process->getStdOutput());
110+
Assert::false($process->isRunning());
111+
});
112+
113+
test('getExitCode() automatically call wait()', function () {
114+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo exit(2);']);
115+
Assert::same(2, $process->getExitCode());
116+
Assert::false($process->isRunning());
117+
});
118+
119+
120+
// Terminating process
121+
122+
test('terminate', function () {
123+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'sleep(5);']);
124+
$process->terminate();
125+
Assert::false($process->isRunning());
126+
});
127+
128+
test('terminate() and then wait()', function () {
129+
$process = Process::runExecutable(PHP_BINARY, ['-f', 'sleep(5);']);
130+
$process->terminate();
131+
$process->wait();
132+
Assert::false($process->isRunning());
133+
});
134+
135+
136+
// Timeout
137+
138+
test('timeout', function () {
139+
Assert::exception(
140+
fn() => Process::runExecutable(PHP_BINARY, ['-r', 'sleep(5);'], timeout: 0.1)->wait(),
141+
ProcessTimeoutException::class,
142+
'Process exceeded the time limit of 0.1 seconds',
143+
);
144+
});
145+
146+
147+
// bypass_shell
148+
149+
if (Helpers::IsWindows) {
150+
test('bypass_shell = false', function () {
151+
$process = Process::runCommand('"' . PHP_BINARY . '" -r "echo 123;"', options: ['bypass_shell' => false]);
152+
Assert::same('123', $process->getStdOutput());
153+
});
154+
}

‎tests/Utils/Process.consume.phpt

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Process;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
test('incremental output consumption', function () {
12+
$process = Process::runExecutable(PHP_BINARY, ['-f', __DIR__ . '/fixtures.process/incremental.php', 'stdout']);
13+
usleep(50_000);
14+
Assert::same('hel', $process->consumeStdOutput());
15+
usleep(50_000);
16+
Assert::same('lo', $process->consumeStdOutput());
17+
usleep(50_000);
18+
Assert::same('wor', $process->consumeStdOutput());
19+
usleep(50_000);
20+
Assert::same('ld', $process->consumeStdOutput());
21+
$process->wait();
22+
Assert::same('', $process->consumeStdOutput());
23+
Assert::same('helloworld', $process->getStdOutput());
24+
});
25+
26+
test('incremental error output consumption', function () {
27+
$process = Process::runExecutable(PHP_BINARY, ['-f', __DIR__ . '/fixtures.process/incremental.php', 'stderr']);
28+
usleep(50_000);
29+
Assert::same('hello' . PHP_EOL, $process->consumeStdError());
30+
usleep(50_000);
31+
Assert::same('world' . PHP_EOL, $process->consumeStdError());
32+
usleep(50_000);
33+
Assert::same('', $process->consumeStdError());
34+
Assert::same('hello' . PHP_EOL . 'world' . PHP_EOL, $process->getStdError());
35+
});

‎tests/Utils/Process.environment.phpt

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Process;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
// Environment variables
12+
13+
test('environment variables', function () {
14+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo getenv("TEST_VAR");'], env: ['TEST_VAR' => '123']);
15+
Assert::same('123', $process->getStdOutput());
16+
});
17+
18+
test('no environment variables', function () {
19+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo !getenv("PATH") ? "ok" : "no";'], env: []);
20+
Assert::same('ok', $process->getStdOutput());
21+
});
22+
23+
test('parent environment variables', function () {
24+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo getenv("PATH") ? "ok" : "no";']);
25+
Assert::same('ok', $process->getStdOutput());
26+
});

‎tests/Utils/Process.input.phpt

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Process;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
// Different input types
12+
13+
test('string as input', function () {
14+
$input = 'Hello Input';
15+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: $input);
16+
Assert::same('Hello Input', $process->getStdOutput());
17+
});
18+
19+
test('stream as input', function () {
20+
$input = fopen('php://memory', 'r+');
21+
fwrite($input, 'Hello Input');
22+
rewind($input);
23+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: $input);
24+
Assert::same('Hello Input', $process->getStdOutput());
25+
});
26+
27+
28+
// Writing input
29+
30+
test('write input', function () {
31+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: null);
32+
$process->writeStdInput('hello' . PHP_EOL);
33+
$process->writeStdInput('world' . PHP_EOL);
34+
$process->closeStdInput();
35+
Assert::same('hello' . PHP_EOL, $process->getStdOutput());
36+
});
37+
38+
test('writeStdInput() after closeStdInput() throws exception', function () {
39+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);'], stdin: null);
40+
$process->writeStdInput('hello' . PHP_EOL);
41+
$process->closeStdInput();
42+
Assert::exception(
43+
fn() => $process->writeStdInput('world' . PHP_EOL),
44+
Nette\InvalidStateException::class,
45+
'Cannot write to process: STDIN pipe is closed',
46+
);
47+
});
48+
49+
test('writeStdInput() throws exception when stdin is not null', function () {
50+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fgets(STDIN);']);
51+
Assert::exception(
52+
fn() => $process->writeStdInput('hello' . PHP_EOL),
53+
Nette\InvalidStateException::class,
54+
'Cannot write to process: STDIN pipe is closed',
55+
);
56+
});

‎tests/Utils/Process.output.phpt

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Process;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
// Different output types
12+
13+
test('output to files', function () {
14+
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
15+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";'], stdout: $tempFile, stderr: false);
16+
$process->wait();
17+
Assert::same('hello', file_get_contents($tempFile));
18+
unlink($tempFile);
19+
});
20+
21+
test('setting stderr to false prevents reading from getStdError() or consumeStdError()', function () {
22+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fwrite(STDERR, "hello");'], stderr: false);
23+
$process->wait();
24+
Assert::exception(
25+
fn() => $process->getStdError(),
26+
LogicException::class,
27+
'Cannot read output: output capturing was not enabled',
28+
);
29+
Assert::exception(
30+
fn() => $process->consumeStdError(),
31+
LogicException::class,
32+
'Cannot read output: output capturing was not enabled',
33+
);
34+
});
35+
36+
test('stream as output', function () {
37+
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
38+
$output = fopen($tempFile, 'w');
39+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";'], stdout: $output);
40+
$process->wait();
41+
fclose($output);
42+
Assert::same('hello', file_get_contents($tempFile));
43+
unlink($tempFile);
44+
});
45+
46+
test('stream as error output', function () {
47+
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
48+
$output = fopen($tempFile, 'w');
49+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo fwrite(STDERR, "hello");'], stderr: $output);
50+
$process->wait();
51+
fclose($output);
52+
Assert::same('hello', file_get_contents($tempFile));
53+
unlink($tempFile);
54+
});
55+
56+
test('changing both stdout and stderr does not trigger callbacks in wait()', function () {
57+
$tempFile = tempnam(sys_get_temp_dir(), 'process_test_');
58+
$output = fopen($tempFile, 'w');
59+
$wasCalled = false;
60+
$process = Process::runExecutable(PHP_BINARY, ['-r', 'echo "hello";'], stdout: $output, stderr: $output);
61+
$process->wait(function () use (&$wasCalled) {
62+
$wasCalled = true;
63+
});
64+
fclose($output);
65+
Assert::false($wasCalled);
66+
unlink($tempFile);
67+
});

‎tests/Utils/Process.piping.phpt

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Helpers;
6+
use Nette\Utils\Process;
7+
use Tester\Assert;
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
11+
12+
if (Helpers::IsWindows) {
13+
Tester\Environment::skip('Process piping is not supported on Windows.');
14+
}
15+
16+
$process1 = Process::runExecutable(
17+
PHP_BINARY,
18+
['-f', __DIR__ . '/fixtures.process/tick.php'],
19+
);
20+
21+
$process2 = Process::runExecutable(
22+
PHP_BINARY,
23+
['-f', __DIR__ . '/fixtures.process/rev.php'],
24+
stdin: $process1,
25+
);
26+
27+
$output = '';
28+
$process2->wait(function ($stdOut, $stdErr) use (&$output) {
29+
$output .= $stdOut;
30+
});
31+
32+
Assert::same('kcit' . PHP_EOL . 'kcit' . PHP_EOL . 'kcit' . PHP_EOL, $output);

0 commit comments

Comments
 (0)
Failed to load comments.