Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/Composer/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
namespace RoadRunner\VersionChecker\Composer;

use Composer\InstalledVersions;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Semver\VersionParser;
use RoadRunner\VersionChecker\Version\Comparator;

final class Package implements PackageInterface
{
Expand Down Expand Up @@ -65,11 +68,30 @@ private function getMinVersion(string $version): string
{
$parser = new VersionParser();

$constraint = $parser->parseConstraints($version);

/** @var non-empty-string $min */
$min = $constraint->getLowerBound()->getVersion();
$min = $this->getLowerBound($parser->parseConstraints($version));

return $min;
}

/**
* Composer orders `3.0` below `2023.1`, so the lowest alternative of an `||` constraint
* is picked in RoadRunner release order instead.
*/
private function getLowerBound(ConstraintInterface $constraint): string
{
if (!$constraint instanceof MultiConstraint || !$constraint->isDisjunctive()) {
return $constraint->getLowerBound()->getVersion();
}

$min = null;
foreach ($constraint->getConstraints() as $alternative) {
$bound = $this->getLowerBound($alternative);
if ($min === null || Comparator::compare($bound, $min) < 0) {
$min = $bound;
}
}

return $min ?? $constraint->getLowerBound()->getVersion();
}
}
6 changes: 6 additions & 0 deletions src/Version/Comparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ public function __construct(?VersionParser $parser = null)
* Compares two RoadRunner versions in release order.
*
* @return int<-1, 1> Negative when $a is older than $b, positive when newer, zero when equal.
*
* @throws \UnexpectedValueException When a version cannot be parsed.
*/
public static function compare(string $a, string $b): int
{
$parser = new VersionParser();
$a = $parser->normalize($a);
$b = $parser->normalize($b);

$lineA = self::releaseLine($a);
$lineB = self::releaseLine($b);

Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/packages/branch/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "fixture/branch",
"require": {
"fixture/roadrunner": "dev-master"
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/packages/calendar/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "fixture/calendar",
"require": {
"fixture/roadrunner": "^2023.1 || ^2024.1"
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/packages/invalid/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "fixture/invalid",
"require": {
"fixture/roadrunner": "latest"
}
}
Empty file.
3 changes: 3 additions & 0 deletions tests/fixtures/packages/no-require/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "fixture/no-require"
}
6 changes: 6 additions & 0 deletions tests/fixtures/packages/stable/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "fixture/stable",
"require": {
"fixture/roadrunner": "^2.12"
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/packages/wildcard/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "fixture/wildcard",
"require": {
"fixture/roadrunner": "*"
}
}
71 changes: 71 additions & 0 deletions tests/src/Unit/Composer/PackageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,54 @@

namespace RoadRunner\VersionChecker\Tests\Unit\Composer;

use Composer\InstalledVersions;
use RoadRunner\VersionChecker\Composer\Package;
use Testo\Assert;
use Testo\Data\DataProvider;
use Testo\Data\DataSet;
use Testo\Lifecycle\AfterTest;
use Testo\Test;

#[Test]
final class PackageTest
{
private const FIXTURES = __DIR__ . '/../../../fixtures/packages';

#[AfterTest]
protected function restoreInstalledVersions(): void
{
// Null drops the override, so the next lookup reads vendor/composer/installed.php again.
InstalledVersions::reload(null);
}

public function testGetRequiredVersionsReturnsLowerBoundsOfSupportedConstraints(): void
{
InstalledVersions::reload([
'root' => self::installedPackage('fixture/root', self::FIXTURES . '/no-require') + ['dev' => true],
'versions' => [
'fixture/stable' => self::installedPackage('fixture/stable', self::FIXTURES . '/stable'),
'fixture/calendar' => self::installedPackage('fixture/calendar', self::FIXTURES . '/calendar'),
'fixture/wildcard' => self::installedPackage('fixture/wildcard', self::FIXTURES . '/wildcard'),
'fixture/branch' => self::installedPackage('fixture/branch', self::FIXTURES . '/branch'),
'fixture/invalid' => self::installedPackage('fixture/invalid', self::FIXTURES . '/invalid'),
'fixture/no-require' => self::installedPackage('fixture/no-require', self::FIXTURES . '/no-require'),
'fixture/no-composer-json' => self::installedPackage('fixture/no-composer-json', self::FIXTURES . '/no-composer-json'),
'fixture/metapackage' => ['type' => 'metapackage', 'install_path' => null] + self::installedPackage('fixture/metapackage', ''),
],
]);

$versions = (new Package())->getRequiredVersions('fixture/roadrunner');

Assert::array($versions)->sameElementsAs(['2.12.0.0-dev', '2023.1.0.0-dev', '0.0.0.0-dev']);
}

public function testGetRequiredVersionsReturnsEmptyListWhenNothingRequiresPackage(): void
{
$versions = (new Package())->getRequiredVersions('fixture/not-required-by-anything');

Assert::same($versions, []);
}

#[DataProvider('isSupportedVersionDataProvider')]
public function testIsSupportedVersion(string $version, bool $expected): void
{
Expand All @@ -32,6 +72,19 @@ public function testGetMinVersion(string $version, string $expected): void
Assert::same($ref->invoke($package, $version), $expected);
}

#[DataSet(['^2025.1 || ^3.0', '2025.1.0.0-dev'], 'calendar or 3.x')]
#[DataSet(['^3.0 || ^2025.1', '2025.1.0.0-dev'], '3.x or calendar')]
#[DataSet(['^3.0 || ^2024.1 || ^2.12', '2.12.0.0-dev'], 'all release lines')]
#[DataSet(['>=2025.1 <2026.0 || >=3.0', '2025.1.0.0-dev'], 'explicit ranges')]
public function testGetMinVersionFollowsReleaseOrderAcrossReleaseLines(string $version, string $expected): void
{
$package = new Package();
$ref = new \ReflectionMethod($package, 'getMinVersion');
$ref->setAccessible(true);

Assert::same($ref->invoke($package, $version), $expected);
}

public static function isSupportedVersionDataProvider(): \Traversable
{
yield ['1.0', true];
Expand All @@ -47,6 +100,8 @@ public static function isSupportedVersionDataProvider(): \Traversable
yield ['dev-feature/some', false];
yield ['<2.0', true];
yield ['<=2.0', true];
yield ['*', true];
yield ['latest', false];
}

public static function getMinVersionDataProvider(): \Traversable
Expand All @@ -65,5 +120,21 @@ public static function getMinVersionDataProvider(): \Traversable
yield ['1.0 - 2.0', '1.0.0.0-dev'];
yield ['<2.0', '0.0.0.0-dev'];
yield ['<=2.0', '0.0.0.0-dev'];
yield ['*', '0.0.0.0-dev'];
yield ['^2023.1 || ^2024.1', '2023.1.0.0-dev'];
}

private static function installedPackage(string $name, string $installPath): array
{
return [
'name' => $name,
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => $installPath,
'aliases' => [],
'dev_requirement' => false,
];
}
}
32 changes: 32 additions & 0 deletions tests/src/Unit/Process/ProcessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace RoadRunner\VersionChecker\Tests\Unit\Process;

