Skip to content

Commit

Permalink
Add basic tests and implementation for isSubsetOf
Browse files Browse the repository at this point in the history
  • Loading branch information
Seldaek committed May 5, 2020
1 parent a660f99 commit 0e30c37
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 23 deletions.
83 changes: 60 additions & 23 deletions src/Constraint/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ class Constraint implements CompilableConstraintInterface
/** @var Bound */
protected $upperBound;

/**
* Sets operator and version to compare with.
*
* @param string $operator
* @param string $version
*
* @throws \InvalidArgumentException if invalid operator is given.
*/
public function __construct($operator, $version)
{
if (!isset(self::$transOpStr[$operator])) {
throw new \InvalidArgumentException(sprintf(
'Invalid operator "%s" given, expected one of: %s',
$operator,
implode(', ', self::getSupportedOperators())
));
}

$this->operator = self::$transOpStr[$operator];
$this->version = $version;
}

/**
* @param ConstraintInterface $provider
*
Expand All @@ -84,6 +106,42 @@ public function matches(ConstraintInterface $provider)
return $provider->matches($this);
}

/**
* {@inheritDoc}
*/
public function isSubsetOf(ConstraintInterface $constraint)
{
if ($constraint instanceof EmptyConstraint) {
return true;
}

if ($constraint instanceof MultiConstraint) {
if ($constraint->isConjunctive()) {
foreach ($constraint->getConstraints() as $c) {
if (!$this->isSubsetOf($c)) {
return false;
}
}

return true;
} else {
foreach ($constraint->getConstraints() as $c) {
if ($this->isSubsetOf($c)) {
return true;
}
}

return false;
}
}

if ($this->operator === self::OP_NE) {
return (string) $constraint === (string) $this;
}

return $constraint->matches($this);
}

/**
* @param string|null $prettyString
*/
Expand Down Expand Up @@ -114,28 +172,6 @@ public static function getSupportedOperators()
return array_keys(self::$transOpStr);
}

/**
* Sets operator and version to compare with.
*
* @param string $operator
* @param string $version
*
* @throws \InvalidArgumentException if invalid operator is given.
*/
public function __construct($operator, $version)
{
if (!isset(self::$transOpStr[$operator])) {
throw new \InvalidArgumentException(sprintf(
'Invalid operator "%s" given, expected one of: %s',
$operator,
implode(', ', self::getSupportedOperators())
));
}

$this->operator = self::$transOpStr[$operator];
$this->version = $version;
}

/**
* @param string $a
* @param string $b
Expand Down Expand Up @@ -175,7 +211,8 @@ public function versionCompare($a, $b, $operator, $compareBranches = false)
return \version_compare($a, $b, $operator);
}

public function compile($otherOperator) {
public function compile($otherOperator)
{
if ($this->version[0] === 'd' && 'dev-' === substr($this->version, 0, 4)) {
if (self::OP_EQ === $this->operator) {
if (self::OP_EQ === $otherOperator) {
Expand Down
16 changes: 16 additions & 0 deletions src/Constraint/ConstraintInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,31 @@

namespace Composer\Semver\Constraint;

/**
* DO NOT IMPLEMENT this interface. It is only meant for usage as a type hint
* where appropriate but we do not support third parties implementing this
* interface themselves, and will do BC breaks to the interface as we see fit.
*/
interface ConstraintInterface
{
/**
* Checks whether the given constraint intersects in any way with this constraint
*
* @param ConstraintInterface $provider
*
* @return bool
*/
public function matches(ConstraintInterface $provider);

/**
* Checks whether the given constraint is wholly contained within this constraint
*
* @param ConstraintInterface $constraint
*
* @return bool
*/
public function isSubsetOf(ConstraintInterface $constraint);

/**
* @return Bound
*/
Expand Down
12 changes: 12 additions & 0 deletions src/Constraint/EmptyConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ public function matches(ConstraintInterface $provider)
return true;
}

