feature #29211 [PhpUnitBridge] Url encoded deprecations helper config (greg0ire)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[PhpUnitBridge] Url encoded deprecations helper config

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #28048
| License       | MIT
| Doc PR        | https://github.com/symfony/symfony-docs/pull/10701

First stab at implementing a new way of configuring the deprecation error handler. Includes a refactoring to keep things manageable.

Rework of #24867, blocked by #29718

TODO:

- [x] make the code 5.5 compatible 😢
- [x] add more tests
- [x] deprecate modes (using echo :P)
- [x] test this on real life projects and add some screenshots
- [x] docs PR
- [x] handle `strict`
- [x] adapt existing CI config

# Quiet configuration

![quiet](https://user-images.githubusercontent.com/657779/49341318-fa78c900-f64b-11e8-9504-a8a9eac4baf8.png)

# Default configuration

![verbose](https://user-images.githubusercontent.com/657779/49341322-10868980-f64c-11e8-9d90-dc3f6a18c335.png)

Commits
-------

1c73f9cfed [PhpUnitBridge] Url encoded deprecations helper config
This commit is contained in:
Fabien Potencier 2019-04-12 11:11:22 +02:00
commit a36fbe3d38
18 changed files with 1052 additions and 162 deletions

View File

@ -11,6 +11,9 @@
namespace Symfony\Bridge\PhpUnit;
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration;
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation;
/**
* Catch deprecation notices and print a summary report at the end of the test suite.
*
@ -18,23 +21,30 @@ namespace Symfony\Bridge\PhpUnit;
*/
class DeprecationErrorHandler
{
const MODE_WEAK = 'weak';
/**
* @deprecated since Symfony 4.3, use max[self]=0 instead
*/
const MODE_WEAK_VENDORS = 'weak_vendors';
const MODE_DISABLED = 'disabled';
private $mode = false;
private $resolvedMode = false;
const MODE_DISABLED = 'disabled';
const MODE_WEAK = 'max[total]=999999&verbose=0';
const MODE_STRICT = 'max[total]=0';
private $mode;
private $configuration;
private $deprecations = [
'unsilencedCount' => 0,
'remainingCount' => 0,
'remaining selfCount' => 0,
'legacyCount' => 0,
'otherCount' => 0,
'remaining vendorCount' => 0,
'remaining directCount' => 0,
'remaining indirectCount' => 0,
'unsilenced' => [],
'remaining' => [],
'remaining self' => [],
'legacy' => [],
'other' => [],
'remaining vendor' => [],
'remaining direct' => [],
'remaining indirect' => [],
];
private static $isRegistered = false;
@ -43,13 +53,16 @@ class DeprecationErrorHandler
/**
* Registers and configures the deprecation handler.
*
* The following reporting modes are supported:
* - use "weak" to hide the deprecation report but keep a global count;
* - use "weak_vendors" to fail only on deprecations triggered in your own code;
* - use "/some-regexp/" to stop the test suite whenever a deprecation
* message matches the given regular expression;
* - use a number to define the upper bound of allowed deprecations,
* making the test suite fail whenever more notices are triggered.
* The mode is a query string with options:
* - "disabled" to disable the deprecation handler
* - "verbose" to enable/disable displaying the deprecation report
* - "max" to configure the number of deprecations to allow before exiting with a non-zero
* status code; it's an array with keys "total", "self", "direct" and "indirect"
*
* The default mode is "max[total]=0&verbose=1".
*
* The mode can alternatively be "/some-regexp/" to stop the test suite whenever
* a deprecation message matches the given regular expression.
*
* @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations
*/
@ -108,76 +121,41 @@ class DeprecationErrorHandler
*/
public function handleError($type, $msg, $file, $line, $context = [])
{
if ((E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) || self::MODE_DISABLED === $mode = $this->getMode()) {
if ((E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) || !$this->getConfiguration()->isEnabled()) {
$ErrorHandler = self::$utilPrefix.'ErrorHandler';
return $ErrorHandler::handleError($type, $msg, $file, $line, $context);
}
$trace = debug_backtrace();
$deprecation = new Deprecation($msg, debug_backtrace(), $file);
$group = 'other';
$isVendor = self::MODE_WEAK_VENDORS === $mode && self::inVendors($file);
$i = \count($trace);
while (1 < $i && (!isset($trace[--$i]['class']) || ('ReflectionMethod' === $trace[$i]['class'] || 0 === strpos($trace[$i]['class'], 'PHPUnit_') || 0 === strpos($trace[$i]['class'], 'PHPUnit\\')))) {
// No-op
}
if (isset($trace[$i]['object']) || isset($trace[$i]['class'])) {
if (isset($trace[$i]['class']) && 0 === strpos($trace[$i]['class'], 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor')) {
$parsedMsg = unserialize($msg);
$msg = $parsedMsg['deprecation'];
$class = $parsedMsg['class'];
$method = $parsedMsg['method'];
// If the deprecation has been triggered via
// \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest()
// then we need to use the serialized information to determine
// if the error has been triggered from vendor code.
$isVendor = self::MODE_WEAK_VENDORS === $mode && isset($parsedMsg['triggering_file']) && self::inVendors($parsedMsg['triggering_file']);
} else {
$class = isset($trace[$i]['object']) ? \get_class($trace[$i]['object']) : $trace[$i]['class'];
$method = $trace[$i]['function'];
}
$Test = self::$utilPrefix.'Test';
if ($deprecation->originatesFromAnObject()) {
$class = $deprecation->originatingClass();
$method = $deprecation->originatingMethod();
if (0 !== error_reporting()) {
$group = 'unsilenced';
} elseif (0 === strpos($method, 'testLegacy')
|| 0 === strpos($method, 'provideLegacy')
|| 0 === strpos($method, 'getLegacy')
|| strpos($class, '\Legacy')
|| \in_array('legacy', $Test::getGroups($class, $method), true)
) {
} elseif ($deprecation->isLegacy(self::$utilPrefix)) {
$group = 'legacy';
} elseif ($isVendor) {
$group = 'remaining vendor';
} elseif (!$deprecation->isSelf()) {
$group = $deprecation->isIndirect() ? 'remaining indirect' : 'remaining direct';
} else {
$group = 'remaining';
$group = 'remaining self';
}
if (isset($mode[0]) && '/' === $mode[0] && preg_match($mode, $msg)) {
$e = new \Exception($msg);
$r = new \ReflectionProperty($e, 'trace');
$r->setAccessible(true);
$r->setValue($e, \array_slice($trace, 1, $i));
echo "\n".ucfirst($group).' deprecation triggered by '.$class.'::'.$method.':';
echo "\n".$msg;
echo "\nStack trace:";
echo "\n".str_replace(' '.getcwd().\DIRECTORY_SEPARATOR, ' ', $e->getTraceAsString());
echo "\n";
if ($this->getConfiguration()->shouldDisplayStackTrace($msg)) {
echo "\n".ucfirst($group).' '.$deprecation->toString();
exit(1);
}
if ('legacy' !== $group && self::MODE_WEAK !== $mode) {
if ('legacy' !== $group) {
$ref = &$this->deprecations[$group][$msg]['count'];
++$ref;
$ref = &$this->deprecations[$group][$msg][$class.'::'.$method];
++$ref;
}
} elseif (self::MODE_WEAK !== $mode) {
} else {
$ref = &$this->deprecations[$group][$msg]['count'];
++$ref;
}
@ -190,9 +168,9 @@ class DeprecationErrorHandler
*/
public function shutdown()
{
$mode = $this->getMode();
$configuration = $this->getConfiguration();
if (isset($mode[0]) && '/' === $mode[0]) {
if ($configuration->isInRegexMode()) {
return;
}
@ -200,28 +178,22 @@ class DeprecationErrorHandler
restore_error_handler();
if ($currErrorHandler !== [$this, 'handleError']) {
echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true, $mode), "\n";
echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n";
}
$groups = ['unsilenced', 'remaining'];
$groups = ['unsilenced', 'remaining self', 'remaining direct', 'remaining indirect', 'legacy', 'other'];
if (self::MODE_WEAK_VENDORS === $mode) {
$groups[] = 'remaining vendor';
}
array_push($groups, 'legacy', 'other');
$this->displayDeprecations($groups, $mode);
$this->displayDeprecations($groups, $configuration);
// store failing status
$isFailing = self::MODE_WEAK !== $mode && $mode < $this->deprecations['unsilencedCount'] + $this->deprecations['remainingCount'] + $this->deprecations['otherCount'];
$isFailing = !$configuration->tolerates($this->deprecations);
// reset deprecations array
foreach ($this->deprecations as $group => $arrayOrInt) {
$this->deprecations[$group] = \is_int($arrayOrInt) ? 0 : [];
}
register_shutdown_function(function () use ($isFailing, $groups, $mode) {
register_shutdown_function(function () use ($isFailing, $groups, $configuration) {
foreach ($this->deprecations as $group => $arrayOrInt) {
if (0 < (\is_int($arrayOrInt) ? $arrayOrInt : \count($arrayOrInt))) {
echo "Shutdown-time deprecations:\n";
@ -229,85 +201,55 @@ class DeprecationErrorHandler
}
}
$this->displayDeprecations($groups, $mode);
$this->displayDeprecations($groups, $configuration);
if ($isFailing || self::MODE_WEAK !== $mode && $mode < $this->deprecations['unsilencedCount'] + $this->deprecations['remainingCount'] + $this->deprecations['otherCount']) {
if ($isFailing || !$configuration->tolerates($this->deprecations)) {
exit(1);
}
});
}
private function getMode()
private function getConfiguration()
{
if (false !== $this->resolvedMode) {
return $this->resolvedMode;
if (null !== $this->configuration) {
return $this->configuration;
}
if (false === $mode = $this->mode) {
$mode = getenv('SYMFONY_DEPRECATIONS_HELPER');
}
if (self::MODE_DISABLED !== $mode
&& self::MODE_WEAK !== $mode
&& self::MODE_WEAK_VENDORS !== $mode
&& (!isset($mode[0]) || '/' !== $mode[0])
) {
$mode = preg_match('/^[1-9][0-9]*$/', $mode) ? (int) $mode : 0;
if ('strict' === $mode) {
return $this->configuration = Configuration::inStrictMode();
}
if (self::MODE_DISABLED === $mode) {
return $this->configuration = Configuration::inDisabledMode();
}
if ('weak' === $mode) {
return $this->configuration = Configuration::inWeakMode();
}
if (self::MODE_WEAK_VENDORS === $mode) {
echo sprintf('Setting SYMFONY_DEPRECATIONS_HELPER to "%s" is deprecated in favor of "max[self]=0"', $mode).PHP_EOL;
exit(1);
}
if (isset($mode[0]) && '/' === $mode[0]) {
return $this->configuration = Configuration::fromRegex($mode);
}
return $this->resolvedMode = $mode;
}
/**
* @param string $path
*
* @return bool
*/
private static function inVendors($path)
{
/** @var string[] absolute paths to vendor directories */
static $vendors;
if (null === $vendors) {
foreach (get_declared_classes() as $class) {
if ('C' !== $class[0] || 0 !== strpos($class, 'ComposerAutoloaderInit')) {
continue;
}
$r = new \ReflectionClass($class);
$v = \dirname(\dirname($r->getFileName()));
if (file_exists($v.'/composer/installed.json')) {
$vendors[] = $v;
}
}
if (preg_match('/^[1-9][0-9]*$/', (string) $mode)) {
return $this->configuration = Configuration::fromNumber($mode);
}
$realPath = realpath($path);
if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) {
return true;
}
foreach ($vendors as $vendor) {
if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) {
return true;
}
}
return false;
return $this->configuration = Configuration::fromUrlEncodedString((string) $mode);
}
/**
* @param string $str
* @param bool $red
* @param mixed $mode
*
* @return string
*/
private static function colorize($str, $red, $mode)
private static function colorize($str, $red)
{
if (!self::hasColorSupport() || self::MODE_WEAK === $mode) {
if (!self::hasColorSupport()) {
return $str;
}
@ -317,36 +259,36 @@ class DeprecationErrorHandler
}
/**
* @param string[] $groups
* @param mixed $mode
* @param string[] $groups
* @param Configuration $configuration
*/
private function displayDeprecations($groups, $mode)
private function displayDeprecations($groups, $configuration)
{
$cmp = function ($a, $b) {
return $b['count'] - $a['count'];
};
foreach ($groups as $group) {
if (!$this->deprecations[$group.'Count']) {
continue;
}
if ($this->deprecations[$group.'Count']) {
echo "\n", self::colorize(
sprintf('%s deprecation notices (%d)', ucfirst($group), $this->deprecations[$group.'Count']),
'legacy' !== $group && 'remaining indirect' !== $group
), "\n";
echo "\n", self::colorize(
sprintf('%s deprecation notices (%d)', ucfirst($group), $this->deprecations[$group.'Count']),
'legacy' !== $group && 'remaining vendor' !== $group,
$mode
), "\n";
if (!$configuration->verboseOutput()) {
continue;
}
uasort($this->deprecations[$group], $cmp);
uasort($this->deprecations[$group], $cmp);
foreach ($this->deprecations[$group] as $msg => $notices) {
echo "\n ", $notices['count'], 'x: ', $msg, "\n";
foreach ($this->deprecations[$group] as $msg => $notices) {
echo "\n ", $notices['count'], 'x: ', $msg, "\n";
arsort($notices);
arsort($notices);
foreach ($notices as $method => $count) {
if ('count' !== $method) {
echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n";
foreach ($notices as $method => $count) {
if ('count' !== $method) {
echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n";
}
}
}
}

View File

@ -0,0 +1,208 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler;
/**
* @internal
*/
class Configuration
{
const GROUPS = ['total', 'indirect', 'direct', 'self'];
/**
* @var int[]
*/
private $thresholds;
/**
* @var string
*/
private $regex;
/**
* @var bool
*/
private $enabled = true;
/**
* @var bool
*/
private $verboseOutput = true;
/**
* @param int[] $thresholds A hash associating groups to thresholds
* @param string $regex Will be matched against messages, to decide
* whether to display a stack trace
* @param bool $verboseOutput
*/
private function __construct(array $thresholds = [], $regex = '', $verboseOutput = true)
{
foreach ($thresholds as $group => $threshold) {
if (!\in_array($group, self::GROUPS, true)) {
throw new \InvalidArgumentException(sprintf('Unrecognized threshold "%s", expected one of "%s"', $group, implode('", "', self::GROUPS)));
}
if (!is_numeric($threshold)) {
throw new \InvalidArgumentException(sprintf('Threshold for group "%s" has invalid value "%s"', $group, $threshold));
}
$this->thresholds[$group] = (int) $threshold;
}
if (isset($this->thresholds['direct'])) {
$this->thresholds += [
'self' => $this->thresholds['direct'],
];
}
if (isset($this->thresholds['indirect'])) {
$this->thresholds += [
'direct' => $this->thresholds['indirect'],
'self' => $this->thresholds['indirect'],
];
}
foreach (self::GROUPS as $group) {
if (!isset($this->thresholds[$group])) {
$this->thresholds[$group] = 999999;
}
}
$this->regex = $regex;
$this->verboseOutput = $verboseOutput;
}
/**
* @return bool
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* @param mixed[] $deprecations
*
* @return bool
*/
public function tolerates(array $deprecations)
{
$deprecationCounts = array_filter($deprecations, function ($key) {
return false !== strpos($key, 'Count') && false === strpos($key, 'legacy');
}, ARRAY_FILTER_USE_KEY);
if (array_sum($deprecationCounts) > $this->thresholds['total']) {
return false;
}
foreach (['self', 'direct', 'indirect'] as $deprecationType) {
if ($deprecationCounts['remaining '.$deprecationType.'Count'] > $this->thresholds[$deprecationType]) {
return false;
}
}
return true;
}
/**
* @param string $message
*
* @return bool
*/
public function shouldDisplayStackTrace($message)
{
return '' !== $this->regex && preg_match($this->regex, $message);
}
/**
* @return bool
*/
public function isInRegexMode()
{
return '' !== $this->regex;
}
/**
* @return bool
*/
public function verboseOutput()
{
return $this->verboseOutput;
}
/**
* @param string $serializedConfiguration an encoded string, for instance
* max[total]=1234&max[indirect]=42
*
* @return self
*/
public static function fromUrlEncodedString(string $serializedConfiguration)
{
parse_str($serializedConfiguration, $normalizedConfiguration);
foreach (array_keys($normalizedConfiguration) as $key) {
if (!\in_array($key, ['max', 'disabled', 'verbose'], true)) {
throw new \InvalidArgumentException(sprintf('Unknown configuration option "%s"', $key));
}
}
if (isset($normalizedConfiguration['disabled'])) {
return self::inDisabledMode();
}
$verboseOutput = true;
if (isset($normalizedConfiguration['verbose'])) {
$verboseOutput = (bool) $normalizedConfiguration['verbose'];
}
return new self(
$normalizedConfiguration['max'] ?? [],
'',
$verboseOutput
);
}
/**
* @return self
*/
public static function inDisabledMode()
{
$configuration = new self();
$configuration->enabled = false;
return $configuration;
}
/**
* @return self
*/
public static function inStrictMode()
{
return new self(['total' => 0]);
}
/**
* @return self
*/
public static function inWeakMode()
{
return new self([], '', false);
}
/**
* @return self
*/
public static function fromNumber(int $upperBound)
{
return new self(['total' => $upperBound]);
}
/**
* @return self
*/
public static function fromRegex(string $regex)
{
return new self([], $regex);
}
}

View File

@ -0,0 +1,291 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler;
use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor;
/**
* @internal
*/
class Deprecation
{
/**
* @var array
*/
private $trace;
/**
* @var string
*/
private $message;
/**
* @var ?string
*/
private $originClass;
/**
* @var ?string
*/
private $originMethod;
/**
* @var bool
*/
private $self;
/** @var string[] absolute paths to vendor directories */
private static $vendors;
/**
* @param string $message
* @param string $file
*/
public function __construct($message, array $trace, $file)
{
$this->trace = $trace;
$this->message = $message;
$i = \count($trace);
while (1 < $i && $this->lineShouldBeSkipped($trace[--$i])) {
// No-op
}
$line = $trace[$i];
$this->self = !$this->pathOriginatesFromVendor($file);
if (isset($line['object']) || isset($line['class'])) {
if (isset($line['class']) && 0 === strpos($line['class'], SymfonyTestsListenerFor::class)) {
$parsedMsg = unserialize($this->message);
$this->message = $parsedMsg['deprecation'];
$this->originClass = $parsedMsg['class'];
$this->originMethod = $parsedMsg['method'];
// If the deprecation has been triggered via
// \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest()
// then we need to use the serialized information to determine
// if the error has been triggered from vendor code.
$this->self = isset($parsedMsg['triggering_file'])
&& $this->pathOriginatesFromVendor($parsedMsg['triggering_file']);
return;
}
$this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class'];
$this->originMethod = $line['function'];
}
}
/**
* @return bool
*/
private function lineShouldBeSkipped(array $line)
{
if (!isset($line['class'])) {
return true;
}
$class = $line['class'];
return 'ReflectionMethod' === $class || 0 === strpos($class, 'PHPUnit_') || 0 === strpos($class, 'PHPUnit\\');
}
/**
* @return bool
*/
public function originatesFromAnObject()
{
return isset($this->originClass);
}
/**
* @return bool
*/
public function isSelf()
{
return $this->self;
}
/**
* @return string
*/
public function originatingClass()
{
if (null === $this->originClass) {
throw new \LogicException('Check with originatesFromAnObject() before calling this method');
}
return $this->originClass;
}
/**
* @return string
*/
public function originatingMethod()
{
if (null === $this->originMethod) {
throw new \LogicException('Check with originatesFromAnObject() before calling this method');
}
return $this->originMethod;
}
/**
* @param string $utilPrefix
*
* @return bool
*/
public function isLegacy($utilPrefix)
{
$test = $utilPrefix.'Test';
$class = $this->originatingClass();
$method = $this->originatingMethod();
return 0 === strpos($method, 'testLegacy')
|| 0 === strpos($method, 'provideLegacy')
|| 0 === strpos($method, 'getLegacy')
|| strpos($class, '\Legacy')
|| \in_array('legacy', $test::getGroups($class, $method), true);
}
/**
* Tells whether both the calling package and the called package are vendor
* packages.
*
* @return bool
*/
public function isIndirect()
{
$erroringFile = $erroringPackage = null;
foreach ($this->trace as $line) {
if (\in_array($line['function'], ['require', 'require_once', 'include', 'include_once'], true)) {
continue;
}
if (!isset($line['file'])) {
continue;
}
$file = $line['file'];
if ('-' === $file || 'Standard input code' === $file || !realpath($file)) {
continue;
}
if (!$this->pathOriginatesFromVendor($file)) {
return false;
}
if (null !== $erroringFile && null !== $erroringPackage) {
$package = $this->getPackage($file);
if ('composer' !== $package && $package !== $erroringPackage) {
return true;
}
continue;
}
$erroringFile = $file;
$erroringPackage = $this->getPackage($file);
}
return false;
}
/**
* pathOriginatesFromVendor() should always be called prior to calling this method.
*
* @param string $path
*
* @return string
*/
private function getPackage($path)
{
$path = realpath($path) ?: $path;
foreach (self::getVendors() as $vendorRoot) {
if (0 === strpos($path, $vendorRoot)) {
$relativePath = substr($path, \strlen($vendorRoot) + 1);
$vendor = strstr($relativePath, \DIRECTORY_SEPARATOR, true);
if (false === $vendor) {
throw new \RuntimeException(sprintf('Could not find directory separator "%s" in path "%s"', \DIRECTORY_SEPARATOR, $relativePath));
}
return rtrim($vendor.'/'.strstr(substr(
$relativePath,
\strlen($vendor) + 1
), \DIRECTORY_SEPARATOR, true), '/');
}
}
throw new \RuntimeException(sprintf('No vendors found for path "%s"', $path));
}
/**
* @return string[] an array of paths
*/
private static function getVendors()
{
if (null === self::$vendors) {
self::$vendors = [];
foreach (get_declared_classes() as $class) {
if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) {
$r = new \ReflectionClass($class);
$v = \dirname(\dirname($r->getFileName()));
if (file_exists($v.'/composer/installed.json')) {
self::$vendors[] = $v;
}
}
}
}
return self::$vendors;
}
/**
* @param string $path
*
* @return bool
*/
private function pathOriginatesFromVendor($path)
{
$realPath = realpath($path);
if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) {
return true;
}
foreach (self::getVendors() as $vendor) {
if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) {
return true;
}
}
return false;
}
/**
* @return string
*/
public function toString()
{
$exception = new \Exception($this->message);
$reflection = new \ReflectionProperty($exception, 'trace');
$reflection->setAccessible(true);
$reflection->setValue($exception, $this->trace);
return 'deprecation triggered by '.$this->originatingClass().'::'.$this->originatingMethod().':'.
"\n".$this->message.
"\nStack trace:".
"\n".str_replace(' '.getcwd().\DIRECTORY_SEPARATOR, ' ', $exception->getTraceAsString()).
"\n";
}
private function getPackageFromLine(array $line)
{
if (!isset($line['file'])) {
return 'internal function';
}
if (!$this->pathOriginatesFromVendor($line['file'])) {
return 'source code';
}
try {
return $this->getPackage($line['file']);
} catch (\RuntimeException $e) {
return 'unknown';
}
}
}

View File

@ -0,0 +1,195 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration;
class ConfigurationTest extends TestCase
{
public function testItThrowsOnStringishValue()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('hi');
Configuration::fromUrlEncodedString('hi');
}
public function testItThrowsOnUnknownConfigurationOption()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('min');
Configuration::fromUrlEncodedString('min[total]=42');
}
public function testItThrowsOnUnknownThreshold()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('deep');
Configuration::fromUrlEncodedString('max[deep]=42');
}
public function testItThrowsOnStringishThreshold()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('forty-two');
Configuration::fromUrlEncodedString('max[total]=forty-two');
}
public function testItNoticesExceededTotalThreshold()
{
$configuration = Configuration::fromUrlEncodedString('max[total]=3');
$this->assertTrue($configuration->tolerates([
'unsilencedCount' => 1,
'remaining selfCount' => 0,
'legacyCount' => 1,
'otherCount' => 0,
'remaining directCount' => 1,
'remaining indirectCount' => 1,
]));
$this->assertFalse($configuration->tolerates([
'unsilencedCount' => 1,
'remaining selfCount' => 1,
'legacyCount' => 1,
'otherCount' => 0,
'remaining directCount' => 1,
'remaining indirectCount' => 1,
]));
}
public function testItNoticesExceededSelfThreshold()
{
$configuration = Configuration::fromUrlEncodedString('max[self]=1');
$this->assertTrue($configuration->tolerates([
'unsilencedCount' => 1234,
'remaining selfCount' => 1,
'legacyCount' => 23,
'otherCount' => 13,
'remaining directCount' => 124,
'remaining indirectCount' => 3244,
]));
$this->assertFalse($configuration->tolerates([
'unsilencedCount' => 1234,
'remaining selfCount' => 2,
'legacyCount' => 23,
'otherCount' => 13,
'remaining directCount' => 124,
'remaining indirectCount' => 3244,
]));
}
public function testItNoticesExceededDirectThreshold()
{
$configuration = Configuration::fromUrlEncodedString('max[direct]=1&max[self]=999999');
$this->assertTrue($configuration->tolerates([
'unsilencedCount' => 1234,
'remaining selfCount' => 123,
'legacyCount' => 23,
'otherCount' => 13,
'remaining directCount' => 1,
'remaining indirectCount' => 3244,
]));
$this->assertFalse($configuration->tolerates([
'unsilencedCount' => 1234,
'remaining selfCount' => 124,
'legacyCount' => 23,
'otherCount' => 13,
'remaining directCount' => 2,
'remaining indirectCount' => 3244,
]));
}
public function testItNoticesExceededIndirectThreshold()
{
$configuration = Configuration::fromUrlEncodedString('max[indirect]=1&max[direct]=999999&max[self]=999999');
$this->assertTrue($configuration->tolerates([
'unsilencedCount' => 1234,
'remaining selfCount' => 123,
'legacyCount' => 23,
'otherCount' => 13,
'remaining directCount' => 1234,
'remaining indirectCount' => 1,
]));
$this->assertFalse($configuration->tolerates([
'unsilencedCount' => 1234,
'remaining selfCount' => 124,
'legacyCount' => 23,
'otherCount' => 13,
'remaining directCount' => 2324,
'remaining indirectCount' => 2,
]));
}
public function testIndirectThresholdIsUsedAsADefaultForDirectAndSelfThreshold()
{
$configuration = Configuration::fromUrlEncodedString('max[indirect]=1');
$this->assertTrue($configuration->tolerates([
'unsilencedCount' => 0,
'remaining selfCount' => 1,
'legacyCount' => 0,
'otherCount' => 0,
'remaining directCount' => 0,
'remaining indirectCount' => 0,
]));
$this->assertFalse($configuration->tolerates([
'unsilencedCount' => 0,
'remaining selfCount' => 2,
'legacyCount' => 0,
'otherCount' => 0,
'remaining directCount' => 0,
'remaining indirectCount' => 0,
]));
$this->assertTrue($configuration->tolerates([
'unsilencedCount' => 0,
'remaining selfCount' => 0,
'legacyCount' => 0,
'otherCount' => 0,
'remaining directCount' => 1,
'remaining indirectCount' => 0,
]));
$this->assertFalse($configuration->tolerates([
'unsilencedCount' => 0,
'remaining selfCount' => 0,
'legacyCount' => 0,
'otherCount' => 0,
'remaining directCount' => 2,
'remaining indirectCount' => 0,
]));
}
public function testItCanTellWhetherToDisplayAStackTrace()
{
$configuration = Configuration::fromUrlEncodedString('');
$this->assertFalse($configuration->shouldDisplayStackTrace('interesting'));
$configuration = Configuration::fromRegex('/^interesting/');
$this->assertFalse($configuration->shouldDisplayStackTrace('uninteresting'));
$this->assertTrue($configuration->shouldDisplayStackTrace('interesting'));
}
public function testItCanBeDisabled()
{
$configuration = Configuration::fromUrlEncodedString('disabled');
$this->assertFalse($configuration->isEnabled());
}
public function testItCanBeShushed()
{
$configuration = Configuration::fromUrlEncodedString('verbose');
$this->assertFalse($configuration->verboseOutput());
}
public function testOutputIsNotVerboseInWeakMode()
{
$configuration = Configuration::inWeakMode();
$this->assertFalse($configuration->verboseOutput());
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation;
class DeprecationTest extends TestCase
{
public function testItCanDetermineTheClassWhereTheDeprecationHappened()
{
$deprecation = new Deprecation('💩', $this->debugBacktrace(), __FILE__);
$this->assertTrue($deprecation->originatesFromAnObject());
$this->assertSame(self::class, $deprecation->originatingClass());
$this->assertSame(__FUNCTION__, $deprecation->originatingMethod());
}
public function testItCanTellWhetherItIsInternal()
{
$deprecation = new Deprecation('💩', $this->debugBacktrace(), __FILE__);
$this->assertTrue($deprecation->isSelf());
}
public function testLegacyTestMethodIsDetectedAsSuch()
{
$deprecation = new Deprecation('💩', $this->debugBacktrace(), __FILE__);
$this->assertTrue($deprecation->isLegacy('whatever'));
}
public function testItCanBeConvertedToAString()
{
$deprecation = new Deprecation('💩', $this->debugBacktrace(), __FILE__);
$this->assertContains('💩', $deprecation->toString());
$this->assertContains(__FUNCTION__, $deprecation->toString());
}
public function testItRulesOutFilesOutsideVendorsAsIndirect()
{
$deprecation = new Deprecation('💩', $this->debugBacktrace(), __FILE__);
$this->assertFalse($deprecation->isIndirect());
}
/**
* This method is here to simulate the extra level from the piece of code
* triggering an error to the error handler
*/
public function debugBacktrace(): array
{
return debug_backtrace();
}
}

View File

@ -73,7 +73,7 @@ Unsilenced deprecation notices (3)
1x: unsilenced bar deprecation
1x in FooTestCase::testNonLegacyBar
Remaining deprecation notices (1)
Remaining self deprecation notices (1)
1x: silenced bar deprecation
1x in FooTestCase::testNonLegacyBar

View File

@ -0,0 +1,24 @@
--TEST--
Test eval()'d deprecation is not considered as self
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = dirname($vendor);
}
define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
require_once __DIR__.'/../../bootstrap.php';
eval("@trigger_error('who knows where I come from?', E_USER_DEPRECATED);");
?>
--EXPECTF--
Other deprecation notices (1)
1x: who knows where I come from?

View File

@ -0,0 +1,14 @@
<?php
namespace acme\lib;
class SomeService
{
public function deprecatedApi()
{
@trigger_error(
__FUNCTION__.' is deprecated! You should stop relying on it!',
E_USER_DEPRECATED
);
}
}

View File

@ -0,0 +1,7 @@
<?php
/* We have not caught up on the deprecations yet and still call the other lib
in a deprecated way. */
include __DIR__.'/../lib/SomeService.php';
$defraculator = new \acme\lib\SomeService();
$defraculator->deprecatedApi();

View File

@ -0,0 +1,38 @@
--TEST--
Test DeprecationErrorHandler in weak vendors mode on vendor file
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0&max[direct]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = dirname($vendor);
}
define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
require_once __DIR__.'/../../bootstrap.php';
eval(<<<'EOPHP'
namespace PHPUnit\Util;
class Test
{
public static function getGroups()
{
return array();
}
}
EOPHP
);
require __DIR__.'/fake_vendor/autoload.php';
require __DIR__.'/fake_vendor/acme/outdated-lib/outdated_file.php';
?>
--EXPECTF--
Remaining indirect deprecation notices (1)
1x: deprecatedApi is deprecated! You should stop relying on it!
1x in SomeService::deprecatedApi from acme\lib

View File

@ -0,0 +1,39 @@
--TEST--
Test DeprecationErrorHandler with quiet output
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=verbose=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = dirname($vendor);
}
define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
require_once __DIR__.'/../../bootstrap.php';
@trigger_error('root deprecation', E_USER_DEPRECATED);
class FooTestCase
{
public function testLegacyFoo()
{
@trigger_error('silenced foo deprecation', E_USER_DEPRECATED);
trigger_error('unsilenced foo deprecation', E_USER_DEPRECATED);
}
}
$foo = new FooTestCase();
$foo->testLegacyFoo();
?>
--EXPECTF--
Unsilenced deprecation notices (1)
Legacy deprecation notices (1)
Other deprecation notices (1)

View File

@ -0,0 +1,74 @@
--TEST--
Test DeprecationErrorHandler with no self deprecations on self deprecation
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = dirname($vendor);
}
define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
require_once __DIR__.'/../../bootstrap.php';
@trigger_error('root deprecation', E_USER_DEPRECATED);
eval(<<<'EOPHP'
namespace PHPUnit\Util;
class Test
{
public static function getGroups()
{
return array();
}
}
EOPHP
);
class FooTestCase
{
public function testLegacyFoo()
{
@trigger_error('silenced foo deprecation', E_USER_DEPRECATED);
trigger_error('unsilenced foo deprecation', E_USER_DEPRECATED);
trigger_error('unsilenced foo deprecation', E_USER_DEPRECATED);
}
public function testNonLegacyBar()
{
@trigger_error('silenced bar deprecation', E_USER_DEPRECATED);
trigger_error('unsilenced bar deprecation', E_USER_DEPRECATED);
}
}
$foo = new FooTestCase();
$foo->testLegacyFoo();
$foo->testNonLegacyBar();
?>
--EXPECTF--
Unsilenced deprecation notices (3)
2x: unsilenced foo deprecation
2x in FooTestCase::testLegacyFoo
1x: unsilenced bar deprecation
1x in FooTestCase::testNonLegacyBar
Remaining self deprecation notices (1)
1x: silenced bar deprecation
1x in FooTestCase::testNonLegacyBar
Legacy deprecation notices (1)
Other deprecation notices (1)
1x: root deprecation

View File

@ -73,7 +73,7 @@ Unsilenced deprecation notices (3)
1x: unsilenced bar deprecation
1x in FooTestCase::testNonLegacyBar
Remaining deprecation notices (1)
Remaining self deprecation notices (1)
1x: silenced bar deprecation
1x in FooTestCase::testNonLegacyBar

View File

@ -3,7 +3,7 @@ Test DeprecationErrorHandler in weak vendors mode on eval()'d deprecation
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=weak_vendors');
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
@ -19,7 +19,6 @@ eval("@trigger_error('who knows where I come from?', E_USER_DEPRECATED);");
?>
--EXPECTF--
Other deprecation notices (1)
1x: who knows where I come from?

View File

@ -3,7 +3,7 @@ Test DeprecationErrorHandler in weak vendors mode on a non vendor file
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=weak_vendors');
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
@ -61,7 +61,7 @@ Unsilenced deprecation notices (3)
1x: unsilenced bar deprecation
1x in FooTestCase::testNonLegacyBar
Remaining deprecation notices (1)
Remaining self deprecation notices (1)
1x: silenced bar deprecation
1x in FooTestCase::testNonLegacyBar

View File

@ -1,10 +1,10 @@
--TEST--
Test DeprecationErrorHandler in weak vendors mode on eval()'d deprecation
Test deprecations coming from a phar are not considered self deprecations
The phar can be regenerated by running php src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/generate_phar.php
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=weak_vendors');
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
@ -21,7 +21,6 @@ include 'phar://deprecation.phar/deprecation.php';
?>
--EXPECTF--
Other deprecation notices (1)
1x: I come from… afar! :D

View File

@ -1,9 +1,9 @@
--TEST--
Test DeprecationErrorHandler in weak vendors mode on vendor file
Test DeprecationErrorHandler with no self deprecations on vendor deprecation
--FILE--
<?php
putenv('SYMFONY_DEPRECATIONS_HELPER=weak_vendors');
putenv('SYMFONY_DEPRECATIONS_HELPER=max[self]=0');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');
@ -28,7 +28,7 @@ Unsilenced deprecation notices (3)
1x: unsilenced bar deprecation
1x in FooTestCase::testNonLegacyBar
Remaining vendor deprecation notices (1)
Remaining direct deprecation notices (1)
1x: silenced bar deprecation
1x in FooTestCase::testNonLegacyBar