Skip to content

Implement #[WithoutErrorHandler] attribute to disable PHPUnit's error handler for a test method #5430

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
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 ChangeLog-10.3.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@ All notable changes of the PHPUnit 10.3 release series are documented in this fi

## [10.3.0] - 2023-08-04

### Added

* [#5428](https://github.com/sebastianbergmann/phpunit/issues/5428): Attribute `#[WithoutErrorHandler]` to disable PHPUnit's error handler for a test method

### Changed

* When a test case class inherits test methods from a parent class then, by default (when no test reordering is requested), the test methods from the class that is highest in the inheritance tree (instead of the class that is lowest in the inheritance tree) are now run first
22 changes: 22 additions & 0 deletions src/Framework/Attributes/WithoutErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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\Framework\Attributes;

use Attribute;

/**
* @psalm-immutable
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*/
#[Attribute(Attribute::TARGET_METHOD)]
final class WithoutErrorHandler
{
}
13 changes: 12 additions & 1 deletion src/Framework/TestRunner.php
Original file line number Diff line number Diff line change
@@ -84,7 +84,9 @@ public function run(TestCase $test): void
$risky = false;
$skipped = false;

ErrorHandler::instance()->enable();
if ($this->shouldErrorHandlerBeUsed($test)) {
ErrorHandler::instance()->enable();
}

$collectCodeCoverage = CodeCoverage::instance()->isActive() &&
$shouldCodeCoverageBeCollected;
@@ -452,4 +454,13 @@ private function saveConfigurationForChildProcess(): string

return $path;
}

private function shouldErrorHandlerBeUsed(TestCase $test): bool
{
if (MetadataRegistry::parser()->forMethod($test::class, $test->name())->isWithoutErrorHandler()->isNotEmpty()) {
return false;
}

return true;
}
}
13 changes: 13 additions & 0 deletions src/Metadata/Metadata.php
Original file line number Diff line number Diff line change
@@ -364,6 +364,11 @@ public static function usesDefaultClass(string $className): UsesDefaultClass
return new UsesDefaultClass(self::CLASS_LEVEL, $className);
}

public static function withoutErrorHandler(): WithoutErrorHandler
{
return new WithoutErrorHandler(self::METHOD_LEVEL);
}

protected function __construct(int $level)
{
$this->level = $level;
@@ -708,4 +713,12 @@ public function isUsesFunction(): bool
{
return false;
}

/**
* @psalm-assert-if-true WithoutErrorHandler $this
*/
public function isWithoutErrorHandler(): bool
{
return false;
}
}
10 changes: 10 additions & 0 deletions src/Metadata/MetadataCollection.php
Original file line number Diff line number Diff line change
@@ -529,4 +529,14 @@ public function isUsesFunction(): self
),
);
}

public function isWithoutErrorHandler(): self
{
return new self(
...array_filter(
$this->metadata,
static fn (Metadata $metadata): bool => $metadata->isWithoutErrorHandler(),
),
);
}
}
8 changes: 8 additions & 0 deletions src/Metadata/Parser/AttributeParser.php
Original file line number Diff line number Diff line change
@@ -64,6 +64,7 @@
use PHPUnit\Framework\Attributes\Ticket;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\Attributes\UsesFunction;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Metadata\Metadata;
use PHPUnit\Metadata\MetadataCollection;
use PHPUnit\Metadata\Version\ConstraintRequirement;
@@ -614,6 +615,13 @@ public function forMethod(string $className, string $methodName): MetadataCollec

$result[] = Metadata::groupOnMethod($attributeInstance->text());

break;

case WithoutErrorHandler::class:
assert($attributeInstance instanceof WithoutErrorHandler);

$result[] = Metadata::withoutErrorHandler();

break;
}
}
23 changes: 23 additions & 0 deletions src/Metadata/WithoutErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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\Metadata;

/**
* @psalm-immutable
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*/
final class WithoutErrorHandler extends Metadata
{
public function isWithoutErrorHandler(): bool
{
return true;
}
}
21 changes: 21 additions & 0 deletions tests/_files/Metadata/Attribute/tests/WithoutErrorHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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\Metadata\Attribute;

use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;

