Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Bridge\PhpUnit; Chris@0: Chris@0: /** Chris@0: * Catch deprecation notices and print a summary report at the end of the test suite. Chris@0: * Chris@0: * @author Nicolas Grekas Chris@0: */ Chris@0: class DeprecationErrorHandler Chris@0: { Chris@0: const MODE_WEAK = 'weak'; Chris@14: const MODE_WEAK_VENDORS = 'weak_vendors'; Chris@0: const MODE_DISABLED = 'disabled'; Chris@0: Chris@0: private static $isRegistered = false; Chris@0: Chris@0: /** Chris@0: * Registers and configures the deprecation handler. Chris@0: * Chris@0: * The following reporting modes are supported: Chris@0: * - use "weak" to hide the deprecation report but keep a global count; Chris@14: * - use "weak_vendors" to act as "weak" but only for vendors; Chris@0: * - use "/some-regexp/" to stop the test suite whenever a deprecation Chris@0: * message matches the given regular expression; Chris@0: * - use a number to define the upper bound of allowed deprecations, Chris@17: * making the test suite fail whenever more notices are triggered. Chris@0: * Chris@0: * @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations Chris@0: */ Chris@0: public static function register($mode = 0) Chris@0: { Chris@0: if (self::$isRegistered) { Chris@0: return; Chris@0: } Chris@0: Chris@14: $UtilPrefix = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_' : 'PHPUnit\Util\\'; Chris@14: Chris@0: $getMode = function () use ($mode) { Chris@0: static $memoizedMode = false; Chris@0: Chris@0: if (false !== $memoizedMode) { Chris@0: return $memoizedMode; Chris@0: } Chris@0: if (false === $mode) { Chris@0: $mode = getenv('SYMFONY_DEPRECATIONS_HELPER'); Chris@0: } Chris@18: if (DeprecationErrorHandler::MODE_DISABLED !== $mode Chris@18: && DeprecationErrorHandler::MODE_WEAK !== $mode Chris@18: && DeprecationErrorHandler::MODE_WEAK_VENDORS !== $mode Chris@17: && (!isset($mode[0]) || '/' !== $mode[0]) Chris@17: ) { Chris@0: $mode = preg_match('/^[1-9][0-9]*$/', $mode) ? (int) $mode : 0; Chris@0: } Chris@0: Chris@0: return $memoizedMode = $mode; Chris@0: }; Chris@0: Chris@14: $inVendors = function ($path) { Chris@14: /** @var string[] absolute paths to vendor directories */ Chris@14: static $vendors; Chris@14: if (null === $vendors) { Chris@14: foreach (get_declared_classes() as $class) { Chris@14: if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { Chris@14: $r = new \ReflectionClass($class); Chris@17: $v = \dirname(\dirname($r->getFileName())); Chris@14: if (file_exists($v.'/composer/installed.json')) { Chris@14: $vendors[] = $v; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: $realPath = realpath($path); Chris@14: if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) { Chris@14: return true; Chris@14: } Chris@14: foreach ($vendors as $vendor) { Chris@17: if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) { Chris@14: return true; Chris@14: } Chris@14: } Chris@14: Chris@14: return false; Chris@14: }; Chris@14: Chris@0: $deprecations = array( Chris@0: 'unsilencedCount' => 0, Chris@0: 'remainingCount' => 0, Chris@0: 'legacyCount' => 0, Chris@0: 'otherCount' => 0, Chris@14: 'remaining vendorCount' => 0, Chris@0: 'unsilenced' => array(), Chris@0: 'remaining' => array(), Chris@0: 'legacy' => array(), Chris@0: 'other' => array(), Chris@14: 'remaining vendor' => array(), Chris@0: ); Chris@14: $deprecationHandler = function ($type, $msg, $file, $line, $context = array()) use (&$deprecations, $getMode, $UtilPrefix, $inVendors) { Chris@18: if ((E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) || DeprecationErrorHandler::MODE_DISABLED === $mode = $getMode()) { Chris@14: $ErrorHandler = $UtilPrefix.'ErrorHandler'; Chris@14: Chris@14: return $ErrorHandler::handleError($type, $msg, $file, $line, $context); Chris@0: } Chris@0: Chris@17: $trace = debug_backtrace(); Chris@0: $group = 'other'; Chris@18: $isVendor = DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $inVendors($file); Chris@0: Chris@17: $i = \count($trace); Chris@14: while (1 < $i && (!isset($trace[--$i]['class']) || ('ReflectionMethod' === $trace[$i]['class'] || 0 === strpos($trace[$i]['class'], 'PHPUnit_') || 0 === strpos($trace[$i]['class'], 'PHPUnit\\')))) { Chris@0: // No-op Chris@0: } Chris@0: Chris@0: if (isset($trace[$i]['object']) || isset($trace[$i]['class'])) { Chris@14: if (isset($trace[$i]['class']) && 0 === strpos($trace[$i]['class'], 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor')) { Chris@14: $parsedMsg = unserialize($msg); Chris@14: $msg = $parsedMsg['deprecation']; Chris@14: $class = $parsedMsg['class']; Chris@14: $method = $parsedMsg['method']; Chris@14: // If the deprecation has been triggered via Chris@14: // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() Chris@14: // then we need to use the serialized information to determine Chris@14: // if the error has been triggered from vendor code. Chris@18: $isVendor = DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && isset($parsedMsg['triggering_file']) && $inVendors($parsedMsg['triggering_file']); Chris@14: } else { Chris@17: $class = isset($trace[$i]['object']) ? \get_class($trace[$i]['object']) : $trace[$i]['class']; Chris@14: $method = $trace[$i]['function']; Chris@14: } Chris@14: Chris@14: $Test = $UtilPrefix.'Test'; Chris@0: Chris@0: if (0 !== error_reporting()) { Chris@0: $group = 'unsilenced'; Chris@0: } elseif (0 === strpos($method, 'testLegacy') Chris@0: || 0 === strpos($method, 'provideLegacy') Chris@0: || 0 === strpos($method, 'getLegacy') Chris@0: || strpos($class, '\Legacy') Chris@17: || \in_array('legacy', $Test::getGroups($class, $method), true) Chris@0: ) { Chris@0: $group = 'legacy'; Chris@14: } elseif ($isVendor) { Chris@14: $group = 'remaining vendor'; Chris@0: } else { Chris@0: $group = 'remaining'; Chris@0: } Chris@0: Chris@0: if (isset($mode[0]) && '/' === $mode[0] && preg_match($mode, $msg)) { Chris@0: $e = new \Exception($msg); Chris@0: $r = new \ReflectionProperty($e, 'trace'); Chris@0: $r->setAccessible(true); Chris@17: $r->setValue($e, \array_slice($trace, 1, $i)); Chris@0: Chris@0: echo "\n".ucfirst($group).' deprecation triggered by '.$class.'::'.$method.':'; Chris@0: echo "\n".$msg; Chris@0: echo "\nStack trace:"; Chris@17: echo "\n".str_replace(' '.getcwd().\DIRECTORY_SEPARATOR, ' ', $e->getTraceAsString()); Chris@0: echo "\n"; Chris@0: Chris@0: exit(1); Chris@0: } Chris@18: if ('legacy' !== $group && DeprecationErrorHandler::MODE_WEAK !== $mode) { Chris@0: $ref = &$deprecations[$group][$msg]['count']; Chris@0: ++$ref; Chris@0: $ref = &$deprecations[$group][$msg][$class.'::'.$method]; Chris@0: ++$ref; Chris@0: } Chris@18: } elseif (DeprecationErrorHandler::MODE_WEAK !== $mode) { Chris@0: $ref = &$deprecations[$group][$msg]['count']; Chris@0: ++$ref; Chris@0: } Chris@0: ++$deprecations[$group.'Count']; Chris@0: }; Chris@0: $oldErrorHandler = set_error_handler($deprecationHandler); Chris@0: Chris@0: if (null !== $oldErrorHandler) { Chris@0: restore_error_handler(); Chris@14: if (array($UtilPrefix.'ErrorHandler', 'handleError') === $oldErrorHandler) { Chris@0: restore_error_handler(); Chris@0: self::register($mode); Chris@0: } Chris@0: } else { Chris@0: self::$isRegistered = true; Chris@0: if (self::hasColorSupport()) { Chris@0: $colorize = function ($str, $red) { Chris@0: $color = $red ? '41;37' : '43;30'; Chris@0: Chris@0: return "\x1B[{$color}m{$str}\x1B[0m"; Chris@0: }; Chris@0: } else { Chris@0: $colorize = function ($str) { return $str; }; Chris@0: } Chris@0: register_shutdown_function(function () use ($getMode, &$deprecations, $deprecationHandler, $colorize) { Chris@0: $mode = $getMode(); Chris@0: if (isset($mode[0]) && '/' === $mode[0]) { Chris@0: return; Chris@0: } Chris@0: $currErrorHandler = set_error_handler('var_dump'); Chris@0: restore_error_handler(); Chris@0: Chris@18: if (DeprecationErrorHandler::MODE_WEAK === $mode) { Chris@0: $colorize = function ($str) { return $str; }; Chris@0: } Chris@0: if ($currErrorHandler !== $deprecationHandler) { Chris@0: echo "\n", $colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n"; Chris@0: } Chris@0: Chris@0: $cmp = function ($a, $b) { Chris@0: return $b['count'] - $a['count']; Chris@0: }; Chris@0: Chris@14: $groups = array('unsilenced', 'remaining'); Chris@18: if (DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode) { Chris@14: $groups[] = 'remaining vendor'; Chris@14: } Chris@14: array_push($groups, 'legacy', 'other'); Chris@0: Chris@14: $displayDeprecations = function ($deprecations) use ($colorize, $cmp, $groups) { Chris@14: foreach ($groups as $group) { Chris@14: if ($deprecations[$group.'Count']) { Chris@14: echo "\n", $colorize( Chris@14: sprintf('%s deprecation notices (%d)', ucfirst($group), $deprecations[$group.'Count']), Chris@14: 'legacy' !== $group && 'remaining vendor' !== $group Chris@14: ), "\n"; Chris@0: Chris@14: uasort($deprecations[$group], $cmp); Chris@0: Chris@14: foreach ($deprecations[$group] as $msg => $notices) { Chris@14: echo "\n ", $notices['count'], 'x: ', $msg, "\n"; Chris@0: Chris@14: arsort($notices); Chris@14: Chris@14: foreach ($notices as $method => $count) { Chris@14: if ('count' !== $method) { Chris@14: echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n"; Chris@14: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@14: if (!empty($notices)) { Chris@14: echo "\n"; Chris@14: } Chris@14: }; Chris@14: Chris@14: $displayDeprecations($deprecations); Chris@14: Chris@14: // store failing status Chris@18: $isFailing = DeprecationErrorHandler::MODE_WEAK !== $mode && $mode < $deprecations['unsilencedCount'] + $deprecations['remainingCount'] + $deprecations['otherCount']; Chris@14: Chris@14: // reset deprecations array Chris@14: foreach ($deprecations as $group => $arrayOrInt) { Chris@17: $deprecations[$group] = \is_int($arrayOrInt) ? 0 : array(); Chris@0: } Chris@0: Chris@14: register_shutdown_function(function () use (&$deprecations, $isFailing, $displayDeprecations, $mode) { Chris@14: foreach ($deprecations as $group => $arrayOrInt) { Chris@17: if (0 < (\is_int($arrayOrInt) ? $arrayOrInt : \count($arrayOrInt))) { Chris@14: echo "Shutdown-time deprecations:\n"; Chris@14: break; Chris@14: } Chris@14: } Chris@14: $displayDeprecations($deprecations); Chris@18: if ($isFailing || DeprecationErrorHandler::MODE_WEAK !== $mode && $mode < $deprecations['unsilencedCount'] + $deprecations['remainingCount'] + $deprecations['otherCount']) { Chris@14: exit(1); Chris@14: } Chris@14: }); Chris@0: }); Chris@0: } Chris@0: } Chris@0: Chris@14: public static function collectDeprecations($outputFile) Chris@14: { Chris@14: $deprecations = array(); Chris@14: $previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = array()) use (&$deprecations, &$previousErrorHandler) { Chris@14: if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { Chris@14: if ($previousErrorHandler) { Chris@14: return $previousErrorHandler($type, $msg, $file, $line, $context); Chris@14: } Chris@14: static $autoload = true; Chris@14: Chris@14: $ErrorHandler = class_exists('PHPUnit_Util_ErrorHandler', $autoload) ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler'; Chris@14: $autoload = false; Chris@14: Chris@14: return $ErrorHandler::handleError($type, $msg, $file, $line, $context); Chris@14: } Chris@14: $deprecations[] = array(error_reporting(), $msg, $file); Chris@14: }); Chris@14: Chris@14: register_shutdown_function(function () use ($outputFile, &$deprecations) { Chris@14: file_put_contents($outputFile, serialize($deprecations)); Chris@14: }); Chris@14: } Chris@14: Chris@16: /** Chris@16: * Returns true if STDOUT is defined and supports colorization. Chris@16: * Chris@16: * Reference: Composer\XdebugHandler\Process::supportsColor Chris@16: * https://github.com/composer/xdebug-handler Chris@16: * Chris@16: * @return bool Chris@16: */ Chris@0: private static function hasColorSupport() Chris@0: { Chris@17: if (!\defined('STDOUT')) { Chris@16: return false; Chris@16: } Chris@16: Chris@17: if ('Hyper' === getenv('TERM_PROGRAM')) { Chris@17: return true; Chris@17: } Chris@17: Chris@17: if (\DIRECTORY_SEPARATOR === '\\') { Chris@17: return (\function_exists('sapi_windows_vt100_support') Chris@16: && sapi_windows_vt100_support(STDOUT)) Chris@0: || false !== getenv('ANSICON') Chris@0: || 'ON' === getenv('ConEmuANSI') Chris@0: || 'xterm' === getenv('TERM'); Chris@0: } Chris@0: Chris@17: if (\function_exists('stream_isatty')) { Chris@16: return stream_isatty(STDOUT); Chris@16: } Chris@16: Chris@17: if (\function_exists('posix_isatty')) { Chris@16: return posix_isatty(STDOUT); Chris@16: } Chris@16: Chris@16: $stat = fstat(STDOUT); Chris@16: // Check if formatted mode is S_IFCHR Chris@16: return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; Chris@0: } Chris@0: }