diff --git a/README.md b/README.md index d75fb0204c..1768ef1e47 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Here is a list of all components that are primarily developed and maintained by * [sebastian/recursion-context](https://github.com/sebastianbergmann/recursion-context) * [sebastian/type](https://github.com/sebastianbergmann/type) * [sebastian/version](https://github.com/sebastianbergmann/version) +* [sebastian/version-requirement](https://github.com/sebastianbergmann/version-requirement) A very special thanks to everyone who has contributed to the [PHPUnit Manual](https://github.com/sebastianbergmann/phpunit-documentation-english). diff --git a/build.xml b/build.xml index d8b84f68c5..8fc7888927 100644 --- a/build.xml +++ b/build.xml @@ -303,6 +303,13 @@ + + + + + + + diff --git a/composer.json b/composer.json index 58ba7e0d06..68ce9012e5 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "security": "https://github.com/sebastianbergmann/phpunit/security/policy" }, "prefer-stable": true, + "minimum-stability": "dev", "require": { "php": ">=8.4.1", "ext-dom": "*", @@ -55,6 +56,7 @@ "sebastian/recursion-context": "^8.0.0", "sebastian/type": "^7.0.1", "sebastian/version": "^7.0.0", + "sebastian/version-requirement": "^1.0", "staabm/side-effects-detector": "^1.0.5" }, "config": { diff --git a/composer.lock b/composer.lock index 9e7067479c..3ed67ce81d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fdd1cf0b879485ea837501dd977847ec", + "content-hash": "ce29f3be7009692e093af4b0ebb7bc68", "packages": [ { "name": "myclabs/deep-copy", @@ -1732,6 +1732,77 @@ ], "time": "2026-02-06T04:52:52+00:00" }, + { + "name": "sebastian/version-requirement", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version-requirement.git", + "reference": "7662b2811918858a5421fbbaebdfa87f68f39190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version-requirement/zipball/7662b2811918858a5421fbbaebdfa87f68f39190", + "reference": "7662b2811918858a5421fbbaebdfa87f68f39190", + "shasum": "" + }, + "require": { + "phar-io/version": "^3.2.1", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.1" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for working with version requirements", + "homepage": "https://github.com/sebastianbergmann/version-requirement", + "support": { + "issues": "https://github.com/sebastianbergmann/version-requirement/issues", + "security": "https://github.com/sebastianbergmann/version-requirement/security/policy", + "source": "https://github.com/sebastianbergmann/version-requirement/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version-requirement", + "type": "tidelift" + } + ], + "time": "2026-06-20T08:35:21+00:00" + }, { "name": "staabm/side-effects-detector", "version": "1.0.5", @@ -1837,7 +1908,7 @@ ], "packages-dev": [], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, diff --git a/src/Metadata/Api/Requirements.php b/src/Metadata/Api/Requirements.php index a1198206ef..5f15be4919 100644 --- a/src/Metadata/Api/Requirements.php +++ b/src/Metadata/Api/Requirements.php @@ -42,6 +42,7 @@ use PHPUnit\Metadata\Version\Requirement; use PHPUnit\Runner\Version; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; +use SebastianBergmann\VersionRequirement\Requirement as RequirementImplementation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit @@ -68,7 +69,7 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam $this->warnAboutIncompleteVersion($metadata->versionRequirement(), $className, $methodName); - if (!$versionRequirement->isSatisfiedBy(PHP_VERSION)) { + if (!$this->isSatisfiedBy($versionRequirement, PHP_VERSION)) { $notSatisfied[] = sprintf( 'PHP %s is required.', $versionRequirement->asString(), @@ -91,7 +92,7 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam if (!extension_loaded($metadata->extension()) || ($metadata->hasVersionRequirement() && - !$metadata->versionRequirement()->isSatisfiedBy($extensionVersion))) { + !$this->isSatisfiedBy($metadata->versionRequirement(), $extensionVersion))) { $notSatisfied[] = sprintf( 'PHP extension %s%s is required.', $metadata->extension(), @@ -107,7 +108,7 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam $this->warnAboutIncompleteVersion($metadata->versionRequirement(), $className, $methodName); - if (!$versionRequirement->isSatisfiedBy(Version::id())) { + if (!$this->isSatisfiedBy($versionRequirement, Version::id())) { $notSatisfied[] = sprintf( 'PHPUnit %s is required.', $versionRequirement->asString(), @@ -269,6 +270,17 @@ public function requiresXdebug(string $className, string $methodName): bool return false; } + private function isSatisfiedBy(Requirement $versionRequirement, string $version): bool + { + $requirement = $versionRequirement->asString(); + + if ($versionRequirement instanceof InvalidVersionRequirement || $requirement === '') { + return false; + } + + return RequirementImplementation::from($requirement)->isSatisfiedBy($version); + } + /** * @param class-string $className * @param non-empty-string $methodName diff --git a/src/Metadata/Version/ComparisonRequirement.php b/src/Metadata/Version/ComparisonRequirement.php index b87222fd0d..936080b840 100644 --- a/src/Metadata/Version/ComparisonRequirement.php +++ b/src/Metadata/Version/ComparisonRequirement.php @@ -9,7 +9,6 @@ */ namespace PHPUnit\Metadata\Version; -use function version_compare; use PHPUnit\Util\VersionComparisonOperator; /** @@ -28,11 +27,6 @@ public function __construct(string $version, VersionComparisonOperator $operator $this->operator = $operator; } - public function isSatisfiedBy(string $version): bool - { - return version_compare($version, $this->version, $this->operator->asString()); - } - public function asString(): string { return $this->operator->asString() . ' ' . $this->version; diff --git a/src/Metadata/Version/ConstraintRequirement.php b/src/Metadata/Version/ConstraintRequirement.php index 752fe7b6d1..0ba6f50b97 100644 --- a/src/Metadata/Version/ConstraintRequirement.php +++ b/src/Metadata/Version/ConstraintRequirement.php @@ -9,11 +9,6 @@ */ namespace PHPUnit\Metadata\Version; -use function assert; -use function preg_replace; -use PharIo\Version\Version; -use PharIo\Version\VersionConstraint; - /** * @immutable * @@ -21,35 +16,15 @@ */ final readonly class ConstraintRequirement extends Requirement { - private VersionConstraint $constraint; + private string $constraint; - public function __construct(VersionConstraint $constraint) + public function __construct(string $constraint) { $this->constraint = $constraint; } - public function isSatisfiedBy(string $version): bool - { - return $this->constraint->complies( - new Version($this->sanitize($version)), - ); - } - public function asString(): string { - return $this->constraint->asString(); - } - - private function sanitize(string $version): string - { - $sanitized = preg_replace( - '/^(\d+\.\d+(?:.\d+)?).*$/', - '$1', - $version, - ); - - assert($sanitized !== null); - - return $sanitized; + return $this->constraint; } } diff --git a/src/Metadata/Version/InvalidVersionRequirement.php b/src/Metadata/Version/InvalidVersionRequirement.php index 9f3e370068..deb04fcb54 100644 --- a/src/Metadata/Version/InvalidVersionRequirement.php +++ b/src/Metadata/Version/InvalidVersionRequirement.php @@ -29,11 +29,6 @@ public function __construct(string $message) $this->message = $message; } - public function isSatisfiedBy(string $version): bool - { - return false; - } - /** * @return non-empty-string */ diff --git a/src/Metadata/Version/Requirement.php b/src/Metadata/Version/Requirement.php index 01f98f7310..321b8b5a18 100644 --- a/src/Metadata/Version/Requirement.php +++ b/src/Metadata/Version/Requirement.php @@ -9,12 +9,14 @@ */ namespace PHPUnit\Metadata\Version; -use function preg_match; -use PharIo\Version\UnsupportedVersionConstraintException; -use PharIo\Version\VersionConstraintParser; +use function assert; +use function explode; use PHPUnit\Metadata\InvalidVersionRequirementException; -use PHPUnit\Util\InvalidVersionOperatorException; use PHPUnit\Util\VersionComparisonOperator; +use SebastianBergmann\VersionRequirement\ComparisonRequirement as ComparisonRequirementImplementation; +use SebastianBergmann\VersionRequirement\ConstraintRequirement as ConstraintRequirementImplementation; +use SebastianBergmann\VersionRequirement\Exception; +use SebastianBergmann\VersionRequirement\Requirement as RequirementImplementation; /** * @immutable @@ -23,35 +25,32 @@ */ abstract readonly class Requirement { - private const string VERSION_COMPARISON = "/(?P!=|<|<=|<>|=|==|>|>=)?\s*(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m"; - /** - * @throws InvalidVersionOperatorException + * @param non-empty-string $versionRequirement + * * @throws InvalidVersionRequirementException */ public static function from(string $versionRequirement): self { try { - return new ConstraintRequirement( - (new VersionConstraintParser)->parse( - $versionRequirement, + $requirement = RequirementImplementation::from($versionRequirement); + } catch (Exception) { + throw new InvalidVersionRequirementException; + } + + if ($requirement instanceof ComparisonRequirementImplementation) { + return new ComparisonRequirement( + $requirement->version(), + new VersionComparisonOperator( + explode(' ', $requirement->asString(), 2)[0], ), ); - } catch (UnsupportedVersionConstraintException) { - if (preg_match(self::VERSION_COMPARISON, $versionRequirement, $matches) > 0) { - return new ComparisonRequirement( - $matches['version'], - new VersionComparisonOperator( - $matches['operator'] !== '' ? $matches['operator'] : '>=', - ), - ); - } } - throw new InvalidVersionRequirementException; - } + assert($requirement instanceof ConstraintRequirementImplementation); - abstract public function isSatisfiedBy(string $version): bool; + return new ConstraintRequirement($requirement->asString()); + } abstract public function asString(): string; } diff --git a/src/Util/VersionComparisonOperator.php b/src/Util/VersionComparisonOperator.php index 9dcba3c32a..0b0d7befcb 100644 --- a/src/Util/VersionComparisonOperator.php +++ b/src/Util/VersionComparisonOperator.php @@ -9,7 +9,8 @@ */ namespace PHPUnit\Util; -use function in_array; +use SebastianBergmann\VersionRequirement\InvalidVersionOperatorException as InvalidVersionOperatorExceptionImplementation; +use SebastianBergmann\VersionRequirement\VersionComparisonOperator as VersionComparisonOperatorImplementation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit @@ -24,15 +25,15 @@ private string $operator; /** - * @param '!='|'<'|'<='|'<>'|'='|'=='|'>'|'>='|'eq'|'ge'|'gt'|'le'|'lt'|'ne' $operator - * * @throws InvalidVersionOperatorException */ public function __construct(string $operator) { - $this->ensureOperatorIsValid($operator); - - $this->operator = $operator; + try { + $this->operator = new VersionComparisonOperatorImplementation($operator)->asString(); + } catch (InvalidVersionOperatorExceptionImplementation) { + throw new InvalidVersionOperatorException($operator); + } } /** @@ -42,16 +43,4 @@ public function asString(): string { return $this->operator; } - - /** - * @param '!='|'<'|'<='|'<>'|'='|'=='|'>'|'>='|'eq'|'ge'|'gt'|'le'|'lt'|'ne' $operator - * - * @throws InvalidVersionOperatorException - */ - private function ensureOperatorIsValid(string $operator): void - { - if (!in_array($operator, ['<', 'lt', '<=', 'le', '>', 'gt', '>=', 'ge', '==', '=', 'eq', '!=', '<>', 'ne'], true)) { - throw new InvalidVersionOperatorException($operator); - } - } } diff --git a/tests/unit/Metadata/MetadataCollectionTest.php b/tests/unit/Metadata/MetadataCollectionTest.php index 591b811d8b..0df5fdaa91 100644 --- a/tests/unit/Metadata/MetadataCollectionTest.php +++ b/tests/unit/Metadata/MetadataCollectionTest.php @@ -14,8 +14,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; -use PHPUnit\Metadata\Version\ComparisonRequirement; -use PHPUnit\Util\VersionComparisonOperator; +use PHPUnit\Metadata\Version\Requirement; use stdClass; #[CoversClass(MetadataCollection::class)] @@ -640,16 +639,10 @@ private function collectionWithOneOfEach(): MetadataCollection Metadata::requiresOperatingSystemOnClass(''), Metadata::requiresPhpExtensionOnClass('', null), Metadata::requiresPhpOnClass( - new ComparisonRequirement( - '8.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 8.0.0'), ), Metadata::requiresPhpunitOnClass( - new ComparisonRequirement( - '10.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 10.0.0'), ), Metadata::requiresPhpunitExtensionOnClass(stdClass::class), Metadata::requiresEnvironmentVariableOnClass('foo', 'bar'), diff --git a/tests/unit/Metadata/MetadataTest.php b/tests/unit/Metadata/MetadataTest.php index 5b0f662074..28e6878b2e 100644 --- a/tests/unit/Metadata/MetadataTest.php +++ b/tests/unit/Metadata/MetadataTest.php @@ -14,9 +14,8 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; -use PHPUnit\Metadata\Version\ComparisonRequirement; +use PHPUnit\Metadata\Version\Requirement; use PHPUnit\TestFixture\Metadata\Attribute\ExampleTrait; -use PHPUnit\Util\VersionComparisonOperator; #[CoversClass(Metadata::class)] #[CoversClassesThatExtendClass(Metadata::class)] @@ -3662,10 +3661,7 @@ public function testCanBeRequiresOperatingSystemFamilyOnMethod(): void public function testCanBeRequiresPhpOnClass(): void { $metadata = Metadata::requiresPhpOnClass( - new ComparisonRequirement( - '8.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 8.0.0'), ); $this->assertFalse($metadata->isAfter()); @@ -3736,10 +3732,7 @@ public function testCanBeRequiresPhpOnClass(): void public function testCanBeRequiresPhpOnMethod(): void { $metadata = Metadata::requiresPhpOnMethod( - new ComparisonRequirement( - '8.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 8.0.0'), ); $this->assertFalse($metadata->isAfter()); @@ -3884,10 +3877,7 @@ public function testCanBeRequiresPhpExtensionWithVersionOnClass(): void { $metadata = Metadata::requiresPhpExtensionOnClass( 'test', - new ComparisonRequirement( - '1.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 1.0.0'), ); $this->assertFalse($metadata->isAfter()); @@ -4034,10 +4024,7 @@ public function testCanBeRequiresPhpExtensionWithVersionOnMethod(): void { $metadata = Metadata::requiresPhpExtensionOnMethod( 'test', - new ComparisonRequirement( - '1.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 1.0.0'), ); $this->assertFalse($metadata->isAfter()); @@ -4110,10 +4097,7 @@ public function testCanBeRequiresPhpExtensionWithVersionOnMethod(): void public function testCanBeRequiresPhpunitOnClass(): void { $metadata = Metadata::requiresPhpunitOnClass( - new ComparisonRequirement( - '10.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 10.0.0'), ); $this->assertFalse($metadata->isAfter()); @@ -4253,10 +4237,7 @@ public function testCanBeRequiresPhpunitExtensionOnClass(): void public function testCanBeRequiresPhpunitOnMethod(): void { $metadata = Metadata::requiresPhpunitOnMethod( - new ComparisonRequirement( - '10.0.0', - new VersionComparisonOperator('>='), - ), + Requirement::from('>= 10.0.0'), ); $this->assertFalse($metadata->isAfter()); diff --git a/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php b/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php index 975054679a..8b1742b19b 100644 --- a/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php +++ b/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php @@ -25,11 +25,4 @@ public function testCanBeRepresentedAsString(): void $this->assertSame('message', $requirement->asString()); } - - public function testIsNeverSatisfied(): void - { - $requirement = new InvalidVersionRequirement('message'); - - $this->assertFalse($requirement->isSatisfiedBy('1.0.0')); - } } diff --git a/tests/unit/Metadata/Version/RequirementTest.php b/tests/unit/Metadata/Version/RequirementTest.php index f94a05bd45..6eb4c378e2 100644 --- a/tests/unit/Metadata/Version/RequirementTest.php +++ b/tests/unit/Metadata/Version/RequirementTest.php @@ -9,9 +9,7 @@ */ namespace PHPUnit\Metadata; -use PharIo\Version\VersionConstraintParser; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; @@ -29,32 +27,6 @@ #[Group('metadata')] final class RequirementTest extends TestCase { - /** - * @return non-empty-list - */ - public static function constraintProvider(): array - { - return [ - [ - true, - '1.0.0', - new ConstraintRequirement( - (new VersionConstraintParser)->parse('1.0.0'), - ), - ], - ]; - } - - /** - * @return non-empty-list - */ - public static function comparisonProvider(): array - { - return [ - [true, '1.0.0', new ComparisonRequirement('1.0.0', new VersionComparisonOperator('='))], - ]; - } - public function testCanBeCreatedFromStringWithVersionConstraint(): void { $requirement = Requirement::from('^1.0'); @@ -63,24 +35,13 @@ public function testCanBeCreatedFromStringWithVersionConstraint(): void $this->assertSame('^1.0', $requirement->asString()); } - #[DataProvider('constraintProvider')] - public function testVersionRequirementCanBeCheckedUsingVersionConstraint(bool $expected, string $version, ConstraintRequirement $requirement): void - { - $this->assertSame($expected, $requirement->isSatisfiedBy($version)); - } - public function testCanBeCreatedFromStringWithSimpleComparison(): void { - $requirement = Requirement::from('>= 1.0'); + $requirement = Requirement::from('>= 1.0.0'); $this->assertInstanceOf(ComparisonRequirement::class, $requirement); - $this->assertSame('>= 1.0', $requirement->asString()); - } - - #[DataProvider('comparisonProvider')] - public function testVersionRequirementCanBeCheckedUsingSimpleComparison(bool $expected, string $version, ComparisonRequirement $requirement): void - { - $this->assertSame($expected, $requirement->isSatisfiedBy($version)); + $this->assertSame('>= 1.0.0', $requirement->asString()); + $this->assertSame('1.0.0', $requirement->version()); } public function testCannotBeCreatedFromInvalidString(): void