Skip to content

Commit 62d2c1a

Browse files
authored
docs(process): document tempest/process usage (#1511)
1 parent 70bc5f8 commit 62d2c1a

File tree

4 files changed

+153
-4
lines changed

4 files changed

+153
-4
lines changed

docs/2-features/16-process.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: Processes
3+
description: "Learn how to run synchronous and asynchronous processes, capture their output, and test them."
4+
---
5+
6+
## Overview
7+
8+
Tempest provides a testable wrapper around the [Symfony Process component](https://symfony.com/doc/current/components/process.html), inspired by [Laravel's own wrapper](https://laravel.com/docs/12.x/processes). It allows you to run one or multiple processes synchronously or asynchronously, while being testable and convenient to use.
9+
10+
## Synchronous processes
11+
12+
The {`Tempest\Process\ProcessExecutor`} interface is the entrypoint for invoking processes. It provides a `run()` method to run a process synchronously, and a `start()` method to run it asynchronously. You may access the interface by [injecting it as a dependency](../1-essentials/05-container.md) in your classes.
13+
14+
```php app/Composer.php
15+
use Tempest\Process\ProcessExecutor;
16+
17+
final readonly class Composer
18+
{
19+
public function __construct(
20+
private ProcessExecutor $executor
21+
) {}
22+
23+
public function update(): void
24+
{
25+
$this->executor->run('composer update');
26+
}
27+
}
28+
```
29+
30+
The `run()` method returns an instance of {b`Tempest\Process\ProcessResult`}, which contains the output of the process, its exit code, and whether it was successful. You can access these properties to handle the result of the process.
31+
32+
```php app/Composer.php
33+
$result = $this->executor->run('composer update');
34+
35+
$result->successful();
36+
$result->failed();
37+
$result->exitCode;
38+
$result->output;
39+
$result->errorOutput;
40+
```
41+
42+
## Asynchronous processes
43+
44+
To run a process asynchronously, you may use the `start()` method instead. This will return an instance of {b`Tempest\Process\InvokedProcess`}, which you can use to monitor the process.
45+
46+
You may send a signal to a running process using the `signal()` method, or stop it using `stop()`. It is also possible to wait for the process using `wait()`, which accepts a callback to capture the live output of the process.
47+
48+
```php app/Composer.php
49+
$this->executor
50+
->start('composer update')
51+
->wait(function (OutputChannel $channel, string $output) {
52+
echo $output;
53+
});
54+
```
55+
56+
## Process pools
57+
58+
It is possible to execute multiple tasks simultaneously using a process pool. To do so, you may call the `pool()` method on the {`Tempest\Process\ProcessExecutor`}. This returns a {b`Tempest\Process\InvokedProcessPool`} instance, which provides convenient methods for managing the processes.
59+
60+
```php
61+
$pool = $this->executor->pool([
62+
'composer update',
63+
'bun install',
64+
]);
65+
66+
$pool->start();
67+
$pool->count();
68+
$pool->forEach(fn (InvokedProcess $process) => /** ... */);
69+
$pool->forEachRunning(fn (InvokedProcess $process) => /** ... */);
70+
$pool->signal(SIGINT);
71+
$pool->stop();
72+
```
73+
74+
Alternatively, if you are only interested in the process outputs, you may use the `concurrently()` method and destructure its results:
75+
76+
```php
77+
[$composer, $bun] = $this->executor->concurrently([
78+
'composer update',
79+
'bun install',
80+
]);
81+
82+
echo $composer;
83+
echo $bun;
84+
```
85+
86+
## Testing
87+
88+
Tempest provides a process testing utility accessible through the `process` property of the [`IntegrationTest`](https://github.com/tempestphp/tempest-framework/blob/main/src/Tempest/Framework/Testing/IntegrationTest.php) test case. You may learn more about testing in the [dedicated chapter](../1-essentials/07-testing.md).
89+
90+
### Mocking processes
91+
92+
Testing process invokation results involves calling `mockProcessResult()` with the command you want to mock and an optional result. This will simulate the command being run and return the result you specified.
93+
94+
```php
95+
// Mocks `composer up` calls
96+
$this->process->mockProcessResult('composer up');
97+
98+
// Call application code...
99+
// ...
100+
101+
// Assert against executed processes
102+
$this->process->assertCommandRan('composer up');
103+
$this->process->assertRan(function (PendingProcess $process, ProcessResult $result) {
104+
// ...
105+
});
106+
```
107+
108+
### Describing asynchronous processes
109+
110+
When dealing with asynchronous processes, you may use the `describe()` method to define the expectations of the process. This allows you to specify the command, the expected output and error output, the exit code, and the amount of times the `running` property should return `true`.
111+
112+
```php
113+
$this->process->mockProcessResults([
114+
'composer up' => $this->process
115+
->describe()
116+
->iterations(1)
117+
->output('Nothing to install, update or remove'),
118+
'bun install' => $this->process
119+
->describe()
120+
->iterations(4)
121+
->output('Checked 225 installs across 274 packages (no changes) [144.00ms]'),
122+
]);
123+
124+
$this->process->assertCommandRan('composer up', function (ProcessResult $result) {
125+
$this->assertSame("Nothing to install, update or remove\n", $result->output);
126+
});
127+
```
128+
129+
In the example above, `composer up` and `bun install` are mocked to return the specified output. They both return `0` as their exit code by default. The `running` property of the process that runs `composer up` will return `true` only once, while the one that runs `bun install` will return `true` four times.
130+
131+
### Allowing process execution
132+
133+
By default, to prevent unintended side effects, Tempest does not actually execute processes during tests. Instead, trying to execute non-mocked processes will throw an exception.
134+
135+
If you prefer to allow process execution, you may change this behavior by calling `allowRunningActualProcesses()` in your test case. This will allow all processes to be executed, and you may still perform assertions on them.
136+
137+
```php
138+
$this->process->allowRunningActualProcesses();
139+
140+
// Call application code...
141+
142+
$this->process->assertCommandRan('composer up');
143+
```

packages/process/src/InvokedProcessPool.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public function wait(): ProcessPoolResults
6161

6262
/**
6363
* Iterates over each running process in the pool and applies the given callback.
64+
*
65+
* @param Closure(InvokedProcess): mixed $callback
6466
*/
6567
public function forEachRunning(\Closure $callback): self
6668
{
@@ -71,6 +73,8 @@ public function forEachRunning(\Closure $callback): self
7173

7274
/**
7375
* Iterates over each invoked process in the pool and applies the given callback.
76+
*
77+
* @param Closure(InvokedProcess): mixed $callback
7478
*/
7579
public function forEach(\Closure $callback): self
7680
{

packages/process/src/ProcessPoolResults.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace Tempest\Process;
44

5-
use Tempest\Support\Arr\ArrayInterface;
5+
use ArrayAccess;
6+
use Countable;
7+
use Iterator;
68
use Tempest\Support\Arr\ImmutableArray;
79

8-
final class ProcessPoolResults implements ArrayInterface
10+
final class ProcessPoolResults implements Iterator, ArrayAccess, Countable
911
{
1012
public function __construct(
1113
/** @var ImmutableArray<ProcessResult> */

packages/process/src/Testing/ProcessTester.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function recordProcessExecutions(): void
4141
/**
4242
* Sets up the specified command or pattern to return the specified result. The command accepts `*` as a placeholder.
4343
*/
44-
public function mockProcessResult(string $command = '*', string|ProcessResult $result = ''): self
44+
public function mockProcessResult(string $command = '*', string|ProcessResult|InvokedProcessDescription $result = ''): self
4545
{
4646
$this->recordProcessExecutions();
4747

@@ -53,7 +53,7 @@ public function mockProcessResult(string $command = '*', string|ProcessResult $r
5353
/**
5454
* Sets up the specified commands or patterns to return the specified results.
5555
*
56-
* @var array<string,string|ProcessResult> $results
56+
* @param array<string,string|ProcessResult|InvokedProcessDescription> $results
5757
*/
5858
public function mockProcessResults(array $results): self
5959
{

0 commit comments

Comments
 (0)