/**
* Technically MultiConstraint can be equivalent to EmptyConstraints,
* but it is hard to detect so we only consider this to be a subset of
* itself
*
* {@inheritDoc}
*/
public function isSubsetOf(ConstraintInterface $constraint)
{
return $constraint instanceof EmptyConstraint;
}

public function compile($operator)
{
return 'true';
Expand Down
28 changes: 28 additions & 0 deletions src/Constraint/MultiConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ public function matches(ConstraintInterface $provider)
return true;
}

/**
* {@inheritDoc}
*/
public function isSubsetOf(ConstraintInterface $constraint)
{
if ($constraint instanceof EmptyConstraint) {
return true;
}

if (true === $this->conjunctive) {
foreach ($this->constraints as $c) {
if (!$c->isSubsetOf($constraint)) {
return false;
}
}

return true;
}

foreach ($this->getConstraints() as $c) {
if ($c->isSubsetOf($constraint)) {
return true;
}
}

return false;
}

/**
* @param string|null $prettyString
*/
Expand Down
87 changes: 87 additions & 0 deletions tests/Constraint/SubsetsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace Composer\Semver\Constraint;

use PHPUnit\Framework\TestCase;
use Composer\Semver\VersionParser;

class SubsetsTest extends TestCase
{
/**
* @dataProvider subsets
*/
public function testIsSubsetOf($aStr, $bStr)
{
$versionParser = new VersionParser;
$a = $versionParser->parseConstraints($aStr);
$b = $versionParser->parseConstraints($bStr);

$this->assertTrue($a->isSubsetOf($b), $aStr.' ('.$a.') should be seen as a subset of '.$bStr.' ('.$b.')');
}

public function subsets()
{
return array(
// x is subset of y
array('*', '*'),
array('1.0.0', '*'),
array('1.0.*', '*'),
array('^1.0 || ^2.0', '*'),
array('^1.0 || ^2.0', '^1.0 || ^2.0'),
array('^1.2', '^1.0 || ^2.0'),
array('^1.0 || ^2.0', '^1.0'),
array('1.2.3', '^1.0 || ^2.0'),
array('2.0.0-dev', '^1.0 || ^2.0'),
array('>= 2.1.0', '>= 2.0.0'),
array('^2.0', '<3.0.0'),
array('3.0.0', '<= 3.0.0'),
array('!= 3.0.0', '*'),
array('!= 3.0.0', '!= 3.0'),
array('!= 3.0.0', '> 3.0 || < 3.0'),
array('!= 3.0.0', '^2.0 || <2 || >3'),
array('>3', '^2 || ^3 || >=4'),
);
}

/**
* @dataProvider notSubsets
*/
public function testIsNotSubsetOf($aStr, $bStr)
{
$versionParser = new VersionParser;
$a = $versionParser->parseConstraints($aStr);
$b = $versionParser->parseConstraints($bStr);

$this->assertFalse($a->isSubsetOf($b), $aStr.' ('.$a.') should not be seen as a subset of '.$bStr.' ('.$b.')');
}

public function notSubsets()
{
return array(
// x is subset of y
array('*', '1.0.0'),
array('*', '1.0.*'),
array('*', '>= 1 || < 1'), // technically this should be a subset
array('*', '^1.0 || ^2.0'),
array('^1.0 || ^2.0', '^1.0, ^2.0'), // buggy constraint on the right here, checking it does not match
array('^1.0 || ^2.0', '^1.2'),
array('^1.0 || ^2.0', '1.2.3'),
array('3.0.0', '^1.0 || ^2.0'),
array('3.0.0', '< 3.0.0'),
array('3.0.0', '>= 3.0.1'),
array('!= 3.0.0', '= 3.0.0'),
array('!= 3.0.0', '!= 3.0.1'),
array('>3', '^2 || ^3 || >4'),
array('^2.1', '^2.0, !=2.1.3'),
);
}
}

0 comments on commit 0e30c37

Please sign in to comment.