Skip to content

Check and restore error/exception global handlers #5619

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .psalm/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,10 @@
<MissingThrowsDocblock>
<code>getMethod</code>
</MissingThrowsDocblock>
<PossiblyNullArgument>
<code><![CDATA[$this->backupGlobalErrorHandlers]]></code>
<code><![CDATA[$this->backupGlobalExceptionHandlers]]></code>
</PossiblyNullArgument>
<PropertyNotSetInConstructor>
<code>$outputBufferingLevel</code>
</PropertyNotSetInConstructor>
Expand Down
151 changes: 142 additions & 9 deletions src/Framework/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use const PHP_URL_PATH;
use function array_keys;
use function array_merge;
use function array_reverse;
use function array_values;
use function assert;
use function basename;
Expand All @@ -28,6 +29,7 @@
use function clearstatcache;
use function count;
use function defined;
use function error_clear_last;
use function explode;
use function getcwd;
use function implode;
Expand All @@ -48,6 +50,10 @@
use function parse_url;
use function pathinfo;
use function preg_replace;
use function restore_error_handler;
use function restore_exception_handler;
use function set_error_handler;
use function set_exception_handler;
use function setlocale;
use function sprintf;
use function str_contains;
Expand Down Expand Up @@ -126,14 +132,24 @@ abstract class TestCase extends Assert implements Reorderable, SelfDescribing, T
*/
private array $backupStaticPropertiesExcludeList = [];
private ?Snapshot $snapshot = null;
private ?bool $runClassInSeparateProcess = null;
private ?bool $runTestInSeparateProcess = null;
private bool $preserveGlobalState = false;
private bool $inIsolation = false;
private ?string $expectedException = null;
private ?string $expectedExceptionMessage = null;
private ?string $expectedExceptionMessageRegExp = null;
private null|int|string $expectedExceptionCode = null;

/**
* @psalm-var list<callable>
*/
private ?array $backupGlobalErrorHandlers = null;

/**
* @psalm-var list<callable>
*/
private ?array $backupGlobalExceptionHandlers = null;
private ?bool $runClassInSeparateProcess = null;
private ?bool $runTestInSeparateProcess = null;
private bool $preserveGlobalState = false;
private bool $inIsolation = false;
private ?string $expectedException = null;
private ?string $expectedExceptionMessage = null;
private ?string $expectedExceptionMessageRegExp = null;
private null|int|string $expectedExceptionCode = null;

/**
* @psalm-var list<ExecutionOrderDependency>
Expand Down Expand Up @@ -618,13 +634,16 @@ final public function runBare(): void
{
$emitter = Event\Facade::emitter();

error_clear_last();
clearstatcache();

$emitter->testPreparationStarted(
$this->valueObjectForEvents(),
);

$this->snapshotGlobalState();
$this->snapshotGlobalErrorExceptionHandlers();
$this->startOutputBuffering();
clearstatcache();

$hookMethods = (new HookMethods)->hookMethods(static::class);
$hasMetRequirements = false;
Expand Down Expand Up @@ -776,6 +795,7 @@ final public function runBare(): void
chdir($currentWorkingDirectory);
}

$this->restoreGlobalErrorExceptionHandlers();
$this->restoreGlobalState();
$this->unregisterCustomComparators();
$this->cleanupIniSettings();
Expand Down Expand Up @@ -1683,6 +1703,119 @@ private function stopOutputBuffering(): bool
return true;
}

private function snapshotGlobalErrorExceptionHandlers(): void
{
$this->backupGlobalErrorHandlers = $this->getActiveErrorHandlers();
$this->backupGlobalExceptionHandlers = $this->getActiveExceptionHandlers();
}

/**
* @throws MoreThanOneDataSetFromDataProviderException
*/
private function restoreGlobalErrorExceptionHandlers(): void
{
$activeErrorHandlers = $this->getActiveErrorHandlers();
$activeExceptionHandlers = $this->getActiveExceptionHandlers();

$message = null;

if ($activeErrorHandlers !== $this->backupGlobalErrorHandlers) {
if (count($activeErrorHandlers) > count($this->backupGlobalErrorHandlers)) {
$message = 'Test code or tested code did not remove its own error handlers';
} else {
$message = 'Test code or tested code removed error handlers other than its own';
}

foreach ($activeErrorHandlers as $handler) {
restore_error_handler();
}

foreach ($this->backupGlobalErrorHandlers as $handler) {
set_error_handler($handler);
}
}

if ($activeExceptionHandlers !== $this->backupGlobalExceptionHandlers) {
if (count($activeExceptionHandlers) > count($this->backupGlobalExceptionHandlers)) {
$message = 'Test code or tested code did not remove its own exception handlers';
} else {
$message = 'Test code or tested code removed exception handlers other than its own';
}

foreach ($activeExceptionHandlers as $handler) {
restore_exception_handler();
}

foreach ($this->backupGlobalExceptionHandlers as $handler) {
set_exception_handler($handler);
}
}

$this->backupGlobalErrorHandlers = null;
$this->backupGlobalExceptionHandlers = null;

if ($message !== null) {
Event\Facade::emitter()->testConsideredRisky(
$this->valueObjectForEvents(),
$message,
);

$this->status = TestStatus::risky($message);
}
}

