diff --git a/src/Contracts/Plugins/ObservesExitCode.php b/src/Contracts/Plugins/ObservesExitCode.php new file mode 100644 index 000000000..d42202571 --- /dev/null +++ b/src/Contracts/Plugins/ObservesExitCode.php @@ -0,0 +1,10 @@ +addOutput($exitCode); } + foreach (Loader::getPlugins(Plugins\ObservesExitCode::class) as $plugin) { + assert($plugin instanceof Plugins\ObservesExitCode); + + $plugin->observeExitCode($exitCode); + } + return $exitCode; } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 39bd3f9e4..c04270c02 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1823,6 +1823,15 @@ PASS Tests\Unit\Overrides\ThrowableBuilder ✓ collision editor can be added to the stack trace + PASS Tests\Unit\Plugins\Actions\CallsAddsOutput + ✓ exit code observers run after every output plugin + ✓ exit code observers receive unchanged codes when there are no output plugins with (0) + ✓ exit code observers receive unchanged codes when there are no output plugins with (1) + ✓ exit code observers receive unchanged codes when there are no output plugins with (2) + ✓ exit code observers receive unchanged codes when there are no output plugins with (42) + ✓ output plugins work without exit code observers + ✓ exit code observers are not called when an output plugin throws + PASS Tests\Unit\Plugins\Concerns\HandleArguments ✓ method hasArgument with ('--long-argument', true) ✓ method hasArgument with ('-a', true) @@ -2321,4 +2330,4 @@ ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 37 skipped, 1648 passed (3649 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 37 skipped, 1655 passed (3663 assertions) diff --git a/tests/Unit/Plugins/Actions/CallsAddsOutput.php b/tests/Unit/Plugins/Actions/CallsAddsOutput.php new file mode 100644 index 000000000..02e379688 --- /dev/null +++ b/tests/Unit/Plugins/Actions/CallsAddsOutput.php @@ -0,0 +1,114 @@ +getProperty('instances'); + $loaded = $loader->getProperty('loaded'); + $previousInstances = $instances->getValue(); + $previousLoaded = $loaded->getValue(); + + try { + $instances->setValue(null, $plugins); + $loaded->setValue(null, true); + + $test(); + } finally { + $instances->setValue(null, $previousInstances); + $loaded->setValue(null, $previousLoaded); + } +} + +test('exit code observers run after every output plugin', function (): void { + $events = new ArrayObject; + $observer = new readonly class($events) implements ObservesExitCode + { + public function __construct(private ArrayObject $events) {} + + public function observeExitCode(int $exitCode): void + { + $this->events[] = ['observed', $exitCode]; + } + }; + $output = new readonly class($events) implements AddsOutput + { + public function __construct(private ArrayObject $events) {} + + public function addOutput(int $exitCode): int + { + $this->events[] = ['output', $exitCode]; + + return $exitCode + 1; + } + }; + + withExitCodePlugins([$observer, $output, clone $observer, clone $output], function () use ($events): void { + expect(CallsAddsOutput::execute(0))->toBe(2) + ->and($events->getArrayCopy())->toBe([ + ['output', 0], + ['output', 1], + ['observed', 2], + ['observed', 2], + ]); + }); +}); + +test('exit code observers receive unchanged codes when there are no output plugins', function (int $exitCode): void { + $observer = new class implements ObservesExitCode + { + public ?int $exitCode = null; + + public function observeExitCode(int $exitCode): void + { + $this->exitCode = $exitCode; + } + }; + + withExitCodePlugins([$observer], function () use ($observer, $exitCode): void { + expect(CallsAddsOutput::execute($exitCode))->toBe($exitCode) + ->and($observer->exitCode)->toBe($exitCode); + }); +})->with([0, 1, 2, 42]); + +test('output plugins work without exit code observers', function (): void { + $output = new class implements AddsOutput + { + public function addOutput(int $exitCode): int + { + return 42; + } + }; + + withExitCodePlugins([$output], function (): void { + expect(CallsAddsOutput::execute(0))->toBe(42); + }); +}); + +test('exit code observers are not called when an output plugin throws', function (): void { + $observer = new class implements ObservesExitCode + { + public bool $called = false; + + public function observeExitCode(int $exitCode): void + { + $this->called = true; + } + }; + $output = new class implements AddsOutput + { + public function addOutput(int $exitCode): int + { + throw new RuntimeException('Output failed'); + } + }; + + withExitCodePlugins([$observer, $output], function () use ($observer): void { + expect(fn (): int => CallsAddsOutput::execute(0))->toThrow(RuntimeException::class, 'Output failed') + ->and($observer->called)->toBeFalse(); + }); +}); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 84f7336c6..324a0230e 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -20,7 +20,7 @@ $output = $run('--exclude-group=integration'); $output = implode("\n", array_slice(explode("\n", (string) $output), -10)); - $expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 29 skipped, 1622 passed (3577 assertions)'; + $expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 29 skipped, 1629 passed (3591 assertions)'; expect($output) ->toContain("Tests: {$expected}")