final class WithoutErrorHandlerTest extends TestCase
{
#[WithoutErrorHandler]
public function testOne(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../phpunit.xsd"
bootstrap="src/Foo.php"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerDeprecations="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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\Event\ErrorHandlerCanBeDisabled;

use function error_get_last;
use function fopen;
use function trigger_error;
use Exception;

final class Foo
{
public function methodA($fileName)
{
$stream_handle = @fopen($fileName, 'wb');

if ($stream_handle === false) {
$error = error_get_last();

throw new Exception($error['message']);
}

return $stream_handle;
}

public function methodB(): ?array
{
@trigger_error('Triggering', E_USER_WARNING);

return error_get_last();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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\Event\ErrorHandlerCanBeDisabled;

use function sys_get_temp_dir;
use function tempnam;
use Exception;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;

final class FooTest extends TestCase
{
#[WithoutErrorHandler]
public function testMethodA(): void
{
$fileName = tempnam(sys_get_temp_dir(), 'RLT') . '/missing/directory';

$this->expectException(Exception::class);
$this->expectExceptionMessage('Failed to open stream');

(new Foo)->methodA($fileName);
}

#[WithoutErrorHandler]
public function testMethodB(): void
{
$this->assertSame('Triggering', (new Foo)->methodB()['message']);
}
}
62 changes: 62 additions & 0 deletions tests/end-to-end/event/error-handler-can-be-disabled.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
The right events are emitted in the right order when PHPUnit's error handler is disabled
--SKIPIF--
<?php declare(strict_types=1);
if (DIRECTORY_SEPARATOR === '\\') {
print "skip: this test does not work on Windows / GitHub Actions\n";
}
--FILE--
<?php declare(strict_types=1);
$traceFile = tempnam(sys_get_temp_dir(), __FILE__);

$_SERVER['argv'][] = '--do-not-cache-result';
$_SERVER['argv'][] = '--no-output';
$_SERVER['argv'][] = '--log-events-text';
$_SERVER['argv'][] = $traceFile;
$_SERVER['argv'][] = '--fail-on-notice';
$_SERVER['argv'][] = '--configuration';
$_SERVER['argv'][] = __DIR__ . '/_files/error-handler-can-be-disabled';

require __DIR__ . '/../../bootstrap.php';

(new PHPUnit\TextUI\Application)->run($_SERVER['argv']);

print file_get_contents($traceFile);

unlink($traceFile);
--EXPECTF--
PHPUnit Started (PHPUnit %s using %s)
Test Runner Configured
Bootstrap Finished (%s/src/Foo.php)
Test Suite Loaded (2 tests)
Event Facade Sealed
Test Runner Started
Test Suite Sorted
Test Runner Execution Started (2 tests)
Test Suite Started (%s/phpunit.xml, 2 tests)
Test Suite Started (default, 2 tests)
Test Suite Started (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest, 2 tests)
Test Preparation Started (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodA)
Test Prepared (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodA)
Assertion Succeeded (Constraint: exception of type "Exception", Value: Exception Object #92 (
'message' => 'fopen(%s/missing/directory): Failed to open stream: No such file or directory'
'string' => ''
'code' => 0
'file' => '%s/src/Foo.php'
'line' => 26
'previous' => null
))
Assertion Succeeded (Constraint: exception message contains 'Failed to open stream', Value: 'fopen(%s/missing/directory): Failed to open stream: No such file or directory')
Test Passed (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodA)
Test Finished (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodA)
Test Preparation Started (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodB)
Test Prepared (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodB)
Assertion Succeeded (Constraint: is identical to 'Triggering', Value: 'Triggering')
Test Passed (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodB)
Test Finished (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest::testMethodB)
Test Suite Finished (PHPUnit\TestFixture\Event\ErrorHandlerCanBeDisabled\FooTest, 2 tests)
Test Suite Finished (default, 2 tests)
Test Suite Finished (%s/phpunit.xml, 2 tests)
Test Runner Execution Finished
Test Runner Finished
PHPUnit Finished (Shell Exit Code: 0)
9 changes: 9 additions & 0 deletions tests/unit/Metadata/MetadataCollectionTest.php
Original file line number Diff line number Diff line change
@@ -475,6 +475,14 @@ public function test_Can_be_filtered_for_UsesFunction(): void
$this->assertTrue($collection->asArray()[0]->isUsesFunction());
}

public function test_Can_be_filtered_for_WithoutErrorHandler(): void
{
$collection = $this->collectionWithOneOfEach()->isWithoutErrorHandler();

$this->assertCount(1, $collection);
$this->assertTrue($collection->asArray()[0]->isWithoutErrorHandler());
}

private function collectionWithOneOfEach(): MetadataCollection
{
return MetadataCollection::fromArray(
@@ -531,6 +539,7 @@ private function collectionWithOneOfEach(): MetadataCollection
Metadata::usesClass(''),
Metadata::usesDefaultClass(''),
Metadata::usesFunction(''),
Metadata::withoutErrorHandler(),
],
);
}
113 changes: 113 additions & 0 deletions tests/unit/Metadata/MetadataTest.php

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions tests/unit/Metadata/Parser/AttributeParserTest.php
Original file line number Diff line number Diff line change
@@ -83,6 +83,7 @@
use PHPUnit\TestFixture\Metadata\Attribute\TestDoxTest;
use PHPUnit\TestFixture\Metadata\Attribute\TestWithTest;
use PHPUnit\TestFixture\Metadata\Attribute\UsesTest;
use PHPUnit\TestFixture\Metadata\Attribute\WithoutErrorHandlerTest;

#[CoversClass(After::class)]
#[CoversClass(AfterClass::class)]
@@ -131,6 +132,7 @@
#[CoversClass(Ticket::class)]
#[CoversClass(UsesClass::class)]
#[CoversClass(UsesFunction::class)]
#[CoversClass(WithoutErrorHandler::class)]
#[CoversClass(AttributeParser::class)]
#[Small]
final class AttributeParserTest extends TestCase
@@ -966,6 +968,15 @@ public function test_parses_Ticket_attribute_on_method(): void
$this->assertSame('another-ticket', $metadata->asArray()[1]->groupName());
}

#[TestDox('Parses #[WithoutErrorHandler] attribute on method')]
public function test_parses_WithoutErrorHandler_attribute_on_method(): void
{
$metadata = (new AttributeParser)->forMethod(WithoutErrorHandlerTest::class, 'testOne')->isWithoutErrorHandler();

$this->assertCount(1, $metadata);
$this->assertTrue($metadata->asArray()[0]->isWithoutErrorHandler());
}

public function test_parses_attributes_for_class_and_method(): void
{
$metadata = (new AttributeParser)->forClassAndMethod(CoversTest::class, 'testOne');