Skip to content
Open
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
9 changes: 9 additions & 0 deletions phpunit.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
</xs:complexType>
</xs:element>
<xs:element name="deprecationTrigger" type="deprecationTriggerType" minOccurs="0"/>
<xs:element name="issueTriggerResolvers" type="issueTriggerResolversType" minOccurs="0"/>
</xs:all>
<xs:attribute name="baseline" type="xs:anyURI"/>
<xs:attribute name="restrictNotices" type="xs:boolean" default="false"/>
Expand Down Expand Up @@ -359,4 +360,12 @@
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="issueTriggerResolversType">
<xs:sequence>
<xs:element name="issueTriggerResolver" type="issueTriggerResolverType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="issueTriggerResolverType">
<xs:attribute name="className" type="xs:string" use="required"/>
</xs:complexType>
</xs:schema>
5 changes: 5 additions & 0 deletions src/Framework/TestRunner/templates/method.tpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php declare(strict_types=1);
use PHPUnit\Event\Facade;
use PHPUnit\Runner\IssueTriggerResolver;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ErrorHandler;
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
Expand Down Expand Up @@ -69,6 +70,10 @@ function __phpunit_run_isolated_test()

ErrorHandler::instance()->useDeprecationTriggers($deprecationTriggers);

foreach (array_reverse($configuration->source()->issueTriggerResolvers()) as $className) {
ErrorHandler::instance()->addIssueTriggerResolver(new $className);
}

$test = new {className}('{methodName}');

$test->setData('{dataName}', unserialize('{data}'));
Expand Down
76 changes: 55 additions & 21 deletions src/Runner/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -80,6 +84,11 @@ final class ErrorHandler
*/
private ?array $deprecationTriggers = null;

/**
* @var non-empty-list<IssueTriggerResolver>
*/
private array $issueTriggerResolvers;

public static function instance(): self
{
$source = ConfigurationRegistry::get()->source();
Expand All @@ -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];
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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(),
);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<array{file: string, line: int, class?: string, function?: string, type: string}> $trace
* @param list<array{file?: string, line?: int, class?: class-string, function?: string, type?: string, args?: list<mixed>}> $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
}

/**
Expand All @@ -386,11 +414,13 @@ private function categorizeFile(string $file, TestMethod $test): Code
}

/**
* @return list<array{file: string, line: int, class?: string, function?: string, type: string}>
* @return list<array{file: string, line: int, class?: string, function?: string, type: string, args?: list<mixed>}>
*/
private function filteredStackTrace(): array
{
$trace = $this->errorStackTrace();
$ignoreArguments = count($this->issueTriggerResolvers) === 1;

$trace = $this->errorStackTrace($ignoreArguments);

if ($this->deprecationTriggers === null) {
return array_values($trace);
Expand Down Expand Up @@ -446,11 +476,15 @@ private function guessDeprecationFrame(): ?array
}

/**
* @return list<array{file: string, line: ?int, class?: class-string, function?: string, type: string}>
* @return list<array{file: string, line: ?int, class?: class-string, function?: string, type: string, args?: list<mixed>}>
*/
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;

Expand Down
29 changes: 29 additions & 0 deletions src/Runner/IssueTriggerResolver/DefaultResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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<array{file?: string, line?: int, class?: class-string, function?: string, type?: string, args?: list<mixed>}> $trace
*/
public function resolve(array $trace, string $message): Resolution
{
return new Resolution(
$trace[0]['file'] ?? null,
$trace[1]['file'] ?? null,
);
}
}
51 changes: 51 additions & 0 deletions src/Runner/IssueTriggerResolver/Resolution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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;
}
}
23 changes: 23 additions & 0 deletions src/Runner/IssueTriggerResolver/Resolver.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 <sebastian@phpunit.de>
*
* 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<array{file?: string, line?: int, class?: class-string, function?: string, type?: string, args?: list<mixed>}> $trace
*/
public function resolve(array $trace, string $message): ?Resolution;
}
37 changes: 37 additions & 0 deletions src/TextUI/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -218,6 +222,7 @@ public function run(array $argv): int
}

$this->configureDeprecationTriggers($configuration);
$this->configureIssueTriggerResolvers($configuration);

$timer = new Timer;
$timer->start();
Expand Down Expand Up @@ -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')) {
Expand Down
1 change: 1 addition & 0 deletions src/TextUI/Configuration/Merger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading