From 3c51eba6dba6688f834b4759ed69e907f464f0db Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 4 Mar 2026 15:59:17 +0100 Subject: [PATCH 1/2] Initial work on #6530 --- phpunit.xsd | 9 ++ src/Framework/TestRunner/templates/method.tpl | 5 ++ src/Runner/ErrorHandler.php | 76 +++++++++++----- .../IssueTriggerResolver/DefaultResolver.php | 29 +++++++ .../IssueTriggerResolver/Resolution.php | 51 +++++++++++ src/Runner/IssueTriggerResolver/Resolver.php | 23 +++++ src/TextUI/Application.php | 37 ++++++++ src/TextUI/Configuration/Merger.php | 1 + src/TextUI/Configuration/Value/Source.php | 17 +++- src/TextUI/Configuration/Xml/Loader.php | 12 +++ tests/_files/configuration_codecoverage.xml | 5 ++ .../_files/issue-trigger-resolver/phpunit.xml | 21 +++++ .../src/FirstPartyClass.php | 18 ++++ .../tests/IssueTriggerResolverTest.php | 22 +++++ .../vendor/Framework.php | 13 +++ .../vendor/FrameworkResolver.php | 24 +++++ .../vendor/autoload.php | 4 + .../error-handler/issue-trigger-resolver.phpt | 36 ++++++++ .../IssueTriggerResolver/ResolutionTest.php | 87 +++++++++++++++++++ .../TextUI/Configuration/Value/SourceTest.php | 65 ++++++++++++++ .../TextUI/Configuration/Xml/LoaderTest.php | 8 ++ 21 files changed, 541 insertions(+), 22 deletions(-) create mode 100644 src/Runner/IssueTriggerResolver/DefaultResolver.php create mode 100644 src/Runner/IssueTriggerResolver/Resolution.php create mode 100644 src/Runner/IssueTriggerResolver/Resolver.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver/phpunit.xml create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver/src/FirstPartyClass.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver/tests/IssueTriggerResolverTest.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/Framework.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/FrameworkResolver.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/autoload.php create mode 100644 tests/end-to-end/error-handler/issue-trigger-resolver.phpt create mode 100644 tests/unit/Runner/IssueTriggerResolver/ResolutionTest.php diff --git a/phpunit.xsd b/phpunit.xsd index e1966b55ea4..e0e37fdadb4 100644 --- a/phpunit.xsd +++ b/phpunit.xsd @@ -24,6 +24,7 @@ + @@ -359,4 +360,12 @@ + + + + + + + + diff --git a/src/Framework/TestRunner/templates/method.tpl b/src/Framework/TestRunner/templates/method.tpl index 78b7b7c40c2..797ae3898f2 100644 --- a/src/Framework/TestRunner/templates/method.tpl +++ b/src/Framework/TestRunner/templates/method.tpl @@ -1,5 +1,6 @@ useDeprecationTriggers($deprecationTriggers); + foreach (array_reverse($configuration->source()->issueTriggerResolvers()) as $className) { + ErrorHandler::instance()->addIssueTriggerResolver(new $className); + } + $test = new {className}('{methodName}'); $test->setData('{dataName}', unserialize('{data}')); diff --git a/src/Runner/ErrorHandler.php b/src/Runner/ErrorHandler.php index 99810114a81..d12375a60a2 100644 --- a/src/Runner/ErrorHandler.php +++ b/src/Runner/ErrorHandler.php @@ -25,8 +25,10 @@ use const E_USER_WARNING; use const E_WARNING; use function array_keys; +use function array_unshift; use function array_values; use function assert; +use function count; use function debug_backtrace; use function defined; use function error_reporting; @@ -44,6 +46,8 @@ use PHPUnit\Metadata\Parser\Registry as MetadataParserRegistry; use PHPUnit\Runner\Baseline\Baseline; use PHPUnit\Runner\Baseline\Issue; +use PHPUnit\Runner\IssueTriggerResolver\DefaultResolver as DefaultIssueTriggerResolver; +use PHPUnit\Runner\IssueTriggerResolver\Resolver as IssueTriggerResolver; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; use PHPUnit\TextUI\Configuration\SourceFilter; use PHPUnit\Util\ExcludeList; @@ -80,6 +84,11 @@ final class ErrorHandler */ private ?array $deprecationTriggers = null; + /** + * @var non-empty-list + */ + private array $issueTriggerResolvers; + public static function instance(): self { $source = ConfigurationRegistry::get()->source(); @@ -99,8 +108,9 @@ public static function instance(): self private function __construct(bool $identifyIssueTrigger) { - $this->excludeList = new ExcludeList; - $this->identifyIssueTrigger = $identifyIssueTrigger; + $this->excludeList = new ExcludeList; + $this->identifyIssueTrigger = $identifyIssueTrigger; + $this->issueTriggerResolvers = [new DefaultIssueTriggerResolver]; } /** @@ -196,7 +206,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil $suppressed, $ignoredByBaseline, $ignoredByTest, - $this->trigger($test, false, $errorFile), + $this->trigger($test, false, $errorString, $errorFile), ); break; @@ -210,7 +220,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil $suppressed, $ignoredByBaseline, $ignoredByTest, - $this->trigger($test, true), + $this->trigger($test, true, $errorString), $this->stackTrace(), ); @@ -302,6 +312,11 @@ public function useDeprecationTriggers(array $deprecationTriggers): void $this->deprecationTriggers = $deprecationTriggers; } + public function addIssueTriggerResolver(IssueTriggerResolver $resolver): void + { + array_unshift($this->issueTriggerResolvers, $resolver); + } + public function enterTestCaseContext(string $className, string $methodName): void { $this->testCaseContext = $this->testCaseContext($className, $methodName); @@ -329,7 +344,7 @@ private function ignoredByBaseline(string $file, int $line, string $description) /** * @param null|non-empty-string $errorFile */ - private function trigger(TestMethod $test, bool $isUserland, ?string $errorFile = null): IssueTrigger + private function trigger(TestMethod $test, bool $isUserland, string $errorString, ?string $errorFile = null): IssueTrigger { if (!$this->identifyIssueTrigger) { return IssueTrigger::from(null, null); @@ -343,26 +358,39 @@ private function trigger(TestMethod $test, bool $isUserland, ?string $errorFile $trace = $this->filteredStackTrace(); - return $this->triggerForUserlandDeprecation($test, $trace); + return $this->triggerForUserlandDeprecation($test, $errorString, $trace); } /** - * @param list $trace + * @param list}> $trace */ - private function triggerForUserlandDeprecation(TestMethod $test, array $trace): IssueTrigger + private function triggerForUserlandDeprecation(TestMethod $test, string $message, array $trace): IssueTrigger { - $callee = null; - $caller = null; + foreach ($this->issueTriggerResolvers as $resolver) { + $result = $resolver->resolve($trace, $message); - if (isset($trace[0]['file'])) { - $callee = $this->categorizeFile($trace[0]['file'], $test); - } + if ($result === null) { + continue; + } + + $callee = null; - if (isset($trace[1]['file'])) { - $caller = $this->categorizeFile($trace[1]['file'], $test); + if ($result->hasCallee()) { + $callee = $this->categorizeFile($result->callee(), $test); + } + + $caller = null; + + if ($result->hasCaller()) { + $caller = $this->categorizeFile($result->caller(), $test); + } + + return IssueTrigger::from($callee, $caller); } - return IssueTrigger::from($callee, $caller); + // @codeCoverageIgnoreStart + return IssueTrigger::from(null, null); + // @codeCoverageIgnoreEnd } /** @@ -386,11 +414,13 @@ private function categorizeFile(string $file, TestMethod $test): Code } /** - * @return list + * @return list}> */ private function filteredStackTrace(): array { - $trace = $this->errorStackTrace(); + $ignoreArguments = count($this->issueTriggerResolvers) === 1; + + $trace = $this->errorStackTrace($ignoreArguments); if ($this->deprecationTriggers === null) { return array_values($trace); @@ -446,11 +476,15 @@ private function guessDeprecationFrame(): ?array } /** - * @return list + * @return list}> */ - private function errorStackTrace(): array + private function errorStackTrace(bool $ignoreArgs = true): array { - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + if ($ignoreArgs) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } else { + $trace = debug_backtrace(); + } $i = 0; diff --git a/src/Runner/IssueTriggerResolver/DefaultResolver.php b/src/Runner/IssueTriggerResolver/DefaultResolver.php new file mode 100644 index 00000000000..0fe6dbecb9a --- /dev/null +++ b/src/Runner/IssueTriggerResolver/DefaultResolver.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Runner\IssueTriggerResolver; + +/** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +final class DefaultResolver implements Resolver +{ + /** + * @param list}> $trace + */ + public function resolve(array $trace, string $message): Resolution + { + return new Resolution( + $trace[0]['file'] ?? null, + $trace[1]['file'] ?? null, + ); + } +} diff --git a/src/Runner/IssueTriggerResolver/Resolution.php b/src/Runner/IssueTriggerResolver/Resolution.php new file mode 100644 index 00000000000..92695a0d51c --- /dev/null +++ b/src/Runner/IssueTriggerResolver/Resolution.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Runner\IssueTriggerResolver; + +/** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + */ +final readonly class Resolution +{ + private ?string $callee; + private ?string $caller; + + public function __construct(?string $callee, ?string $caller) + { + $this->callee = $callee; + $this->caller = $caller; + } + + /** + * @phpstan-assert-if-true !null $this->callee + */ + public function hasCallee(): bool + { + return $this->callee !== null; + } + + public function callee(): ?string + { + return $this->callee; + } + + /** + * @phpstan-assert-if-true !null $this->caller + */ + public function hasCaller(): bool + { + return $this->caller !== null; + } + + public function caller(): ?string + { + return $this->caller; + } +} diff --git a/src/Runner/IssueTriggerResolver/Resolver.php b/src/Runner/IssueTriggerResolver/Resolver.php new file mode 100644 index 00000000000..24c41c7b49e --- /dev/null +++ b/src/Runner/IssueTriggerResolver/Resolver.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Runner\IssueTriggerResolver; + +/** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + */ +interface Resolver +{ + /** + * Return null to defer to the next resolver in the chain. + * + * @param list}> $trace + */ + public function resolve(array $trace, string $message): ?Resolution; +} diff --git a/src/TextUI/Application.php b/src/TextUI/Application.php index 361eee75bc2..148ab78b3f6 100644 --- a/src/TextUI/Application.php +++ b/src/TextUI/Application.php @@ -11,12 +11,15 @@ use const PHP_EOL; use const PHP_VERSION; +use function array_reverse; use function assert; use function class_exists; +use function class_implements; use function defined; use function dirname; use function explode; use function function_exists; +use function in_array; use function is_file; use function method_exists; use function printf; @@ -52,6 +55,7 @@ use PHPUnit\Runner\Extension\Facade as ExtensionFacade; use PHPUnit\Runner\Extension\PharLoader; use PHPUnit\Runner\GarbageCollection\GarbageCollectionHandler; +use PHPUnit\Runner\IssueTriggerResolver\Resolver; use PHPUnit\Runner\Phpt\TestCase as PhptTestCase; use PHPUnit\Runner\ResultCache\DefaultResultCache; use PHPUnit\Runner\ResultCache\NullResultCache; @@ -218,6 +222,7 @@ public function run(array $argv): int } $this->configureDeprecationTriggers($configuration); + $this->configureIssueTriggerResolvers($configuration); $timer = new Timer; $timer->start(); @@ -840,6 +845,38 @@ private function configureDeprecationTriggers(Configuration $configuration): voi } } + private function configureIssueTriggerResolvers(Configuration $configuration): void + { + $classNames = $configuration->source()->issueTriggerResolvers(); + + foreach (array_reverse($classNames) as $className) { + if (!class_exists($className)) { + EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( + sprintf( + 'Class %s cannot be used as an issue trigger resolver because it does not exist', + $className, + ), + ); + + continue; + } + + if (!in_array(Resolver::class, class_implements($className), true)) { + EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( + sprintf( + 'Class %s cannot be used as an issue trigger resolver because it does not implement %s', + $className, + Resolver::class, + ), + ); + + continue; + } + + ErrorHandler::instance()->addIssueTriggerResolver(new $className); + } + } + private function preload(): void { if (!defined('PHPUNIT_COMPOSER_INSTALL')) { diff --git a/src/TextUI/Configuration/Merger.php b/src/TextUI/Configuration/Merger.php index 379f2a3423a..5a4bd36671f 100644 --- a/src/TextUI/Configuration/Merger.php +++ b/src/TextUI/Configuration/Merger.php @@ -957,6 +957,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC $xmlConfiguration->source()->ignoreDirectDeprecations(), $xmlConfiguration->source()->ignoreIndirectDeprecations(), $xmlConfiguration->source()->identifyIssueTrigger(), + $xmlConfiguration->source()->issueTriggerResolvers(), ), $testResultCacheFile, $coverageClover, diff --git a/src/TextUI/Configuration/Value/Source.php b/src/TextUI/Configuration/Value/Source.php index 9e246ff164c..c8811b197cc 100644 --- a/src/TextUI/Configuration/Value/Source.php +++ b/src/TextUI/Configuration/Value/Source.php @@ -44,11 +44,17 @@ */ private array $deprecationTriggers; + /** + * @var list + */ + private array $issueTriggerResolvers; + /** * @param ?non-empty-string $baseline * @param array{functions: list, methods: list} $deprecationTriggers + * @param list $issueTriggerResolvers */ - public function __construct(?string $baseline, bool $ignoreBaseline, FilterDirectoryCollection $includeDirectories, FilterFileCollection $includeFiles, FilterDirectoryCollection $excludeDirectories, FilterFileCollection $excludeFiles, bool $restrictNotices, bool $restrictWarnings, bool $ignoreSuppressionOfDeprecations, bool $ignoreSuppressionOfPhpDeprecations, bool $ignoreSuppressionOfErrors, bool $ignoreSuppressionOfNotices, bool $ignoreSuppressionOfPhpNotices, bool $ignoreSuppressionOfWarnings, bool $ignoreSuppressionOfPhpWarnings, array $deprecationTriggers, bool $ignoreSelfDeprecations, bool $ignoreDirectDeprecations, bool $ignoreIndirectDeprecations, bool $identifyIssueTrigger) + public function __construct(?string $baseline, bool $ignoreBaseline, FilterDirectoryCollection $includeDirectories, FilterFileCollection $includeFiles, FilterDirectoryCollection $excludeDirectories, FilterFileCollection $excludeFiles, bool $restrictNotices, bool $restrictWarnings, bool $ignoreSuppressionOfDeprecations, bool $ignoreSuppressionOfPhpDeprecations, bool $ignoreSuppressionOfErrors, bool $ignoreSuppressionOfNotices, bool $ignoreSuppressionOfPhpNotices, bool $ignoreSuppressionOfWarnings, bool $ignoreSuppressionOfPhpWarnings, array $deprecationTriggers, bool $ignoreSelfDeprecations, bool $ignoreDirectDeprecations, bool $ignoreIndirectDeprecations, bool $identifyIssueTrigger, array $issueTriggerResolvers = []) { $this->baseline = $baseline; $this->ignoreBaseline = $ignoreBaseline; @@ -70,6 +76,7 @@ public function __construct(?string $baseline, bool $ignoreBaseline, FilterDirec $this->ignoreDirectDeprecations = $ignoreDirectDeprecations; $this->ignoreIndirectDeprecations = $ignoreIndirectDeprecations; $this->identifyIssueTrigger = $identifyIssueTrigger; + $this->issueTriggerResolvers = $issueTriggerResolvers; } /** @@ -199,4 +206,12 @@ public function identifyIssueTrigger(): bool { return $this->identifyIssueTrigger; } + + /** + * @return list + */ + public function issueTriggerResolvers(): array + { + return $this->issueTriggerResolvers; + } } diff --git a/src/TextUI/Configuration/Xml/Loader.php b/src/TextUI/Configuration/Xml/Loader.php index b4e3c74d553..51472c5e530 100644 --- a/src/TextUI/Configuration/Xml/Loader.php +++ b/src/TextUI/Configuration/Xml/Loader.php @@ -365,6 +365,17 @@ private function source(string $filename, DOMXPath $xpath): Source $deprecationTriggers['methods'][] = $methodNode->textContent; } + $issueTriggerResolvers = []; + $issueTriggerResolverNodes = $xpath->query('source/issueTriggerResolvers/issueTriggerResolver'); + + assert($issueTriggerResolverNodes instanceof DOMNodeList); + + foreach ($issueTriggerResolverNodes as $node) { + assert($node instanceof DOMElement); + + $issueTriggerResolvers[] = $node->getAttribute('className'); + } + return new Source( $baseline, false, @@ -386,6 +397,7 @@ private function source(string $filename, DOMXPath $xpath): Source $ignoreDirectDeprecations, $ignoreIndirectDeprecations, $identifyIssueTrigger, + $issueTriggerResolvers, ); } diff --git a/tests/_files/configuration_codecoverage.xml b/tests/_files/configuration_codecoverage.xml index 55f1e5653e5..b2ed6eab2ed 100644 --- a/tests/_files/configuration_codecoverage.xml +++ b/tests/_files/configuration_codecoverage.xml @@ -19,6 +19,11 @@ PHPUnit\TestFixture\DeprecationTrigger\trigger_deprecation PHPUnit\TestFixture\DeprecationTrigger\DeprecationTrigger::triggerDeprecation + + + + + + + + + tests + + + + + + src + + + + + + + diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver/src/FirstPartyClass.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/src/FirstPartyClass.php new file mode 100644 index 00000000000..12ea6d54e60 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/src/FirstPartyClass.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver; + +final class FirstPartyClass +{ + public function callFramework(): void + { + (new Framework)->trigger(); + } +} diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver/tests/IssueTriggerResolverTest.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/tests/IssueTriggerResolverTest.php new file mode 100644 index 00000000000..989c582ff70 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/tests/IssueTriggerResolverTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver; + +use PHPUnit\Framework\TestCase; + +final class IssueTriggerResolverTest extends TestCase +{ + public function testDeprecationViaFramework(): void + { + (new FirstPartyClass)->callFramework(); + + $this->assertTrue(true); + } +} diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/Framework.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/Framework.php new file mode 100644 index 00000000000..e6fb524e8b4 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/Framework.php @@ -0,0 +1,13 @@ + $trace + */ + public function resolve(array $trace, string $message): ?Resolution + { + if (isset($trace[0]['file']) && str_contains($trace[0]['file'], 'Framework.php')) { + return new Resolution( + $trace[1]['file'] ?? null, + $trace[2]['file'] ?? null, + ); + } + + return null; + } +} diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/autoload.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/autoload.php new file mode 100644 index 00000000000..6e6fb0272d8 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver/vendor/autoload.php @@ -0,0 +1,4 @@ +run($_SERVER['argv']); +--EXPECTF-- +PHPUnit Started (PHPUnit %s using %s) +Test Runner Configured +Bootstrap Finished (%sautoload.php) +Event Facade Sealed +Test Suite Loaded (1 test) +Test Runner Started +Test Suite Sorted +Test Runner Execution Started (1 test) +Test Suite Started (%sphpunit.xml, 1 test) +Test Suite Started (default, 1 test) +Test Suite Started (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest, 1 test) +Test Preparation Started (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest::testDeprecationViaFramework) +Test Prepared (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest::testDeprecationViaFramework) +Test Triggered Deprecation (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest::testDeprecationViaFramework, issue triggered by test code calling into first-party code, suppressed using operator) in %s:%d +framework deprecation +Test Passed (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest::testDeprecationViaFramework) +Test Finished (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest::testDeprecationViaFramework) +Test Suite Finished (PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\IssueTriggerResolverTest, 1 test) +Test Suite Finished (default, 1 test) +Test Suite Finished (%sphpunit.xml, 1 test) +Test Runner Execution Finished +Test Runner Finished +PHPUnit Finished (Shell Exit Code: 0) diff --git a/tests/unit/Runner/IssueTriggerResolver/ResolutionTest.php b/tests/unit/Runner/IssueTriggerResolver/ResolutionTest.php new file mode 100644 index 00000000000..a17d74583da --- /dev/null +++ b/tests/unit/Runner/IssueTriggerResolver/ResolutionTest.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Runner\IssueTriggerResolver; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Small; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Resolution::class)] +#[Small] +#[Group('test-runner')] +final class ResolutionTest extends TestCase +{ + public function testHasCalleeReturnsTrueWhenCalleeIsSet(): void + { + $resolution = new Resolution('/path/to/callee.php', null); + + $this->assertTrue($resolution->hasCallee()); + } + + public function testHasCalleeReturnsFalseWhenCalleeIsNull(): void + { + $resolution = new Resolution(null, null); + + $this->assertFalse($resolution->hasCallee()); + } + + public function testCalleeReturnsSetValue(): void + { + $resolution = new Resolution('/path/to/callee.php', null); + + $this->assertSame('/path/to/callee.php', $resolution->callee()); + } + + public function testCalleeReturnsNullWhenNotSet(): void + { + $resolution = new Resolution(null, null); + + $this->assertNull($resolution->callee()); + } + + public function testHasCallerReturnsTrueWhenCallerIsSet(): void + { + $resolution = new Resolution(null, '/path/to/caller.php'); + + $this->assertTrue($resolution->hasCaller()); + } + + public function testHasCallerReturnsFalseWhenCallerIsNull(): void + { + $resolution = new Resolution(null, null); + + $this->assertFalse($resolution->hasCaller()); + } + + public function testCallerReturnsSetValue(): void + { + $resolution = new Resolution(null, '/path/to/caller.php'); + + $this->assertSame('/path/to/caller.php', $resolution->caller()); + } + + public function testCallerReturnsNullWhenNotSet(): void + { + $resolution = new Resolution(null, null); + + $this->assertNull($resolution->caller()); + } + + public function testBothCalleeAndCallerCanBeSet(): void + { + $resolution = new Resolution('/path/to/callee.php', '/path/to/caller.php'); + + $this->assertTrue($resolution->hasCallee()); + $this->assertTrue($resolution->hasCaller()); + $this->assertSame('/path/to/callee.php', $resolution->callee()); + $this->assertSame('/path/to/caller.php', $resolution->caller()); + } +} diff --git a/tests/unit/TextUI/Configuration/Value/SourceTest.php b/tests/unit/TextUI/Configuration/Value/SourceTest.php index dae20eaa9d7..e72669f73ce 100644 --- a/tests/unit/TextUI/Configuration/Value/SourceTest.php +++ b/tests/unit/TextUI/Configuration/Value/SourceTest.php @@ -820,6 +820,71 @@ public function testMayBeEmpty(): void $this->assertFalse($source->notEmpty()); } + public function testIssueTriggerResolversDefaultsToEmpty(): void + { + $source = new Source( + null, + false, + FilterDirectoryCollection::fromArray([]), + FilterFileCollection::fromArray([]), + FilterDirectoryCollection::fromArray([]), + FilterFileCollection::fromArray([]), + false, + false, + false, + false, + false, + false, + false, + false, + false, + [ + 'functions' => [], + 'methods' => [], + ], + false, + false, + false, + true, + ); + + $this->assertSame([], $source->issueTriggerResolvers()); + } + + public function testHasIssueTriggerResolvers(): void + { + $resolvers = ['FirstResolver', 'SecondResolver']; + + $source = new Source( + null, + false, + FilterDirectoryCollection::fromArray([]), + FilterFileCollection::fromArray([]), + FilterDirectoryCollection::fromArray([]), + FilterFileCollection::fromArray([]), + false, + false, + false, + false, + false, + false, + false, + false, + false, + [ + 'functions' => [], + 'methods' => [], + ], + false, + false, + false, + true, + $resolvers, + ); + + $this->assertSame($resolvers, $source->issueTriggerResolvers()); + } + public function testMayNotBeEmpty(): void { $source = new Source( diff --git a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php index b3d04d0c576..9af1920bab5 100644 --- a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php +++ b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php @@ -183,6 +183,14 @@ public function testSourceConfigurationIsReadCorrectly(): void $this->assertTrue($source->ignoreSelfDeprecations()); $this->assertTrue($source->ignoreDirectDeprecations()); $this->assertTrue($source->ignoreIndirectDeprecations()); + + $this->assertSame( + [ + 'PHPUnit\TestFixture\IssueTriggerResolver\FirstResolver', + 'PHPUnit\TestFixture\IssueTriggerResolver\SecondResolver', + ], + $source->issueTriggerResolvers(), + ); } public function testCodeCoverageConfigurationIsReadCorrectly(): void From 1f41438ad0e06eb8cb44af30a627c8b535b26cd6 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 4 Mar 2026 17:23:28 +0100 Subject: [PATCH 2/2] Add tests for unhappy paths --- .../phpunit.xml | 17 ++++++++++++ .../src/InvalidResolver.php | 14 ++++++++++ .../tests/IssueTriggerResolverTest.php | 20 ++++++++++++++ .../vendor/autoload.php | 2 ++ .../phpunit.xml | 17 ++++++++++++ .../tests/IssueTriggerResolverTest.php | 20 ++++++++++++++ .../vendor/autoload.php | 1 + .../issue-trigger-resolver-invalid-class.phpt | 27 +++++++++++++++++++ ...ue-trigger-resolver-nonexistent-class.phpt | 27 +++++++++++++++++++ 9 files changed, 145 insertions(+) create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/phpunit.xml create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/src/InvalidResolver.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/tests/IssueTriggerResolverTest.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/vendor/autoload.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/phpunit.xml create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/tests/IssueTriggerResolverTest.php create mode 100644 tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/vendor/autoload.php create mode 100644 tests/end-to-end/error-handler/issue-trigger-resolver-invalid-class.phpt create mode 100644 tests/end-to-end/error-handler/issue-trigger-resolver-nonexistent-class.phpt diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/phpunit.xml b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/phpunit.xml new file mode 100644 index 00000000000..854aec1e46a --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/phpunit.xml @@ -0,0 +1,17 @@ + + + + + tests + + + + + + + + + diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/src/InvalidResolver.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/src/InvalidResolver.php new file mode 100644 index 00000000000..efaad63c8b8 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/src/InvalidResolver.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver; + +final class InvalidResolver +{ +} diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/tests/IssueTriggerResolverTest.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/tests/IssueTriggerResolverTest.php new file mode 100644 index 00000000000..ace74168b68 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/tests/IssueTriggerResolverTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver; + +use PHPUnit\Framework\TestCase; + +final class IssueTriggerResolverTest extends TestCase +{ + public function testSomething(): void + { + $this->assertTrue(true); + } +} diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/vendor/autoload.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/vendor/autoload.php new file mode 100644 index 00000000000..491cdb69f72 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-invalid-class/vendor/autoload.php @@ -0,0 +1,2 @@ + + + + + tests + + + + + + + + + diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/tests/IssueTriggerResolverTest.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/tests/IssueTriggerResolverTest.php new file mode 100644 index 00000000000..ace74168b68 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/tests/IssueTriggerResolverTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver; + +use PHPUnit\Framework\TestCase; + +final class IssueTriggerResolverTest extends TestCase +{ + public function testSomething(): void + { + $this->assertTrue(true); + } +} diff --git a/tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/vendor/autoload.php b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/vendor/autoload.php new file mode 100644 index 00000000000..f4165e81256 --- /dev/null +++ b/tests/end-to-end/error-handler/_files/issue-trigger-resolver-nonexistent-class/vendor/autoload.php @@ -0,0 +1 @@ +run($_SERVER['argv']); +--EXPECTF-- +PHPUnit %s by Sebastian Bergmann and contributors. + +Runtime: %s +Configuration: %s + +. 1 / 1 (100%) + +Time: %s, Memory: %s + +There was 1 PHPUnit test runner warning: + +1) Class PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\InvalidResolver cannot be used as an issue trigger resolver because it does not implement PHPUnit\Runner\IssueTriggerResolver\Resolver + +OK, but there were issues! +Tests: 1, Assertions: 1, PHPUnit Warnings: 1. diff --git a/tests/end-to-end/error-handler/issue-trigger-resolver-nonexistent-class.phpt b/tests/end-to-end/error-handler/issue-trigger-resolver-nonexistent-class.phpt new file mode 100644 index 00000000000..bf90fc67fae --- /dev/null +++ b/tests/end-to-end/error-handler/issue-trigger-resolver-nonexistent-class.phpt @@ -0,0 +1,27 @@ +--TEST-- +Issue trigger resolver configuration: nonexistent class emits PHPUnit warning +--FILE-- +run($_SERVER['argv']); +--EXPECTF-- +PHPUnit %s by Sebastian Bergmann and contributors. + +Runtime: %s +Configuration: %s + +. 1 / 1 (100%) + +Time: %s, Memory: %s + +There was 1 PHPUnit test runner warning: + +1) Class PHPUnit\TestFixture\ErrorHandler\IssueTriggerResolver\NonExistentResolver cannot be used as an issue trigger resolver because it does not exist + +OK, but there were issues! +Tests: 1, Assertions: 1, PHPUnit Warnings: 1.