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
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ 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 %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.
diff --git a/tests/end-to-end/error-handler/issue-trigger-resolver.phpt b/tests/end-to-end/error-handler/issue-trigger-resolver.phpt
new file mode 100644
index 00000000000..611d88ff2e3
--- /dev/null
+++ b/tests/end-to-end/error-handler/issue-trigger-resolver.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Custom IssueTriggerResolver overrides caller/callee detection for issue trigger classification
+--FILE--
+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