diff --git a/.gitattributes b/.gitattributes index 14c3c359..0a9dafc4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore +/phpunit-9.6.xml.dist export-ignore /.git* export-ignore diff --git a/Attribute/DnsSensitive.php b/Attribute/DnsSensitive.php new file mode 100644 index 00000000..4c80ec5e --- /dev/null +++ b/Attribute/DnsSensitive.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class DnsSensitive +{ + public function __construct( + public readonly ?string $class = null, + ) { + } +} diff --git a/Attribute/TimeSensitive.php b/Attribute/TimeSensitive.php new file mode 100644 index 00000000..da9e816a --- /dev/null +++ b/Attribute/TimeSensitive.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class TimeSensitive +{ + public function __construct( + public readonly ?string $class = null, + ) { + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a8be6586..579fd88a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ CHANGELOG ========= +7.4 +--- + + * Add support for mocking the `strtotime()` function + +7.3 +--- + + * Enable configuring clock and DNS mock namespaces with attributes + * Add support for CAA record type in DnsMock for improved DNS mocking capabilities + +7.2 +--- + + * Add a PHPUnit extension that registers the clock mock and DNS mock and the `DebugClassLoader` from the ErrorHandler component if present + * Add `ExpectUserDeprecationMessageTrait` with a polyfill of PHPUnit's `expectUserDeprecationMessage()` + * Use `total` for asserting deprecation count when a group is not defined + 6.4 --- diff --git a/ClassExistsMock.php b/ClassExistsMock.php index bbf9b76a..61582425 100644 --- a/ClassExistsMock.php +++ b/ClassExistsMock.php @@ -24,10 +24,8 @@ class ClassExistsMock * Configures the classes to be checked upon existence. * * @param array $classes Mocked class names as keys (case-sensitive, without leading root namespace slash) and booleans as values - * - * @return void */ - public static function withMockedClasses(array $classes) + public static function withMockedClasses(array $classes): void { self::$classes = $classes; } @@ -36,59 +34,42 @@ public static function withMockedClasses(array $classes) * Configures the enums to be checked upon existence. * * @param array $enums Mocked enums names as keys (case-sensitive, without leading root namespace slash) and booleans as values - * - * @return void */ - public static function withMockedEnums(array $enums) + public static function withMockedEnums(array $enums): void { self::$enums = $enums; self::$classes += $enums; } - /** - * @return bool - */ - public static function class_exists($name, $autoload = true) + public static function class_exists($name, $autoload = true): bool { $name = ltrim($name, '\\'); return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \class_exists($name, $autoload); } - /** - * @return bool - */ - public static function interface_exists($name, $autoload = true) + public static function interface_exists($name, $autoload = true): bool { $name = ltrim($name, '\\'); return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \interface_exists($name, $autoload); } - /** - * @return bool - */ - public static function trait_exists($name, $autoload = true) + public static function trait_exists($name, $autoload = true): bool { $name = ltrim($name, '\\'); return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \trait_exists($name, $autoload); } - /** - * @return bool - */ - public static function enum_exists($name, $autoload = true) + public static function enum_exists($name, $autoload = true): bool { $name = ltrim($name, '\\'); return isset(self::$enums[$name]) ? (bool) self::$enums[$name] : \enum_exists($name, $autoload); } - /** - * @return void - */ - public static function register($class) + public static function register($class): void { $self = static::class; @@ -96,7 +77,7 @@ public static function register($class) if (0 < strpos($class, '\\Tests\\')) { $ns = str_replace('\\Tests\\', '\\', $class); $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); - } elseif (0 === strpos($class, 'Tests\\')) { + } elseif (str_starts_with($class, 'Tests\\')) { $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); } foreach ($mockedNs as $ns) { diff --git a/ClockMock.php b/ClockMock.php index 64a7ac8f..9a9c910e 100644 --- a/ClockMock.php +++ b/ClockMock.php @@ -19,10 +19,7 @@ class ClockMock { private static $now; - /** - * @return bool|null - */ - public static function withClockMock($enable = null) + public static function withClockMock($enable = null): ?bool { if (null === $enable) { return null !== self::$now; @@ -33,10 +30,7 @@ public static function withClockMock($enable = null) return null; } - /** - * @return int - */ - public static function time() + public static function time(): int { if (null === self::$now) { return \time(); @@ -45,10 +39,7 @@ public static function time() return (int) self::$now; } - /** - * @return int - */ - public static function sleep($s) + public static function sleep($s): int { if (null === self::$now) { return \sleep($s); @@ -59,10 +50,7 @@ public static function sleep($s) return 0; } - /** - * @return void - */ - public static function usleep($us) + public static function usleep($us): void { if (null === self::$now) { \usleep($us); @@ -71,6 +59,9 @@ public static function usleep($us) } } + /** + * @return string|float + */ public static function microtime($asFloat = false) { if (null === self::$now) { @@ -81,13 +72,10 @@ public static function microtime($asFloat = false) return self::$now; } - return sprintf('%0.6f00 %d', self::$now - (int) self::$now, (int) self::$now); + return \sprintf('%0.6f00 %d', self::$now - (int) self::$now, (int) self::$now); } - /** - * @return string - */ - public static function date($format, $timestamp = null) + public static function date($format, $timestamp = null): string { if (null === $timestamp) { $timestamp = self::time(); @@ -96,10 +84,7 @@ public static function date($format, $timestamp = null) return \date($format, $timestamp); } - /** - * @return string - */ - public static function gmdate($format, $timestamp = null) + public static function gmdate($format, $timestamp = null): string { if (null === $timestamp) { $timestamp = self::time(); @@ -116,7 +101,7 @@ public static function hrtime($asNumber = false) $ns = (self::$now - (int) self::$now) * 1000000000; if ($asNumber) { - $number = sprintf('%d%d', (int) self::$now, $ns); + $number = \sprintf('%d%d', (int) self::$now, $ns); return \PHP_INT_SIZE === 8 ? (int) $number : (float) $number; } @@ -125,9 +110,18 @@ public static function hrtime($asNumber = false) } /** - * @return void + * @return false|int */ - public static function register($class) + public static function strtotime(string $datetime, ?int $timestamp = null) + { + if (null === $timestamp) { + $timestamp = self::time(); + } + + return \strtotime($datetime, $timestamp); + } + + public static function register($class): void { $self = static::class; @@ -135,7 +129,7 @@ public static function register($class) if (0 < strpos($class, '\\Tests\\')) { $ns = str_replace('\\Tests\\', '\\', $class); $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); - } elseif (0 === strpos($class, 'Tests\\')) { + } elseif (str_starts_with($class, 'Tests\\')) { $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); } foreach ($mockedNs as $ns) { @@ -179,6 +173,11 @@ function hrtime(\$asNumber = false) { return \\$self::hrtime(\$asNumber); } + +function strtotime(\$datetime, \$timestamp = null) +{ + return \\$self::strtotime(\$datetime, \$timestamp); +} EOPHP ); } diff --git a/ConstraintTrait.php b/ConstraintTrait.php index ceb60418..9090cc4c 100644 --- a/ConstraintTrait.php +++ b/ConstraintTrait.php @@ -14,12 +14,7 @@ use PHPUnit\Framework\Constraint\Constraint; $r = new \ReflectionClass(Constraint::class); -if ($r->getProperty('exporter')->isProtected()) { - trait ConstraintTrait - { - use Legacy\ConstraintTraitForV7; - } -} elseif (!$r->getMethod('evaluate')->hasReturnType()) { +if (!$r->getMethod('evaluate')->hasReturnType()) { trait ConstraintTrait { use Legacy\ConstraintTraitForV8; diff --git a/CoverageListener.php b/CoverageListener.php index f1a89ff8..c3fa8ec5 100644 --- a/CoverageListener.php +++ b/CoverageListener.php @@ -29,7 +29,7 @@ class CoverageListener implements TestListener public function __construct(?callable $sutFqcnResolver = null, bool $warningOnSutNotFound = false) { $this->sutFqcnResolver = $sutFqcnResolver ?? static function (Test $test): ?string { - $class = \get_class($test); + $class = $test::class; $sutFqcn = str_replace('\\Tests\\', '\\', $class); $sutFqcn = preg_replace('{Test$}', '', $sutFqcn); @@ -46,7 +46,7 @@ public function startTest(Test $test): void return; } - $annotations = TestUtil::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); + $annotations = TestUtil::parseTestMethodAnnotations($test::class, $test->getName(false)); $ignoredAnnotations = ['covers', 'coversDefaultClass', 'coversNothing']; @@ -86,13 +86,10 @@ public function startTest(Test $test): void private function addCoversForClassToAnnotationCache(Test $test, array $covers): void { $r = new \ReflectionProperty(TestUtil::class, 'annotationCache'); - if (\PHP_VERSION_ID < 80100) { - $r->setAccessible(true); - } $cache = $r->getValue(); $cache = array_replace_recursive($cache, [ - \get_class($test) => [ + $test::class => [ 'covers' => $covers, ], ]); @@ -102,12 +99,9 @@ private function addCoversForClassToAnnotationCache(Test $test, array $covers): private function addCoversForDocBlockInsideRegistry(Test $test, array $covers): void { - $docBlock = Registry::getInstance()->forClassName(\get_class($test)); + $docBlock = Registry::getInstance()->forClassName($test::class); $symbolAnnotations = new \ReflectionProperty($docBlock, 'symbolAnnotations'); - if (\PHP_VERSION_ID < 80100) { - $symbolAnnotations->setAccessible(true); - } // Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException $covers = array_filter($covers, function (string $class) { diff --git a/DeprecationErrorHandler.php b/DeprecationErrorHandler.php index 8b78ca71..b53d90e8 100644 --- a/DeprecationErrorHandler.php +++ b/DeprecationErrorHandler.php @@ -97,7 +97,7 @@ public static function collectDeprecations($outputFile) { $deprecations = []; $previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$previousErrorHandler) { - if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) { + if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || !str_contains($msg, '" targeting switch is equivalent to "break'))) { if ($previousErrorHandler) { return $previousErrorHandler($type, $msg, $file, $line, $context); } @@ -129,7 +129,7 @@ public static function collectDeprecations($outputFile) */ public function handleError($type, $msg, $file, $line, $context = []) { - if ((\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) || !$this->getConfiguration()->isEnabled()) { + if ((\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || !str_contains($msg, '" targeting switch is equivalent to "break'))) || !$this->getConfiguration()->isEnabled()) { return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context); } @@ -286,13 +286,7 @@ private function getConfiguration() return $this->configuration = Configuration::fromUrlEncodedString((string) $mode); } - /** - * @param string $str - * @param bool $red - * - * @return string - */ - private static function colorize($str, $red) + private static function colorize(string $str, bool $red): string { if (!self::hasColorSupport()) { return $str; @@ -304,12 +298,9 @@ private static function colorize($str, $red) } /** - * @param string[] $groups - * @param Configuration $configuration - * - * @throws \InvalidArgumentException + * @param string[] $groups */ - private function displayDeprecations($groups, $configuration) + private function displayDeprecations(array $groups, Configuration $configuration): void { $cmp = function ($a, $b) { return $b->count() - $a->count(); @@ -415,10 +406,8 @@ private static function getPhpUnitErrorHandler(): callable * * Reference: Composer\XdebugHandler\Process::supportsColor * https://github.com/composer/xdebug-handler - * - * @return bool */ - private static function hasColorSupport() + private static function hasColorSupport(): bool { if (!\defined('STDOUT')) { return false; @@ -429,11 +418,18 @@ private static function hasColorSupport() return false; } - if (!self::isTty()) { + // Follow https://force-color.org/ + if ('' !== (($_SERVER['FORCE_COLOR'] ?? getenv('FORCE_COLOR'))[0] ?? '')) { + return true; + } + + // Detect msysgit/mingw and assume this is a tty because detection + // does not work correctly, see https://github.com/composer/composer/issues/9690 + if (!@stream_isatty(\STDOUT) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { return false; } - if ('\\' === \DIRECTORY_SEPARATOR && \function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support(\STDOUT)) { + if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support(\STDOUT)) { return true; } @@ -452,34 +448,4 @@ private static function hasColorSupport() // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); } - - /** - * Checks if the stream is a TTY, i.e; whether the output stream is connected to a terminal. - * - * Reference: Composer\Util\Platform::isTty - * https://github.com/composer/composer - */ - private static function isTty(): bool - { - // Detect msysgit/mingw and assume this is a tty because detection - // does not work correctly, see https://github.com/composer/composer/issues/9690 - if (\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { - return true; - } - - // Modern cross-platform function, includes the fstat fallback so if it is present we trust it - if (\function_exists('stream_isatty')) { - return @stream_isatty(\STDOUT); - } - - // Only trusting this if it is positive, otherwise prefer fstat fallback. - if (\function_exists('posix_isatty') && @posix_isatty(\STDOUT)) { - return true; - } - - $stat = @fstat(\STDOUT); - - // Check if formatted mode is S_IFCHR - return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; - } } diff --git a/DeprecationErrorHandler/Configuration.php b/DeprecationErrorHandler/Configuration.php index 108b6373..c984b73d 100644 --- a/DeprecationErrorHandler/Configuration.php +++ b/DeprecationErrorHandler/Configuration.php @@ -70,7 +70,7 @@ class Configuration * @param string $baselineFile The path to the baseline file * @param string|null $logFile The path to the log file */ - private function __construct(array $thresholds = [], $regex = '', $verboseOutput = [], $ignoreFile = '', $generateBaseline = false, $baselineFile = '', $logFile = null) + private function __construct(array $thresholds = [], string $regex = '', array $verboseOutput = [], string $ignoreFile = '', bool $generateBaseline = false, string $baselineFile = '', ?string $logFile = null) { $groups = ['total', 'indirect', 'direct', 'self']; @@ -96,7 +96,7 @@ private function __construct(array $thresholds = [], $regex = '', $verboseOutput } foreach ($groups as $group) { if (!isset($this->thresholds[$group])) { - $this->thresholds[$group] = 999999; + $this->thresholds[$group] = $this->thresholds['total'] ?? 999999; } } $this->regex = $regex; @@ -279,10 +279,7 @@ public function writeBaseline(): void file_put_contents($this->baselineFile, json_encode($map, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); } - /** - * @param string $message - */ - public function shouldDisplayStackTrace($message): bool + public function shouldDisplayStackTrace(string $message): bool { return '' !== $this->regex && preg_match($this->regex, $message); } @@ -308,10 +305,9 @@ public function getLogFile(): ?string } /** - * @param string $serializedConfiguration an encoded string, for instance - * max[total]=1234&max[indirect]=42 + * @param string $serializedConfiguration An encoded string, for instance max[total]=1234&max[indirect]=42 */ - public static function fromUrlEncodedString($serializedConfiguration): self + public static function fromUrlEncodedString(string $serializedConfiguration): self { parse_str($serializedConfiguration, $normalizedConfiguration); foreach (array_keys($normalizedConfiguration) as $key) { diff --git a/DeprecationErrorHandler/Deprecation.php b/DeprecationErrorHandler/Deprecation.php index 082509fd..caf0b825 100644 --- a/DeprecationErrorHandler/Deprecation.php +++ b/DeprecationErrorHandler/Deprecation.php @@ -55,12 +55,7 @@ class Deprecation private $originalFilesStack; - /** - * @param string $message - * @param string $file - * @param bool $languageDeprecation - */ - public function __construct($message, array $trace, $file, $languageDeprecation = false) + public function __construct(string $message, array $trace, string $file, bool $languageDeprecation = false) { if (DebugClassLoader::class === ($trace[2]['class'] ?? '')) { $this->triggeringClass = $trace[2]['args'][0]; @@ -104,7 +99,7 @@ public function __construct($message, array $trace, $file, $languageDeprecation $this->getOriginalFilesStack(); array_splice($this->originalFilesStack, 0, $j, [$this->triggeringFile]); - 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))) { + if (preg_match('/(?|"([^"]++)" that is deprecated|should implement method "(?:static )?([^:]++))/', $message, $m) || (!str_contains($message, '()" will return') && !str_contains($message, 'native return type declaration') && preg_match('/^(?:The|Method) "([^":]++)/', $message, $m))) { $this->triggeringFile = (new \ReflectionClass($m[1]))->getFileName(); array_unshift($this->originalFilesStack, $this->triggeringFile); } @@ -142,7 +137,7 @@ public function __construct($message, array $trace, $file, $languageDeprecation return; } - if (!isset($line['class'], $trace[$i - 2]['function']) || 0 !== strpos($line['class'], SymfonyTestsListenerFor::class)) { + if (!isset($line['class'], $trace[$i - 2]['function']) || !str_starts_with($line['class'], SymfonyTestsListenerFor::class)) { $this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class']; $this->originMethod = $line['function']; @@ -152,38 +147,27 @@ public function __construct($message, array $trace, $file, $languageDeprecation $test = $line['args'][0] ?? null; if (($test instanceof TestCase || $test instanceof TestSuite) && ('trigger_error' !== $trace[$i - 2]['function'] || isset($trace[$i - 2]['class']))) { - $this->originClass = \get_class($test); + $this->originClass = $test::class; $this->originMethod = $test->getName(); - - return; } } - /** - * @return bool - */ - private function lineShouldBeSkipped(array $line) + private function lineShouldBeSkipped(array $line): bool { if (!isset($line['class'])) { return true; } $class = $line['class']; - return 'ReflectionMethod' === $class || 0 === strpos($class, 'PHPUnit\\'); + return 'ReflectionMethod' === $class || str_starts_with($class, 'PHPUnit\\'); } - /** - * @return bool - */ - public function originatesFromDebugClassLoader() + public function originatesFromDebugClassLoader(): bool { return isset($this->triggeringClass); } - /** - * @return string - */ - public function triggeringClass() + public function triggeringClass(): string { if (null === $this->triggeringClass) { throw new \LogicException('Check with originatesFromDebugClassLoader() before calling this method.'); @@ -192,18 +176,12 @@ public function triggeringClass() return $this->triggeringClass; } - /** - * @return bool - */ - public function originatesFromAnObject() + public function originatesFromAnObject(): bool { return isset($this->originClass); } - /** - * @return string - */ - public function originatingClass() + public function originatingClass(): string { if (null === $this->originClass) { throw new \LogicException('Check with originatesFromAnObject() before calling this method.'); @@ -211,13 +189,10 @@ public function originatingClass() $class = $this->originClass; - return false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; + return str_contains($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; } - /** - * @return string - */ - public function originatingMethod() + public function originatingMethod(): string { if (null === $this->originMethod) { throw new \LogicException('Check with originatesFromAnObject() before calling this method.'); @@ -226,18 +201,12 @@ public function originatingMethod() return $this->originMethod; } - /** - * @return string - */ - public function getMessage() + public function getMessage(): string { return $this->message; } - /** - * @return bool - */ - public function isLegacy() + public function isLegacy(): bool { if (!$this->originClass || (new \ReflectionClass($this->originClass))->isInternal()) { return false; @@ -246,35 +215,30 @@ public function isLegacy() $method = $this->originatingMethod(); $groups = class_exists(Groups::class, false) ? [new Groups(), 'groups'] : [Test::class, 'getGroups']; - return 0 === strpos($method, 'testLegacy') - || 0 === strpos($method, 'provideLegacy') - || 0 === strpos($method, 'getLegacy') + return str_starts_with($method, 'testLegacy') + || str_starts_with($method, 'provideLegacy') + || str_starts_with($method, 'getLegacy') || strpos($this->originClass, '\Legacy') || \in_array('legacy', $groups($this->originClass, $method), true); } - /** - * @return bool - */ - public function isMuted() + public function isMuted(): bool { if ('Function ReflectionType::__toString() is deprecated' !== $this->message) { return false; } if (isset($this->trace[1]['class'])) { - return 0 === strpos($this->trace[1]['class'], 'PHPUnit\\'); + return str_starts_with($this->trace[1]['class'], 'PHPUnit\\'); } - return false !== strpos($this->triggeringFile, \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR.'phpunit'.\DIRECTORY_SEPARATOR); + return str_contains($this->triggeringFile, \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR.'phpunit'.\DIRECTORY_SEPARATOR); } /** * Tells whether both the calling package and the called package are vendor * packages. - * - * @return string */ - public function getType() + public function getType(): string { $pathType = $this->getPathType($this->triggeringFile); if ($this->languageDeprecation && self::PATH_TYPE_VENDOR === $pathType) { @@ -331,16 +295,12 @@ private function getOriginalFilesStack() /** * getPathType() should always be called prior to calling this method. - * - * @param string $path - * - * @return string */ - private function getPackage($path) + private function getPackage(string $path): string { $path = realpath($path) ?: $path; foreach (self::getVendors() as $vendorRoot) { - if (0 === strpos($path, $vendorRoot)) { + if (str_starts_with($path, $vendorRoot)) { $relativePath = substr($path, \strlen($vendorRoot) + 1); $vendor = strstr($relativePath, \DIRECTORY_SEPARATOR, true); if (false === $vendor) { @@ -357,7 +317,7 @@ private function getPackage($path) /** * @return string[] */ - private static function getVendors() + private static function getVendors(): array { if (null === self::$vendors) { self::$vendors = $paths = []; @@ -366,7 +326,7 @@ private static function getVendors() self::$vendors[] = \dirname((new \ReflectionClass(DebugClassLoader::class))->getFileName()); } foreach (get_declared_classes() as $class) { - if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); $v = \dirname($r->getFileName(), 2); if (file_exists($v.'/composer/installed.json')) { @@ -381,7 +341,7 @@ private static function getVendors() } foreach ($paths as $path) { foreach (self::$vendors as $vendor) { - if (0 !== strpos($path, $vendor)) { + if (!str_starts_with($path, $vendor)) { self::$internalPaths[] = $path; } } @@ -404,25 +364,20 @@ private static function addSourcePathsFromPrefixes(array $prefixesByNamespace, a return $paths; } - /** - * @param string $path - * - * @return string - */ - private function getPathType($path) + private function getPathType(string $path): string { $realPath = realpath($path); if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) { return self::PATH_TYPE_UNDETERMINED; } foreach (self::getVendors() as $vendor) { - if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) { + if (str_starts_with($realPath, $vendor) && false !== strpbrk(substr($realPath, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) { return self::PATH_TYPE_VENDOR; } } foreach (self::$internalPaths as $internalPath) { - if (0 === strpos($realPath, $internalPath)) { + if (str_starts_with($realPath, $internalPath)) { return self::PATH_TYPE_SELF; } } @@ -430,16 +385,10 @@ private function getPathType($path) return self::PATH_TYPE_UNDETERMINED; } - /** - * @return string - */ - public function toString() + public function toString(): string { $exception = new \Exception($this->message); $reflection = new \ReflectionProperty($exception, 'trace'); - if (\PHP_VERSION_ID < 80100) { - $reflection->setAccessible(true); - } $reflection->setValue($exception, $this->trace); return ($this->originatesFromAnObject() ? 'deprecation triggered by '.$this->originatingClass().'::'.$this->originatingMethod().":\n" : '') diff --git a/DeprecationErrorHandler/DeprecationGroup.php b/DeprecationErrorHandler/DeprecationGroup.php index 23b95e19..cc4b9c04 100644 --- a/DeprecationErrorHandler/DeprecationGroup.php +++ b/DeprecationErrorHandler/DeprecationGroup.php @@ -23,21 +23,13 @@ final class DeprecationGroup */ private $deprecationNotices = []; - /** - * @param string $message - * @param string $class - * @param string $method - */ - public function addNoticeFromObject($message, $class, $method) + public function addNoticeFromObject(string $message, string $class, string $method): void { $this->deprecationNotice($message)->addObjectOccurrence($class, $method); $this->addNotice(); } - /** - * @param string $message - */ - public function addNoticeFromProceduralCode($message) + public function addNoticeFromProceduralCode(string $message): void { $this->deprecationNotice($message)->addProceduralOccurrence(); $this->addNotice(); @@ -48,10 +40,7 @@ public function addNotice() ++$this->count; } - /** - * @param string $message - */ - private function deprecationNotice($message): DeprecationNotice + private function deprecationNotice(string $message): DeprecationNotice { return $this->deprecationNotices[$message] ?? $this->deprecationNotices[$message] = new DeprecationNotice(); } diff --git a/DnsMock.php b/DnsMock.php index f0145e7d..bc2ac1a8 100644 --- a/DnsMock.php +++ b/DnsMock.php @@ -30,24 +30,20 @@ class DnsMock 'NAPTR' => \DNS_NAPTR, 'TXT' => \DNS_TXT, 'HINFO' => \DNS_HINFO, + 'CAA' => '\\' !== \DIRECTORY_SEPARATOR ? \DNS_CAA : 0, ]; /** * Configures the mock values for DNS queries. * * @param array $hosts Mocked hosts as keys, arrays of DNS records as returned by dns_get_record() as values - * - * @return void */ - public static function withMockedHosts(array $hosts) + public static function withMockedHosts(array $hosts): void { self::$hosts = $hosts; } - /** - * @return bool - */ - public static function checkdnsrr($hostname, $type = 'MX') + public static function checkdnsrr($hostname, $type = 'MX'): bool { if (!self::$hosts) { return \checkdnsrr($hostname, $type); @@ -68,10 +64,7 @@ public static function checkdnsrr($hostname, $type = 'MX') return false; } - /** - * @return bool - */ - public static function getmxrr($hostname, &$mxhosts, &$weight = null) + public static function getmxrr($hostname, &$mxhosts, &$weight = null): bool { if (!self::$hosts) { return \getmxrr($hostname, $mxhosts, $weight); @@ -169,10 +162,7 @@ public static function dns_get_record($hostname, $type = \DNS_ANY, &$authns = nu return $records; } - /** - * @return void - */ - public static function register($class) + public static function register($class): void { $self = static::class; @@ -180,7 +170,7 @@ public static function register($class) if (0 < strpos($class, '\\Tests\\')) { $ns = str_replace('\\Tests\\', '\\', $class); $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); - } elseif (0 === strpos($class, 'Tests\\')) { + } elseif (str_starts_with($class, 'Tests\\')) { $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); } foreach ($mockedNs as $ns) { diff --git a/ExpectUserDeprecationMessageTrait.php b/ExpectUserDeprecationMessageTrait.php new file mode 100644 index 00000000..ed94c84f --- /dev/null +++ b/ExpectUserDeprecationMessageTrait.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +use PHPUnit\Runner\Version; + +if (version_compare(Version::id(), '11.0.0', '<')) { + trait ExpectUserDeprecationMessageTrait + { + use ExpectDeprecationTrait; + + final protected function expectUserDeprecationMessage(string $expectedUserDeprecationMessage): void + { + $this->expectDeprecation(str_replace('%', '%%', $expectedUserDeprecationMessage)); + } + } +} else { + trait ExpectUserDeprecationMessageTrait + { + } +} diff --git a/Extension/EnableClockMockSubscriber.php b/Extension/EnableClockMockSubscriber.php new file mode 100644 index 00000000..b3d56334 --- /dev/null +++ b/Extension/EnableClockMockSubscriber.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Extension; + +use PHPUnit\Event\Code\TestMethod; +use PHPUnit\Event\Test\PreparationStarted; +use PHPUnit\Event\Test\PreparationStartedSubscriber; +use PHPUnit\Metadata\Group; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; +use Symfony\Bridge\PhpUnit\ClockMock; +use Symfony\Bridge\PhpUnit\Metadata\AttributeReader; + +/** + * @internal + */ +class EnableClockMockSubscriber implements PreparationStartedSubscriber +{ + public function __construct( + private AttributeReader $reader, + ) { + } + + public function notify(PreparationStarted $event): void + { + $test = $event->test(); + + if (!$test instanceof TestMethod) { + return; + } + + foreach ($test->metadata() as $metadata) { + if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) { + ClockMock::withClockMock(true); + break; + } + } + + if ($this->reader->forClassAndMethod($test->className(), $test->methodName(), TimeSensitive::class)) { + ClockMock::withClockMock(true); + } + } +} diff --git a/Extension/RegisterClockMockSubscriber.php b/Extension/RegisterClockMockSubscriber.php new file mode 100644 index 00000000..b89f1640 --- /dev/null +++ b/Extension/RegisterClockMockSubscriber.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Extension; + +use PHPUnit\Event\Code\TestMethod; +use PHPUnit\Event\TestSuite\Loaded; +use PHPUnit\Event\TestSuite\LoadedSubscriber; +use PHPUnit\Metadata\Group; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; +use Symfony\Bridge\PhpUnit\ClockMock; +use Symfony\Bridge\PhpUnit\Metadata\AttributeReader; + +/** + * @internal + */ +class RegisterClockMockSubscriber implements LoadedSubscriber +{ + public function __construct( + private AttributeReader $reader, + ) { + } + + public function notify(Loaded $event): void + { + foreach ($event->testSuite()->tests() as $test) { + if (!$test instanceof TestMethod) { + continue; + } + + foreach ($test->metadata() as $metadata) { + if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) { + ClockMock::register($test->className()); + } + } + + foreach ($this->reader->forClassAndMethod($test->className(), $test->methodName(), TimeSensitive::class) as $attribute) { + ClockMock::register($attribute->class ?? $test->className()); + } + } + } +} diff --git a/Extension/RegisterDnsMockSubscriber.php b/Extension/RegisterDnsMockSubscriber.php new file mode 100644 index 00000000..80e9a337 --- /dev/null +++ b/Extension/RegisterDnsMockSubscriber.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Extension; + +use PHPUnit\Event\Code\TestMethod; +use PHPUnit\Event\TestSuite\Loaded; +use PHPUnit\Event\TestSuite\LoadedSubscriber; +use PHPUnit\Metadata\Group; +use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive; +use Symfony\Bridge\PhpUnit\DnsMock; +use Symfony\Bridge\PhpUnit\Metadata\AttributeReader; + +/** + * @internal + */ +class RegisterDnsMockSubscriber implements LoadedSubscriber +{ + public function __construct( + private AttributeReader $reader, + ) { + } + + public function notify(Loaded $event): void + { + foreach ($event->testSuite()->tests() as $test) { + if (!$test instanceof TestMethod) { + continue; + } + + foreach ($test->metadata() as $metadata) { + if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) { + DnsMock::register($test->className()); + } + } + + foreach ($this->reader->forClassAndMethod($test->className(), $test->methodName(), DnsSensitive::class) as $attribute) { + DnsMock::register($attribute->class ?? $test->className()); + } + } + } +} diff --git a/Legacy/CommandForV7.php b/Legacy/CommandForV8.php similarity index 97% rename from Legacy/CommandForV7.php rename to Legacy/CommandForV8.php index 99a1e683..ffeb2b81 100644 --- a/Legacy/CommandForV7.php +++ b/Legacy/CommandForV8.php @@ -19,7 +19,7 @@ /** * @internal */ -class CommandForV7 extends BaseCommand +class CommandForV8 extends BaseCommand { protected function createRunner(): BaseRunner { diff --git a/Legacy/ConstraintTraitForV7.php b/Legacy/ConstraintTraitForV7.php deleted file mode 100644 index b132f473..00000000 --- a/Legacy/ConstraintTraitForV7.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use SebastianBergmann\Exporter\Exporter; - -/** - * @internal - */ -trait ConstraintTraitForV7 -{ - use ConstraintLogicTrait; - - /** - * @return bool|null - */ - public function evaluate($other, $description = '', $returnResult = false) - { - return $this->doEvaluate($other, $description, $returnResult); - } - - public function count(): int - { - return $this->doCount(); - } - - public function toString(): string - { - return $this->doToString(); - } - - protected function additionalFailureDescription($other): string - { - return $this->doAdditionalFailureDescription($other); - } - - protected function exporter(): Exporter - { - if (null === $this->exporter) { - $this->exporter = new Exporter(); - } - - return $this->exporter; - } - - protected function failureDescription($other): string - { - return $this->doFailureDescription($other); - } - - protected function matches($other): bool - { - return $this->doMatches($other); - } -} diff --git a/Legacy/ExpectDeprecationTraitForV8_4.php b/Legacy/ExpectDeprecationTraitForV8_4.php index d1596352..95823c2c 100644 --- a/Legacy/ExpectDeprecationTraitForV8_4.php +++ b/Legacy/ExpectDeprecationTraitForV8_4.php @@ -22,7 +22,7 @@ trait ExpectDeprecationTraitForV8_4 public function expectDeprecation(): void { if (1 > \func_num_args() || !\is_string($message = func_get_arg(0))) { - throw new \InvalidArgumentException(sprintf('The "%s()" method requires the string $message argument.', __FUNCTION__)); + throw new \InvalidArgumentException(\sprintf('The "%s()" method requires the string $message argument.', __FUNCTION__)); } // Expected deprecations set by isolated tests need to be written to a file @@ -52,7 +52,7 @@ public function expectDeprecation(): void */ public function expectDeprecationMessage(string $message): void { - throw new \BadMethodCallException(sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait, pass the message to expectDeprecation() instead.', __FUNCTION__)); + throw new \BadMethodCallException(\sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait, pass the message to expectDeprecation() instead.', __FUNCTION__)); } /** @@ -60,6 +60,6 @@ public function expectDeprecationMessage(string $message): void */ public function expectDeprecationMessageMatches(string $regularExpression): void { - throw new \BadMethodCallException(sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait.', __FUNCTION__)); + throw new \BadMethodCallException(\sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait.', __FUNCTION__)); } } diff --git a/Legacy/PolyfillTestCaseTrait.php b/Legacy/PolyfillTestCaseTrait.php deleted file mode 100644 index 8673bdc0..00000000 --- a/Legacy/PolyfillTestCaseTrait.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use PHPUnit\Framework\Error\Error; -use PHPUnit\Framework\Error\Notice; -use PHPUnit\Framework\Error\Warning; - -/** - * This trait is @internal. - */ -trait PolyfillTestCaseTrait -{ - /** - * @param string $messageRegExp - * - * @return void - */ - public function expectExceptionMessageMatches($messageRegExp) - { - $this->expectExceptionMessageRegExp($messageRegExp); - } - - /** - * @return void - */ - public function expectNotice() - { - $this->expectException(Notice::class); - } - - /** - * @param string $message - * - * @return void - */ - public function expectNoticeMessage($message) - { - $this->expectExceptionMessage($message); - } - - /** - * @param string $regularExpression - * - * @return void - */ - public function expectNoticeMessageMatches($regularExpression) - { - $this->expectExceptionMessageMatches($regularExpression); - } - - /** - * @return void - */ - public function expectWarning() - { - $this->expectException(Warning::class); - } - - /** - * @param string $message - * - * @return void - */ - public function expectWarningMessage($message) - { - $this->expectExceptionMessage($message); - } - - /** - * @param string $regularExpression - * - * @return void - */ - public function expectWarningMessageMatches($regularExpression) - { - $this->expectExceptionMessageMatches($regularExpression); - } - - /** - * @return void - */ - public function expectError() - { - $this->expectException(Error::class); - } - - /** - * @param string $message - * - * @return void - */ - public function expectErrorMessage($message) - { - $this->expectExceptionMessage($message); - } - - /** - * @param string $regularExpression - * - * @return void - */ - public function expectErrorMessageMatches($regularExpression) - { - $this->expectExceptionMessageMatches($regularExpression); - } -} diff --git a/Legacy/SymfonyTestsListenerTrait.php b/Legacy/SymfonyTestsListenerTrait.php index 4e591c76..2b7a3d1b 100644 --- a/Legacy/SymfonyTestsListenerTrait.php +++ b/Legacy/SymfonyTestsListenerTrait.php @@ -58,7 +58,7 @@ public function __construct(array $mockedNamespaces = []) (new ExcludeList())->getExcludedDirectories(); ExcludeList::addDirectory(\dirname((new \ReflectionClass(__CLASS__))->getFileName(), 2)); } elseif (method_exists(Blacklist::class, 'addDirectory')) { - (new BlackList())->getBlacklistedDirectories(); + (new Blacklist())->getBlacklistedDirectories(); Blacklist::addDirectory(\dirname((new \ReflectionClass(__CLASS__))->getFileName(), 2)); } else { Blacklist::$blacklistedClassNames[__CLASS__] = 2; @@ -125,7 +125,7 @@ public function startTestSuite($suite): void if (!$test instanceof TestCase) { continue; } - if (null === Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) { + if (null === Test::getPreserveGlobalStateSettings($test::class, $test->getName(false))) { $test->setPreserveGlobalState(false); } } @@ -182,7 +182,7 @@ public function startTestSuite($suite): void continue; } if ($test instanceof TestCase - && isset($this->wasSkipped[\get_class($test)][$test->getName()]) + && isset($this->wasSkipped[$test::class][$test->getName()]) ) { $skipped[] = $test; } @@ -197,10 +197,10 @@ public function addSkippedTest($test, \Exception $e, $time): void if (0 < $this->state) { if ($test instanceof DataProviderTestSuite) { foreach ($test->tests() as $testWithDataProvider) { - $this->isSkipped[\get_class($testWithDataProvider)][$testWithDataProvider->getName()] = 1; + $this->isSkipped[$testWithDataProvider::class][$testWithDataProvider->getName()] = 1; } } else { - $this->isSkipped[\get_class($test)][$test->getName()] = 1; + $this->isSkipped[$test::class][$test->getName()] = 1; } } } @@ -221,15 +221,15 @@ public function startTest($test): void putenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE='.tempnam(sys_get_temp_dir(), 'expectdeprec')); } - $groups = Test::getGroups(\get_class($test), $test->getName(false)); + $groups = Test::getGroups($test::class, $test->getName(false)); if (!$this->runsInSeparateProcess) { if (\in_array('time-sensitive', $groups, true)) { - ClockMock::register(\get_class($test)); + ClockMock::register($test::class); ClockMock::withClockMock(true); } if (\in_array('dns-sensitive', $groups, true)) { - DnsMock::register(\get_class($test)); + DnsMock::register($test::class); } } @@ -237,7 +237,7 @@ public function startTest($test): void return; } - $annotations = Test::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); + $annotations = Test::parseTestMethodAnnotations($test::class, $test->getName(false)); if (isset($annotations['class']['expectedDeprecation'])) { $test->getTestResultObject()->addError($test, new AssertionFailedError('"@expectedDeprecation" annotations are not allowed at the class level.'), 0); @@ -275,14 +275,14 @@ public function endTest($test, $time): void DebugClassLoader::checkClasses(); } - $className = \get_class($test); + $className = $test::class; $groups = Test::getGroups($className, $test->getName(false)); if ($this->checkNumAssertions) { $assertions = \count(self::$expectedDeprecations) + $test->getNumAssertions(); if ($test instanceof TestCase && $test->doesNotPerformAssertions() && $assertions > 0) { - $test->getTestResultObject()->addFailure($test, new RiskyTestError(sprintf('This test is annotated with "@doesNotPerformAssertions", but performed %s assertions', $assertions)), $time); - } elseif ($test instanceof TestCase && $assertions === 0 && !$test->doesNotPerformAssertions() && $test->getTestResultObject()->noneSkipped()) { + $test->getTestResultObject()->addFailure($test, new RiskyTestError(\sprintf('This test is annotated with "@doesNotPerformAssertions", but performed %s assertions', $assertions)), $time); + } elseif ($test instanceof TestCase && 0 === $assertions && !$test->doesNotPerformAssertions() && $test->getTestResultObject()->noneSkipped()) { $test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time); } @@ -343,7 +343,7 @@ public static function handleError($type, $msg, $file, $line, $context = []) return $h ? $h($type, $msg, $file, $line, $context) : false; } - // If the message is serialized we need to extract the message. This occurs when the error is triggered by + // If the message is serialized we need to extract the message. This occurs when the error is triggered // by the isolated test path in \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest(). $parsedMsg = @unserialize($msg); if (\is_array($parsedMsg)) { @@ -364,9 +364,6 @@ private function willBeIsolated(TestCase $test): bool } $r = new \ReflectionProperty($test, 'runTestInSeparateProcess'); - if (\PHP_VERSION_ID < 80100) { - $r->setAccessible(true); - } return $r->getValue($test) ?? false; } diff --git a/Metadata/AttributeReader.php b/Metadata/AttributeReader.php new file mode 100644 index 00000000..ca4e4c47 --- /dev/null +++ b/Metadata/AttributeReader.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Metadata; + +/** + * @internal + * + * @template T of object + */ +final class AttributeReader +{ + /** + * @var array, list>> + */ + private array $cache = []; + + /** + * @param class-string $className + * @param class-string $name + * + * @return list + */ + public function forClass(string $className, string $name): array + { + $attributes = $this->cache[$className] ??= $this->readAttributes(new \ReflectionClass($className)); + + return $attributes[$name] ?? []; + } + + /** + * @param class-string $className + * @param class-string $name + * + * @return list + */ + public function forMethod(string $className, string $methodName, string $name): array + { + $attributes = $this->cache[$className.'::'.$methodName] ??= $this->readAttributes(new \ReflectionMethod($className, $methodName)); + + return $attributes[$name] ?? []; + } + + /** + * @param class-string $className + * @param class-string $name + * + * @return list + */ + public function forClassAndMethod(string $className, string $methodName, string $name): array + { + return [ + ...$this->forClass($className, $name), + ...$this->forMethod($className, $methodName, $name), + ]; + } + + private function readAttributes(\ReflectionClass|\ReflectionMethod $reflection): array + { + $attributeInstances = []; + + foreach ($reflection->getAttributes() as $attribute) { + if (!str_starts_with($name = $attribute->getName(), 'Symfony\\Bridge\\PhpUnit\\Attribute\\')) { + continue; + } + + $attributeInstances[$name][] = $attribute->newInstance(); + } + + return $attributeInstances; + } +} diff --git a/SymfonyExtension.php b/SymfonyExtension.php new file mode 100644 index 00000000..a4f28a78 --- /dev/null +++ b/SymfonyExtension.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +use Doctrine\Deprecations\Deprecation; +use PHPUnit\Event\Code\Test; +use PHPUnit\Event\Code\TestMethod; +use PHPUnit\Event\Test\BeforeTestMethodErrored; +use PHPUnit\Event\Test\BeforeTestMethodErroredSubscriber; +use PHPUnit\Event\Test\Errored; +use PHPUnit\Event\Test\ErroredSubscriber; +use PHPUnit\Event\Test\Finished; +use PHPUnit\Event\Test\FinishedSubscriber; +use PHPUnit\Event\Test\Skipped; +use PHPUnit\Event\Test\SkippedSubscriber; +use PHPUnit\Metadata\Group; +use PHPUnit\Runner\Extension\Extension; +use PHPUnit\Runner\Extension\Facade; +use PHPUnit\Runner\Extension\ParameterCollection; +use PHPUnit\TextUI\Configuration\Configuration; +use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; +use Symfony\Bridge\PhpUnit\Extension\EnableClockMockSubscriber; +use Symfony\Bridge\PhpUnit\Extension\RegisterClockMockSubscriber; +use Symfony\Bridge\PhpUnit\Extension\RegisterDnsMockSubscriber; +use Symfony\Bridge\PhpUnit\Metadata\AttributeReader; +use Symfony\Component\ErrorHandler\DebugClassLoader; + +class SymfonyExtension implements Extension +{ + public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void + { + if (class_exists(DebugClassLoader::class)) { + DebugClassLoader::enable(); + } + + if (class_exists(Deprecation::class)) { + Deprecation::withoutDeduplication(); + } + + $reader = new AttributeReader(); + + if ($parameters->has('clock-mock-namespaces')) { + foreach (explode(',', $parameters->get('clock-mock-namespaces')) as $namespace) { + ClockMock::register($namespace.'\DummyClass'); + } + } + + $facade->registerSubscriber(new RegisterClockMockSubscriber($reader)); + $facade->registerSubscriber(new EnableClockMockSubscriber($reader)); + $facade->registerSubscriber(new class($reader) implements ErroredSubscriber { + public function __construct(private AttributeReader $reader) + { + } + + public function notify(Errored $event): void + { + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); + } + }); + $facade->registerSubscriber(new class($reader) implements FinishedSubscriber { + public function __construct(private AttributeReader $reader) + { + } + + public function notify(Finished $event): void + { + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); + } + }); + $facade->registerSubscriber(new class($reader) implements SkippedSubscriber { + public function __construct(private AttributeReader $reader) + { + } + + public function notify(Skipped $event): void + { + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); + } + }); + + if (interface_exists(BeforeTestMethodErroredSubscriber::class)) { + $facade->registerSubscriber(new class($reader) implements BeforeTestMethodErroredSubscriber { + public function __construct(private AttributeReader $reader) + { + } + + public function notify(BeforeTestMethodErrored $event): void + { + if (method_exists($event, 'test')) { + SymfonyExtension::disableClockMock($event->test(), $this->reader); + SymfonyExtension::disableDnsMock($event->test(), $this->reader); + } else { + ClockMock::withClockMock(false); + DnsMock::withMockedHosts([]); + } + } + }); + } + + if ($parameters->has('dns-mock-namespaces')) { + foreach (explode(',', $parameters->get('dns-mock-namespaces')) as $namespace) { + DnsMock::register($namespace.'\DummyClass'); + } + } + + $facade->registerSubscriber(new RegisterDnsMockSubscriber($reader)); + } + + /** + * @internal + */ + public static function disableClockMock(Test $test, AttributeReader $reader): void + { + if (self::hasGroup($test, 'time-sensitive', $reader, TimeSensitive::class)) { + ClockMock::withClockMock(false); + } + } + + /** + * @internal + */ + public static function disableDnsMock(Test $test, AttributeReader $reader): void + { + if (self::hasGroup($test, 'dns-sensitive', $reader, DnsSensitive::class)) { + DnsMock::withMockedHosts([]); + } + } + + /** + * @internal + */ + public static function hasGroup(Test $test, string $groupName, AttributeReader $reader, string $attribute): bool + { + if (!$test instanceof TestMethod) { + return false; + } + + foreach ($test->metadata() as $metadata) { + if ($metadata instanceof Group && $groupName === $metadata->groupName()) { + return true; + } + } + + return [] !== $reader->forClassAndMethod($test->className(), $test->methodName(), $attribute); + } +} diff --git a/Tests/ClockMockTest.php b/Tests/ClockMockTest.php index 7df7865d..95c354e1 100644 --- a/Tests/ClockMockTest.php +++ b/Tests/ClockMockTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\PhpUnit\Tests; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ClockMock; @@ -19,6 +20,7 @@ * * @covers \Symfony\Bridge\PhpUnit\ClockMock */ +#[CoversClass(ClockMock::class)] class ClockMockTest extends TestCase { public static function setUpBeforeClass(): void @@ -79,4 +81,9 @@ public function testHrTimeAsNumber() { $this->assertSame(1234567890125000000, hrtime(true)); } + + public function testStrToTime() + { + $this->assertSame(1234567890, strtotime('now')); + } } diff --git a/Tests/CoverageListenerTest.php b/Tests/CoverageListenerTest.php index 19408df6..9d6e26ed 100644 --- a/Tests/CoverageListenerTest.php +++ b/Tests/CoverageListenerTest.php @@ -11,39 +11,29 @@ namespace Symfony\Bridge\PhpUnit\Tests; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; +#[RequiresPhpunit('<10')] class CoverageListenerTest extends TestCase { public function test() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('This test cannot be run on Windows.'); - } - - exec('type phpdbg 2> /dev/null', $output, $returnCode); - - if (0 === $returnCode) { - $php = 'phpdbg -qrr'; - } else { - exec('php --ri xdebug -d zend_extension=xdebug.so 2> /dev/null', $output, $returnCode); - if (0 !== $returnCode) { - $this->markTestSkipped('Xdebug is required to run this test.'); - } - $php = 'php -d zend_extension=xdebug.so'; - } - $dir = __DIR__.'/../Tests/Fixtures/coverage'; $phpunit = $_SERVER['argv'][0]; + $php = $this->findCoverageDriver(); + + $output = ''; exec("$php $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text --colors=never 2> /dev/null", $output); $output = implode("\n", $output); $this->assertMatchesRegularExpression('/FooCov\n\s*Methods:\s+100.00%[^\n]+Lines:\s+100.00%/', $output); + $output = ''; exec("$php $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text --colors=never 2> /dev/null", $output); $output = implode("\n", $output); - if (false === strpos($output, 'FooCov')) { + if (!str_contains($output, 'FooCov')) { $this->addToAssertionCount(1); } else { $this->assertMatchesRegularExpression('/FooCov\n\s*Methods:\s+0.00%[^\n]+Lines:\s+0.00%/', $output); @@ -54,4 +44,28 @@ public function test() $this->assertStringNotContainsString("CoversDefaultClassTest::test\nCould not find the tested class.", $output); $this->assertStringNotContainsString("CoversNothingTest::test\nCould not find the tested class.", $output); } + + private function findCoverageDriver(): string + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot be run on Windows.'); + } + + exec('php --ri xdebug -d zend_extension=xdebug 2> /dev/null', $output, $returnCode); + if (0 === $returnCode) { + return 'php -d zend_extension=xdebug'; + } + + exec('php --ri pcov -d zend_extension=pcov 2> /dev/null', $output, $returnCode); + if (0 === $returnCode) { + return 'php -d zend_extension=pcov'; + } + + exec('type phpdbg 2> /dev/null', $output, $returnCode); + if (0 === $returnCode) { + return 'phpdbg -qrr'; + } + + $this->markTestSkipped('Xdebug or pvoc is required to run this test.'); + } } diff --git a/Tests/DeprecationErrorHandler/ConfigurationTest.php b/Tests/DeprecationErrorHandler/ConfigurationTest.php index a2259fc1..3faadf33 100644 --- a/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -11,12 +11,15 @@ namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; use Symfony\Component\ErrorHandler\DebugClassLoader; +#[RequiresPhpunit('<10')] class ConfigurationTest extends TestCase { private $files; @@ -192,6 +195,7 @@ public static function provideItCanBeDisabled(): array /** * @dataProvider provideItCanBeDisabled */ + #[DataProvider('provideItCanBeDisabled')] public function testItCanBeDisabled(string $encodedString, bool $expectedEnabled) { $configuration = Configuration::fromUrlEncodedString($encodedString); @@ -238,6 +242,7 @@ public function testOutputIsNotVerboseInWeakMode() /** * @dataProvider provideDataForToleratesForGroup */ + #[DataProvider('provideDataForToleratesForGroup')] public function testToleratesForIndividualGroups(string $deprecationsHelper, array $deprecationsPerType, array $expected) { $configuration = Configuration::fromUrlEncodedString($deprecationsHelper); @@ -245,7 +250,7 @@ public function testToleratesForIndividualGroups(string $deprecationsHelper, arr $groups = $this->buildGroups($deprecationsPerType); foreach ($expected as $groupName => $tolerates) { - $this->assertSame($tolerates, $configuration->toleratesForGroup($groupName, $groups), sprintf('Deprecation type "%s" is %s', $groupName, $tolerates ? 'tolerated' : 'not tolerated')); + $this->assertSame($tolerates, $configuration->toleratesForGroup($groupName, $groups), \sprintf('Deprecation type "%s" is %s', $groupName, $tolerates ? 'tolerated' : 'not tolerated')); } } @@ -474,7 +479,7 @@ public function testBaselineGenerationWithDeprecationTriggeredByDebugClassLoader $trace[2] = [ 'class' => DebugClassLoader::class, 'function' => 'testBaselineGenerationWithDeprecationTriggeredByDebugClassLoader', - 'args' => [self::class] + 'args' => [self::class], ]; $deprecation = new Deprecation('Deprecation by debug class loader', $trace, ''); @@ -512,7 +517,7 @@ public function testBaselineFileException() $filename = $this->createFile(); unlink($filename); $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('The baselineFile "%s" does not exist.', $filename)); + $this->expectExceptionMessage(\sprintf('The baselineFile "%s" does not exist.', $filename)); Configuration::fromUrlEncodedString('baselineFile='.urlencode($filename)); } @@ -526,7 +531,7 @@ public function testBaselineFileWriteError() $this->expectExceptionMessageMatches('/[Ff]ailed to open stream: Permission denied/'); set_error_handler(static function (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null): bool { - if ($errno & (E_WARNING | E_WARNING)) { + if ($errno & (\E_WARNING | \E_WARNING)) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } @@ -592,7 +597,7 @@ public function testIgnoreFileException() $filename = $this->createFile(); unlink($filename); $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('The ignoreFile "%s" does not exist.', $filename)); + $this->expectExceptionMessage(\sprintf('The ignoreFile "%s" does not exist.', $filename)); Configuration::fromUrlEncodedString('ignoreFile='.urlencode($filename)); } diff --git a/Tests/DeprecationErrorHandler/DeprecationGroupTest.php b/Tests/DeprecationErrorHandler/DeprecationGroupTest.php index df746e5e..38c9e27e 100644 --- a/Tests/DeprecationErrorHandler/DeprecationGroupTest.php +++ b/Tests/DeprecationErrorHandler/DeprecationGroupTest.php @@ -1,10 +1,21 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; +#[RequiresPhpunit('<10')] final class DeprecationGroupTest extends TestCase { public function testItGroupsByMessage() diff --git a/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php b/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php index c0a88c44..6e093613 100644 --- a/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php +++ b/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php @@ -1,10 +1,21 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationNotice; +#[RequiresPhpunit('<10')] final class DeprecationNoticeTest extends TestCase { public function testItGroupsByCaller() diff --git a/Tests/DeprecationErrorHandler/DeprecationTest.php b/Tests/DeprecationErrorHandler/DeprecationTest.php index 2a7643a9..bad55d1e 100644 --- a/Tests/DeprecationErrorHandler/DeprecationTest.php +++ b/Tests/DeprecationErrorHandler/DeprecationTest.php @@ -11,11 +11,14 @@ namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7; +#[RequiresPhpunit('<10')] class DeprecationTest extends TestCase { private static $vendorDir; @@ -28,7 +31,7 @@ private static function getVendorDir() } foreach (get_declared_classes() as $class) { - if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); $vendorDir = \dirname($r->getFileName(), 2); if (file_exists($vendorDir.'/composer/installed.json') && @mkdir($vendorDir.'/myfakevendor/myfakepackage1', 0777, true)) { @@ -88,6 +91,7 @@ public function testItRulesOutFilesOutsideVendorsAsIndirect() /** * @dataProvider mutedProvider */ + #[DataProvider('mutedProvider')] public function testItMutesOnlySpecificErrorMessagesWhenTheCallingCodeIsInPhpunit($muted, $callingClass, $message) { $trace = $this->debugBacktrace(); @@ -170,6 +174,7 @@ public static function providerGetTypeDetectsSelf(): array /** * @dataProvider providerGetTypeDetectsSelf */ + #[DataProvider('providerGetTypeDetectsSelf')] public function testGetTypeDetectsSelf(string $expectedType, string $message, string $traceClass, string $file) { $trace = [ @@ -233,6 +238,7 @@ public static function providerGetTypeUsesRightTrace(): array /** * @dataProvider providerGetTypeUsesRightTrace */ + #[DataProvider('providerGetTypeUsesRightTrace')] public function testGetTypeUsesRightTrace(string $expectedType, string $message, array $trace) { $deprecation = new Deprecation( @@ -268,16 +274,13 @@ private static function removeDir($dir) public static function setUpBeforeClass(): void { foreach (get_declared_classes() as $class) { - if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); $v = \dirname($r->getFileName(), 2); if (file_exists($v.'/composer/installed.json')) { $loader = require $v.'/autoload.php'; $reflection = new \ReflectionClass($loader); $prop = $reflection->getProperty('prefixDirsPsr4'); - if (\PHP_VERSION_ID < 80100) { - $prop->setAccessible(true); - } $currentValue = $prop->getValue($loader); self::$prefixDirsPsr4[] = [$prop, $loader, $currentValue]; $currentValue['Symfony\\Bridge\\PhpUnit\\'] = [realpath(__DIR__.'/../..')]; diff --git a/Tests/DeprecationErrorHandler/deprecation/deprecation.php b/Tests/DeprecationErrorHandler/deprecation/deprecation.php index 92efd950..0ea3e5c3 100644 --- a/Tests/DeprecationErrorHandler/deprecation/deprecation.php +++ b/Tests/DeprecationErrorHandler/deprecation/deprecation.php @@ -1,3 +1,12 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + @trigger_error('I come from… afar! :D', \E_USER_DEPRECATED); diff --git a/Tests/DeprecationErrorHandler/fake_app/AppService.php b/Tests/DeprecationErrorHandler/fake_app/AppService.php index 2b6cb316..b8f6dc25 100644 --- a/Tests/DeprecationErrorHandler/fake_app/AppService.php +++ b/Tests/DeprecationErrorHandler/fake_app/AppService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace App\Services; use acme\lib\SomeService; @@ -20,9 +29,9 @@ public function selfDeprecation(bool $useContracts = false) { $args = [__FUNCTION__, __FUNCTION__]; if ($useContracts) { - trigger_deprecation('App', '3.0', sprintf('%s is deprecated, use %s_new instead.', ...$args)); + trigger_deprecation('App', '3.0', \sprintf('%s is deprecated, use %s_new instead.', ...$args)); } else { - @trigger_error(sprintf('Since App 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); + @trigger_error(\sprintf('Since App 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); } } diff --git a/Tests/DeprecationErrorHandler/fake_app/BarService.php b/Tests/DeprecationErrorHandler/fake_app/BarService.php index 868de5bd..5e0d66c0 100644 --- a/Tests/DeprecationErrorHandler/fake_app/BarService.php +++ b/Tests/DeprecationErrorHandler/fake_app/BarService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace App\Services; use acme\lib\ExtendsDeprecatedClassFromOtherVendor; diff --git a/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php b/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php index b4305e0d..2105c3ca 100644 --- a/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php +++ b/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace App\Services; use fcy\lib\DeprecatedClass; diff --git a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/ExtendsDeprecatedClassFromOtherVendor.php b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/ExtendsDeprecatedClassFromOtherVendor.php index f748109d..600faca8 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/ExtendsDeprecatedClassFromOtherVendor.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/ExtendsDeprecatedClassFromOtherVendor.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace acme\lib; use fcy\lib\DeprecatedClass; diff --git a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/PhpDeprecation.php b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/PhpDeprecation.php index 26a3237e..e38211b1 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/PhpDeprecation.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/PhpDeprecation.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace acme\lib; class PhpDeprecation implements \Serializable diff --git a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php index cc237e61..6064426f 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace acme\lib; use bar\lib\AnotherService; @@ -10,9 +19,9 @@ public function deprecatedApi(bool $useContracts = false) { $args = [__FUNCTION__, __FUNCTION__]; if ($useContracts) { - trigger_deprecation('acme/lib', '3.0', sprintf('%s is deprecated, use %s_new instead.', ...$args)); + trigger_deprecation('acme/lib', '3.0', \sprintf('%s is deprecated, use %s_new instead.', ...$args)); } else { - @trigger_error(sprintf('Since acme/lib 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); + @trigger_error(\sprintf('Since acme/lib 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); } } diff --git a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/deprecation_riddled.php b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/deprecation_riddled.php index c6507d7f..5784566c 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/deprecation_riddled.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/deprecation_riddled.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + eval(<<<'EOPHP' namespace PHPUnit\Util; diff --git a/Tests/DeprecationErrorHandler/fake_vendor/autoload.php b/Tests/DeprecationErrorHandler/fake_vendor/autoload.php index 3c4471bc..68db330c 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/autoload.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/autoload.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + require_once __DIR__.'/composer/autoload_real.php'; return ComposerAutoloaderInitFake::getLoader(); diff --git a/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php b/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php index 2e2f0f9b..27241866 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace bar\lib; class AnotherService @@ -8,9 +17,9 @@ public function deprecatedApi(bool $useContracts = false) { $args = [__FUNCTION__, __FUNCTION__]; if ($useContracts) { - trigger_deprecation('bar/lib', '3.0', sprintf('%s is deprecated, use %s_new instead.', ...$args)); + trigger_deprecation('bar/lib', '3.0', \sprintf('%s is deprecated, use %s_new instead.', ...$args)); } else { - @trigger_error(sprintf('Since bar/lib 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); + @trigger_error(\sprintf('Since bar/lib 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); } } } diff --git a/Tests/DeprecationErrorHandler/fake_vendor/composer/autoload_real.php b/Tests/DeprecationErrorHandler/fake_vendor/composer/autoload_real.php index 4b80d96c..231ae4f5 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/composer/autoload_real.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/composer/autoload_real.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + class ComposerLoaderFake { public function getPrefixes() @@ -27,7 +36,7 @@ public function loadClass($className) public function findFile($class) { foreach ($this->getPrefixesPsr4() as $prefix => $baseDirs) { - if (0 !== strpos($class, $prefix)) { + if (!str_starts_with($class, $prefix)) { continue; } diff --git a/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php b/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php index f6672cea..16edcaf6 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php +++ b/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace fcy\lib; /** diff --git a/Tests/DeprecationErrorHandler/fake_vendor_bis/autoload.php b/Tests/DeprecationErrorHandler/fake_vendor_bis/autoload.php index c1c96392..f1aec32c 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor_bis/autoload.php +++ b/Tests/DeprecationErrorHandler/fake_vendor_bis/autoload.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + require_once __DIR__.'/composer/autoload_real.php'; return ComposerAutoloaderInitFakeBis::getLoader(); diff --git a/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/autoload_real.php b/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/autoload_real.php index aabb103e..2a52065d 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/autoload_real.php +++ b/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/autoload_real.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + class ComposerLoaderFakeBis { public function getPrefixes() @@ -17,7 +26,7 @@ public function getPrefixesPsr4() public function loadClass($className) { foreach ($this->getPrefixesPsr4() as $prefix => $baseDirs) { - if (0 !== strpos($className, $prefix)) { + if (!str_starts_with($className, $prefix)) { continue; } diff --git a/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php b/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php index 8ab32307..d9c67b1c 100644 --- a/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php +++ b/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace foo\lib; class SomeOtherService diff --git a/Tests/DeprecationErrorHandler/generate_phar.php b/Tests/DeprecationErrorHandler/generate_phar.php index 75125d51..df875111 100644 --- a/Tests/DeprecationErrorHandler/generate_phar.php +++ b/Tests/DeprecationErrorHandler/generate_phar.php @@ -1,4 +1,13 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + $phar = new Phar(__DIR__.\DIRECTORY_SEPARATOR.'deprecation.phar', 0, 'deprecation.phar'); $phar->buildFromDirectory(__DIR__.\DIRECTORY_SEPARATOR.'deprecation'); diff --git a/Tests/DeprecationErrorHandler/log_file.phpt b/Tests/DeprecationErrorHandler/log_file.phpt index de0f4e2e..866e9686 100644 --- a/Tests/DeprecationErrorHandler/log_file.phpt +++ b/Tests/DeprecationErrorHandler/log_file.phpt @@ -1,8 +1,10 @@ --TEST-- Test DeprecationErrorHandler with log file +--SKIPIF-- +=')) echo 'Skipping on PHPUnit 10+'; --FILE-- --FILE-- expectDeprecation('foo'); @@ -36,6 +41,8 @@ public function testOne() * * @runInSeparateProcess */ + #[Group('legacy')] + #[RunInSeparateProcess] public function testOneInIsolation() { $this->expectDeprecation('foo'); @@ -47,6 +54,7 @@ public function testOneInIsolation() * * @group legacy */ + #[Group('legacy')] public function testMany() { $this->expectDeprecation('foo'); @@ -62,6 +70,7 @@ public function testMany() * * @expectedDeprecation foo */ + #[Group('legacy')] public function testOneWithAnnotation() { $this->expectDeprecation('bar'); @@ -77,6 +86,7 @@ public function testOneWithAnnotation() * @expectedDeprecation foo * @expectedDeprecation bar */ + #[Group('legacy')] public function testManyWithAnnotation() { $this->expectDeprecation('ccc'); diff --git a/Tests/ExpectedDeprecationAnnotationTest.php b/Tests/ExpectedDeprecationAnnotationTest.php index 329bf694..a3112cf7 100644 --- a/Tests/ExpectedDeprecationAnnotationTest.php +++ b/Tests/ExpectedDeprecationAnnotationTest.php @@ -11,8 +11,11 @@ namespace Symfony\Bridge\PhpUnit\Tests; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; +#[RequiresPhpunit('<10')] final class ExpectedDeprecationAnnotationTest extends TestCase { /** @@ -22,6 +25,7 @@ final class ExpectedDeprecationAnnotationTest extends TestCase * * @expectedDeprecation foo */ + #[Group('legacy')] public function testOne() { @trigger_error('foo', \E_USER_DEPRECATED); @@ -35,6 +39,7 @@ public function testOne() * @expectedDeprecation foo * @expectedDeprecation bar */ + #[Group('legacy')] public function testMany() { @trigger_error('foo', \E_USER_DEPRECATED); diff --git a/Tests/FailTests/ExpectDeprecationTraitTestFail.php b/Tests/FailTests/ExpectDeprecationTraitTestFail.php index f2eb1b1b..10da25f4 100644 --- a/Tests/FailTests/ExpectDeprecationTraitTestFail.php +++ b/Tests/FailTests/ExpectDeprecationTraitTestFail.php @@ -11,6 +11,9 @@ namespace Symfony\Bridge\PhpUnit\Tests\FailTests; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -20,6 +23,7 @@ * This class is deliberately suffixed with *TestFail.php so that it is ignored * by PHPUnit. This test is designed to fail. See ../expectdeprecationfail.phpt. */ +#[RequiresPhpunit('<10')] final class ExpectDeprecationTraitTestFail extends TestCase { use ExpectDeprecationTrait; @@ -29,6 +33,7 @@ final class ExpectDeprecationTraitTestFail extends TestCase * * @group legacy */ + #[Group('legacy')] public function testOne() { $this->expectDeprecation('foo'); @@ -42,6 +47,8 @@ public function testOne() * * @runInSeparateProcess */ + #[Group('legacy')] + #[RunInSeparateProcess] public function testOneInIsolation() { $this->expectDeprecation('foo'); diff --git a/Tests/FailTests/NoAssertionsTestNotRisky.php b/Tests/FailTests/NoAssertionsTestNotRisky.php index 2c5832e4..bd259a50 100644 --- a/Tests/FailTests/NoAssertionsTestNotRisky.php +++ b/Tests/FailTests/NoAssertionsTestNotRisky.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\PhpUnit\Tests\FailTests; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -18,6 +19,7 @@ * This class is deliberately suffixed with *TestRisky.php so that it is ignored * by PHPUnit. This test is designed to fail. See ../expectnotrisky.phpt. */ +#[RequiresPhpunit('<10')] final class NoAssertionsTestNotRisky extends TestCase { use ExpectDeprecationTrait; diff --git a/Tests/FailTests/NoAssertionsTestRisky.php b/Tests/FailTests/NoAssertionsTestRisky.php index 4a22baf1..559b86b8 100644 --- a/Tests/FailTests/NoAssertionsTestRisky.php +++ b/Tests/FailTests/NoAssertionsTestRisky.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\PhpUnit\Tests\FailTests; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -18,6 +20,7 @@ * This class is deliberately suffixed with *TestRisky.php so that it is ignored * by PHPUnit. This test is designed to fail. See ../expectrisky.phpt. */ +#[RequiresPhpunit('<10')] final class NoAssertionsTestRisky extends TestCase { use ExpectDeprecationTrait; @@ -27,6 +30,7 @@ final class NoAssertionsTestRisky extends TestCase * * @group legacy */ + #[Group('legacy')] public function testOne() { $this->expectNotToPerformAssertions(); diff --git a/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist b/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist index 797407e1..1dbca04b 100644 --- a/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist +++ b/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist @@ -1,30 +1,26 @@ - - + + + src + + tests - - - - src - - - - + true diff --git a/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist b/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist index 4af525d0..40680ab2 100644 --- a/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist +++ b/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist @@ -1,23 +1,20 @@ - - + + + src + + tests - - - - src - - diff --git a/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php b/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php index d764638d..503d675b 100644 --- a/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php +++ b/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php @@ -9,11 +9,13 @@ * file that was distributed with this source code. */ +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; /** * @coversDefaultClass \DateTime */ +#[CoversClass(DateTime::class)] class CoversDefaultClassTest extends TestCase { public function test() diff --git a/Tests/Fixtures/coverage/tests/CoversNothingTest.php b/Tests/Fixtures/coverage/tests/CoversNothingTest.php index e60ea97e..8e3cb0a7 100644 --- a/Tests/Fixtures/coverage/tests/CoversNothingTest.php +++ b/Tests/Fixtures/coverage/tests/CoversNothingTest.php @@ -9,11 +9,13 @@ * file that was distributed with this source code. */ +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; /** * @coversNothing */ +#[CoversNothing] class CoversNothingTest extends TestCase { public function test() diff --git a/Tests/Fixtures/coverage/tests/CoversTest.php b/Tests/Fixtures/coverage/tests/CoversTest.php index f6d34060..67ac74d7 100644 --- a/Tests/Fixtures/coverage/tests/CoversTest.php +++ b/Tests/Fixtures/coverage/tests/CoversTest.php @@ -9,13 +9,15 @@ * file that was distributed with this source code. */ +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +/** + * @covers \DateTime + */ +#[CoversClass(DateTime::class)] class CoversTest extends TestCase { - /** - * @covers \DateTime - */ public function test() { $this->assertTrue(true); diff --git a/Tests/Fixtures/symfonyextension/phpunit-with-extension.xml.dist b/Tests/Fixtures/symfonyextension/phpunit-with-extension.xml.dist new file mode 100644 index 00000000..6e159dcb --- /dev/null +++ b/Tests/Fixtures/symfonyextension/phpunit-with-extension.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + + src + + + + + + + + + + diff --git a/Tests/Fixtures/symfonyextension/phpunit-without-extension.xml.dist b/Tests/Fixtures/symfonyextension/phpunit-without-extension.xml.dist new file mode 100644 index 00000000..843be2fa --- /dev/null +++ b/Tests/Fixtures/symfonyextension/phpunit-without-extension.xml.dist @@ -0,0 +1,22 @@ + + + + + tests + + + + + + src + + + diff --git a/Tests/Fixtures/symfonyextension/src/ClassExtendingFinalClass.php b/Tests/Fixtures/symfonyextension/src/ClassExtendingFinalClass.php new file mode 100644 index 00000000..e3377aaf --- /dev/null +++ b/Tests/Fixtures/symfonyextension/src/ClassExtendingFinalClass.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src; + +class ClassExtendingFinalClass extends FinalClass +{ +} diff --git a/Tests/Fixtures/symfonyextension/src/FinalClass.php b/Tests/Fixtures/symfonyextension/src/FinalClass.php new file mode 100644 index 00000000..8a320dd3 --- /dev/null +++ b/Tests/Fixtures/symfonyextension/src/FinalClass.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src; + +/** + * @final + */ +class FinalClass +{ +} diff --git a/Tests/Fixtures/symfonyextension/tests/bootstrap.php b/Tests/Fixtures/symfonyextension/tests/bootstrap.php new file mode 100644 index 00000000..385e7ea7 --- /dev/null +++ b/Tests/Fixtures/symfonyextension/tests/bootstrap.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\ClassExtendingFinalClass; +use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\FinalClass; + +spl_autoload_register(function ($class) { + if (FinalClass::class === $class) { + require __DIR__.'/../src/FinalClass.php'; + } elseif (ClassExtendingFinalClass::class === $class) { + require __DIR__.'/../src/ClassExtendingFinalClass.php'; + } +}); + +require __DIR__.'/../../../../SymfonyExtension.php'; +require __DIR__.'/../../../../Attribute/DnsSensitive.php'; +require __DIR__.'/../../../../Attribute/TimeSensitive.php'; +require __DIR__.'/../../../../Extension/EnableClockMockSubscriber.php'; +require __DIR__.'/../../../../Extension/RegisterClockMockSubscriber.php'; +require __DIR__.'/../../../../Extension/RegisterDnsMockSubscriber.php'; +require __DIR__.'/../../../../Metadata/AttributeReader.php'; + +if (file_exists(__DIR__.'/../../../../vendor/autoload.php')) { + require __DIR__.'/../../../../vendor/autoload.php'; +} elseif (file_exists(__DIR__.'/../../../..//../../../../vendor/autoload.php')) { + require __DIR__.'/../../../../../../../../vendor/autoload.php'; +} diff --git a/Tests/Metadata/AttributeReaderTest.php b/Tests/Metadata/AttributeReaderTest.php new file mode 100644 index 00000000..eb3a7765 --- /dev/null +++ b/Tests/Metadata/AttributeReaderTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests\Metadata; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; +use Symfony\Bridge\PhpUnit\Metadata\AttributeReader; +use Symfony\Bridge\PhpUnit\Tests\Metadata\Fixtures\FooBar; + +class AttributeReaderTest extends TestCase +{ + /** + * @dataProvider provideReadCases + */ + #[DataProvider('provideReadCases')] + public function testAttributesAreRead(string $method, string $attributeClass, array $expected) + { + $reader = new AttributeReader(); + + $attributes = $reader->forClassAndMethod(FooBar::class, $method, $attributeClass); + + self::assertContainsOnlyInstancesOf($attributeClass, $attributes); + self::assertSame($expected, array_column($attributes, 'class')); + } + + public static function provideReadCases(): iterable + { + yield ['testOne', DnsSensitive::class, [ + 'App\Foo\Bar\A', + 'App\Foo\Bar\B', + 'App\Foo\Baz\C', + ]]; + yield ['testTwo', DnsSensitive::class, [ + 'App\Foo\Bar\A', + 'App\Foo\Bar\B', + ]]; + yield ['testThree', DnsSensitive::class, [ + 'App\Foo\Bar\A', + 'App\Foo\Bar\B', + 'App\Foo\Corge\F', + ]]; + + yield ['testOne', TimeSensitive::class, [ + 'App\Foo\Bar\A', + ]]; + yield ['testTwo', TimeSensitive::class, [ + 'App\Foo\Bar\A', + 'App\Foo\Qux\D', + 'App\Foo\Qux\E', + ]]; + yield ['testThree', TimeSensitive::class, [ + 'App\Foo\Bar\A', + 'App\Foo\Corge\G', + ]]; + } + + public function testAttributesAreCached() + { + $reader = new AttributeReader(); + $cacheRef = new \ReflectionProperty(AttributeReader::class, 'cache'); + + self::assertSame([], $cacheRef->getValue($reader)); + + $reader->forClass(FooBar::class, TimeSensitive::class); + + self::assertCount(1, $cache = $cacheRef->getValue($reader)); + self::assertArrayHasKey(FooBar::class, $cache); + self::assertAttributesCount($cache[FooBar::class], 2, 1); + + $reader->forMethod(FooBar::class, 'testThree', DnsSensitive::class); + + self::assertCount(2, $cache = $cacheRef->getValue($reader)); + self::assertArrayHasKey($key = FooBar::class.'::testThree', $cache); + self::assertAttributesCount($cache[$key], 1, 1); + } + + private static function assertAttributesCount(array $attributes, int $expectedDnsCount, int $expectedTimeCount): void + { + self::assertArrayHasKey(DnsSensitive::class, $attributes); + self::assertCount($expectedDnsCount, $attributes[DnsSensitive::class]); + self::assertArrayHasKey(TimeSensitive::class, $attributes); + self::assertCount($expectedTimeCount, $attributes[TimeSensitive::class]); + } +} diff --git a/Tests/Metadata/Fixtures/FooBar.php b/Tests/Metadata/Fixtures/FooBar.php new file mode 100644 index 00000000..63b9d28d --- /dev/null +++ b/Tests/Metadata/Fixtures/FooBar.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests\Metadata\Fixtures; + +use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; + +#[DnsSensitive('App\Foo\Bar\A')] +#[DnsSensitive('App\Foo\Bar\B')] +#[TimeSensitive('App\Foo\Bar\A')] +final class FooBar +{ + #[DnsSensitive('App\Foo\Baz\C')] + public function testOne() + { + } + + #[TimeSensitive('App\Foo\Qux\D')] + #[TimeSensitive('App\Foo\Qux\E')] + public function testTwo() + { + } + + #[DnsSensitive('App\Foo\Corge\F')] + #[TimeSensitive('App\Foo\Corge\G')] + public function testThree() + { + } +} diff --git a/Tests/OnlyExpectingDeprecationSkippedTest.php b/Tests/OnlyExpectingDeprecationSkippedTest.php index 593e0b4e..aede756a 100644 --- a/Tests/OnlyExpectingDeprecationSkippedTest.php +++ b/Tests/OnlyExpectingDeprecationSkippedTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\PhpUnit\Tests; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; /** @@ -18,6 +20,7 @@ * * @requires extension ext-dummy */ +#[RequiresPhpExtension('ext-dummy')] final class OnlyExpectingDeprecationSkippedTest extends TestCase { /** @@ -27,6 +30,7 @@ final class OnlyExpectingDeprecationSkippedTest extends TestCase * * @expectedDeprecation unreachable */ + #[Group('legacy')] public function testExpectingOnlyDeprecations() { $this->fail('should never be ran.'); diff --git a/Tests/ProcessIsolationTest.php b/Tests/ProcessIsolationTest.php index 04bf6ec8..d86e2db6 100644 --- a/Tests/ProcessIsolationTest.php +++ b/Tests/ProcessIsolationTest.php @@ -11,6 +11,9 @@ namespace Symfony\Bridge\PhpUnit\Tests; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Exception; use PHPUnit\Framework\TestCase; /** @@ -20,6 +23,8 @@ * * @runTestsInSeparateProcesses */ +#[RequiresPhpunit('<10')] +#[Group('legacy')] class ProcessIsolationTest extends TestCase { /** @@ -33,7 +38,7 @@ public function testIsolation() public function testCallingOtherErrorHandler() { - $this->expectException(\PHPUnit\Framework\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Test that PHPUnit\'s error handler fires.'); trigger_error('Test that PHPUnit\'s error handler fires.', \E_USER_WARNING); diff --git a/Tests/SymfonyExtension.php b/Tests/SymfonyExtension.php new file mode 100644 index 00000000..1219c27b --- /dev/null +++ b/Tests/SymfonyExtension.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive; +use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive; +use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\ClassExtendingFinalClass; +use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\FinalClass; + +#[DnsSensitive('App\Foo\A')] +#[TimeSensitive('App\Foo\A')] +class SymfonyExtension extends TestCase +{ + public function testExtensionOfFinalClass() + { + $this->expectUserDeprecationMessage(\sprintf('The "%s" class is considered final. It may change without further notice as of its next major version. You should not extend it from "%s".', FinalClass::class, ClassExtendingFinalClass::class)); + + new ClassExtendingFinalClass(); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testTimeMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\time', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testMicrotimeMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\microtime', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testSleepMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\sleep', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testUsleepMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\usleep', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testDateMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\date', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testGmdateMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\gmdate', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('time-sensitive')] + #[TimeSensitive('App\Bar\B')] + public function testHrtimeMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\hrtime', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testCheckdnsrrMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\checkdnsrr', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testDnsCheckRecordMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\dns_check_record', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testGetmxrrMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\getmxrr', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testDnsGetMxMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\dns_get_mx', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testGethostbyaddrMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\gethostbyaddr', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testGethostbynameMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\gethostbyname', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testGethostbynamelMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\gethostbynamel', $namespace))); + } + + #[DataProvider('mockedNamespaces')] + #[Group('dns-sensitive')] + #[DnsSensitive('App\Bar\B')] + public function testDnsGetRecordMockIsRegistered(string $namespace) + { + $this->assertTrue(\function_exists(\sprintf('%s\dns_get_record', $namespace))); + } + + public static function mockedNamespaces(): iterable + { + yield 'test class namespace' => [__NAMESPACE__]; + yield 'namespace derived from test namespace' => ['Symfony\Bridge\PhpUnit']; + yield 'explicitly configured namespace' => ['App']; + yield 'explicitly configured namespace through attribute on class' => ['App\Foo']; + yield 'explicitly configured namespace through attribute on method' => ['App\Bar']; + } +} diff --git a/Tests/SymfonyExtensionWithManualRegister.php b/Tests/SymfonyExtensionWithManualRegister.php new file mode 100644 index 00000000..c02d6f1c --- /dev/null +++ b/Tests/SymfonyExtensionWithManualRegister.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ClockMock; +use Symfony\Bridge\PhpUnit\DnsMock; + +class SymfonyExtensionWithManualRegister extends TestCase +{ + public static function setUpBeforeClass(): void + { + ClockMock::register(self::class); + ClockMock::withClockMock(strtotime('2024-05-20 15:30:00')); + + DnsMock::register(self::class); + DnsMock::withMockedHosts([ + 'example.com' => [ + ['type' => 'A', 'ip' => '1.2.3.4'], + ], + ]); + } + + public static function tearDownAfterClass(): void + { + ClockMock::withClockMock(false); + DnsMock::withMockedHosts([]); + } + + public function testDate() + { + self::assertSame('2024-05-20 15:30:00', date('Y-m-d H:i:s')); + } + + public function testGetHostByName() + { + self::assertSame('1.2.3.4', gethostbyname('example.com')); + } + + public function testTime() + { + self::assertSame(1716219000, time()); + } + + public function testDnsGetRecord() + { + self::assertSame([[ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 1, + 'type' => 'A', + 'ip' => '1.2.3.4', + ]], dns_get_record('example.com')); + } +} diff --git a/Tests/expectdeprecationfail.phpt b/Tests/expectdeprecationfail.phpt index f968cd18..be302235 100644 --- a/Tests/expectdeprecationfail.phpt +++ b/Tests/expectdeprecationfail.phpt @@ -1,5 +1,7 @@ --TEST-- Test ExpectDeprecationTrait failing tests +--SKIPIF-- +=')) echo 'Skipping on PHPUnit 10+'; --FILE-- =')) echo 'Skipping on PHPUnit 10+'; --FILE-- =')) echo 'Skipping on PHPUnit 10+'; --FILE-- = 80000) { - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '9.6') ?: '9.6'; -} elseif (\PHP_VERSION_ID >= 70200) { - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.5') ?: '8.5'; -} else { - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '7.5') ?: '7.5'; -} +$PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '9.6') ?: '9.6'; $MAX_PHPUNIT_VERSION = $getEnvVar('SYMFONY_MAX_PHPUNIT_VERSION', false); @@ -111,6 +105,11 @@ $PHPUNIT_VERSION = $MAX_PHPUNIT_VERSION; } +if (version_compare($PHPUNIT_VERSION, '10.0', '>=') && version_compare($PHPUNIT_VERSION, '11.0', '<')) { + fwrite(\STDERR, 'This script does not work with PHPUnit 10.'.\PHP_EOL); + exit(1); +} + $PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN); $COMPOSER_JSON = getenv('COMPOSER') ?: 'composer.json'; @@ -146,7 +145,7 @@ } } -if ('disabled' === $getEnvVar('SYMFONY_DEPRECATIONS_HELPER')) { +if ('disabled' === $getEnvVar('SYMFONY_DEPRECATIONS_HELPER') || version_compare($PHPUNIT_VERSION, '11.0', '>=')) { putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); } @@ -166,7 +165,7 @@ $prevCacheDir = getenv('COMPOSER_CACHE_DIR'); if ($prevCacheDir) { if (false === $absoluteCacheDir = realpath($prevCacheDir)) { - @mkdir($prevCacheDir, 0777, true); + @mkdir($prevCacheDir, 0o777, true); $absoluteCacheDir = realpath($prevCacheDir); } if ($absoluteCacheDir) { @@ -175,14 +174,14 @@ $prevCacheDir = false; } } -$SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml' : '')); +$SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'); $SYMFONY_PHPUNIT_REQUIRE = $getEnvVar('SYMFONY_PHPUNIT_REQUIRE', ''); $configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, $SYMFONY_PHPUNIT_REQUIRE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT])); $PHPUNIT_VERSION_DIR = sprintf('phpunit-%s-%d', $PHPUNIT_VERSION, $PHPUNIT_REMOVE_RETURN_TYPEHINT); if (!file_exists("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit") || $configurationHash !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION_DIR.md5")) { // Build a standalone phpunit without symfony/yaml nor prophecy by default - @mkdir($PHPUNIT_DIR, 0777, true); + @mkdir($PHPUNIT_DIR, 0o777, true); chdir($PHPUNIT_DIR); if (file_exists("$PHPUNIT_VERSION_DIR")) { passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s 2> NUL' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old"))); @@ -237,9 +236,6 @@ if ($SYMFONY_PHPUNIT_REQUIRE) { $passthruOrFail("$COMPOSER require --no-update ".$SYMFONY_PHPUNIT_REQUIRE); } - if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { - $passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); - } if (preg_match('{\^((\d++\.)\d++)[\d\.]*$}', $info['requires']['php'], $phpVersion) && version_compare($phpVersion[2].'99', \PHP_VERSION, '<')) { $passthruOrFail("$COMPOSER config platform.php \"$phpVersion[1].99\""); @@ -264,9 +260,8 @@ } $prevRoot = getenv('COMPOSER_ROOT_VERSION'); putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); - $q = '\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 80000 ? '"' : ''; // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS - $exit = proc_close(proc_open("$q$COMPOSER update --no-dev --prefer-dist --no-progress $q", [], $p, getcwd())); + $exit = proc_close(proc_open("$COMPOSER update --no-dev --prefer-dist --no-progress", [], $p, getcwd())); putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : '')); if ($prevCacheDir) { putenv("COMPOSER_CACHE_DIR=$prevCacheDir"); @@ -276,44 +271,45 @@ } // Mutate TestCase code - $alteredCode = file_get_contents($alteredFile = './src/Framework/TestCase.php'); - if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) { - $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode); - } - $alteredCode = preg_replace('/abstract class TestCase[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;", $alteredCode, 1); - file_put_contents($alteredFile, $alteredCode); + if (version_compare($PHPUNIT_VERSION, '11.0', '<')) { + $alteredCode = file_get_contents($alteredFile = './src/Framework/TestCase.php'); + if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) { + $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode); + } - // Mutate Assert code - $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php'); - $alteredCode = preg_replace('/abstract class Assert[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1); - file_put_contents($alteredFile, $alteredCode); + // Mutate Assert code + $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php'); + $alteredCode = preg_replace('/abstract class Assert[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1); + file_put_contents($alteredFile, $alteredCode); - file_put_contents('phpunit', <<<'EOPHP' -getExcludedDirectories(); - PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); - class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); -} elseif (method_exists(\PHPUnit\Util\Blacklist::class, 'addDirectory')) { - (new PHPUnit\Util\BlackList())->getBlacklistedDirectories(); - PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); - class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); -} else { - PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListPhpunit'] = 1; - PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListSimplePhpunit'] = 1; -} + if (!class_exists(\SymfonyExcludeListPhpunit::class, false)) { + class SymfonyExcludeListPhpunit {} + } + if (method_exists(\PHPUnit\Util\ExcludeList::class, 'addDirectory')) { + (new PHPUnit\Util\Excludelist())->getExcludedDirectories(); + PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); + class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); + } elseif (method_exists(\PHPUnit\Util\Blacklist::class, 'addDirectory')) { + (new PHPUnit\Util\BlackList())->getBlacklistedDirectories(); + PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); + class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); + } else { + PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListPhpunit'] = 1; + PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListSimplePhpunit'] = 1; + } -Symfony\Bridge\PhpUnit\TextUI\Command::main(); + Symfony\Bridge\PhpUnit\TextUI\Command::main(); + + EOPHP + ); + } -EOPHP - ); chdir('..'); file_put_contents(".$PHPUNIT_VERSION_DIR.md5", $configurationHash); chdir($oldPwd); @@ -334,16 +330,7 @@ class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Bla } chdir($oldPwd); -if ($PHPUNIT_VERSION < 8.0) { - $argv = array_filter($argv, function ($v) use (&$argc) { - if ('--do-not-cache-result' !== $v) { - return true; - } - --$argc; - - return false; - }); -} elseif (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), \FILTER_VALIDATE_BOOLEAN)) { +if (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), \FILTER_VALIDATE_BOOLEAN)) { $argv[] = '--do-not-cache-result'; ++$argc; } @@ -381,6 +368,10 @@ class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Bla $cmd .= '%2$s'; } +if (version_compare($PHPUNIT_VERSION, '11.0', '>=')) { + $GLOBALS['_composer_autoload_path'] = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/vendor/autoload.php"; +} + if ($components) { $skippedTests = $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] ?? false; $runningProcs = []; @@ -457,7 +448,7 @@ class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Bla } } } elseif (!isset($argv[1]) || 'install' !== $argv[1] || file_exists('install')) { - if (!class_exists(\SymfonyExcludeListSimplePhpunit::class, false)) { + if (!class_exists(SymfonyExcludeListSimplePhpunit::class, false)) { class SymfonyExcludeListSimplePhpunit { } diff --git a/bootstrap.php b/bootstrap.php index f11b7ab7..5191beff 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -9,10 +9,14 @@ * file that was distributed with this source code. */ -use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Deprecations\Deprecation; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; +// Skip if we're using PHPUnit >=10 +if (class_exists(PHPUnit\Metadata\Metadata::class)) { + return; +} + // Detect if we need to serialize deprecations to a file. if (in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) { DeprecationErrorHandler::collectDeprecations($file); @@ -21,7 +25,7 @@ } // Detect if we're loaded by an actual run of phpunit -if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists(\PHPUnit\TextUI\Command::class, false)) { +if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists(PHPUnit\TextUI\Command::class, false)) { return; } @@ -31,19 +35,6 @@ if (class_exists(Deprecation::class)) { Deprecation::withoutDeduplication(); - - if (\PHP_VERSION_ID < 80000) { - // Ignore deprecations about the annotation mapping driver when it's not possible to move to the attribute driver yet - Deprecation::ignoreDeprecations('https://github.com/doctrine/orm/issues/10098'); - } -} - -if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { - if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { - AnnotationRegistry::registerUniqueLoader('class_exists'); - } elseif (method_exists(AnnotationRegistry::class, 'registerLoader')) { - AnnotationRegistry::registerLoader('class_exists'); - } } if ('disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) { diff --git a/composer.json b/composer.json index a42c737f..d1e6c3ba 100644 --- a/composer.json +++ b/composer.json @@ -18,17 +18,13 @@ } ], "require": { - "php": ">=7.1.3 EVEN ON LATEST SYMFONY VERSIONS TO ALLOW USING", + "php": ">=8.1.0 EVEN ON LATEST SYMFONY VERSIONS TO ALLOW USING", "php": "THIS BRIDGE WHEN TESTING LOWEST SYMFONY VERSIONS.", - "php": ">=7.1.3" + "php": ">=8.1.0" }, "require-dev": { - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/polyfill-php81": "^1.27" - }, - "conflict": { - "phpunit/phpunit": "<7.5|9.1.2" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4.3|^7.0.3|^8.0" }, "autoload": { "files": [ "bootstrap.php" ], diff --git a/phpunit-9.6.xml.dist b/phpunit-9.6.xml.dist new file mode 100644 index 00000000..6f9542e1 --- /dev/null +++ b/phpunit-9.6.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + ./Tests/DeprecationErrorHandler/ + + + + + + ./ + + + ./Tests + ./vendor + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cde576e2..7e310594 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,11 @@ @@ -19,7 +20,7 @@ - + ./ @@ -27,5 +28,5 @@ ./Tests ./vendor - +