├── CHANGELOG.md ├── ClassExistsMock.php ├── ClockMock.php ├── ConstraintTrait.php ├── CoverageListener.php ├── DeprecationErrorHandler.php ├── DeprecationErrorHandler ├── Configuration.php ├── Deprecation.php ├── DeprecationGroup.php └── DeprecationNotice.php ├── DnsMock.php ├── ExpectDeprecationTrait.php ├── ExpectUserDeprecationMessageTrait.php ├── Extension ├── EnableClockMockSubscriber.php ├── RegisterClockMockSubscriber.php └── RegisterDnsMockSubscriber.php ├── LICENSE ├── Legacy ├── CommandForV7.php ├── CommandForV9.php ├── ConstraintLogicTrait.php ├── ConstraintTraitForV7.php ├── ConstraintTraitForV8.php ├── ConstraintTraitForV9.php ├── ExpectDeprecationTraitBeforeV8_4.php ├── ExpectDeprecationTraitForV8_4.php ├── PolyfillAssertTrait.php ├── PolyfillTestCaseTrait.php ├── SymfonyTestsListenerForV7.php └── SymfonyTestsListenerTrait.php ├── README.md ├── SymfonyExtension.php ├── SymfonyTestsListener.php ├── TextUI └── Command.php ├── bin ├── simple-phpunit └── simple-phpunit.php ├── bootstrap.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.2 5 | --- 6 | 7 | * Add a PHPUnit extension that registers the clock mock and DNS mock and the `DebugClassLoader` from the ErrorHandler component if present 8 | * Add `ExpectUserDeprecationMessageTrait` with a polyfill of PHPUnit's `expectUserDeprecationMessage()` 9 | * Use `total` for asserting deprecation count when a group is not defined 10 | 11 | 6.4 12 | --- 13 | 14 | * Allow setting the locale using `SYMFONY_PHPUNIT_LOCALE` env var 15 | 16 | 6.3 17 | --- 18 | 19 | * Add support for mocking the `enum_exists` function 20 | * Enable reporting of deprecations triggered by Doctrine by default 21 | 22 | 6.2 23 | --- 24 | 25 | * Add support for mocking the `hrtime()` function 26 | 27 | 6.1 28 | --- 29 | 30 | * Add option `ignoreFile` to configure a file that lists deprecation messages to ignore 31 | 32 | 6.0 33 | --- 34 | 35 | * Remove `SetUpTearDownTrait` 36 | 37 | 5.3 38 | --- 39 | 40 | * bumped the minimum PHP version to 7.1.3 41 | * bumped the minimum PHPUnit version to 7.5 42 | * deprecated the `SetUpTearDownTrait` trait, use original methods with "void" return typehint. 43 | * added `logFile` option to write deprecations to a file instead of echoing them 44 | 45 | 5.1.0 46 | ----- 47 | 48 | * ignore verbosity settings when the build fails because of deprecations 49 | * added per-group verbosity 50 | * added `ExpectDeprecationTrait` to be able to define an expected deprecation from inside a test 51 | * deprecated the `@expectedDeprecation` annotation, use the `ExpectDeprecationTrait::expectDeprecation()` method instead 52 | 53 | 5.0.0 54 | ----- 55 | 56 | * removed `weak_vendor` mode, use `max[self]=0` instead 57 | 58 | 4.4.0 59 | ----- 60 | 61 | * made the bridge act as a polyfill for newest PHPUnit features 62 | * added `SetUpTearDownTrait` to allow working around the `void` return-type added by PHPUnit 8 63 | * added namespace aliases for PHPUnit < 6 64 | 65 | 4.3.0 66 | ----- 67 | 68 | * added `ClassExistsMock` 69 | * bumped PHP version from 5.3.3 to 5.5.9 70 | * split simple-phpunit bin into php file with code and a shell script 71 | 72 | 4.1.0 73 | ----- 74 | 75 | * Search for `SYMFONY_PHPUNIT_VERSION`, `SYMFONY_PHPUNIT_REMOVE`, 76 | `SYMFONY_PHPUNIT_DIR` env var in `phpunit.xml` then in `phpunit.xml.dist` 77 | 78 | 4.0.0 79 | ----- 80 | 81 | * support for the `testLegacy` prefix in method names to mark a test as legacy 82 | has been dropped, use the `@group legacy` notation instead 83 | * support for the `Legacy` prefix in class names to mark tests as legacy has 84 | been dropped, use the `@group legacy` notation instead 85 | * support for passing an array of mocked namespaces not indexed by the mock 86 | feature to the constructor of the `SymfonyTestsListenerTrait` class was 87 | dropped 88 | 89 | 3.4.0 90 | ----- 91 | 92 | * added a `CoverageListener` to enhance the code coverage report 93 | * all deprecations but those from tests marked with `@group legacy` are always 94 | displayed when not in `weak` mode 95 | 96 | 3.3.0 97 | ----- 98 | 99 | * using the `testLegacy` prefix in method names to mark a test as legacy is 100 | deprecated, use the `@group legacy` notation instead 101 | * using the `Legacy` prefix in class names to mark a test as legacy is deprecated, 102 | use the `@group legacy` notation instead 103 | 104 | 3.1.0 105 | ----- 106 | 107 | * passing a numerically indexed array to the constructor of the `SymfonyTestsListenerTrait` 108 | is deprecated, pass an array of namespaces indexed by the mocked feature instead 109 | -------------------------------------------------------------------------------- /ClassExistsMock.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | /** 15 | * @author Roland Franssen 16 | */ 17 | class ClassExistsMock 18 | { 19 | private static $classes = []; 20 | 21 | private static $enums = []; 22 | 23 | /** 24 | * Configures the classes to be checked upon existence. 25 | * 26 | * @param array $classes Mocked class names as keys (case-sensitive, without leading root namespace slash) and booleans as values 27 | */ 28 | public static function withMockedClasses(array $classes): void 29 | { 30 | self::$classes = $classes; 31 | } 32 | 33 | /** 34 | * Configures the enums to be checked upon existence. 35 | * 36 | * @param array $enums Mocked enums names as keys (case-sensitive, without leading root namespace slash) and booleans as values 37 | */ 38 | public static function withMockedEnums(array $enums): void 39 | { 40 | self::$enums = $enums; 41 | self::$classes += $enums; 42 | } 43 | 44 | public static function class_exists($name, $autoload = true): bool 45 | { 46 | $name = ltrim($name, '\\'); 47 | 48 | return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \class_exists($name, $autoload); 49 | } 50 | 51 | public static function interface_exists($name, $autoload = true): bool 52 | { 53 | $name = ltrim($name, '\\'); 54 | 55 | return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \interface_exists($name, $autoload); 56 | } 57 | 58 | public static function trait_exists($name, $autoload = true): bool 59 | { 60 | $name = ltrim($name, '\\'); 61 | 62 | return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \trait_exists($name, $autoload); 63 | } 64 | 65 | public static function enum_exists($name, $autoload = true):bool 66 | { 67 | $name = ltrim($name, '\\'); 68 | 69 | return isset(self::$enums[$name]) ? (bool) self::$enums[$name] : \enum_exists($name, $autoload); 70 | } 71 | 72 | public static function register($class): void 73 | { 74 | $self = static::class; 75 | 76 | $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; 77 | if (0 < strpos($class, '\\Tests\\')) { 78 | $ns = str_replace('\\Tests\\', '\\', $class); 79 | $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); 80 | } elseif (0 === strpos($class, 'Tests\\')) { 81 | $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); 82 | } 83 | foreach ($mockedNs as $ns) { 84 | foreach (['class', 'interface', 'trait', 'enum'] as $type) { 85 | if (\function_exists($ns.'\\'.$type.'_exists')) { 86 | continue; 87 | } 88 | eval(<< 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | /** 15 | * @author Nicolas Grekas 16 | * @author Dominic Tubach 17 | */ 18 | class ClockMock 19 | { 20 | private static $now; 21 | 22 | public static function withClockMock($enable = null): ?bool 23 | { 24 | if (null === $enable) { 25 | return null !== self::$now; 26 | } 27 | 28 | self::$now = is_numeric($enable) ? (float) $enable : ($enable ? microtime(true) : null); 29 | 30 | return null; 31 | } 32 | 33 | public static function time(): int 34 | { 35 | if (null === self::$now) { 36 | return \time(); 37 | } 38 | 39 | return (int) self::$now; 40 | } 41 | 42 | public static function sleep($s): int 43 | { 44 | if (null === self::$now) { 45 | return \sleep($s); 46 | } 47 | 48 | self::$now += (int) $s; 49 | 50 | return 0; 51 | } 52 | 53 | public static function usleep($us): void 54 | { 55 | if (null === self::$now) { 56 | \usleep($us); 57 | } else { 58 | self::$now += $us / 1000000; 59 | } 60 | } 61 | 62 | /** 63 | * @return string|float 64 | */ 65 | public static function microtime($asFloat = false) 66 | { 67 | if (null === self::$now) { 68 | return \microtime($asFloat); 69 | } 70 | 71 | if ($asFloat) { 72 | return self::$now; 73 | } 74 | 75 | return \sprintf('%0.6f00 %d', self::$now - (int) self::$now, (int) self::$now); 76 | } 77 | 78 | public static function date($format, $timestamp = null): string 79 | { 80 | if (null === $timestamp) { 81 | $timestamp = self::time(); 82 | } 83 | 84 | return \date($format, $timestamp); 85 | } 86 | 87 | public static function gmdate($format, $timestamp = null): string 88 | { 89 | if (null === $timestamp) { 90 | $timestamp = self::time(); 91 | } 92 | 93 | return \gmdate($format, $timestamp); 94 | } 95 | 96 | /** 97 | * @return array|int|float 98 | */ 99 | public static function hrtime($asNumber = false) 100 | { 101 | $ns = (self::$now - (int) self::$now) * 1000000000; 102 | 103 | if ($asNumber) { 104 | $number = \sprintf('%d%d', (int) self::$now, $ns); 105 | 106 | return \PHP_INT_SIZE === 8 ? (int) $number : (float) $number; 107 | } 108 | 109 | return [(int) self::$now, (int) $ns]; 110 | } 111 | 112 | public static function register($class): void 113 | { 114 | $self = static::class; 115 | 116 | $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; 117 | if (0 < strpos($class, '\\Tests\\')) { 118 | $ns = str_replace('\\Tests\\', '\\', $class); 119 | $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); 120 | } elseif (0 === strpos($class, 'Tests\\')) { 121 | $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); 122 | } 123 | foreach ($mockedNs as $ns) { 124 | if (\function_exists($ns.'\time')) { 125 | continue; 126 | } 127 | eval(<< 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | 16 | $r = new \ReflectionClass(Constraint::class); 17 | if ($r->getProperty('exporter')->isProtected()) { 18 | trait ConstraintTrait 19 | { 20 | use Legacy\ConstraintTraitForV7; 21 | } 22 | } elseif (!$r->getMethod('evaluate')->hasReturnType()) { 23 | trait ConstraintTrait 24 | { 25 | use Legacy\ConstraintTraitForV8; 26 | } 27 | } else { 28 | trait ConstraintTrait 29 | { 30 | use Legacy\ConstraintTraitForV9; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CoverageListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | use PHPUnit\Framework\Test; 15 | use PHPUnit\Framework\TestCase; 16 | use PHPUnit\Framework\TestListener; 17 | use PHPUnit\Framework\TestListenerDefaultImplementation; 18 | use PHPUnit\Framework\Warning; 19 | use PHPUnit\Util\Annotation\Registry; 20 | use PHPUnit\Util\Test as TestUtil; 21 | 22 | class CoverageListener implements TestListener 23 | { 24 | use TestListenerDefaultImplementation; 25 | 26 | private $sutFqcnResolver; 27 | private $warningOnSutNotFound; 28 | 29 | public function __construct(?callable $sutFqcnResolver = null, bool $warningOnSutNotFound = false) 30 | { 31 | $this->sutFqcnResolver = $sutFqcnResolver ?? static function (Test $test): ?string { 32 | $class = \get_class($test); 33 | 34 | $sutFqcn = str_replace('\\Tests\\', '\\', $class); 35 | $sutFqcn = preg_replace('{Test$}', '', $sutFqcn); 36 | 37 | return class_exists($sutFqcn) ? $sutFqcn : null; 38 | }; 39 | 40 | $this->warningOnSutNotFound = $warningOnSutNotFound; 41 | } 42 | 43 | public function startTest(Test $test): void 44 | { 45 | if (!$test instanceof TestCase) { 46 | return; 47 | } 48 | 49 | $annotations = TestUtil::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); 50 | 51 | $ignoredAnnotations = ['covers', 'coversDefaultClass', 'coversNothing']; 52 | 53 | foreach ($ignoredAnnotations as $annotation) { 54 | if (isset($annotations['class'][$annotation]) || isset($annotations['method'][$annotation])) { 55 | return; 56 | } 57 | } 58 | 59 | $sutFqcn = ($this->sutFqcnResolver)($test); 60 | if (!$sutFqcn) { 61 | if ($this->warningOnSutNotFound) { 62 | $test->getTestResultObject()->addWarning($test, new Warning('Could not find the tested class.'), 0); 63 | } 64 | 65 | return; 66 | } 67 | 68 | $covers = $sutFqcn; 69 | if (!\is_array($sutFqcn)) { 70 | $covers = [$sutFqcn]; 71 | while ($parent = get_parent_class($sutFqcn)) { 72 | $covers[] = $parent; 73 | $sutFqcn = $parent; 74 | } 75 | } 76 | 77 | if (class_exists(Registry::class)) { 78 | $this->addCoversForDocBlockInsideRegistry($test, $covers); 79 | 80 | return; 81 | } 82 | 83 | $this->addCoversForClassToAnnotationCache($test, $covers); 84 | } 85 | 86 | private function addCoversForClassToAnnotationCache(Test $test, array $covers): void 87 | { 88 | $r = new \ReflectionProperty(TestUtil::class, 'annotationCache'); 89 | $r->setAccessible(true); 90 | 91 | $cache = $r->getValue(); 92 | $cache = array_replace_recursive($cache, [ 93 | \get_class($test) => [ 94 | 'covers' => $covers, 95 | ], 96 | ]); 97 | 98 | $r->setValue(TestUtil::class, $cache); 99 | } 100 | 101 | private function addCoversForDocBlockInsideRegistry(Test $test, array $covers): void 102 | { 103 | $docBlock = Registry::getInstance()->forClassName(\get_class($test)); 104 | 105 | $symbolAnnotations = new \ReflectionProperty($docBlock, 'symbolAnnotations'); 106 | $symbolAnnotations->setAccessible(true); 107 | 108 | // Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException 109 | $covers = array_filter($covers, function (string $class) { 110 | $reflector = new \ReflectionClass($class); 111 | 112 | return $reflector->isUserDefined(); 113 | }); 114 | 115 | $symbolAnnotations->setValue($docBlock, array_replace($docBlock->symbolAnnotations(), [ 116 | 'covers' => $covers, 117 | ])); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /DeprecationErrorHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use PHPUnit\Framework\TestResult; 16 | use PHPUnit\Runner\ErrorHandler; 17 | use PHPUnit\Util\Error\Handler; 18 | use PHPUnit\Util\ErrorHandler as UtilErrorHandler; 19 | use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; 20 | use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; 21 | use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; 22 | use Symfony\Component\ErrorHandler\DebugClassLoader; 23 | 24 | /** 25 | * Catch deprecation notices and print a summary report at the end of the test suite. 26 | * 27 | * @author Nicolas Grekas 28 | */ 29 | class DeprecationErrorHandler 30 | { 31 | public const MODE_DISABLED = 'disabled'; 32 | public const MODE_WEAK = 'max[total]=999999&verbose=0'; 33 | public const MODE_STRICT = 'max[total]=0'; 34 | 35 | private $mode; 36 | private $configuration; 37 | 38 | /** 39 | * @var DeprecationGroup[] 40 | */ 41 | private $deprecationGroups = []; 42 | 43 | private static $isRegistered = false; 44 | private static $errorHandler; 45 | 46 | public function __construct() 47 | { 48 | $this->resetDeprecationGroups(); 49 | } 50 | 51 | /** 52 | * Registers and configures the deprecation handler. 53 | * 54 | * The mode is a query string with options: 55 | * - "disabled" to enable/disable the deprecation handler 56 | * - "verbose" to enable/disable displaying the deprecation report 57 | * - "quiet" to disable displaying the deprecation report only for some groups (i.e. quiet[]=other) 58 | * - "max" to configure the number of deprecations to allow before exiting with a non-zero 59 | * status code; it's an array with keys "total", "self", "direct" and "indirect" 60 | * 61 | * The default mode is "max[total]=0&verbose=1". 62 | * 63 | * The mode can alternatively be "/some-regexp/" to stop the test suite whenever 64 | * a deprecation message matches the given regular expression. 65 | * 66 | * @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations 67 | */ 68 | public static function register($mode = 0) 69 | { 70 | if (self::$isRegistered) { 71 | return; 72 | } 73 | 74 | $handler = new self(); 75 | $oldErrorHandler = set_error_handler([$handler, 'handleError']); 76 | 77 | if (null !== $oldErrorHandler) { 78 | restore_error_handler(); 79 | 80 | if ( 81 | $oldErrorHandler instanceof UtilErrorHandler 82 | || [UtilErrorHandler::class, 'handleError'] === $oldErrorHandler 83 | || $oldErrorHandler instanceof ErrorHandler 84 | || [ErrorHandler::class, 'handleError'] === $oldErrorHandler 85 | ) { 86 | restore_error_handler(); 87 | self::register($mode); 88 | } 89 | } else { 90 | $handler->mode = $mode; 91 | self::$isRegistered = true; 92 | register_shutdown_function([$handler, 'shutdown']); 93 | } 94 | } 95 | 96 | public static function collectDeprecations($outputFile) 97 | { 98 | $deprecations = []; 99 | $previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$previousErrorHandler) { 100 | if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) { 101 | if ($previousErrorHandler) { 102 | return $previousErrorHandler($type, $msg, $file, $line, $context); 103 | } 104 | 105 | return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context); 106 | } 107 | 108 | $filesStack = []; 109 | foreach (debug_backtrace() as $frame) { 110 | if (!isset($frame['file']) || \in_array($frame['function'], ['require', 'require_once', 'include', 'include_once'], true)) { 111 | continue; 112 | } 113 | 114 | $filesStack[] = $frame['file']; 115 | } 116 | 117 | $deprecations[] = [error_reporting() & $type, $msg, $file, $filesStack]; 118 | 119 | return null; 120 | }); 121 | 122 | register_shutdown_function(function () use ($outputFile, &$deprecations) { 123 | file_put_contents($outputFile, serialize($deprecations)); 124 | }); 125 | } 126 | 127 | /** 128 | * @internal 129 | */ 130 | public function handleError($type, $msg, $file, $line, $context = []) 131 | { 132 | if ((\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) || !$this->getConfiguration()->isEnabled()) { 133 | return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context); 134 | } 135 | 136 | $trace = debug_backtrace(); 137 | 138 | if (isset($trace[1]['function'], $trace[1]['args'][0]) && ('trigger_error' === $trace[1]['function'] || 'user_error' === $trace[1]['function'])) { 139 | $msg = $trace[1]['args'][0]; 140 | } 141 | 142 | $deprecation = new Deprecation($msg, $trace, $file, \E_DEPRECATED === $type); 143 | if ($deprecation->isMuted()) { 144 | return null; 145 | } 146 | if ($this->getConfiguration()->isIgnoredDeprecation($deprecation)) { 147 | return null; 148 | } 149 | if ($this->getConfiguration()->isBaselineDeprecation($deprecation)) { 150 | return null; 151 | } 152 | 153 | $msg = $deprecation->getMessage(); 154 | 155 | if (\E_DEPRECATED !== $type && (error_reporting() & $type)) { 156 | $group = 'unsilenced'; 157 | } elseif ($deprecation->isLegacy()) { 158 | $group = 'legacy'; 159 | } else { 160 | $group = [ 161 | Deprecation::TYPE_SELF => 'self', 162 | Deprecation::TYPE_DIRECT => 'direct', 163 | Deprecation::TYPE_INDIRECT => 'indirect', 164 | Deprecation::TYPE_UNDETERMINED => 'other', 165 | ][$deprecation->getType()]; 166 | } 167 | 168 | if ($this->getConfiguration()->shouldDisplayStackTrace($msg)) { 169 | echo "\n".ucfirst($group).' '.$deprecation->toString(); 170 | 171 | exit(1); 172 | } 173 | 174 | if ('legacy' === $group) { 175 | $this->deprecationGroups[$group]->addNotice(); 176 | } elseif ($deprecation->originatesFromAnObject()) { 177 | $class = $deprecation->originatingClass(); 178 | $method = $deprecation->originatingMethod(); 179 | $this->deprecationGroups[$group]->addNoticeFromObject($msg, $class, $method); 180 | } else { 181 | $this->deprecationGroups[$group]->addNoticeFromProceduralCode($msg); 182 | } 183 | 184 | return null; 185 | } 186 | 187 | /** 188 | * @internal 189 | */ 190 | public function shutdown() 191 | { 192 | $configuration = $this->getConfiguration(); 193 | 194 | if ($configuration->isInRegexMode()) { 195 | return; 196 | } 197 | 198 | if (class_exists(DebugClassLoader::class, false)) { 199 | DebugClassLoader::checkClasses(); 200 | } 201 | $currErrorHandler = set_error_handler('is_int'); 202 | restore_error_handler(); 203 | 204 | if ($currErrorHandler !== [$this, 'handleError']) { 205 | echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n"; 206 | } 207 | 208 | $groups = array_keys($this->deprecationGroups); 209 | 210 | // store failing status 211 | $isFailing = !$configuration->tolerates($this->deprecationGroups); 212 | 213 | $this->displayDeprecations($groups, $configuration); 214 | 215 | $this->resetDeprecationGroups(); 216 | 217 | register_shutdown_function(function () use ($isFailing, $groups, $configuration) { 218 | foreach ($this->deprecationGroups as $group) { 219 | if ($group->count() > 0) { 220 | echo "Shutdown-time deprecations:\n"; 221 | break; 222 | } 223 | } 224 | 225 | $isFailingAtShutdown = !$configuration->tolerates($this->deprecationGroups); 226 | $this->displayDeprecations($groups, $configuration); 227 | 228 | if ($configuration->isGeneratingBaseline()) { 229 | $configuration->writeBaseline(); 230 | } 231 | 232 | if ($isFailing || $isFailingAtShutdown) { 233 | exit(1); 234 | } 235 | }); 236 | } 237 | 238 | private function resetDeprecationGroups() 239 | { 240 | $this->deprecationGroups = [ 241 | 'unsilenced' => new DeprecationGroup(), 242 | 'self' => new DeprecationGroup(), 243 | 'direct' => new DeprecationGroup(), 244 | 'indirect' => new DeprecationGroup(), 245 | 'legacy' => new DeprecationGroup(), 246 | 'other' => new DeprecationGroup(), 247 | ]; 248 | } 249 | 250 | private function getConfiguration() 251 | { 252 | if (null !== $this->configuration) { 253 | return $this->configuration; 254 | } 255 | if (false === $mode = $this->mode) { 256 | $mode = $_SERVER['SYMFONY_DEPRECATIONS_HELPER'] ?? $_ENV['SYMFONY_DEPRECATIONS_HELPER'] ?? getenv('SYMFONY_DEPRECATIONS_HELPER'); 257 | } 258 | if ('strict' === $mode) { 259 | return $this->configuration = Configuration::inStrictMode(); 260 | } 261 | if (self::MODE_DISABLED === $mode) { 262 | return $this->configuration = Configuration::inDisabledMode(); 263 | } 264 | if ('weak' === $mode) { 265 | return $this->configuration = Configuration::inWeakMode(); 266 | } 267 | if (isset($mode[0]) && '/' === $mode[0]) { 268 | return $this->configuration = Configuration::fromRegex($mode); 269 | } 270 | 271 | if (preg_match('/^[1-9][0-9]*$/', (string) $mode)) { 272 | return $this->configuration = Configuration::fromNumber($mode); 273 | } 274 | 275 | if (!$mode) { 276 | return $this->configuration = Configuration::fromNumber(0); 277 | } 278 | 279 | return $this->configuration = Configuration::fromUrlEncodedString((string) $mode); 280 | } 281 | 282 | private static function colorize(string $str, bool $red): string 283 | { 284 | if (!self::hasColorSupport()) { 285 | return $str; 286 | } 287 | 288 | $color = $red ? '41;37' : '43;30'; 289 | 290 | return "\x1B[{$color}m{$str}\x1B[0m"; 291 | } 292 | 293 | /** 294 | * @param string[] $groups 295 | */ 296 | private function displayDeprecations(array $groups, Configuration $configuration): void 297 | { 298 | $cmp = function ($a, $b) { 299 | return $b->count() - $a->count(); 300 | }; 301 | 302 | if ($configuration->shouldWriteToLogFile()) { 303 | if (false === $handle = @fopen($file = $configuration->getLogFile(), 'a')) { 304 | throw new \InvalidArgumentException(\sprintf('The configured log file "%s" is not writeable.', $file)); 305 | } 306 | } else { 307 | $handle = fopen('php://output', 'w'); 308 | } 309 | 310 | foreach ($groups as $group) { 311 | if ($this->deprecationGroups[$group]->count()) { 312 | $deprecationGroupMessage = \sprintf( 313 | '%s deprecation notices (%d)', 314 | \in_array($group, ['direct', 'indirect', 'self'], true) ? "Remaining $group" : ucfirst($group), 315 | $this->deprecationGroups[$group]->count() 316 | ); 317 | if ($configuration->shouldWriteToLogFile()) { 318 | fwrite($handle, "\n$deprecationGroupMessage\n"); 319 | } else { 320 | fwrite($handle, "\n".self::colorize($deprecationGroupMessage, 'legacy' !== $group && 'indirect' !== $group)."\n"); 321 | } 322 | 323 | // Skip the verbose output if the group is quiet and not failing according to its threshold: 324 | if ('legacy' !== $group && !$configuration->verboseOutput($group) && $configuration->toleratesForGroup($group, $this->deprecationGroups)) { 325 | continue; 326 | } 327 | $notices = $this->deprecationGroups[$group]->notices(); 328 | uasort($notices, $cmp); 329 | 330 | foreach ($notices as $msg => $notice) { 331 | fwrite($handle, \sprintf("\n %sx: %s\n", $notice->count(), $msg)); 332 | 333 | $countsByCaller = $notice->getCountsByCaller(); 334 | arsort($countsByCaller); 335 | $limit = 5; 336 | 337 | foreach ($countsByCaller as $method => $count) { 338 | if ('count' !== $method) { 339 | if (!$limit--) { 340 | fwrite($handle, " ...\n"); 341 | break; 342 | } 343 | fwrite($handle, \sprintf(" %dx in %s\n", $count, preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method))); 344 | } 345 | } 346 | } 347 | } 348 | } 349 | 350 | if (!empty($notices)) { 351 | fwrite($handle, "\n"); 352 | } 353 | } 354 | 355 | private static function getPhpUnitErrorHandler(): callable 356 | { 357 | if (!$eh = self::$errorHandler) { 358 | if (class_exists(Handler::class)) { 359 | $eh = self::$errorHandler = Handler::class; 360 | } elseif (method_exists(UtilErrorHandler::class, '__invoke')) { 361 | $eh = self::$errorHandler = UtilErrorHandler::class; 362 | } elseif (method_exists(ErrorHandler::class, '__invoke')) { 363 | $eh = self::$errorHandler = ErrorHandler::class; 364 | } else { 365 | return self::$errorHandler = 'PHPUnit\Util\ErrorHandler::handleError'; 366 | } 367 | } 368 | 369 | if ('PHPUnit\Util\ErrorHandler::handleError' === $eh) { 370 | return $eh; 371 | } 372 | 373 | foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { 374 | if (!isset($frame['object'])) { 375 | continue; 376 | } 377 | 378 | if ($frame['object'] instanceof TestResult) { 379 | return new $eh( 380 | $frame['object']->getConvertDeprecationsToExceptions(), 381 | $frame['object']->getConvertErrorsToExceptions(), 382 | $frame['object']->getConvertNoticesToExceptions(), 383 | $frame['object']->getConvertWarningsToExceptions() 384 | ); 385 | } elseif (ErrorHandler::class === $eh && $frame['object'] instanceof TestCase) { 386 | return function (int $errorNumber, string $errorString, string $errorFile, int $errorLine) { 387 | ErrorHandler::instance()($errorNumber, $errorString, $errorFile, $errorLine); 388 | 389 | return true; 390 | }; 391 | } 392 | } 393 | 394 | return function () { return false; }; 395 | } 396 | 397 | /** 398 | * Returns true if STDOUT is defined and supports colorization. 399 | * 400 | * Reference: Composer\XdebugHandler\Process::supportsColor 401 | * https://github.com/composer/xdebug-handler 402 | */ 403 | private static function hasColorSupport(): bool 404 | { 405 | if (!\defined('STDOUT')) { 406 | return false; 407 | } 408 | 409 | // Follow https://no-color.org/ 410 | if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) { 411 | return false; 412 | } 413 | 414 | // Follow https://force-color.org/ 415 | if ('' !== (($_SERVER['FORCE_COLOR'] ?? getenv('FORCE_COLOR'))[0] ?? '')) { 416 | return true; 417 | } 418 | 419 | // Detect msysgit/mingw and assume this is a tty because detection 420 | // does not work correctly, see https://github.com/composer/composer/issues/9690 421 | if (!@stream_isatty(\STDOUT) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { 422 | return false; 423 | } 424 | 425 | if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support(\STDOUT)) { 426 | return true; 427 | } 428 | 429 | if ('Hyper' === getenv('TERM_PROGRAM') 430 | || false !== getenv('COLORTERM') 431 | || false !== getenv('ANSICON') 432 | || 'ON' === getenv('ConEmuANSI') 433 | ) { 434 | return true; 435 | } 436 | 437 | if ('dumb' === $term = (string) getenv('TERM')) { 438 | return false; 439 | } 440 | 441 | // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 442 | return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /DeprecationErrorHandler/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; 13 | 14 | /** 15 | * @internal 16 | */ 17 | class Configuration 18 | { 19 | /** 20 | * @var int[] 21 | */ 22 | private $thresholds; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $regex; 28 | 29 | /** 30 | * @var bool 31 | */ 32 | private $enabled = true; 33 | 34 | /** 35 | * @var bool[] 36 | */ 37 | private $verboseOutput; 38 | 39 | /** 40 | * @var string[] 41 | */ 42 | private $ignoreDeprecationPatterns = []; 43 | 44 | /** 45 | * @var bool 46 | */ 47 | private $generateBaseline = false; 48 | 49 | /** 50 | * @var string 51 | */ 52 | private $baselineFile = ''; 53 | 54 | /** 55 | * @var array 56 | */ 57 | private $baselineDeprecations = []; 58 | 59 | /** 60 | * @var string|null 61 | */ 62 | private $logFile; 63 | 64 | /** 65 | * @param int[] $thresholds A hash associating groups to thresholds 66 | * @param string $regex Will be matched against messages, to decide whether to display a stack trace 67 | * @param bool[] $verboseOutput Keyed by groups 68 | * @param string $ignoreFile The path to the ignore deprecation patterns file 69 | * @param bool $generateBaseline Whether to generate or update the baseline file 70 | * @param string $baselineFile The path to the baseline file 71 | * @param string|null $logFile The path to the log file 72 | */ 73 | private function __construct(array $thresholds = [], string $regex = '', array $verboseOutput = [], string $ignoreFile = '', bool $generateBaseline = false, string $baselineFile = '', ?string $logFile = null) 74 | { 75 | $groups = ['total', 'indirect', 'direct', 'self']; 76 | 77 | foreach ($thresholds as $group => $threshold) { 78 | if (!\in_array($group, $groups, true)) { 79 | throw new \InvalidArgumentException(\sprintf('Unrecognized threshold "%s", expected one of "%s".', $group, implode('", "', $groups))); 80 | } 81 | if (!is_numeric($threshold)) { 82 | throw new \InvalidArgumentException(\sprintf('Threshold for group "%s" has invalid value "%s".', $group, $threshold)); 83 | } 84 | $this->thresholds[$group] = (int) $threshold; 85 | } 86 | if (isset($this->thresholds['direct'])) { 87 | $this->thresholds += [ 88 | 'self' => $this->thresholds['direct'], 89 | ]; 90 | } 91 | if (isset($this->thresholds['indirect'])) { 92 | $this->thresholds += [ 93 | 'direct' => $this->thresholds['indirect'], 94 | 'self' => $this->thresholds['indirect'], 95 | ]; 96 | } 97 | foreach ($groups as $group) { 98 | if (!isset($this->thresholds[$group])) { 99 | $this->thresholds[$group] = $this->thresholds['total'] ?? 999999; 100 | } 101 | } 102 | $this->regex = $regex; 103 | 104 | $this->verboseOutput = [ 105 | 'unsilenced' => true, 106 | 'direct' => true, 107 | 'indirect' => true, 108 | 'self' => true, 109 | 'other' => true, 110 | ]; 111 | 112 | foreach ($verboseOutput as $group => $status) { 113 | if (!isset($this->verboseOutput[$group])) { 114 | throw new \InvalidArgumentException(\sprintf('Unsupported verbosity group "%s", expected one of "%s".', $group, implode('", "', array_keys($this->verboseOutput)))); 115 | } 116 | $this->verboseOutput[$group] = $status; 117 | } 118 | 119 | if ($ignoreFile) { 120 | if (!is_file($ignoreFile)) { 121 | throw new \InvalidArgumentException(\sprintf('The ignoreFile "%s" does not exist.', $ignoreFile)); 122 | } 123 | set_error_handler(static function ($t, $m) use ($ignoreFile, &$line) { 124 | throw new \RuntimeException(\sprintf('Invalid pattern found in "%s" on line "%d"', $ignoreFile, 1 + $line).substr($m, 12)); 125 | }); 126 | try { 127 | foreach (file($ignoreFile) as $line => $pattern) { 128 | if ('#' !== (trim($pattern)[0] ?? '#')) { 129 | preg_match($pattern, ''); 130 | $this->ignoreDeprecationPatterns[] = $pattern; 131 | } 132 | } 133 | } finally { 134 | restore_error_handler(); 135 | } 136 | } 137 | 138 | if ($generateBaseline && !$baselineFile) { 139 | throw new \InvalidArgumentException('You cannot use the "generateBaseline" configuration option without providing a "baselineFile" configuration option.'); 140 | } 141 | $this->generateBaseline = $generateBaseline; 142 | $this->baselineFile = $baselineFile; 143 | if ($this->baselineFile && !$this->generateBaseline) { 144 | if (is_file($this->baselineFile)) { 145 | $map = json_decode(file_get_contents($this->baselineFile)); 146 | foreach ($map as $baseline_deprecation) { 147 | $this->baselineDeprecations[$baseline_deprecation->location][$baseline_deprecation->message] = $baseline_deprecation->count; 148 | } 149 | } else { 150 | throw new \InvalidArgumentException(\sprintf('The baselineFile "%s" does not exist.', $this->baselineFile)); 151 | } 152 | } 153 | 154 | $this->logFile = $logFile; 155 | } 156 | 157 | public function isEnabled(): bool 158 | { 159 | return $this->enabled; 160 | } 161 | 162 | /** 163 | * @param DeprecationGroup[] $deprecationGroups 164 | */ 165 | public function tolerates(array $deprecationGroups): bool 166 | { 167 | $grandTotal = 0; 168 | 169 | foreach ($deprecationGroups as $name => $group) { 170 | if ('legacy' !== $name) { 171 | $grandTotal += $group->count(); 172 | } 173 | } 174 | 175 | if ($grandTotal > $this->thresholds['total']) { 176 | return false; 177 | } 178 | 179 | foreach (['self', 'direct', 'indirect'] as $deprecationType) { 180 | if ($deprecationGroups[$deprecationType]->count() > $this->thresholds[$deprecationType]) { 181 | return false; 182 | } 183 | } 184 | 185 | return true; 186 | } 187 | 188 | public function isIgnoredDeprecation(Deprecation $deprecation): bool 189 | { 190 | if (!$this->ignoreDeprecationPatterns) { 191 | return false; 192 | } 193 | $result = @preg_filter($this->ignoreDeprecationPatterns, '$0', $deprecation->getMessage()); 194 | if (\PREG_NO_ERROR !== preg_last_error()) { 195 | throw new \RuntimeException(preg_last_error_msg()); 196 | } 197 | 198 | return (bool) $result; 199 | } 200 | 201 | /** 202 | * @param array $deprecationGroups 203 | * 204 | * @return bool true if the threshold is not reached for the deprecation type nor for the total 205 | */ 206 | public function toleratesForGroup(string $groupName, array $deprecationGroups): bool 207 | { 208 | $grandTotal = 0; 209 | 210 | foreach ($deprecationGroups as $type => $group) { 211 | if ('legacy' !== $type) { 212 | $grandTotal += $group->count(); 213 | } 214 | } 215 | 216 | if ($grandTotal > $this->thresholds['total']) { 217 | return false; 218 | } 219 | 220 | if (\in_array($groupName, ['self', 'direct', 'indirect'], true) && $deprecationGroups[$groupName]->count() > $this->thresholds[$groupName]) { 221 | return false; 222 | } 223 | 224 | return true; 225 | } 226 | 227 | public function isBaselineDeprecation(Deprecation $deprecation): bool 228 | { 229 | if ($deprecation->isLegacy()) { 230 | return false; 231 | } 232 | 233 | if ($deprecation->originatesFromDebugClassLoader()) { 234 | $location = $deprecation->triggeringClass(); 235 | } elseif ($deprecation->originatesFromAnObject()) { 236 | $location = $deprecation->originatingClass().'::'.$deprecation->originatingMethod(); 237 | } else { 238 | $location = 'procedural code'; 239 | } 240 | 241 | $message = $deprecation->getMessage(); 242 | $result = isset($this->baselineDeprecations[$location][$message]) && $this->baselineDeprecations[$location][$message] > 0; 243 | if ($this->generateBaseline) { 244 | if ($result) { 245 | ++$this->baselineDeprecations[$location][$message]; 246 | } else { 247 | $this->baselineDeprecations[$location][$message] = 1; 248 | $result = true; 249 | } 250 | } elseif ($result) { 251 | --$this->baselineDeprecations[$location][$message]; 252 | } 253 | 254 | return $result; 255 | } 256 | 257 | public function isGeneratingBaseline(): bool 258 | { 259 | return $this->generateBaseline; 260 | } 261 | 262 | public function getBaselineFile(): string 263 | { 264 | return $this->baselineFile; 265 | } 266 | 267 | public function writeBaseline(): void 268 | { 269 | $map = []; 270 | foreach ($this->baselineDeprecations as $location => $messages) { 271 | foreach ($messages as $message => $count) { 272 | $map[] = [ 273 | 'location' => $location, 274 | 'message' => $message, 275 | 'count' => $count, 276 | ]; 277 | } 278 | } 279 | file_put_contents($this->baselineFile, json_encode($map, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); 280 | } 281 | 282 | public function shouldDisplayStackTrace(string $message): bool 283 | { 284 | return '' !== $this->regex && preg_match($this->regex, $message); 285 | } 286 | 287 | public function isInRegexMode(): bool 288 | { 289 | return '' !== $this->regex; 290 | } 291 | 292 | public function verboseOutput($group): bool 293 | { 294 | return $this->verboseOutput[$group]; 295 | } 296 | 297 | public function shouldWriteToLogFile(): bool 298 | { 299 | return null !== $this->logFile; 300 | } 301 | 302 | public function getLogFile(): ?string 303 | { 304 | return $this->logFile; 305 | } 306 | 307 | /** 308 | * @param string $serializedConfiguration An encoded string, for instance max[total]=1234&max[indirect]=42 309 | */ 310 | public static function fromUrlEncodedString(string $serializedConfiguration): self 311 | { 312 | parse_str($serializedConfiguration, $normalizedConfiguration); 313 | foreach (array_keys($normalizedConfiguration) as $key) { 314 | if (!\in_array($key, ['max', 'disabled', 'verbose', 'quiet', 'ignoreFile', 'generateBaseline', 'baselineFile', 'logFile'], true)) { 315 | throw new \InvalidArgumentException(\sprintf('Unknown configuration option "%s".', $key)); 316 | } 317 | } 318 | 319 | $normalizedConfiguration += [ 320 | 'max' => ['total' => 0], 321 | 'disabled' => false, 322 | 'verbose' => true, 323 | 'quiet' => [], 324 | 'ignoreFile' => '', 325 | 'generateBaseline' => false, 326 | 'baselineFile' => '', 327 | 'logFile' => null, 328 | ]; 329 | 330 | if ('' === $normalizedConfiguration['disabled'] || filter_var($normalizedConfiguration['disabled'], \FILTER_VALIDATE_BOOLEAN)) { 331 | return self::inDisabledMode(); 332 | } 333 | 334 | $verboseOutput = []; 335 | foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { 336 | $verboseOutput[$group] = filter_var($normalizedConfiguration['verbose'], \FILTER_VALIDATE_BOOLEAN); 337 | } 338 | 339 | if (\is_array($normalizedConfiguration['quiet'])) { 340 | foreach ($normalizedConfiguration['quiet'] as $shushedGroup) { 341 | $verboseOutput[$shushedGroup] = false; 342 | } 343 | } 344 | 345 | return new self( 346 | $normalizedConfiguration['max'], 347 | '', 348 | $verboseOutput, 349 | $normalizedConfiguration['ignoreFile'], 350 | filter_var($normalizedConfiguration['generateBaseline'], \FILTER_VALIDATE_BOOLEAN), 351 | $normalizedConfiguration['baselineFile'], 352 | $normalizedConfiguration['logFile'] 353 | ); 354 | } 355 | 356 | public static function inDisabledMode(): self 357 | { 358 | $configuration = new self(); 359 | $configuration->enabled = false; 360 | 361 | return $configuration; 362 | } 363 | 364 | public static function inStrictMode(): self 365 | { 366 | return new self(['total' => 0]); 367 | } 368 | 369 | public static function inWeakMode(): self 370 | { 371 | $verboseOutput = []; 372 | foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { 373 | $verboseOutput[$group] = false; 374 | } 375 | 376 | return new self([], '', $verboseOutput); 377 | } 378 | 379 | public static function fromNumber($upperBound): self 380 | { 381 | return new self(['total' => $upperBound]); 382 | } 383 | 384 | public static function fromRegex($regex): self 385 | { 386 | return new self([], $regex); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /DeprecationErrorHandler/Deprecation.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; 13 | 14 | use Doctrine\Deprecations\Deprecation as DoctrineDeprecation; 15 | use PHPUnit\Framework\TestCase; 16 | use PHPUnit\Framework\TestSuite; 17 | use PHPUnit\Metadata\Api\Groups; 18 | use PHPUnit\Util\Test; 19 | use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor; 20 | use Symfony\Component\ErrorHandler\DebugClassLoader; 21 | 22 | class_exists(Groups::class); 23 | 24 | /** 25 | * @internal 26 | */ 27 | class Deprecation 28 | { 29 | public const PATH_TYPE_VENDOR = 'path_type_vendor'; 30 | public const PATH_TYPE_SELF = 'path_type_internal'; 31 | public const PATH_TYPE_UNDETERMINED = 'path_type_undetermined'; 32 | 33 | public const TYPE_SELF = 'type_self'; 34 | public const TYPE_DIRECT = 'type_direct'; 35 | public const TYPE_INDIRECT = 'type_indirect'; 36 | public const TYPE_UNDETERMINED = 'type_undetermined'; 37 | 38 | private $trace = []; 39 | private $message; 40 | private $languageDeprecation; 41 | private $originClass; 42 | private $originMethod; 43 | private $triggeringFile; 44 | private $triggeringClass; 45 | 46 | /** @var string[] Absolute paths to vendor directories */ 47 | private static $vendors; 48 | 49 | /** 50 | * @var string[] Absolute paths to source or tests of the project, cache 51 | * directories excluded because it is based on autoloading 52 | * rules and cache systems typically do not use those 53 | */ 54 | private static $internalPaths = []; 55 | 56 | private $originalFilesStack; 57 | 58 | public function __construct(string $message, array $trace, string $file, bool $languageDeprecation = false) 59 | { 60 | if (DebugClassLoader::class === ($trace[2]['class'] ?? '')) { 61 | $this->triggeringClass = $trace[2]['args'][0]; 62 | } 63 | 64 | switch ($trace[2]['function'] ?? '') { 65 | case 'trigger_deprecation': 66 | $file = $trace[2]['file']; 67 | array_splice($trace, 1, 1); 68 | break; 69 | 70 | case 'delegateTriggerToBackend': 71 | if (DoctrineDeprecation::class === ($trace[2]['class'] ?? '')) { 72 | $file = $trace[3]['file']; 73 | array_splice($trace, 1, 2); 74 | } 75 | break; 76 | } 77 | 78 | $this->trace = $trace; 79 | $this->message = $message; 80 | $this->languageDeprecation = $languageDeprecation; 81 | 82 | $i = \count($trace); 83 | while (1 < $i && $this->lineShouldBeSkipped($trace[--$i])) { 84 | // No-op 85 | } 86 | 87 | $line = $trace[$i]; 88 | $this->triggeringFile = $file; 89 | 90 | for ($j = 1; $j < $i; ++$j) { 91 | if (!isset($trace[$j]['function'], $trace[1 + $j]['class'], $trace[1 + $j]['args'][0])) { 92 | continue; 93 | } 94 | 95 | if ('trigger_error' === $trace[$j]['function'] && !isset($trace[$j]['class'])) { 96 | if (DebugClassLoader::class === $trace[1 + $j]['class']) { 97 | $class = $trace[1 + $j]['args'][0]; 98 | $this->triggeringFile = isset($trace[1 + $j]['args'][1]) ? realpath($trace[1 + $j]['args'][1]) : (new \ReflectionClass($class))->getFileName(); 99 | $this->getOriginalFilesStack(); 100 | array_splice($this->originalFilesStack, 0, $j, [$this->triggeringFile]); 101 | 102 | if (preg_match('/(?|"([^"]++)" that is deprecated|should implement method "(?:static )?([^:]++))/', $message, $m) || (false === strpos($message, '()" will return') && false === strpos($message, 'native return type declaration') && preg_match('/^(?:The|Method) "([^":]++)/', $message, $m))) { 103 | $this->triggeringFile = (new \ReflectionClass($m[1]))->getFileName(); 104 | array_unshift($this->originalFilesStack, $this->triggeringFile); 105 | } 106 | } 107 | 108 | break; 109 | } 110 | } 111 | 112 | if (!isset($line['object']) && !isset($line['class'])) { 113 | return; 114 | } 115 | 116 | set_error_handler(function () {}); 117 | try { 118 | $parsedMsg = unserialize($this->message); 119 | } finally { 120 | restore_error_handler(); 121 | } 122 | if ($parsedMsg && isset($parsedMsg['deprecation'])) { 123 | $this->message = $parsedMsg['deprecation']; 124 | $this->originClass = $parsedMsg['class']; 125 | $this->originMethod = $parsedMsg['method']; 126 | if (isset($parsedMsg['files_stack'])) { 127 | $this->originalFilesStack = $parsedMsg['files_stack']; 128 | } 129 | // If the deprecation has been triggered via 130 | // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() 131 | // then we need to use the serialized information to determine 132 | // if the error has been triggered from vendor code. 133 | if (isset($parsedMsg['triggering_file'])) { 134 | $this->triggeringFile = $parsedMsg['triggering_file']; 135 | } 136 | 137 | return; 138 | } 139 | 140 | if (!isset($line['class'], $trace[$i - 2]['function']) || 0 !== strpos($line['class'], SymfonyTestsListenerFor::class)) { 141 | $this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class']; 142 | $this->originMethod = $line['function']; 143 | 144 | return; 145 | } 146 | 147 | $test = $line['args'][0] ?? null; 148 | 149 | if (($test instanceof TestCase || $test instanceof TestSuite) && ('trigger_error' !== $trace[$i - 2]['function'] || isset($trace[$i - 2]['class']))) { 150 | $this->originClass = \get_class($test); 151 | $this->originMethod = $test->getName(); 152 | } 153 | } 154 | 155 | private function lineShouldBeSkipped(array $line): bool 156 | { 157 | if (!isset($line['class'])) { 158 | return true; 159 | } 160 | $class = $line['class']; 161 | 162 | return 'ReflectionMethod' === $class || 0 === strpos($class, 'PHPUnit\\'); 163 | } 164 | 165 | public function originatesFromDebugClassLoader(): bool 166 | { 167 | return isset($this->triggeringClass); 168 | } 169 | 170 | public function triggeringClass(): string 171 | { 172 | if (null === $this->triggeringClass) { 173 | throw new \LogicException('Check with originatesFromDebugClassLoader() before calling this method.'); 174 | } 175 | 176 | return $this->triggeringClass; 177 | } 178 | 179 | public function originatesFromAnObject(): bool 180 | { 181 | return isset($this->originClass); 182 | } 183 | 184 | public function originatingClass(): string 185 | { 186 | if (null === $this->originClass) { 187 | throw new \LogicException('Check with originatesFromAnObject() before calling this method.'); 188 | } 189 | 190 | $class = $this->originClass; 191 | 192 | return false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; 193 | } 194 | 195 | public function originatingMethod(): string 196 | { 197 | if (null === $this->originMethod) { 198 | throw new \LogicException('Check with originatesFromAnObject() before calling this method.'); 199 | } 200 | 201 | return $this->originMethod; 202 | } 203 | 204 | public function getMessage(): string 205 | { 206 | return $this->message; 207 | } 208 | 209 | public function isLegacy(): bool 210 | { 211 | if (!$this->originClass || (new \ReflectionClass($this->originClass))->isInternal()) { 212 | return false; 213 | } 214 | 215 | $method = $this->originatingMethod(); 216 | $groups = class_exists(Groups::class, false) ? [new Groups(), 'groups'] : [Test::class, 'getGroups']; 217 | 218 | return 0 === strpos($method, 'testLegacy') 219 | || 0 === strpos($method, 'provideLegacy') 220 | || 0 === strpos($method, 'getLegacy') 221 | || strpos($this->originClass, '\Legacy') 222 | || \in_array('legacy', $groups($this->originClass, $method), true); 223 | } 224 | 225 | public function isMuted(): bool 226 | { 227 | if ('Function ReflectionType::__toString() is deprecated' !== $this->message) { 228 | return false; 229 | } 230 | if (isset($this->trace[1]['class'])) { 231 | return 0 === strpos($this->trace[1]['class'], 'PHPUnit\\'); 232 | } 233 | 234 | return false !== strpos($this->triggeringFile, \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR.'phpunit'.\DIRECTORY_SEPARATOR); 235 | } 236 | 237 | /** 238 | * Tells whether both the calling package and the called package are vendor 239 | * packages. 240 | */ 241 | public function getType(): string 242 | { 243 | $pathType = $this->getPathType($this->triggeringFile); 244 | if ($this->languageDeprecation && self::PATH_TYPE_VENDOR === $pathType) { 245 | // the triggering file must be used for language deprecations 246 | return self::TYPE_INDIRECT; 247 | } 248 | if (self::PATH_TYPE_SELF === $pathType) { 249 | return self::TYPE_SELF; 250 | } 251 | if (self::PATH_TYPE_UNDETERMINED === $pathType) { 252 | return self::TYPE_UNDETERMINED; 253 | } 254 | $erroringFile = $erroringPackage = null; 255 | 256 | foreach ($this->getOriginalFilesStack() as $file) { 257 | if ('-' === $file || 'Standard input code' === $file || !realpath($file)) { 258 | continue; 259 | } 260 | if (self::PATH_TYPE_SELF === $pathType = $this->getPathType($file)) { 261 | return self::TYPE_DIRECT; 262 | } 263 | if (self::PATH_TYPE_UNDETERMINED === $pathType) { 264 | return self::TYPE_UNDETERMINED; 265 | } 266 | if (null !== $erroringFile && null !== $erroringPackage) { 267 | $package = $this->getPackage($file); 268 | if ('composer' !== $package && $package !== $erroringPackage) { 269 | return self::TYPE_INDIRECT; 270 | } 271 | continue; 272 | } 273 | $erroringFile = $file; 274 | $erroringPackage = $this->getPackage($file); 275 | } 276 | 277 | return self::TYPE_DIRECT; 278 | } 279 | 280 | private function getOriginalFilesStack() 281 | { 282 | if (null === $this->originalFilesStack) { 283 | $this->originalFilesStack = []; 284 | foreach ($this->trace as $frame) { 285 | if (!isset($frame['file'], $frame['function']) || (!isset($frame['class']) && \in_array($frame['function'], ['require', 'require_once', 'include', 'include_once'], true))) { 286 | continue; 287 | } 288 | 289 | $this->originalFilesStack[] = $frame['file']; 290 | } 291 | } 292 | 293 | return $this->originalFilesStack; 294 | } 295 | 296 | /** 297 | * getPathType() should always be called prior to calling this method. 298 | */ 299 | private function getPackage(string $path): string 300 | { 301 | $path = realpath($path) ?: $path; 302 | foreach (self::getVendors() as $vendorRoot) { 303 | if (0 === strpos($path, $vendorRoot)) { 304 | $relativePath = substr($path, \strlen($vendorRoot) + 1); 305 | $vendor = strstr($relativePath, \DIRECTORY_SEPARATOR, true); 306 | if (false === $vendor) { 307 | return 'symfony'; 308 | } 309 | 310 | return rtrim($vendor.'/'.strstr(substr($relativePath, \strlen($vendor) + 1), \DIRECTORY_SEPARATOR, true), '/'); 311 | } 312 | } 313 | 314 | throw new \RuntimeException(\sprintf('No vendors found for path "%s".', $path)); 315 | } 316 | 317 | /** 318 | * @return string[] 319 | */ 320 | private static function getVendors(): array 321 | { 322 | if (null === self::$vendors) { 323 | self::$vendors = $paths = []; 324 | self::$vendors[] = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Legacy'; 325 | if (class_exists(DebugClassLoader::class, false)) { 326 | self::$vendors[] = \dirname((new \ReflectionClass(DebugClassLoader::class))->getFileName()); 327 | } 328 | foreach (get_declared_classes() as $class) { 329 | if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { 330 | $r = new \ReflectionClass($class); 331 | $v = \dirname($r->getFileName(), 2); 332 | if (file_exists($v.'/composer/installed.json')) { 333 | self::$vendors[] = $v; 334 | $loader = require $v.'/autoload.php'; 335 | $paths = self::addSourcePathsFromPrefixes( 336 | array_merge($loader->getPrefixes(), $loader->getPrefixesPsr4()), 337 | $paths 338 | ); 339 | } 340 | } 341 | } 342 | foreach ($paths as $path) { 343 | foreach (self::$vendors as $vendor) { 344 | if (0 !== strpos($path, $vendor)) { 345 | self::$internalPaths[] = $path; 346 | } 347 | } 348 | } 349 | } 350 | 351 | return self::$vendors; 352 | } 353 | 354 | private static function addSourcePathsFromPrefixes(array $prefixesByNamespace, array $paths): array 355 | { 356 | foreach ($prefixesByNamespace as $prefixes) { 357 | foreach ($prefixes as $prefix) { 358 | if (false !== realpath($prefix)) { 359 | $paths[] = realpath($prefix); 360 | } 361 | } 362 | } 363 | 364 | return $paths; 365 | } 366 | 367 | private function getPathType(string $path): string 368 | { 369 | $realPath = realpath($path); 370 | if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) { 371 | return self::PATH_TYPE_UNDETERMINED; 372 | } 373 | foreach (self::getVendors() as $vendor) { 374 | if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) { 375 | return self::PATH_TYPE_VENDOR; 376 | } 377 | } 378 | 379 | foreach (self::$internalPaths as $internalPath) { 380 | if (0 === strpos($realPath, $internalPath)) { 381 | return self::PATH_TYPE_SELF; 382 | } 383 | } 384 | 385 | return self::PATH_TYPE_UNDETERMINED; 386 | } 387 | 388 | public function toString(): string 389 | { 390 | $exception = new \Exception($this->message); 391 | $reflection = new \ReflectionProperty($exception, 'trace'); 392 | $reflection->setAccessible(true); 393 | $reflection->setValue($exception, $this->trace); 394 | 395 | return ($this->originatesFromAnObject() ? 'deprecation triggered by '.$this->originatingClass().'::'.$this->originatingMethod().":\n" : '') 396 | .$this->message."\n" 397 | ."Stack trace:\n" 398 | .str_replace(' '.getcwd().\DIRECTORY_SEPARATOR, ' ', $exception->getTraceAsString())."\n"; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /DeprecationErrorHandler/DeprecationGroup.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; 13 | 14 | /** 15 | * @internal 16 | */ 17 | final class DeprecationGroup 18 | { 19 | private $count = 0; 20 | 21 | /** 22 | * @var DeprecationNotice[] keys are messages 23 | */ 24 | private $deprecationNotices = []; 25 | 26 | public function addNoticeFromObject(string $message, string $class, string $method): void 27 | { 28 | $this->deprecationNotice($message)->addObjectOccurrence($class, $method); 29 | $this->addNotice(); 30 | } 31 | 32 | public function addNoticeFromProceduralCode(string $message): void 33 | { 34 | $this->deprecationNotice($message)->addProceduralOccurrence(); 35 | $this->addNotice(); 36 | } 37 | 38 | public function addNotice() 39 | { 40 | ++$this->count; 41 | } 42 | 43 | private function deprecationNotice(string $message): DeprecationNotice 44 | { 45 | return $this->deprecationNotices[$message] ?? $this->deprecationNotices[$message] = new DeprecationNotice(); 46 | } 47 | 48 | public function count(): int 49 | { 50 | return $this->count; 51 | } 52 | 53 | public function notices(): array 54 | { 55 | return $this->deprecationNotices; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DeprecationErrorHandler/DeprecationNotice.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; 13 | 14 | /** 15 | * @internal 16 | */ 17 | final class DeprecationNotice 18 | { 19 | private $count = 0; 20 | 21 | /** 22 | * @var int[] 23 | */ 24 | private $countsByCaller = []; 25 | 26 | public function addObjectOccurrence($class, $method) 27 | { 28 | if (!isset($this->countsByCaller["$class::$method"])) { 29 | $this->countsByCaller["$class::$method"] = 0; 30 | } 31 | ++$this->countsByCaller["$class::$method"]; 32 | ++$this->count; 33 | } 34 | 35 | public function addProceduralOccurrence() 36 | { 37 | ++$this->count; 38 | } 39 | 40 | public function getCountsByCaller(): array 41 | { 42 | return $this->countsByCaller; 43 | } 44 | 45 | public function count(): int 46 | { 47 | return $this->count; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DnsMock.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | /** 15 | * @author Nicolas Grekas 16 | */ 17 | class DnsMock 18 | { 19 | private static $hosts = []; 20 | private static $dnsTypes = [ 21 | 'A' => \DNS_A, 22 | 'MX' => \DNS_MX, 23 | 'NS' => \DNS_NS, 24 | 'SOA' => \DNS_SOA, 25 | 'PTR' => \DNS_PTR, 26 | 'CNAME' => \DNS_CNAME, 27 | 'AAAA' => \DNS_AAAA, 28 | 'A6' => \DNS_A6, 29 | 'SRV' => \DNS_SRV, 30 | 'NAPTR' => \DNS_NAPTR, 31 | 'TXT' => \DNS_TXT, 32 | 'HINFO' => \DNS_HINFO, 33 | ]; 34 | 35 | /** 36 | * Configures the mock values for DNS queries. 37 | * 38 | * @param array $hosts Mocked hosts as keys, arrays of DNS records as returned by dns_get_record() as values 39 | */ 40 | public static function withMockedHosts(array $hosts): void 41 | { 42 | self::$hosts = $hosts; 43 | } 44 | 45 | public static function checkdnsrr($hostname, $type = 'MX'): bool 46 | { 47 | if (!self::$hosts) { 48 | return \checkdnsrr($hostname, $type); 49 | } 50 | if (isset(self::$hosts[$hostname])) { 51 | $type = strtoupper($type); 52 | 53 | foreach (self::$hosts[$hostname] as $record) { 54 | if ($record['type'] === $type) { 55 | return true; 56 | } 57 | if ('ANY' === $type && isset(self::$dnsTypes[$record['type']]) && 'HINFO' !== $record['type']) { 58 | return true; 59 | } 60 | } 61 | } 62 | 63 | return false; 64 | } 65 | 66 | public static function getmxrr($hostname, &$mxhosts, &$weight = null): bool 67 | { 68 | if (!self::$hosts) { 69 | return \getmxrr($hostname, $mxhosts, $weight); 70 | } 71 | $mxhosts = $weight = []; 72 | 73 | if (isset(self::$hosts[$hostname])) { 74 | foreach (self::$hosts[$hostname] as $record) { 75 | if ('MX' === $record['type']) { 76 | $mxhosts[] = $record['host']; 77 | $weight[] = $record['pri']; 78 | } 79 | } 80 | } 81 | 82 | return (bool) $mxhosts; 83 | } 84 | 85 | public static function gethostbyaddr($ipAddress) 86 | { 87 | if (!self::$hosts) { 88 | return \gethostbyaddr($ipAddress); 89 | } 90 | foreach (self::$hosts as $hostname => $records) { 91 | foreach ($records as $record) { 92 | if ('A' === $record['type'] && $ipAddress === $record['ip']) { 93 | return $hostname; 94 | } 95 | if ('AAAA' === $record['type'] && $ipAddress === $record['ipv6']) { 96 | return $hostname; 97 | } 98 | } 99 | } 100 | 101 | return $ipAddress; 102 | } 103 | 104 | public static function gethostbyname($hostname) 105 | { 106 | if (!self::$hosts) { 107 | return \gethostbyname($hostname); 108 | } 109 | if (isset(self::$hosts[$hostname])) { 110 | foreach (self::$hosts[$hostname] as $record) { 111 | if ('A' === $record['type']) { 112 | return $record['ip']; 113 | } 114 | } 115 | } 116 | 117 | return $hostname; 118 | } 119 | 120 | public static function gethostbynamel($hostname) 121 | { 122 | if (!self::$hosts) { 123 | return \gethostbynamel($hostname); 124 | } 125 | $ips = false; 126 | 127 | if (isset(self::$hosts[$hostname])) { 128 | $ips = []; 129 | 130 | foreach (self::$hosts[$hostname] as $record) { 131 | if ('A' === $record['type']) { 132 | $ips[] = $record['ip']; 133 | } 134 | } 135 | } 136 | 137 | return $ips; 138 | } 139 | 140 | public static function dns_get_record($hostname, $type = \DNS_ANY, &$authns = null, &$addtl = null, $raw = false) 141 | { 142 | if (!self::$hosts) { 143 | return \dns_get_record($hostname, $type, $authns, $addtl, $raw); 144 | } 145 | 146 | $records = false; 147 | 148 | if (isset(self::$hosts[$hostname])) { 149 | if (\DNS_ANY === $type) { 150 | $type = \DNS_ALL; 151 | } 152 | $records = []; 153 | 154 | foreach (self::$hosts[$hostname] as $record) { 155 | if ((self::$dnsTypes[$record['type']] ?? 0) & $type) { 156 | $records[] = array_merge(['host' => $hostname, 'class' => 'IN', 'ttl' => 1, 'type' => $record['type']], $record); 157 | } 158 | } 159 | } 160 | 161 | return $records; 162 | } 163 | 164 | public static function register($class): void 165 | { 166 | $self = static::class; 167 | 168 | $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; 169 | if (0 < strpos($class, '\\Tests\\')) { 170 | $ns = str_replace('\\Tests\\', '\\', $class); 171 | $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); 172 | } elseif (0 === strpos($class, 'Tests\\')) { 173 | $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); 174 | } 175 | foreach ($mockedNs as $ns) { 176 | if (\function_exists($ns.'\checkdnsrr')) { 177 | continue; 178 | } 179 | eval(<< 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | use Symfony\Bridge\PhpUnit\Legacy\ExpectDeprecationTraitBeforeV8_4; 15 | use Symfony\Bridge\PhpUnit\Legacy\ExpectDeprecationTraitForV8_4; 16 | 17 | if (version_compare(\PHPUnit\Runner\Version::id(), '8.4.0', '<')) { 18 | trait ExpectDeprecationTrait 19 | { 20 | use ExpectDeprecationTraitBeforeV8_4; 21 | } 22 | } else { 23 | /** 24 | * @method void expectDeprecation(string $message) 25 | */ 26 | trait ExpectDeprecationTrait 27 | { 28 | use ExpectDeprecationTraitForV8_4; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ExpectUserDeprecationMessageTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | use PHPUnit\Runner\Version; 15 | 16 | if (version_compare(Version::id(), '11.0.0', '<')) { 17 | trait ExpectUserDeprecationMessageTrait 18 | { 19 | use ExpectDeprecationTrait; 20 | 21 | final protected function expectUserDeprecationMessage(string $expectedUserDeprecationMessage): void 22 | { 23 | $this->expectDeprecation(str_replace('%', '%%', $expectedUserDeprecationMessage)); 24 | } 25 | } 26 | } else { 27 | trait ExpectUserDeprecationMessageTrait 28 | { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Extension/EnableClockMockSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Extension; 13 | 14 | use PHPUnit\Event\Code\TestMethod; 15 | use PHPUnit\Event\Test\PreparationStarted; 16 | use PHPUnit\Event\Test\PreparationStartedSubscriber; 17 | use PHPUnit\Metadata\Group; 18 | use Symfony\Bridge\PhpUnit\ClockMock; 19 | 20 | /** 21 | * @internal 22 | */ 23 | class EnableClockMockSubscriber implements PreparationStartedSubscriber 24 | { 25 | public function notify(PreparationStarted $event): void 26 | { 27 | $test = $event->test(); 28 | 29 | if (!$test instanceof TestMethod) { 30 | return; 31 | } 32 | 33 | foreach ($test->metadata() as $metadata) { 34 | if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) { 35 | ClockMock::withClockMock(true); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Extension/RegisterClockMockSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Extension; 13 | 14 | use PHPUnit\Event\Code\TestMethod; 15 | use PHPUnit\Event\TestSuite\Loaded; 16 | use PHPUnit\Event\TestSuite\LoadedSubscriber; 17 | use PHPUnit\Metadata\Group; 18 | use Symfony\Bridge\PhpUnit\ClockMock; 19 | 20 | /** 21 | * @internal 22 | */ 23 | class RegisterClockMockSubscriber implements LoadedSubscriber 24 | { 25 | public function notify(Loaded $event): void 26 | { 27 | foreach ($event->testSuite()->tests() as $test) { 28 | if (!$test instanceof TestMethod) { 29 | continue; 30 | } 31 | 32 | foreach ($test->metadata() as $metadata) { 33 | if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) { 34 | ClockMock::register($test->className()); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Extension/RegisterDnsMockSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Extension; 13 | 14 | use PHPUnit\Event\Code\TestMethod; 15 | use PHPUnit\Event\TestSuite\Loaded; 16 | use PHPUnit\Event\TestSuite\LoadedSubscriber; 17 | use PHPUnit\Metadata\Group; 18 | use Symfony\Bridge\PhpUnit\DnsMock; 19 | 20 | /** 21 | * @internal 22 | */ 23 | class RegisterDnsMockSubscriber implements LoadedSubscriber 24 | { 25 | public function notify(Loaded $event): void 26 | { 27 | foreach ($event->testSuite()->tests() as $test) { 28 | if (!$test instanceof TestMethod) { 29 | continue; 30 | } 31 | 32 | foreach ($test->metadata() as $metadata) { 33 | if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) { 34 | DnsMock::register($test->className()); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Legacy/CommandForV7.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use PHPUnit\TextUI\Command as BaseCommand; 15 | use PHPUnit\TextUI\TestRunner as BaseRunner; 16 | use PHPUnit\Util\Configuration; 17 | use Symfony\Bridge\PhpUnit\SymfonyTestsListener; 18 | 19 | /** 20 | * @internal 21 | */ 22 | class CommandForV7 extends BaseCommand 23 | { 24 | protected function createRunner(): BaseRunner 25 | { 26 | $this->arguments['listeners'] ?? $this->arguments['listeners'] = []; 27 | 28 | $registeredLocally = false; 29 | 30 | foreach ($this->arguments['listeners'] as $registeredListener) { 31 | if ($registeredListener instanceof SymfonyTestsListener) { 32 | $registeredListener->globalListenerDisabled(); 33 | $registeredLocally = true; 34 | break; 35 | } 36 | } 37 | 38 | if (isset($this->arguments['configuration'])) { 39 | $configuration = $this->arguments['configuration']; 40 | if (!$configuration instanceof Configuration) { 41 | $configuration = Configuration::getInstance($this->arguments['configuration']); 42 | } 43 | foreach ($configuration->getListenerConfiguration() as $registeredListener) { 44 | if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { 45 | $registeredLocally = true; 46 | break; 47 | } 48 | } 49 | } 50 | 51 | if (!$registeredLocally) { 52 | $this->arguments['listeners'][] = new SymfonyTestsListener(); 53 | } 54 | 55 | return parent::createRunner(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Legacy/CommandForV9.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use PHPUnit\TextUI\Command as BaseCommand; 15 | use PHPUnit\TextUI\Configuration\Configuration as LegacyConfiguration; 16 | use PHPUnit\TextUI\Configuration\Registry; 17 | use PHPUnit\TextUI\TestRunner as BaseRunner; 18 | use PHPUnit\TextUI\XmlConfiguration\Configuration; 19 | use PHPUnit\TextUI\XmlConfiguration\Loader; 20 | use Symfony\Bridge\PhpUnit\SymfonyTestsListener; 21 | 22 | /** 23 | * @internal 24 | */ 25 | class CommandForV9 extends BaseCommand 26 | { 27 | protected function createRunner(): BaseRunner 28 | { 29 | $this->arguments['listeners'] ?? $this->arguments['listeners'] = []; 30 | 31 | $registeredLocally = false; 32 | 33 | foreach ($this->arguments['listeners'] as $registeredListener) { 34 | if ($registeredListener instanceof SymfonyTestsListener) { 35 | $registeredListener->globalListenerDisabled(); 36 | $registeredLocally = true; 37 | break; 38 | } 39 | } 40 | 41 | if (isset($this->arguments['configuration'])) { 42 | $configuration = $this->arguments['configuration']; 43 | 44 | if (!class_exists(Configuration::class) && !$configuration instanceof LegacyConfiguration) { 45 | $configuration = Registry::getInstance()->get($this->arguments['configuration']); 46 | } elseif (class_exists(Configuration::class) && !$configuration instanceof Configuration) { 47 | $configuration = (new Loader())->load($this->arguments['configuration']); 48 | } 49 | 50 | foreach ($configuration->listeners() as $registeredListener) { 51 | if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener->className(), '\\')) { 52 | $registeredLocally = true; 53 | break; 54 | } 55 | } 56 | } 57 | 58 | if (!$registeredLocally) { 59 | $this->arguments['listeners'][] = new SymfonyTestsListener(); 60 | } 61 | 62 | return parent::createRunner(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Legacy/ConstraintLogicTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | /** 15 | * @internal 16 | */ 17 | trait ConstraintLogicTrait 18 | { 19 | private function doEvaluate($other, $description, $returnResult) 20 | { 21 | $success = false; 22 | 23 | if ($this->matches($other)) { 24 | $success = true; 25 | } 26 | 27 | if ($returnResult) { 28 | return $success; 29 | } 30 | 31 | if (!$success) { 32 | $this->fail($other, $description); 33 | } 34 | 35 | return null; 36 | } 37 | 38 | private function doAdditionalFailureDescription($other): string 39 | { 40 | return ''; 41 | } 42 | 43 | private function doCount(): int 44 | { 45 | return 1; 46 | } 47 | 48 | private function doFailureDescription($other): string 49 | { 50 | return $this->exporter()->export($other).' '.$this->toString(); 51 | } 52 | 53 | private function doMatches($other): bool 54 | { 55 | return false; 56 | } 57 | 58 | private function doToString(): string 59 | { 60 | return ''; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Legacy/ConstraintTraitForV7.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use SebastianBergmann\Exporter\Exporter; 15 | 16 | /** 17 | * @internal 18 | */ 19 | trait ConstraintTraitForV7 20 | { 21 | use ConstraintLogicTrait; 22 | 23 | /** 24 | * @return bool|null 25 | */ 26 | public function evaluate($other, $description = '', $returnResult = false) 27 | { 28 | return $this->doEvaluate($other, $description, $returnResult); 29 | } 30 | 31 | public function count(): int 32 | { 33 | return $this->doCount(); 34 | } 35 | 36 | public function toString(): string 37 | { 38 | return $this->doToString(); 39 | } 40 | 41 | protected function additionalFailureDescription($other): string 42 | { 43 | return $this->doAdditionalFailureDescription($other); 44 | } 45 | 46 | protected function exporter(): Exporter 47 | { 48 | if (null === $this->exporter) { 49 | $this->exporter = new Exporter(); 50 | } 51 | 52 | return $this->exporter; 53 | } 54 | 55 | protected function failureDescription($other): string 56 | { 57 | return $this->doFailureDescription($other); 58 | } 59 | 60 | protected function matches($other): bool 61 | { 62 | return $this->doMatches($other); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Legacy/ConstraintTraitForV8.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | /** 15 | * @internal 16 | */ 17 | trait ConstraintTraitForV8 18 | { 19 | use ConstraintLogicTrait; 20 | 21 | /** 22 | * @return bool|null 23 | */ 24 | public function evaluate($other, $description = '', $returnResult = false) 25 | { 26 | return $this->doEvaluate($other, $description, $returnResult); 27 | } 28 | 29 | public function count(): int 30 | { 31 | return $this->doCount(); 32 | } 33 | 34 | public function toString(): string 35 | { 36 | return $this->doToString(); 37 | } 38 | 39 | protected function additionalFailureDescription($other): string 40 | { 41 | return $this->doAdditionalFailureDescription($other); 42 | } 43 | 44 | protected function failureDescription($other): string 45 | { 46 | return $this->doFailureDescription($other); 47 | } 48 | 49 | protected function matches($other): bool 50 | { 51 | return $this->doMatches($other); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Legacy/ConstraintTraitForV9.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | /** 15 | * @internal 16 | */ 17 | trait ConstraintTraitForV9 18 | { 19 | use ConstraintLogicTrait; 20 | 21 | public function evaluate($other, string $description = '', bool $returnResult = false): ?bool 22 | { 23 | return $this->doEvaluate($other, $description, $returnResult); 24 | } 25 | 26 | public function count(): int 27 | { 28 | return $this->doCount(); 29 | } 30 | 31 | public function toString(): string 32 | { 33 | return $this->doToString(); 34 | } 35 | 36 | protected function additionalFailureDescription($other): string 37 | { 38 | return $this->doAdditionalFailureDescription($other); 39 | } 40 | 41 | protected function failureDescription($other): string 42 | { 43 | return $this->doFailureDescription($other); 44 | } 45 | 46 | protected function matches($other): bool 47 | { 48 | return $this->doMatches($other); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Legacy/ExpectDeprecationTraitBeforeV8_4.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | /** 15 | * @internal, use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait instead. 16 | */ 17 | trait ExpectDeprecationTraitBeforeV8_4 18 | { 19 | /** 20 | * @param string $message 21 | */ 22 | protected function expectDeprecation($message): void 23 | { 24 | // Expected deprecations set by isolated tests need to be written to a file 25 | // so that the test running process can take account of them. 26 | if ($file = getenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE')) { 27 | $this->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); 28 | $expectedDeprecations = file_get_contents($file); 29 | if ($expectedDeprecations) { 30 | $expectedDeprecations = array_merge(unserialize($expectedDeprecations), [$message]); 31 | } else { 32 | $expectedDeprecations = [$message]; 33 | } 34 | file_put_contents($file, serialize($expectedDeprecations)); 35 | 36 | return; 37 | } 38 | 39 | if (!SymfonyTestsListenerTrait::$previousErrorHandler) { 40 | SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']); 41 | } 42 | 43 | SymfonyTestsListenerTrait::$expectedDeprecations[] = $message; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Legacy/ExpectDeprecationTraitForV8_4.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | /** 15 | * @internal use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait instead 16 | */ 17 | trait ExpectDeprecationTraitForV8_4 18 | { 19 | /** 20 | * @param string $message 21 | */ 22 | public function expectDeprecation(): void 23 | { 24 | if (1 > \func_num_args() || !\is_string($message = func_get_arg(0))) { 25 | throw new \InvalidArgumentException(sprintf('The "%s()" method requires the string $message argument.', __FUNCTION__)); 26 | } 27 | 28 | // Expected deprecations set by isolated tests need to be written to a file 29 | // so that the test running process can take account of them. 30 | if ($file = getenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE')) { 31 | $this->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); 32 | $expectedDeprecations = file_get_contents($file); 33 | if ($expectedDeprecations) { 34 | $expectedDeprecations = array_merge(unserialize($expectedDeprecations), [$message]); 35 | } else { 36 | $expectedDeprecations = [$message]; 37 | } 38 | file_put_contents($file, serialize($expectedDeprecations)); 39 | 40 | return; 41 | } 42 | 43 | if (!SymfonyTestsListenerTrait::$previousErrorHandler) { 44 | SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']); 45 | } 46 | 47 | SymfonyTestsListenerTrait::$expectedDeprecations[] = $message; 48 | } 49 | 50 | /** 51 | * @internal use expectDeprecation() instead 52 | */ 53 | public function expectDeprecationMessage(string $message): void 54 | { 55 | throw new \BadMethodCallException(sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait, pass the message to expectDeprecation() instead.', __FUNCTION__)); 56 | } 57 | 58 | /** 59 | * @internal use expectDeprecation() instead 60 | */ 61 | public function expectDeprecationMessageMatches(string $regularExpression): void 62 | { 63 | throw new \BadMethodCallException(sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait.', __FUNCTION__)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Legacy/PolyfillAssertTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use PHPUnit\Framework\Constraint\LogicalNot; 15 | use PHPUnit\Framework\Constraint\TraversableContains; 16 | 17 | /** 18 | * This trait is @internal. 19 | */ 20 | trait PolyfillAssertTrait 21 | { 22 | /** 23 | * @param iterable $haystack 24 | * @param string $message 25 | * 26 | * @return void 27 | */ 28 | public static function assertContainsEquals($needle, $haystack, $message = '') 29 | { 30 | $constraint = new TraversableContains($needle, false, false); 31 | static::assertThat($haystack, $constraint, $message); 32 | } 33 | 34 | /** 35 | * @param iterable $haystack 36 | * @param string $message 37 | * 38 | * @return void 39 | */ 40 | public static function assertNotContainsEquals($needle, $haystack, $message = '') 41 | { 42 | $constraint = new LogicalNot(new TraversableContains($needle, false, false)); 43 | static::assertThat($haystack, $constraint, $message); 44 | } 45 | 46 | /** 47 | * @param string $filename 48 | * @param string $message 49 | * 50 | * @return void 51 | */ 52 | public static function assertIsNotReadable($filename, $message = '') 53 | { 54 | static::assertNotIsReadable($filename, $message); 55 | } 56 | 57 | /** 58 | * @param string $filename 59 | * @param string $message 60 | * 61 | * @return void 62 | */ 63 | public static function assertIsNotWritable($filename, $message = '') 64 | { 65 | static::assertNotIsWritable($filename, $message); 66 | } 67 | 68 | /** 69 | * @param string $directory 70 | * @param string $message 71 | * 72 | * @return void 73 | */ 74 | public static function assertDirectoryDoesNotExist($directory, $message = '') 75 | { 76 | static::assertDirectoryNotExists($directory, $message); 77 | } 78 | 79 | /** 80 | * @param string $directory 81 | * @param string $message 82 | * 83 | * @return void 84 | */ 85 | public static function assertDirectoryIsNotReadable($directory, $message = '') 86 | { 87 | static::assertDirectoryNotIsReadable($directory, $message); 88 | } 89 | 90 | /** 91 | * @param string $directory 92 | * @param string $message 93 | * 94 | * @return void 95 | */ 96 | public static function assertDirectoryIsNotWritable($directory, $message = '') 97 | { 98 | static::assertDirectoryNotIsWritable($directory, $message); 99 | } 100 | 101 | /** 102 | * @param string $filename 103 | * @param string $message 104 | * 105 | * @return void 106 | */ 107 | public static function assertFileDoesNotExist($filename, $message = '') 108 | { 109 | static::assertFileNotExists($filename, $message); 110 | } 111 | 112 | /** 113 | * @param string $filename 114 | * @param string $message 115 | * 116 | * @return void 117 | */ 118 | public static function assertFileIsNotReadable($filename, $message = '') 119 | { 120 | static::assertFileNotIsReadable($filename, $message); 121 | } 122 | 123 | /** 124 | * @param string $filename 125 | * @param string $message 126 | * 127 | * @return void 128 | */ 129 | public static function assertFileIsNotWritable($filename, $message = '') 130 | { 131 | static::assertFileNotIsWritable($filename, $message); 132 | } 133 | 134 | /** 135 | * @param string $pattern 136 | * @param string $string 137 | * @param string $message 138 | * 139 | * @return void 140 | */ 141 | public static function assertMatchesRegularExpression($pattern, $string, $message = '') 142 | { 143 | static::assertRegExp($pattern, $string, $message); 144 | } 145 | 146 | /** 147 | * @param string $pattern 148 | * @param string $string 149 | * @param string $message 150 | * 151 | * @return void 152 | */ 153 | public static function assertDoesNotMatchRegularExpression($pattern, $string, $message = '') 154 | { 155 | static::assertNotRegExp($pattern, $string, $message); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Legacy/PolyfillTestCaseTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use PHPUnit\Framework\Error\Error; 15 | use PHPUnit\Framework\Error\Notice; 16 | use PHPUnit\Framework\Error\Warning; 17 | 18 | /** 19 | * This trait is @internal. 20 | */ 21 | trait PolyfillTestCaseTrait 22 | { 23 | /** 24 | * @param string $messageRegExp 25 | * 26 | * @return void 27 | */ 28 | public function expectExceptionMessageMatches($messageRegExp) 29 | { 30 | $this->expectExceptionMessageRegExp($messageRegExp); 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | public function expectNotice() 37 | { 38 | $this->expectException(Notice::class); 39 | } 40 | 41 | /** 42 | * @param string $message 43 | * 44 | * @return void 45 | */ 46 | public function expectNoticeMessage($message) 47 | { 48 | $this->expectExceptionMessage($message); 49 | } 50 | 51 | /** 52 | * @param string $regularExpression 53 | * 54 | * @return void 55 | */ 56 | public function expectNoticeMessageMatches($regularExpression) 57 | { 58 | $this->expectExceptionMessageMatches($regularExpression); 59 | } 60 | 61 | /** 62 | * @return void 63 | */ 64 | public function expectWarning() 65 | { 66 | $this->expectException(Warning::class); 67 | } 68 | 69 | /** 70 | * @param string $message 71 | * 72 | * @return void 73 | */ 74 | public function expectWarningMessage($message) 75 | { 76 | $this->expectExceptionMessage($message); 77 | } 78 | 79 | /** 80 | * @param string $regularExpression 81 | * 82 | * @return void 83 | */ 84 | public function expectWarningMessageMatches($regularExpression) 85 | { 86 | $this->expectExceptionMessageMatches($regularExpression); 87 | } 88 | 89 | /** 90 | * @return void 91 | */ 92 | public function expectError() 93 | { 94 | $this->expectException(Error::class); 95 | } 96 | 97 | /** 98 | * @param string $message 99 | * 100 | * @return void 101 | */ 102 | public function expectErrorMessage($message) 103 | { 104 | $this->expectExceptionMessage($message); 105 | } 106 | 107 | /** 108 | * @param string $regularExpression 109 | * 110 | * @return void 111 | */ 112 | public function expectErrorMessageMatches($regularExpression) 113 | { 114 | $this->expectExceptionMessageMatches($regularExpression); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Legacy/SymfonyTestsListenerForV7.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use PHPUnit\Framework\Test; 15 | use PHPUnit\Framework\TestListener; 16 | use PHPUnit\Framework\TestListenerDefaultImplementation; 17 | use PHPUnit\Framework\TestSuite; 18 | 19 | /** 20 | * Collects and replays skipped tests. 21 | * 22 | * @author Nicolas Grekas 23 | * 24 | * @internal 25 | */ 26 | class SymfonyTestsListenerForV7 implements TestListener 27 | { 28 | use TestListenerDefaultImplementation; 29 | 30 | private $trait; 31 | 32 | public function __construct(array $mockedNamespaces = []) 33 | { 34 | $this->trait = new SymfonyTestsListenerTrait($mockedNamespaces); 35 | } 36 | 37 | public function globalListenerDisabled() 38 | { 39 | $this->trait->globalListenerDisabled(); 40 | } 41 | 42 | public function startTestSuite(TestSuite $suite): void 43 | { 44 | $this->trait->startTestSuite($suite); 45 | } 46 | 47 | public function addSkippedTest(Test $test, \Throwable $t, float $time): void 48 | { 49 | $this->trait->addSkippedTest($test, $t, $time); 50 | } 51 | 52 | public function startTest(Test $test): void 53 | { 54 | $this->trait->startTest($test); 55 | } 56 | 57 | public function endTest(Test $test, float $time): void 58 | { 59 | $this->trait->endTest($test, $time); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Legacy/SymfonyTestsListenerTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\Legacy; 13 | 14 | use Doctrine\Common\Annotations\AnnotationRegistry; 15 | use PHPUnit\Framework\AssertionFailedError; 16 | use PHPUnit\Framework\DataProviderTestSuite; 17 | use PHPUnit\Framework\RiskyTestError; 18 | use PHPUnit\Framework\TestCase; 19 | use PHPUnit\Framework\TestSuite; 20 | use PHPUnit\Runner\BaseTestRunner; 21 | use PHPUnit\Util\Blacklist; 22 | use PHPUnit\Util\ExcludeList; 23 | use PHPUnit\Util\Test; 24 | use Symfony\Bridge\PhpUnit\ClockMock; 25 | use Symfony\Bridge\PhpUnit\DnsMock; 26 | use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; 27 | use Symfony\Component\ErrorHandler\DebugClassLoader; 28 | 29 | /** 30 | * PHP 5.3 compatible trait-like shared implementation. 31 | * 32 | * @author Nicolas Grekas 33 | * 34 | * @internal 35 | */ 36 | class SymfonyTestsListenerTrait 37 | { 38 | public static $expectedDeprecations = []; 39 | public static $previousErrorHandler; 40 | private static $gatheredDeprecations = []; 41 | private static $globallyEnabled = false; 42 | private $state = -1; 43 | private $skippedFile = false; 44 | private $wasSkipped = []; 45 | private $isSkipped = []; 46 | private $runsInSeparateProcess = false; 47 | private $checkNumAssertions = false; 48 | 49 | /** 50 | * @param array $mockedNamespaces List of namespaces, indexed by mocked features (time-sensitive or dns-sensitive) 51 | */ 52 | public function __construct(array $mockedNamespaces = []) 53 | { 54 | setlocale(\LC_ALL, $_ENV['SYMFONY_PHPUNIT_LOCALE'] ?? 'C'); 55 | 56 | if (class_exists(ExcludeList::class)) { 57 | (new ExcludeList())->getExcludedDirectories(); 58 | ExcludeList::addDirectory(\dirname((new \ReflectionClass(__CLASS__))->getFileName(), 2)); 59 | } elseif (method_exists(Blacklist::class, 'addDirectory')) { 60 | (new BlackList())->getBlacklistedDirectories(); 61 | Blacklist::addDirectory(\dirname((new \ReflectionClass(__CLASS__))->getFileName(), 2)); 62 | } else { 63 | Blacklist::$blacklistedClassNames[__CLASS__] = 2; 64 | } 65 | 66 | $enableDebugClassLoader = class_exists(DebugClassLoader::class); 67 | 68 | foreach ($mockedNamespaces as $type => $namespaces) { 69 | if (!\is_array($namespaces)) { 70 | $namespaces = [$namespaces]; 71 | } 72 | if ('time-sensitive' === $type) { 73 | foreach ($namespaces as $ns) { 74 | ClockMock::register($ns.'\DummyClass'); 75 | } 76 | } 77 | if ('dns-sensitive' === $type) { 78 | foreach ($namespaces as $ns) { 79 | DnsMock::register($ns.'\DummyClass'); 80 | } 81 | } 82 | if ('debug-class-loader' === $type) { 83 | $enableDebugClassLoader = $namespaces && $namespaces[0]; 84 | } 85 | } 86 | if ($enableDebugClassLoader) { 87 | DebugClassLoader::enable(); 88 | } 89 | if (self::$globallyEnabled) { 90 | $this->state = -2; 91 | } else { 92 | self::$globallyEnabled = true; 93 | } 94 | } 95 | 96 | public function __sleep() 97 | { 98 | throw new \BadMethodCallException('Cannot serialize '.__CLASS__); 99 | } 100 | 101 | public function __wakeup() 102 | { 103 | throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); 104 | } 105 | 106 | public function __destruct() 107 | { 108 | if (0 < $this->state) { 109 | file_put_contents($this->skippedFile, 'isSkipped, true).';'); 110 | } 111 | } 112 | 113 | public function globalListenerDisabled(): void 114 | { 115 | self::$globallyEnabled = false; 116 | $this->state = -1; 117 | } 118 | 119 | public function startTestSuite($suite): void 120 | { 121 | $suiteName = $suite->getName(); 122 | 123 | foreach ($suite->tests() as $test) { 124 | if (!$test instanceof TestCase) { 125 | continue; 126 | } 127 | if (null === Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) { 128 | $test->setPreserveGlobalState(false); 129 | } 130 | } 131 | 132 | if (-1 === $this->state) { 133 | echo "Testing $suiteName\n"; 134 | $this->state = 0; 135 | 136 | if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { 137 | if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { 138 | AnnotationRegistry::registerUniqueLoader('class_exists'); 139 | } elseif (method_exists(AnnotationRegistry::class, 'registerLoader')) { 140 | AnnotationRegistry::registerLoader('class_exists'); 141 | } 142 | } 143 | 144 | if ($this->skippedFile = getenv('SYMFONY_PHPUNIT_SKIPPED_TESTS')) { 145 | $this->state = 1; 146 | 147 | if (file_exists($this->skippedFile)) { 148 | $this->state = 2; 149 | 150 | if (!$this->wasSkipped = require $this->skippedFile) { 151 | echo "All tests already ran successfully.\n"; 152 | $suite->setTests([]); 153 | } 154 | } 155 | } 156 | $testSuites = [$suite]; 157 | for ($i = 0; isset($testSuites[$i]); ++$i) { 158 | foreach ($testSuites[$i]->tests() as $test) { 159 | if ($test instanceof TestSuite) { 160 | if (!class_exists($test->getName(), false)) { 161 | $testSuites[] = $test; 162 | continue; 163 | } 164 | $groups = Test::getGroups($test->getName()); 165 | if (\in_array('time-sensitive', $groups, true)) { 166 | ClockMock::register($test->getName()); 167 | } 168 | if (\in_array('dns-sensitive', $groups, true)) { 169 | DnsMock::register($test->getName()); 170 | } 171 | } 172 | } 173 | } 174 | } elseif (2 === $this->state) { 175 | $suites = [$suite]; 176 | $skipped = []; 177 | while ($s = array_shift($suites)) { 178 | foreach ($s->tests() as $test) { 179 | if ($test instanceof TestSuite) { 180 | $suites[] = $test; 181 | continue; 182 | } 183 | if ($test instanceof TestCase 184 | && isset($this->wasSkipped[\get_class($test)][$test->getName()]) 185 | ) { 186 | $skipped[] = $test; 187 | } 188 | } 189 | } 190 | $suite->setTests($skipped); 191 | } 192 | } 193 | 194 | public function addSkippedTest($test, \Exception $e, $time): void 195 | { 196 | if (0 < $this->state) { 197 | if ($test instanceof DataProviderTestSuite) { 198 | foreach ($test->tests() as $testWithDataProvider) { 199 | $this->isSkipped[\get_class($testWithDataProvider)][$testWithDataProvider->getName()] = 1; 200 | } 201 | } else { 202 | $this->isSkipped[\get_class($test)][$test->getName()] = 1; 203 | } 204 | } 205 | } 206 | 207 | public function startTest($test): void 208 | { 209 | if (-2 < $this->state && $test instanceof TestCase) { 210 | // This event is triggered before the test is re-run in isolation 211 | if ($this->willBeIsolated($test)) { 212 | $this->runsInSeparateProcess = tempnam(sys_get_temp_dir(), 'deprec'); 213 | putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$this->runsInSeparateProcess); 214 | putenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE='.tempnam(sys_get_temp_dir(), 'expectdeprec')); 215 | } 216 | 217 | $groups = Test::getGroups(\get_class($test), $test->getName(false)); 218 | 219 | if (!$this->runsInSeparateProcess) { 220 | if (\in_array('time-sensitive', $groups, true)) { 221 | ClockMock::register(\get_class($test)); 222 | ClockMock::withClockMock(true); 223 | } 224 | if (\in_array('dns-sensitive', $groups, true)) { 225 | DnsMock::register(\get_class($test)); 226 | } 227 | } 228 | 229 | if (!$test->getTestResultObject()) { 230 | return; 231 | } 232 | 233 | $annotations = Test::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); 234 | 235 | if (isset($annotations['class']['expectedDeprecation'])) { 236 | $test->getTestResultObject()->addError($test, new AssertionFailedError('"@expectedDeprecation" annotations are not allowed at the class level.'), 0); 237 | } 238 | if (isset($annotations['method']['expectedDeprecation']) || $this->checkNumAssertions = method_exists($test, 'expectDeprecation') && (new \ReflectionMethod($test, 'expectDeprecation'))->getFileName() === (new \ReflectionMethod(ExpectDeprecationTrait::class, 'expectDeprecation'))->getFileName()) { 239 | if (isset($annotations['method']['expectedDeprecation'])) { 240 | self::$expectedDeprecations = $annotations['method']['expectedDeprecation']; 241 | self::$previousErrorHandler = set_error_handler([self::class, 'handleError']); 242 | @trigger_error('Since symfony/phpunit-bridge 5.1: Using "@expectedDeprecation" annotations in tests is deprecated, use the "ExpectDeprecationTrait::expectDeprecation()" method instead.', \E_USER_DEPRECATED); 243 | } 244 | 245 | if ($this->checkNumAssertions) { 246 | $this->checkNumAssertions = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything(); 247 | } 248 | 249 | $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); 250 | } 251 | } 252 | } 253 | 254 | public function endTest($test, $time): void 255 | { 256 | if ($file = getenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE')) { 257 | putenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE'); 258 | $expectedDeprecations = file_get_contents($file); 259 | if ($expectedDeprecations) { 260 | self::$expectedDeprecations = array_merge(self::$expectedDeprecations, unserialize($expectedDeprecations)); 261 | if (!self::$previousErrorHandler) { 262 | self::$previousErrorHandler = set_error_handler([self::class, 'handleError']); 263 | } 264 | } 265 | } 266 | 267 | if (class_exists(DebugClassLoader::class, false)) { 268 | DebugClassLoader::checkClasses(); 269 | } 270 | 271 | $className = \get_class($test); 272 | $groups = Test::getGroups($className, $test->getName(false)); 273 | 274 | if ($this->checkNumAssertions) { 275 | $assertions = \count(self::$expectedDeprecations) + $test->getNumAssertions(); 276 | if ($test->doesNotPerformAssertions() && $assertions > 0) { 277 | $test->getTestResultObject()->addFailure($test, new RiskyTestError(sprintf('This test is annotated with "@doesNotPerformAssertions", but performed %s assertions', $assertions)), $time); 278 | } elseif ($assertions === 0 && !$test->doesNotPerformAssertions() && $test->getTestResultObject()->noneSkipped()) { 279 | $test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time); 280 | } 281 | 282 | $this->checkNumAssertions = false; 283 | } 284 | 285 | if ($this->runsInSeparateProcess) { 286 | $deprecations = file_get_contents($this->runsInSeparateProcess); 287 | unlink($this->runsInSeparateProcess); 288 | putenv('SYMFONY_DEPRECATIONS_SERIALIZE'); 289 | foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) { 290 | $error = serialize(['deprecation' => $deprecation[1], 'class' => $className, 'method' => $test->getName(false), 'triggering_file' => $deprecation[2] ?? null, 'files_stack' => $deprecation[3] ?? []]); 291 | if ($deprecation[0]) { 292 | // unsilenced on purpose 293 | trigger_error($error, \E_USER_DEPRECATED); 294 | } else { 295 | @trigger_error($error, \E_USER_DEPRECATED); 296 | } 297 | } 298 | $this->runsInSeparateProcess = false; 299 | } 300 | 301 | if (self::$expectedDeprecations) { 302 | if (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true)) { 303 | $test->addToAssertionCount(\count(self::$expectedDeprecations)); 304 | } 305 | 306 | restore_error_handler(); 307 | 308 | if (!\in_array('legacy', $groups, true)) { 309 | $test->getTestResultObject()->addError($test, new AssertionFailedError('Only tests with the "@group legacy" annotation can expect a deprecation.'), 0); 310 | } elseif (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) { 311 | try { 312 | $prefix = "@expectedDeprecation:\n"; 313 | $test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", self::$expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", self::$gatheredDeprecations)."\n"); 314 | } catch (AssertionFailedError $e) { 315 | $test->getTestResultObject()->addFailure($test, $e, $time); 316 | } 317 | } 318 | 319 | self::$expectedDeprecations = self::$gatheredDeprecations = []; 320 | self::$previousErrorHandler = null; 321 | } 322 | if (!$this->runsInSeparateProcess && -2 < $this->state && $test instanceof TestCase) { 323 | if (\in_array('time-sensitive', $groups, true)) { 324 | ClockMock::withClockMock(false); 325 | } 326 | if (\in_array('dns-sensitive', $groups, true)) { 327 | DnsMock::withMockedHosts([]); 328 | } 329 | } 330 | } 331 | 332 | public static function handleError($type, $msg, $file, $line, $context = []) 333 | { 334 | if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { 335 | $h = self::$previousErrorHandler; 336 | 337 | return $h ? $h($type, $msg, $file, $line, $context) : false; 338 | } 339 | // If the message is serialized we need to extract the message. This occurs when the error is triggered by 340 | // by the isolated test path in \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest(). 341 | $parsedMsg = @unserialize($msg); 342 | if (\is_array($parsedMsg)) { 343 | $msg = $parsedMsg['deprecation']; 344 | } 345 | if (error_reporting() & $type) { 346 | $msg = 'Unsilenced deprecation: '.$msg; 347 | } 348 | self::$gatheredDeprecations[] = $msg; 349 | 350 | return true; 351 | } 352 | 353 | private function willBeIsolated(TestCase $test): bool 354 | { 355 | if ($test->isInIsolation()) { 356 | return false; 357 | } 358 | 359 | $r = new \ReflectionProperty($test, 'runTestInSeparateProcess'); 360 | $r->setAccessible(true); 361 | 362 | return $r->getValue($test) ?? false; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHPUnit Bridge 2 | ============== 3 | 4 | The PHPUnit bridge provides utilities for [PHPUnit](https://phpunit.de/), 5 | especially user deprecation notices management. 6 | 7 | Resources 8 | --------- 9 | 10 | * [Documentation](https://symfony.com/doc/current/components/phpunit_bridge.html) 11 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 12 | * [Report issues](https://github.com/symfony/symfony/issues) and 13 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 14 | in the [main Symfony repository](https://github.com/symfony/symfony) 15 | -------------------------------------------------------------------------------- /SymfonyExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | use PHPUnit\Event\Code\Test; 15 | use PHPUnit\Event\Code\TestMethod; 16 | use PHPUnit\Event\Test\BeforeTestMethodErrored; 17 | use PHPUnit\Event\Test\BeforeTestMethodErroredSubscriber; 18 | use PHPUnit\Event\Test\Errored; 19 | use PHPUnit\Event\Test\ErroredSubscriber; 20 | use PHPUnit\Event\Test\Finished; 21 | use PHPUnit\Event\Test\FinishedSubscriber; 22 | use PHPUnit\Event\Test\Skipped; 23 | use PHPUnit\Event\Test\SkippedSubscriber; 24 | use PHPUnit\Metadata\Group; 25 | use PHPUnit\Runner\Extension\Extension; 26 | use PHPUnit\Runner\Extension\Facade; 27 | use PHPUnit\Runner\Extension\ParameterCollection; 28 | use PHPUnit\TextUI\Configuration\Configuration; 29 | use Symfony\Bridge\PhpUnit\Extension\EnableClockMockSubscriber; 30 | use Symfony\Bridge\PhpUnit\Extension\RegisterClockMockSubscriber; 31 | use Symfony\Bridge\PhpUnit\Extension\RegisterDnsMockSubscriber; 32 | use Symfony\Component\ErrorHandler\DebugClassLoader; 33 | 34 | class SymfonyExtension implements Extension 35 | { 36 | public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void 37 | { 38 | if (class_exists(DebugClassLoader::class)) { 39 | DebugClassLoader::enable(); 40 | } 41 | 42 | if ($parameters->has('clock-mock-namespaces')) { 43 | foreach (explode(',', $parameters->get('clock-mock-namespaces')) as $namespace) { 44 | ClockMock::register($namespace.'\DummyClass'); 45 | } 46 | } 47 | 48 | $facade->registerSubscriber(new RegisterClockMockSubscriber()); 49 | $facade->registerSubscriber(new EnableClockMockSubscriber()); 50 | $facade->registerSubscriber(new class implements ErroredSubscriber { 51 | public function notify(Errored $event): void 52 | { 53 | SymfonyExtension::disableClockMock($event->test()); 54 | SymfonyExtension::disableDnsMock($event->test()); 55 | } 56 | }); 57 | $facade->registerSubscriber(new class implements FinishedSubscriber { 58 | public function notify(Finished $event): void 59 | { 60 | SymfonyExtension::disableClockMock($event->test()); 61 | SymfonyExtension::disableDnsMock($event->test()); 62 | } 63 | }); 64 | $facade->registerSubscriber(new class implements SkippedSubscriber { 65 | public function notify(Skipped $event): void 66 | { 67 | SymfonyExtension::disableClockMock($event->test()); 68 | SymfonyExtension::disableDnsMock($event->test()); 69 | } 70 | }); 71 | 72 | if (interface_exists(BeforeTestMethodErroredSubscriber::class)) { 73 | $facade->registerSubscriber(new class implements BeforeTestMethodErroredSubscriber { 74 | public function notify(BeforeTestMethodErrored $event): void 75 | { 76 | if (method_exists($event, 'test')) { 77 | SymfonyExtension::disableClockMock($event->test()); 78 | SymfonyExtension::disableDnsMock($event->test()); 79 | } else { 80 | ClockMock::withClockMock(false); 81 | DnsMock::withMockedHosts([]); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | if ($parameters->has('dns-mock-namespaces')) { 88 | foreach (explode(',', $parameters->get('dns-mock-namespaces')) as $namespace) { 89 | DnsMock::register($namespace.'\DummyClass'); 90 | } 91 | } 92 | 93 | $facade->registerSubscriber(new RegisterDnsMockSubscriber()); 94 | } 95 | 96 | /** 97 | * @internal 98 | */ 99 | public static function disableClockMock(Test $test): void 100 | { 101 | if (self::hasGroup($test, 'time-sensitive')) { 102 | ClockMock::withClockMock(false); 103 | } 104 | } 105 | 106 | /** 107 | * @internal 108 | */ 109 | public static function disableDnsMock(Test $test): void 110 | { 111 | if (self::hasGroup($test, 'dns-sensitive')) { 112 | DnsMock::withMockedHosts([]); 113 | } 114 | } 115 | 116 | /** 117 | * @internal 118 | */ 119 | public static function hasGroup(Test $test, string $groupName): bool 120 | { 121 | if (!$test instanceof TestMethod) { 122 | return false; 123 | } 124 | 125 | foreach ($test->metadata() as $metadata) { 126 | if ($metadata instanceof Group && $groupName === $metadata->groupName()) { 127 | return true; 128 | } 129 | } 130 | 131 | return false; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /SymfonyTestsListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit; 13 | 14 | class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); 15 | 16 | if (false) { 17 | class SymfonyTestsListener 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TextUI/Command.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\PhpUnit\TextUI; 13 | 14 | if (version_compare(\PHPUnit\Runner\Version::id(), '9.0.0', '<')) { 15 | class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV7', 'Symfony\Bridge\PhpUnit\TextUI\Command'); 16 | } else { 17 | class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV9', 'Symfony\Bridge\PhpUnit\TextUI\Command'); 18 | } 19 | 20 | if (false) { 21 | class Command 22 | { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bin/simple-phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | require __DIR__.DIRECTORY_SEPARATOR.'simple-phpunit.php'; 14 | -------------------------------------------------------------------------------- /bin/simple-phpunit.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | // Please update when phpunit needs to be reinstalled with fresh deps: 13 | // Cache-Id: 2021-02-04 11:00 UTC 14 | 15 | if ('cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI) { 16 | throw new Exception('This script must be run from the command line.'); 17 | } 18 | 19 | error_reporting(-1); 20 | 21 | global $argv, $argc; 22 | $argv = $_SERVER['argv'] ?? []; 23 | $argc = $_SERVER['argc'] ?? 0; 24 | $getEnvVar = function ($name, $default = false) use ($argv) { 25 | if (false !== $value = getenv($name)) { 26 | return $value; 27 | } 28 | 29 | static $phpunitConfig = null; 30 | if (null === $phpunitConfig) { 31 | $phpunitConfigFilename = null; 32 | $getPhpUnitConfig = function ($probableConfig) use (&$getPhpUnitConfig) { 33 | if (!$probableConfig) { 34 | return null; 35 | } 36 | if (is_dir($probableConfig)) { 37 | return $getPhpUnitConfig($probableConfig.\DIRECTORY_SEPARATOR.'phpunit.xml'); 38 | } 39 | 40 | if (file_exists($probableConfig)) { 41 | return $probableConfig; 42 | } 43 | if (file_exists($probableConfig.'.dist')) { 44 | return $probableConfig.'.dist'; 45 | } 46 | 47 | return null; 48 | }; 49 | 50 | foreach ($argv as $cliArgumentIndex => $cliArgument) { 51 | if ('--' === $cliArgument) { 52 | break; 53 | } 54 | // long option 55 | if ('--configuration' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) { 56 | $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]); 57 | break; 58 | } 59 | // short option 60 | if (0 === strpos($cliArgument, '-c')) { 61 | if ('-c' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) { 62 | $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]); 63 | } else { 64 | $phpunitConfigFilename = $getPhpUnitConfig(substr($cliArgument, 2)); 65 | } 66 | break; 67 | } 68 | } 69 | 70 | $phpunitConfigFilename = $phpunitConfigFilename ?: $getPhpUnitConfig('phpunit.xml'); 71 | 72 | if ($phpunitConfigFilename) { 73 | $phpunitConfig = new DOMDocument(); 74 | $phpunitConfig->load($phpunitConfigFilename); 75 | } else { 76 | $phpunitConfig = false; 77 | } 78 | } 79 | if (false !== $phpunitConfig) { 80 | $var = new DOMXPath($phpunitConfig); 81 | foreach ($var->query('//php/server[@name="'.$name.'"]') as $var) { 82 | return $var->getAttribute('value'); 83 | } 84 | foreach ($var->query('//php/env[@name="'.$name.'"]') as $var) { 85 | return $var->getAttribute('value'); 86 | } 87 | } 88 | 89 | return $default; 90 | }; 91 | 92 | $passthruOrFail = function ($command) { 93 | passthru($command, $status); 94 | 95 | if ($status) { 96 | exit($status); 97 | } 98 | }; 99 | 100 | if (\PHP_VERSION_ID >= 80000) { 101 | $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '9.6') ?: '9.6'; 102 | } else { 103 | $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.5') ?: '8.5'; 104 | } 105 | 106 | $MAX_PHPUNIT_VERSION = $getEnvVar('SYMFONY_MAX_PHPUNIT_VERSION', false); 107 | 108 | if ($MAX_PHPUNIT_VERSION && version_compare($MAX_PHPUNIT_VERSION, $PHPUNIT_VERSION, '<')) { 109 | $PHPUNIT_VERSION = $MAX_PHPUNIT_VERSION; 110 | } 111 | 112 | if (version_compare($PHPUNIT_VERSION, '10.0', '>=') && version_compare($PHPUNIT_VERSION, '11.0', '<')) { 113 | fwrite(STDERR, 'This script does not work with PHPUnit 10.'.\PHP_EOL); 114 | exit(1); 115 | } 116 | 117 | $PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN); 118 | 119 | $COMPOSER_JSON = getenv('COMPOSER') ?: 'composer.json'; 120 | 121 | $root = __DIR__; 122 | while (!file_exists($root.'/'.$COMPOSER_JSON) || file_exists($root.'/DeprecationErrorHandler.php')) { 123 | if ($root === dirname($root)) { 124 | break; 125 | } 126 | $root = dirname($root); 127 | } 128 | 129 | $oldPwd = getcwd(); 130 | $PHPUNIT_DIR = rtrim($getEnvVar('SYMFONY_PHPUNIT_DIR', $root.'/vendor/bin/.phpunit'), '/'.\DIRECTORY_SEPARATOR); 131 | $PHP = defined('PHP_BINARY') ? \PHP_BINARY : 'php'; 132 | $PHP = escapeshellarg($PHP); 133 | if ('phpdbg' === \PHP_SAPI) { 134 | $PHP .= ' -qrr'; 135 | } 136 | 137 | $defaultEnvs = [ 138 | 'COMPOSER' => 'composer.json', 139 | 'COMPOSER_VENDOR_DIR' => 'vendor', 140 | 'COMPOSER_BIN_DIR' => 'bin', 141 | 'SYMFONY_SIMPLE_PHPUNIT_BIN_DIR' => __DIR__, 142 | ]; 143 | 144 | foreach ($defaultEnvs as $envName => $envValue) { 145 | if ($envValue !== getenv($envName)) { 146 | putenv("$envName=$envValue"); 147 | $_SERVER[$envName] = $_ENV[$envName] = $envValue; 148 | } 149 | } 150 | 151 | if ('disabled' === $getEnvVar('SYMFONY_DEPRECATIONS_HELPER') || version_compare($PHPUNIT_VERSION, '11.0', '>=')) { 152 | putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); 153 | } 154 | 155 | if (!$getEnvVar('DOCTRINE_DEPRECATIONS')) { 156 | putenv('DOCTRINE_DEPRECATIONS=trigger'); 157 | $_SERVER['DOCTRINE_DEPRECATIONS'] = $_ENV['DOCTRINE_DEPRECATIONS'] = 'trigger'; 158 | } 159 | 160 | $COMPOSER = ($COMPOSER = getenv('COMPOSER_BINARY')) 161 | || file_exists($COMPOSER = $oldPwd.'/composer.phar') 162 | || ($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', shell_exec('where.exe composer.phar 2> NUL')) : shell_exec('which composer.phar 2> /dev/null')))) 163 | || ($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', shell_exec('where.exe composer 2> NUL')) : shell_exec('which composer 2> /dev/null')))) 164 | || file_exists($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? shell_exec('git rev-parse --show-toplevel 2> NUL') : shell_exec('git rev-parse --show-toplevel 2> /dev/null'))).\DIRECTORY_SEPARATOR.'composer.phar') 165 | ? ('#!/usr/bin/env php' === file_get_contents($COMPOSER, false, null, 0, 18) ? $PHP : '').' '.escapeshellarg($COMPOSER) // detect shell wrappers by looking at the shebang 166 | : 'composer'; 167 | 168 | $prevCacheDir = getenv('COMPOSER_CACHE_DIR'); 169 | if ($prevCacheDir) { 170 | if (false === $absoluteCacheDir = realpath($prevCacheDir)) { 171 | @mkdir($prevCacheDir, 0777, true); 172 | $absoluteCacheDir = realpath($prevCacheDir); 173 | } 174 | if ($absoluteCacheDir) { 175 | putenv("COMPOSER_CACHE_DIR=$absoluteCacheDir"); 176 | } else { 177 | $prevCacheDir = false; 178 | } 179 | } 180 | $SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml' : '')); 181 | $SYMFONY_PHPUNIT_REQUIRE = $getEnvVar('SYMFONY_PHPUNIT_REQUIRE', ''); 182 | $configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, $SYMFONY_PHPUNIT_REQUIRE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT])); 183 | $PHPUNIT_VERSION_DIR = sprintf('phpunit-%s-%d', $PHPUNIT_VERSION, $PHPUNIT_REMOVE_RETURN_TYPEHINT); 184 | if (!file_exists("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit") || $configurationHash !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION_DIR.md5")) { 185 | // Build a standalone phpunit without symfony/yaml nor prophecy by default 186 | 187 | @mkdir($PHPUNIT_DIR, 0777, true); 188 | chdir($PHPUNIT_DIR); 189 | if (file_exists("$PHPUNIT_VERSION_DIR")) { 190 | passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s 2> NUL' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old"))); 191 | rename("$PHPUNIT_VERSION_DIR", "$PHPUNIT_VERSION_DIR.old"); 192 | passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old"))); 193 | } 194 | 195 | $info = []; 196 | foreach (explode("\n", `$COMPOSER info --no-ansi -a -n phpunit/phpunit "$PHPUNIT_VERSION.*"`) as $line) { 197 | $line = rtrim($line); 198 | 199 | if (!$info && preg_match('/^versions +: /', $line)) { 200 | $info['versions'] = explode(', ', ltrim(substr($line, 9), ': ')); 201 | } elseif (isset($info['requires'])) { 202 | if ('' === $line) { 203 | break; 204 | } 205 | 206 | $line = explode(' ', $line, 2); 207 | $info['requires'][$line[0]] = $line[1]; 208 | } elseif ($info && 'requires' === $line) { 209 | $info['requires'] = []; 210 | } 211 | } 212 | 213 | if (in_array('--colors=never', $argv, true) || (isset($argv[$i = array_search('never', $argv, true) - 1]) && '--colors' === $argv[$i])) { 214 | $COMPOSER .= ' --no-ansi'; 215 | } else { 216 | $COMPOSER .= ' --ansi'; 217 | } 218 | 219 | $info += [ 220 | 'versions' => [], 221 | 'requires' => ['php' => '*'], 222 | ]; 223 | 224 | $stableVersions = array_filter($info['versions'], function ($v) { 225 | return !preg_match('/-dev$|^dev-/', $v); 226 | }); 227 | 228 | if (!$stableVersions) { 229 | $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress -s dev phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); 230 | } else { 231 | $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); 232 | } 233 | 234 | @copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd'); 235 | chdir("$PHPUNIT_VERSION_DIR"); 236 | if ($SYMFONY_PHPUNIT_REMOVE) { 237 | $passthruOrFail("$COMPOSER remove --no-update --no-interaction ".$SYMFONY_PHPUNIT_REMOVE); 238 | } 239 | if ($SYMFONY_PHPUNIT_REQUIRE) { 240 | $passthruOrFail("$COMPOSER require --no-update --no-interaction ".$SYMFONY_PHPUNIT_REQUIRE); 241 | } 242 | if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { 243 | $passthruOrFail("$COMPOSER require --no-update --no-interaction phpunit/phpunit-mock-objects \"~3.1.0\""); 244 | } 245 | 246 | if (preg_match('{\^((\d++\.)\d++)[\d\.]*$}', $info['requires']['php'], $phpVersion) && version_compare($phpVersion[2].'99', \PHP_VERSION, '<')) { 247 | $passthruOrFail("$COMPOSER config platform.php \"$phpVersion[1].99\""); 248 | } else { 249 | $passthruOrFail("$COMPOSER config --unset platform.php"); 250 | } 251 | if (file_exists($path = $root.'/vendor/symfony/phpunit-bridge')) { 252 | $haystack = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR"; 253 | $rootLen = strlen($root); 254 | 255 | $p = ($rootLen <= strlen($haystack) ? str_repeat('../', substr_count($haystack, '/', $rootLen)) : '').'vendor/symfony/phpunit-bridge'; 256 | if (realpath($p) === realpath($path)) { 257 | $path = $p; 258 | } 259 | $passthruOrFail("$COMPOSER require --no-update --no-interaction symfony/phpunit-bridge \"*@dev\""); 260 | $passthruOrFail("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', \DIRECTORY_SEPARATOR, $path))); 261 | if ('\\' === \DIRECTORY_SEPARATOR) { 262 | file_put_contents('composer.json', preg_replace('/^( {8})"phpunit-bridge": \{$/m', "$0\n$1 ".'"options": {"symlink": false},', file_get_contents('composer.json'))); 263 | } 264 | } else { 265 | $passthruOrFail("$COMPOSER require --no-update --no-interaction symfony/phpunit-bridge \"*\""); 266 | } 267 | $prevRoot = getenv('COMPOSER_ROOT_VERSION'); 268 | putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); 269 | $q = '\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 80000 ? '"' : ''; 270 | // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS 271 | $exit = proc_close(proc_open("$q$COMPOSER update --no-dev --prefer-dist --no-progress $q", [], $p, getcwd())); 272 | putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : '')); 273 | if ($prevCacheDir) { 274 | putenv("COMPOSER_CACHE_DIR=$prevCacheDir"); 275 | } 276 | if ($exit) { 277 | exit($exit); 278 | } 279 | 280 | // Mutate TestCase code 281 | if (version_compare($PHPUNIT_VERSION, '11.0', '<')) { 282 | $alteredCode = file_get_contents($alteredFile = './src/Framework/TestCase.php'); 283 | if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) { 284 | $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode); 285 | } 286 | $alteredCode = preg_replace('/abstract class TestCase[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;", $alteredCode, 1); 287 | file_put_contents($alteredFile, $alteredCode); 288 | 289 | // Mutate Assert code 290 | $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php'); 291 | $alteredCode = preg_replace('/abstract class Assert[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1); 292 | file_put_contents($alteredFile, $alteredCode); 293 | 294 | file_put_contents('phpunit', <<<'EOPHP' 295 | getExcludedDirectories(); 305 | PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); 306 | class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); 307 | } elseif (method_exists(\PHPUnit\Util\Blacklist::class, 'addDirectory')) { 308 | (new PHPUnit\Util\BlackList())->getBlacklistedDirectories(); 309 | PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); 310 | class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); 311 | } else { 312 | PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListPhpunit'] = 1; 313 | PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListSimplePhpunit'] = 1; 314 | } 315 | 316 | Symfony\Bridge\PhpUnit\TextUI\Command::main(); 317 | 318 | EOPHP 319 | ); 320 | } 321 | 322 | chdir('..'); 323 | file_put_contents(".$PHPUNIT_VERSION_DIR.md5", $configurationHash); 324 | chdir($oldPwd); 325 | } 326 | 327 | // Create a symlink with a predictable path pointing to the currently used version. 328 | // This is useful for static analytics tools such as PHPStan having to load PHPUnit's classes 329 | // and for other testing libraries such as Behat using PHPUnit's assertions. 330 | chdir($PHPUNIT_DIR); 331 | if ('\\' === \DIRECTORY_SEPARATOR) { 332 | passthru('rmdir /S /Q phpunit 2> NUL'); 333 | passthru(sprintf('mklink /j phpunit %s > NUL 2>&1', escapeshellarg($PHPUNIT_VERSION_DIR))); 334 | } else { 335 | if (file_exists('phpunit')) { 336 | @unlink('phpunit'); 337 | } 338 | @symlink($PHPUNIT_VERSION_DIR, 'phpunit'); 339 | } 340 | chdir($oldPwd); 341 | 342 | if ($PHPUNIT_VERSION < 8.0) { 343 | $argv = array_filter($argv, function ($v) use (&$argc) { 344 | if ('--do-not-cache-result' !== $v) { 345 | return true; 346 | } 347 | --$argc; 348 | 349 | return false; 350 | }); 351 | } elseif (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), \FILTER_VALIDATE_BOOLEAN)) { 352 | $argv[] = '--do-not-cache-result'; 353 | ++$argc; 354 | } 355 | 356 | $components = []; 357 | $cmd = array_map('escapeshellarg', $argv); 358 | $exit = 0; 359 | 360 | if (isset($argv[1]) && 'symfony' === $argv[1] && !file_exists('symfony') && file_exists('src/Symfony')) { 361 | $argv[1] = 'src/Symfony'; 362 | } 363 | if (isset($argv[1]) && is_dir($argv[1]) && !file_exists($argv[1].'/phpunit.xml.dist')) { 364 | // Find Symfony components in plain php for Windows portability 365 | 366 | $finder = new RecursiveDirectoryIterator($argv[1], FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::UNIX_PATHS); 367 | $finder = new RecursiveIteratorIterator($finder); 368 | $finder->setMaxDepth(getenv('SYMFONY_PHPUNIT_MAX_DEPTH') ?: 3); 369 | 370 | foreach ($finder as $file => $fileInfo) { 371 | if ('phpunit.xml.dist' === $file) { 372 | $components[] = dirname($fileInfo->getPathname()); 373 | } 374 | } 375 | if ($components) { 376 | array_shift($cmd); 377 | } 378 | } 379 | 380 | $cmd[0] = sprintf('%s %s --colors=%s', $PHP, escapeshellarg("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit"), '' === $getEnvVar('NO_COLOR', '') ? 'always' : 'never'); 381 | $cmd = str_replace('%', '%%', implode(' ', $cmd)).' %1$s'; 382 | 383 | if ('\\' === \DIRECTORY_SEPARATOR) { 384 | $cmd = 'cmd /v:on /d /c "('.$cmd.')%2$s"'; 385 | } else { 386 | $cmd .= '%2$s'; 387 | } 388 | 389 | if (version_compare($PHPUNIT_VERSION, '11.0', '>=')) { 390 | $GLOBALS['_composer_autoload_path'] = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/vendor/autoload.php"; 391 | } 392 | 393 | if ($components) { 394 | $skippedTests = $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] ?? false; 395 | $runningProcs = []; 396 | 397 | foreach ($components as $component) { 398 | // Run phpunit tests in parallel 399 | 400 | if ($skippedTests) { 401 | putenv("SYMFONY_PHPUNIT_SKIPPED_TESTS=$component/$skippedTests"); 402 | } 403 | 404 | $c = escapeshellarg($component); 405 | 406 | if ($proc = proc_open(sprintf($cmd, $c, " > $c/phpunit.stdout 2> $c/phpunit.stderr"), [], $pipes)) { 407 | $runningProcs[$component] = $proc; 408 | } else { 409 | $exit = 1; 410 | echo "\033[41mKO\033[0m $component\n\n"; 411 | } 412 | } 413 | 414 | $lastOutput = null; 415 | $lastOutputTime = null; 416 | 417 | while ($runningProcs) { 418 | usleep(300000); 419 | $terminatedProcs = []; 420 | foreach ($runningProcs as $component => $proc) { 421 | $procStatus = proc_get_status($proc); 422 | if (!$procStatus['running']) { 423 | $terminatedProcs[$component] = $procStatus['exitcode']; 424 | unset($runningProcs[$component]); 425 | proc_close($proc); 426 | } 427 | } 428 | 429 | if (!$terminatedProcs && 1 === count($runningProcs)) { 430 | $component = key($runningProcs); 431 | 432 | $output = file_get_contents("$component/phpunit.stdout"); 433 | $output .= file_get_contents("$component/phpunit.stderr"); 434 | 435 | if ($lastOutput !== $output) { 436 | $lastOutput = $output; 437 | $lastOutputTime = microtime(true); 438 | } elseif (microtime(true) - $lastOutputTime > 60) { 439 | echo "\033[41mTimeout\033[0m $component\n\n"; 440 | 441 | if ('\\' === \DIRECTORY_SEPARATOR) { 442 | exec(sprintf('taskkill /F /T /PID %d 2>&1', $procStatus['pid']), $output, $exitCode); 443 | } else { 444 | proc_terminate(current($runningProcs)); 445 | } 446 | } 447 | } 448 | 449 | foreach ($terminatedProcs as $component => $procStatus) { 450 | foreach (['out', 'err'] as $file) { 451 | $file = "$component/phpunit.std$file"; 452 | readfile($file); 453 | unlink($file); 454 | } 455 | 456 | // Fail on any individual component failures but ignore some error codes on Windows when APCu is enabled: 457 | // STATUS_STACK_BUFFER_OVERRUN (-1073740791/0xC0000409) 458 | // STATUS_ACCESS_VIOLATION (-1073741819/0xC0000005) 459 | // STATUS_HEAP_CORRUPTION (-1073740940/0xC0000374) 460 | if ($procStatus && ('\\' !== \DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !filter_var(ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN) || !in_array($procStatus, [-1073740791, -1073741819, -1073740940]))) { 461 | $exit = $procStatus; 462 | echo "\033[41mKO\033[0m $component\n\n"; 463 | } else { 464 | echo "\033[32mOK\033[0m $component\n\n"; 465 | } 466 | } 467 | } 468 | } elseif (!isset($argv[1]) || 'install' !== $argv[1] || file_exists('install')) { 469 | if (!class_exists(\SymfonyExcludeListSimplePhpunit::class, false)) { 470 | class SymfonyExcludeListSimplePhpunit 471 | { 472 | } 473 | } 474 | array_splice($argv, 1, 0, ['--colors='.('' === $getEnvVar('NO_COLOR', '') ? 'always' : 'never')]); 475 | $_SERVER['argv'] = $argv; 476 | $_SERVER['argc'] = ++$argc; 477 | include "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit"; 478 | } 479 | 480 | exit($exit); 481 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | use Doctrine\Common\Annotations\AnnotationRegistry; 13 | use Doctrine\Deprecations\Deprecation; 14 | use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; 15 | 16 | // Detect if we need to serialize deprecations to a file. 17 | if (in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) { 18 | DeprecationErrorHandler::collectDeprecations($file); 19 | 20 | return; 21 | } 22 | 23 | // Detect if we're loaded by an actual run of phpunit 24 | if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists(\PHPUnit\TextUI\Command::class, false)) { 25 | return; 26 | } 27 | 28 | if (isset($fileIdentifier)) { 29 | unset($GLOBALS['__composer_autoload_files'][$fileIdentifier]); 30 | } 31 | 32 | if (class_exists(Deprecation::class)) { 33 | Deprecation::withoutDeduplication(); 34 | 35 | if (\PHP_VERSION_ID < 80000) { 36 | // Ignore deprecations about the annotation mapping driver when it's not possible to move to the attribute driver yet 37 | Deprecation::ignoreDeprecations('https://github.com/doctrine/orm/issues/10098'); 38 | } 39 | } 40 | 41 | if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { 42 | if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { 43 | AnnotationRegistry::registerUniqueLoader('class_exists'); 44 | } elseif (method_exists(AnnotationRegistry::class, 'registerLoader')) { 45 | AnnotationRegistry::registerLoader('class_exists'); 46 | } 47 | } 48 | 49 | if ('disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) { 50 | DeprecationErrorHandler::register(getenv('SYMFONY_DEPRECATIONS_HELPER')); 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/phpunit-bridge", 3 | "type": "symfony-bridge", 4 | "description": "Provides utilities for PHPUnit, especially user deprecation notices management", 5 | "keywords": [ 6 | "testing" 7 | ], 8 | "homepage": "https://symfony.com", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Nicolas Grekas", 13 | "email": "p@tchwork.com" 14 | }, 15 | { 16 | "name": "Symfony Community", 17 | "homepage": "https://symfony.com/contributors" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=7.2.5 EVEN ON LATEST SYMFONY VERSIONS TO ALLOW USING", 22 | "php": "THIS BRIDGE WHEN TESTING LOWEST SYMFONY VERSIONS.", 23 | "php": ">=7.2.5" 24 | }, 25 | "require-dev": { 26 | "symfony/deprecation-contracts": "^2.5|^3.0", 27 | "symfony/error-handler": "^5.4|^6.4|^7.0", 28 | "symfony/polyfill-php81": "^1.27" 29 | }, 30 | "conflict": { 31 | "phpunit/phpunit": "<7.5|9.1.2" 32 | }, 33 | "autoload": { 34 | "files": [ "bootstrap.php" ], 35 | "psr-4": { "Symfony\\Bridge\\PhpUnit\\": "" }, 36 | "exclude-from-classmap": [ 37 | "/Tests/", 38 | "/bin/" 39 | ] 40 | }, 41 | "bin": [ 42 | "bin/simple-phpunit" 43 | ], 44 | "minimum-stability": "dev", 45 | "extra": { 46 | "thanks": { 47 | "name": "phpunit/phpunit", 48 | "url": "https://github.com/sebastianbergmann/phpunit" 49 | } 50 | } 51 | } 52 | --------------------------------------------------------------------------------