Skip to content

Commit e269e3d

Browse files
Merge branch 'main' into 221
2 parents f1431ff + 3d1a9ff commit e269e3d

7 files changed

Lines changed: 188 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Latest
2020
* [#207](https://github.com/cleverage/process-bundle/issues/207) Fix JsonStreamReaderTask / JsonStreamWriterTask: throw an explicit `\UnexpectedValueException` when a line decodes to a scalar, create the missing parent directory when writing. Update documentation, add tests.
2121
* [#208](https://github.com/cleverage/process-bundle/issues/208) Fix FolderBrowserTask, InputFolderBrowserTask, CsvReaderTask and LineReaderTask (and their `Input*` variants): reset the state at the end of the iteration, so that a following input is read from its beginning. Update documentation, add tests.
2222
* [#201](https://github.com/cleverage/process-bundle/issues/201) Fix minor defects: error messages of TransformerTrait, RulesTransformer and ExpressionLanguageMapTransformer, useless `setRequired()` in ImplodeTransformer and SprintfTransformer, stray namespace in TrimTransformer, wrong or missing docblocks. Add tests.
23+
* [#220](https://github.com/cleverage/process-bundle/issues/220) Fix the stop error strategy: throw a `ProcessFailedException` (with the original exception as `previous`) instead of a `FatalError`, so that the command exits with a non-zero code when a process fails and `ProcessLauncherTask` detects failed sub-processes. Update documentation, add tests.
2324
* [#221](https://github.com/cleverage/process-bundle/issues/221) Fix CsvSplitterTask: each produced file contains `max_lines` data lines (instead of `max_lines - 2`), no infinite loop with `max_lines` <= 2 (`max_lines` must now be an integer greater than 0), no header-only file at the end. Update documentation, add tests.
2425

2526
## Deprecated

‎docs/01-quick_start.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ Once everything is working fine, you may want to automate your processes. The st
262262
0 */2 * * * /path/to/project/bin/console cleverage:process:execute <my_process> --env=prod
263263
```
264264

265+
When a process fails, the command exits with a non-zero code, so failures can be detected by the scheduler (or by a
266+
CI job, a supervisor...).
267+
265268
This bundle does not store any execution history in database. Process and task logs are sent to dedicated Monolog
266269
channels (`cleverage_process`, `cleverage_process_task` and `cleverage_process_transformer`, see
267270
[logging](03-custom_tasks.md#logging)), so you can route them to any handler. Each record is enriched with the

‎docs/04-advanced_workflow.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ When a process is executed (with the `cleverage:process:execute` command or with
2323

2424
If an exception is thrown at any point, the `cleverage_process.fail` event is dispatched and the exception is rethrown.
2525
Note that a task error handled by the `stop` strategy does not surface as the original exception: the process manager
26-
throws a new `Symfony\Component\ErrorHandler\Error\FatalError` (an `\Error`, not an `\Exception`), whose message
27-
contains the process code, the task code and the original message; the original exception is not attached as
28-
`previous` (it is only available in the task error log record).
26+
throws a `CleverAge\ProcessBundle\Exception\ProcessFailedException` (a `\RuntimeException`), whose message contains
27+
the process code, the task code and the original message. The original exception is available with `getPrevious()`.
2928

3029
### Executing a process from PHP
3130

@@ -111,7 +110,7 @@ on the `cleverage_process_task` channel with the `log_level` of the task (`criti
111110
default):
112111
- `skip`: the current output is dropped, and the process continues with the next input (e.g. the next line of a CSV
113112
file)
114-
- `stop`: the whole process stops and fails (a `FatalError` is thrown by the process manager, see
113+
- `stop`: the whole process stops and fails (a `ProcessFailedException` is thrown by the process manager, see
115114
[process execution flow](#process-execution-flow))
116115

117116
Before applying the strategy, the task input is sent to the tasks listed in `error_outputs` (unless the task already

‎docs/06-testing.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ public function execute(string $processCode, mixed $input = null, array $context
102102

103103
`$input` is given to the process `entry_point`, `$context` is the same as the `--context` option of the command, and
104104
the returned value is the last output of the process `end_point` (`null` if there is none). If the process fails, the
105-
exception is rethrown; but a task error handled by the `stop` error strategy is thrown as a new
106-
`Symfony\Component\ErrorHandler\Error\FatalError` (an `\Error`), which only keeps the original message (the original
107-
exception is not attached as `previous`). Test it with `expectException(FatalError::class)` and
108-
`expectExceptionMessageMatches()` rather than with the original exception class.
105+
exception is rethrown; but a task error handled by the `stop` error strategy is thrown as a
106+
`CleverAge\ProcessBundle\Exception\ProcessFailedException`, the original exception being available with
107+
`getPrevious()`. Test it with `expectException(ProcessFailedException::class)`, and check the original exception with
108+
`getPrevious()` if needed.
109109

110110
```yaml
111111
# config/packages/test/clever_age_process.yaml
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the CleverAge/ProcessBundle package.
7+
*
8+
* Copyright (c) Clever-Age
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace CleverAge\ProcessBundle\Exception;
15+
16+
/**
17+
* Thrown when a process is stopped by a task error (error strategy "stop").
18+
*
19+
* The original exception is available with getPrevious().
20+
*/
21+
class ProcessFailedException extends \RuntimeException implements ProcessExceptionInterface
22+
{
23+
public static function create(string $processCode, string $taskCode, \Throwable $previous): self
24+
{
25+
$errorStr = "Process {$processCode} has failed during process {$taskCode}";
26+
$errorStr .= " with message: '{$previous->getMessage()}'.\n";
27+
28+
return new self($errorStr, 0, $previous);
29+
}
30+
}

‎src/Manager/ProcessManager.php‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
1919
use CleverAge\ProcessBundle\Event\ProcessEvent;
2020
use CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException;
21+
use CleverAge\ProcessBundle\Exception\ProcessFailedException;
2122
use CleverAge\ProcessBundle\Logger\ProcessLogger;
2223
use CleverAge\ProcessBundle\Logger\TaskLogger;
2324
use CleverAge\ProcessBundle\Model\BlockingTaskInterface;
@@ -30,7 +31,6 @@
3031
use CleverAge\ProcessBundle\Model\TaskInterface;
3132
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
3233
use Symfony\Component\DependencyInjection\ContainerInterface;
33-
use Symfony\Component\ErrorHandler\Error\FatalError;
3434
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
3535

3636
/**
@@ -290,12 +290,7 @@ protected function process(TaskConfiguration $taskConfiguration, int $executionF
290290
if ($state->isStopped()) {
291291
$exception = $state->getException();
292292
if ($exception instanceof \Throwable) {
293-
$m = "Process {$state->getProcessConfiguration()
294-
->getCode()} has failed";
295-
$m .= " during process {$state->getTaskConfiguration()
296-
->getCode()}";
297-
$m .= " with message: '{$exception->getMessage()}'.\n";
298-
throw new FatalError($m, -1, ['file' => $exception->getFile(), 'line' => $exception->getLine(), 'type' => 500, 'message' => $exception->getMessage()]);
293+
throw ProcessFailedException::create($state->getProcessConfiguration()->getCode(), $state->getTaskConfiguration()->getCode(), $exception);
299294
}
300295

301296
return;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the CleverAge/ProcessBundle package.
7+
*
8+
* Copyright (c) Clever-Age
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace CleverAge\ProcessBundle\Tests\Manager;
15+
16+
use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
17+
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
18+
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
19+
use CleverAge\ProcessBundle\Event\ProcessEvent;
20+
use CleverAge\ProcessBundle\Exception\ProcessFailedException;
21+
use CleverAge\ProcessBundle\Logger\AbstractLogger;
22+
use CleverAge\ProcessBundle\Logger\ProcessLogger;
23+
use CleverAge\ProcessBundle\Logger\TaskLogger;
24+
use CleverAge\ProcessBundle\Manager\ProcessManager;
25+
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
26+
use CleverAge\ProcessBundle\Model\ProcessHistory;
27+
use CleverAge\ProcessBundle\Model\ProcessState;
28+
use CleverAge\ProcessBundle\Model\TaskInterface;
29+
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
30+
use CleverAge\ProcessBundle\Task\ConstantOutputTask;
31+
use PHPUnit\Framework\TestCase;
32+
use Psr\Log\NullLogger;
33+
use Symfony\Component\DependencyInjection\Container;
34+
use Symfony\Component\EventDispatcher\EventDispatcher;
35+
36+
#[\PHPUnit\Framework\Attributes\CoversClass(ProcessManager::class)]
37+
#[\PHPUnit\Framework\Attributes\CoversClass(ProcessFailedException::class)]
38+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfiguration::class)]
39+
#[\PHPUnit\Framework\Attributes\UsesClass(TaskConfiguration::class)]
40+
#[\PHPUnit\Framework\Attributes\UsesClass(ContextualOptionResolver::class)]
41+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessEvent::class)]
42+
#[\PHPUnit\Framework\Attributes\UsesClass(AbstractLogger::class)]
43+
#[\PHPUnit\Framework\Attributes\UsesClass(AbstractConfigurableTask::class)]
44+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessHistory::class)]
45+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessState::class)]
46+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfigurationRegistry::class)]
47+
#[\PHPUnit\Framework\Attributes\UsesClass(ConstantOutputTask::class)]
48+
class ProcessManagerStopStrategyTest extends TestCase
49+
{
50+
public function testStopStrategyThrowsAProcessFailedExceptionWithTheOriginalException(): void
51+
{
52+
$originalException = new \LogicException('Something went wrong', 42);
53+
$processManager = $this->createProcessManager($originalException);
54+
55+
try {
56+
$processManager->execute('test.process');
57+
self::fail('The process should have failed');
58+
} catch (\Exception $exception) { // An \Exception, not an \Error: catchable with catch (\Exception)
59+
self::assertInstanceOf(ProcessFailedException::class, $exception);
60+
self::assertSame(
61+
"Process test.process has failed during process fail with message: 'Something went wrong'.\n",
62+
$exception->getMessage()
63+
);
64+
self::assertSame($originalException, $exception->getPrevious());
65+
}
66+
}
67+
68+
public function testSkipStrategyDoesNotThrow(): void
69+
{
70+
$processManager = $this->createProcessManager(new \LogicException('Something went wrong'), 'skip');
71+
72+
self::assertNull($processManager->execute('test.process'));
73+
}
74+
75+
private function createProcessManager(\Throwable $exception, string $errorStrategy = 'stop'): ProcessManager
76+
{
77+
$failingTask = new class($exception) implements TaskInterface {
78+
public function __construct(
79+
private readonly \Throwable $exception,
80+
) {
81+
}
82+
83+
public function execute(ProcessState $state): void
84+
{
85+
throw $this->exception;
86+
}
87+
};
88+
89+
$container = new Container();
90+
$container->set('test.constant', new ConstantOutputTask());
91+
$container->set('test.failing', $failingTask);
92+
93+
$registry = new ProcessConfigurationRegistry(
94+
[
95+
'test.process' => [
96+
'options' => [],
97+
'entry_point' => null,
98+
'end_point' => null,
99+
'description' => '',
100+
'help' => '',
101+
'public' => true,
102+
'tasks' => [
103+
'entry' => $this->createTaskConfiguration('@test.constant', ['output' => 'value'], ['fail']),
104+
'fail' => $this->createTaskConfiguration('@test.failing', [], [], $errorStrategy),
105+
],
106+
],
107+
],
108+
'stop'
109+
);
110+
111+
return new ProcessManager(
112+
$container,
113+
new ProcessLogger(new NullLogger()),
114+
new TaskLogger(new NullLogger()),
115+
$registry,
116+
new ContextualOptionResolver(),
117+
new EventDispatcher(),
118+
);
119+
}
120+
121+
/**
122+
* @param array<string, mixed> $options
123+
* @param list<string> $outputs
124+
*
125+
* @return array<string, mixed>
126+
*/
127+
private function createTaskConfiguration(
128+
string $service,
129+
array $options = [],
130+
array $outputs = [],
131+
?string $errorStrategy = null,
132+
): array {
133+
return [
134+
'service' => $service,
135+
'options' => $options,
136+
'description' => '',
137+
'help' => '',
138+
'outputs' => $outputs,
139+
'errors' => [],
140+
'error_outputs' => [],
141+
'error_strategy' => $errorStrategy,
142+
'log_level' => null,
143+
];
144+
}
145+
}

0 commit comments

Comments
 (0)