use RoadRunner\VersionChecker\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Testo\Assert;
use Testo\Expect;
use Testo\Test;

#[Test]
final class ProcessTest
{
public function testExecReturnsOutput(): void
{
$process = new Process();

$output = $process->exec([\PHP_BINARY, '-r', 'echo "rr version 2024.3.0";']);

Assert::same($output, 'rr version 2024.3.0');
}

public function testExecThrowsOnNonZeroExitCode(): void
{
$process = new Process();

Expect::exception(ProcessFailedException::class);
$process->exec([\PHP_BINARY, '-r', 'exit(1);']);
}
}
36 changes: 36 additions & 0 deletions tests/src/Unit/Version/ComparatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use RoadRunner\VersionChecker\Version\Comparator;
use Testo\Assert;
use Testo\Data\DataProvider;
use Testo\Data\DataSet;
use Testo\Expect;
use Testo\Test;

#[Test]
Expand Down Expand Up @@ -64,6 +66,16 @@ public static function greaterThanDataProvider(): \Traversable
yield ['3.1.0', '3.0.0', false];
yield ['3.0.0', '3.1.0', true];
yield ['3.0.0-beta.1', '3.0.0', true];
// '0' is satisfied by any installed release
yield ['0', '2.12.3', true];
// a release candidate is older than its release
yield ['2023.1.0', '2023.1.0-rc.2', false];
yield ['2023.1.0-rc.2', '2023.1.0', true];
// the `v` prefix is ignored
yield ['v2023.1.0', '2023.1.0', true];
yield ['v3.0.0', '2025.1.5', false];
// a dev branch is older than any release
yield ['dev-master', '2.12.3', true];
}

public static function lessThanDataProvider(): \Traversable
Expand All @@ -89,6 +101,8 @@ public static function lessThanDataProvider(): \Traversable
yield ['3.0', '2025.1.5', true];
yield ['2023.1', '2.12.3', true];
yield ['2.12', '2024.3.0', false];
yield ['v3.0.0', '2025.1.5', true];
yield ['2023.1.0', '2023.1.0-rc.2', true];
}

public static function equalDataProvider(): \Traversable
Expand All @@ -111,6 +125,9 @@ public static function equalDataProvider(): \Traversable
yield ['2.0.0-alpha.1', '2.0.0-alpha.1', true];
yield ['3.0.0', 'v3.0.0', true];
yield ['2025.1.0', '3.0.0', false];
yield ['v2023.1.0', '2023.1.0', true];
yield ['3.0', '3.0.0', true];
yield ['2023.1.0-rc.2', '2023.1.0-RC2', true];
}

#[DataProvider('compareDataProvider')]
Expand All @@ -119,6 +136,24 @@ public function testCompare(string $a, string $b, int $expected): void
Assert::same(Comparator::compare($a, $b), $expected);
}

#[DataSet(['v3.0.1', '3.0.0', 1], 'v prefix, newer patch')]
#[DataSet(['v2023.1.0', '2023.1.0', 0], 'v prefix, same version')]
#[DataSet(['3.0', '3.0.0', 0], 'short form of the same version')]
public function testCompareAcceptsUnnormalizedVersions(string $a, string $b, int $expected): void
{
Assert::same(Comparator::compare($a, $b), $expected);
}

#[DataSet(['*'], 'wildcard')]
#[DataSet(['latest'], 'word')]
public function testRejectsNonVersionStrings(string $requested): void
{
$comparator = new Comparator();

Expect::exception(\UnexpectedValueException::class);
$comparator->greaterThan($requested, '3.0.0');
}

public static function compareDataProvider(): \Traversable
{
yield ['3.0.0.0', '3.0.0.0', 0];
Expand All @@ -129,5 +164,6 @@ public static function compareDataProvider(): \Traversable
yield ['2025.1.0.0', '2024.3.0.0', 1];
yield ['3.0.1.0', '3.0.0.0', 1];
yield ['dev-master', '3.0.0.0', -1];
yield ['3.0.0.0', 'dev-master', 1];
}
}
Loading
Loading