-
-
Notifications
You must be signed in to change notification settings - Fork 17
feat(error-handler): add ErrorHandlerInterceptor plugin #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rossaddison
wants to merge
9
commits into
php-testo:1.x
Choose a base branch
from
rossaddison:feat/73-error-handler-interceptor
base: 1.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f3bb577
fix(infection): skip mutations to #[TestInline] attribute arguments (…
rossaddison c40a136
fix(phpunit-mirror): add .placeholder.php so EmptyRun stub directory …
rossaddison baa0449
fix(infection): re-enable @default mutators alongside global-ignoreSo…
rossaddison 841aa25
feat(error-handler): add ErrorHandlerInterceptor plugin (#73)
rossaddison bbef946
Potential fix for pull request finding
rossaddison e8a7008
fix(error-handler): make ErrorHandlerInterceptor fiber-safe; promote …
rossaddison 265bc5a
Merge branch '1.x' into feat/73-error-handler-interceptor
rossaddison 5746d31
Merge branch '1.x' into feat/73-error-handler-interceptor
rossaddison 86a9ff0
Merge branch '1.x' into feat/73-error-handler-interceptor
rossaddison File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| { | ||
| "name": "testo/error-handler", | ||
| "description": "Error handler interceptor plugin for the Testo testing framework.", | ||
| "license": "BSD-3-Clause", | ||
| "type": "library", | ||
| "keywords": [ | ||
| "testo", | ||
| "error-handler", | ||
| "test" | ||
| ], | ||
| "authors": [ | ||
| { | ||
| "name": "Aleksei Gagarin (roxblnfk)", | ||
| "homepage": "https://github.com/roxblnfk" | ||
| } | ||
| ], | ||
| "funding": [ | ||
| { | ||
| "type": "boosty", | ||
| "url": "https://boosty.to/roxblnfk" | ||
| } | ||
| ], | ||
| "require": { | ||
| "php": ">=8.2", | ||
| "testo/testo": "0.10.34 - 1" | ||
| }, | ||
| "autoload": { | ||
| "psr-4": { | ||
| "Testo\\ErrorHandler\\": "src/" | ||
| } | ||
| }, | ||
| "minimum-stability": "dev", | ||
| "prefer-stable": true, | ||
| "extra": { | ||
| "branch-alias": { | ||
| "dev-1.x": "1.x-dev" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Testo\ErrorHandler; | ||
|
|
||
| /** | ||
| * A single PHP error captured during test execution. | ||
| * | ||
| * @api | ||
| */ | ||
| final readonly class CapturedError | ||
| { | ||
| public function __construct( | ||
| public int $severity, | ||
| public string $message, | ||
| public string $file, | ||
| public int $line, | ||
| ) {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Testo\ErrorHandler; | ||
|
|
||
| /** | ||
| * Collection of PHP errors accumulated during test execution. | ||
| * | ||
| * Stored as a {@see \Testo\Core\Context\TestResult} attribute under the key {@see CapturedErrors::class}. | ||
| * Renderers that wish to display collected errors should retrieve it from the result. | ||
| * | ||
| * @api | ||
| */ | ||
| final readonly class CapturedErrors | ||
| { | ||
| /** @param list<CapturedError> $errors */ | ||
| public function __construct( | ||
| public array $errors, | ||
| ) {} | ||
|
|
||
| public function isEmpty(): bool | ||
| { | ||
| return $this->errors === []; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Testo\ErrorHandler; | ||
|
|
||
| use Internal\Container\Container; | ||
| use Testo\Common\PluginConfigurator; | ||
| use Testo\ErrorHandler\Internal\ErrorHandlerInterceptor; | ||
| use Testo\Pipeline\InterceptorCollector; | ||
|
|
||
| /** | ||
| * Plugin that captures PHP errors raised during test execution. | ||
| * | ||
| * By default errors are collected and stored as a {@see CapturedErrors} attribute | ||
| * on the {@see \Testo\Core\Context\TestResult}, but the test still passes. Pass | ||
| * {@see $failOnError}: true to make any captured error fail the test instead. | ||
| * | ||
| * @api | ||
| */ | ||
| final readonly class ErrorHandlerPlugin implements PluginConfigurator | ||
| { | ||
| /** | ||
| * @param bool $failOnError When true, a captured PHP error fails the test. When false | ||
| * (default) errors are collected but the test result is unchanged. | ||
| */ | ||
| public function __construct( | ||
| private bool $failOnError = false, | ||
| ) {} | ||
|
|
||
| #[\Override] | ||
| public function configure(Container $container): void | ||
| { | ||
| $container->get(InterceptorCollector::class) | ||
| ->addInterceptor(new ErrorHandlerInterceptor($this->failOnError)); | ||
| } | ||
| } |
112 changes: 112 additions & 0 deletions
112
plugin/error-handler/src/Internal/ErrorHandlerInterceptor.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Testo\ErrorHandler\Internal; | ||
|
|
||
| use Testo\Core\Context\TestInfo; | ||
| use Testo\Core\Context\TestResult; | ||
| use Testo\Core\Value\Status; | ||
| use Testo\ErrorHandler\CapturedError; | ||
| use Testo\ErrorHandler\CapturedErrors; | ||
| use Testo\Pipeline\Attribute\InterceptorOptions; | ||
| use Testo\Pipeline\Middleware\TestRunInterceptor; | ||
|
|
||
| /** | ||
| * Intercepts PHP errors raised during test execution. | ||
| * | ||
| * Registers a custom error handler via {@see \set_error_handler()} before the test runs and | ||
| * restores the previous handler afterward via {@see \restore_error_handler()}. Accumulated | ||
| * errors are stored in the returned {@see TestResult} as a {@see CapturedErrors} attribute. | ||
| * | ||
| * @internal | ||
| * @psalm-internal Testo\ErrorHandler | ||
| */ | ||
| #[InterceptorOptions(order: InterceptorOptions::ORDER_CLOSE_TO_TEST)] | ||
| final readonly class ErrorHandlerInterceptor implements TestRunInterceptor | ||
| { | ||
| /** | ||
| * @param bool $failOnError When true, any captured error upgrades a passing test to | ||
| * {@see Status::Failed} with the first error wrapped in an | ||
| * {@see \ErrorException} as the failure. | ||
| */ | ||
| public function __construct( | ||
| private bool $failOnError = false, | ||
| ) {} | ||
|
|
||
| #[\Override] | ||
| public function runTest(TestInfo $info, callable $next): TestResult | ||
| { | ||
| /** @var list<CapturedError> $errors */ | ||
| $errors = []; | ||
|
|
||
| $handler = static function (int $severity, string $message, string $file, int $line) use (&$errors): bool { | ||
| $errors[] = new CapturedError($severity, $message, $file, $line); | ||
| return true; | ||
| }; | ||
|
|
||
| $result = $this->run($info, $next, $handler); | ||
|
|
||
| if ($errors === []) { | ||
| return $result; | ||
| } | ||
|
|
||
| $result = $result->withAttribute(CapturedErrors::class, new CapturedErrors($errors)); | ||
|
|
||
| if ($this->failOnError && $result->status === Status::Passed) { | ||
| $first = $errors[0]; | ||
| $result = $result | ||
| ->with(status: Status::Failed) | ||
| ->withFailure(new \ErrorException($first->message, 0, $first->severity, $first->file, $first->line)); | ||
| } | ||
|
|
||
| return $result; | ||
| } | ||
|
|
||
| /** | ||
| * Runs the test with {@see $handler} installed via {@see \set_error_handler()}, keeping it | ||
| * bound to this test across fiber suspensions. | ||
| * | ||
| * set_error_handler()/restore_error_handler() operate on one process-global stack, so under | ||
| * concurrent (fiber-based) execution — where sibling tests interleave with this one — a plain | ||
| * install-before/restore-after around $next() would leak errors into the wrong test's | ||
| * CapturedErrors, and an interleaved resume could pop a sibling's handler instead of ours. On | ||
| * every suspension we restore whichever handler was active before this test installed its own | ||
| * (the native stack does that for free); on resumption we re-install this test's handler. | ||
| * Mirrors {@see \Testo\Bridge\Mockery\Internal\MockeryInterceptor::run()} and | ||
| * {@see \Testo\Application\Internal\MessengerHub::scope()}. | ||
| * | ||
| * @param callable(TestInfo): TestResult $next | ||
| */ | ||
| private function run(TestInfo $info, callable $next, \Closure $handler): TestResult | ||
| { | ||
| \set_error_handler($handler); | ||
| try { | ||
| if (\Fiber::getCurrent() === null) { | ||
| return $next($info); | ||
| } | ||
|
|
||
| $fiber = new \Fiber(static fn(): TestResult => $next($info)); | ||
| $value = $fiber->start(); | ||
| while (!$fiber->isTerminated()) { | ||
| \restore_error_handler(); | ||
| try { | ||
| $resume = \Fiber::suspend($value); | ||
| } catch (\Throwable $e) { | ||
| \set_error_handler($handler); | ||
| $value = $fiber->throw($e); | ||
| continue; | ||
| } | ||
|
|
||
| \set_error_handler($handler); | ||
| $value = $fiber->resume($resume); | ||
| } | ||
|
|
||
| /** @var TestResult $result */ | ||
| $result = $fiber->getReturn(); | ||
| return $result; | ||
| } finally { | ||
| \restore_error_handler(); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove all unrelated changes from other PRs