/**
* @return list<callable>
*/
private function getActiveErrorHandlers(): array
{
$res = [];

while (true) {
$previousHandler = set_error_handler(static fn () => false);
restore_error_handler();

if ($previousHandler === null) {
break;
}
$res[] = $previousHandler;
restore_error_handler();
}
$res = array_reverse($res);

foreach ($res as $handler) {
set_error_handler($handler);
}

return $res;
}

/**
* @return list<callable>
*/
private function getActiveExceptionHandlers(): array
{
$res = [];

while (true) {
$previousHandler = set_exception_handler(static fn () => null);
restore_exception_handler();

if ($previousHandler === null) {
break;
}
$res[] = $previousHandler;
restore_exception_handler();
}
$res = array_reverse($res);

foreach ($res as $handler) {
set_exception_handler($handler);
}

return $res;
}

private function snapshotGlobalState(): void
{
if ($this->runTestInSeparateProcess || $this->inIsolation ||
Expand Down
3 changes: 0 additions & 3 deletions src/Framework/TestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use function assert;
use function class_exists;
use function defined;
use function error_clear_last;
use function extension_loaded;
use function get_include_path;
use function hrtime;
Expand Down Expand Up @@ -85,8 +84,6 @@ public function run(TestCase $test): void
$risky = false;
$skipped = false;

error_clear_last();

if ($this->shouldErrorHandlerBeUsed($test)) {
ErrorHandler::instance()->enable();
}
Expand Down
69 changes: 69 additions & 0 deletions tests/end-to-end/regression/5592.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
--TEST--
https://github.com/sebastianbergmann/phpunit/pull/5592
--FILE--
<?php declare(strict_types=1);
$_SERVER['argv'][] = '--do-not-cache-result';
$_SERVER['argv'][] = '--no-configuration';
$_SERVER['argv'][] = __DIR__ . '/5592/Issue5592Test.php';

set_exception_handler(static fn () => null);

require_once __DIR__ . '/../../bootstrap.php';
(new PHPUnit\TextUI\Application)->run($_SERVER['argv']);
--EXPECTF--
PHPUnit %s by Sebastian Bergmann and contributors.

Runtime: %s

.FF.FF 6 / 6 (100%)

Time: %s, Memory: %s

There were 4 failures:

1) PHPUnit\TestFixture\Issue5592Test::testAddedErrorHandler
Failed asserting that false is true.

%sIssue5592Test.php:%i

2) PHPUnit\TestFixture\Issue5592Test::testRemovedErrorHandler
Failed asserting that false is true.

%sIssue5592Test.php:%i

3) PHPUnit\TestFixture\Issue5592Test::testAddedExceptionHandler
Failed asserting that false is true.

%sIssue5592Test.php:%i

4) PHPUnit\TestFixture\Issue5592Test::testRemovedExceptionHandler
Failed asserting that false is true.

%sIssue5592Test.php:%i

--

There were 4 risky tests:

1) PHPUnit\TestFixture\Issue5592Test::testAddedErrorHandler
Test code or tested code did not remove its own error handlers

%sIssue5592Test.php:%i

2) PHPUnit\TestFixture\Issue5592Test::testRemovedErrorHandler
Test code or tested code removed error handlers other than its own

%sIssue5592Test.php:%i

3) PHPUnit\TestFixture\Issue5592Test::testAddedExceptionHandler
Test code or tested code did not remove its own exception handlers

%sIssue5592Test.php:%i

4) PHPUnit\TestFixture\Issue5592Test::testRemovedExceptionHandler
Test code or tested code removed exception handlers other than its own

%sIssue5592Test.php:%i

FAILURES!
Tests: 6, Assertions: 6, Failures: 4, Risky: 4.
57 changes: 57 additions & 0 deletions tests/end-to-end/regression/5592/Issue5592Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture;

use function restore_error_handler;
use function restore_exception_handler;
use function set_error_handler;
use function set_exception_handler;
use PHPUnit\Framework\TestCase;

class Issue5592Test extends TestCase
{
public function testAddedAndRemovedErrorHandler(): void
{
set_error_handler(static fn () => false);
restore_error_handler();
$this->assertTrue(true);
}

public function testAddedErrorHandler(): void
{
set_error_handler(static fn () => false);
$this->assertTrue(false);
}

public function testRemovedErrorHandler(): void
{
restore_error_handler();
$this->assertTrue(false);
}

public function testAddedAndRemovedExceptionHandler(): void
{
set_exception_handler(static fn () => null);
restore_exception_handler();
$this->assertTrue(true);
}

public function testAddedExceptionHandler(): void
{
set_exception_handler(static fn () => null);
$this->assertTrue(false);
}

public function testRemovedExceptionHandler(): void
{
restore_exception_handler();
$this->assertTrue(false);
}
}