Last active
March 27, 2025 15:57
-
-
Save hopeseekr/fdb0ff9bd0957ea8d226ccc2def88d99 to your computer and use it in GitHub Desktop.
Automated Development of Composer version constraints reverse-engineering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Bettergist\Tests\Feature; | |
use Bettergist\Tests\TestCase; | |
use Composer\Semver\Constraint\Constraint; | |
use Composer\Semver\Semver; | |
use Composer\Semver\VersionParser; | |
class ComposerConstraintsMatcherTest extends TestCase | |
{ | |
function matchesVersionAuthoritative(string $constraint, string $version): bool | |
{ | |
return Semver::satisfies($version, $constraint); | |
} | |
/** | |
* Check if a Composer version constraint is valid. | |
* | |
* @param string $constraint The version constraint to validate. | |
* @return bool True if valid, false if invalid. | |
*/ | |
function isValidVersionConstraint(string $constraint): bool { | |
$parser = new VersionParser(); | |
try { | |
// This will throw an exception if the constraint is invalid. | |
$parser->parseConstraints($constraint); | |
return true; | |
} catch (\UnexpectedValueException $e) { | |
return false; | |
} | |
} | |
public function testCanDetermineIfAConstraintIsValid() | |
{ | |
$invalid = [ | |
'x' | |
]; | |
foreach ($invalid as $c) { | |
self::assertFalse($this->isValidVersionConstraint($c)); | |
} | |
} | |
private function ensure2Dots(string $version): string | |
{ | |
$versionParts = explode('.', $version); | |
if (count($versionParts) < 3) { | |
$version .= str_repeat('.0', 3 - count($versionParts)); | |
} | |
return $version; | |
} | |
private function versionSatisfies(string $constraints, string $version): bool | |
{ | |
// Normalize version to have at least 3 parts | |
$version = $this->ensure2Dots($version); | |
// Split constraint by OR operator | |
$orConstraints = explode('|', $constraints); | |
foreach ($orConstraints as $orConstraint) { | |
$orConstraint = trim($orConstraint); | |
// Split by AND operator (,) | |
$andConstraints = array_map('trim', explode(',', $orConstraint)); | |
$allAndSatisfied = true; | |
foreach ($andConstraints as $singleConstraint) { | |
if (!$this->satisfiesSingleConstraint($singleConstraint, $version)) { | |
$allAndSatisfied = false; | |
break; | |
} | |
} | |
if ($allAndSatisfied) { | |
return true; // If all AND conditions in this OR branch are satisfied, return true | |
} | |
} | |
return false; | |
} | |
private function satisfiesSingleConstraint(string $constraint, string $version): bool | |
{ | |
// Handle edge cases like 'x' or '*' | |
if ($constraint === 'x' || $constraint === '*') { | |
return true; | |
} | |
// Always return true for git branch constraints. | |
if (str_ends_with($constraint, '-dev') || str_ends_with($constraint, '-rc')) { | |
return true; | |
} | |
// Strip v... | |
$constraint = preg_replace('/([><=~^]+)?v/i', '$1', $constraint); | |
$version = preg_replace('/v([0-9]+)/i', '$1', $version); | |
// Normalize hyphen ranges first | |
$constraint = $this->normalizeHyphenRanges($constraint); | |
// Remove spaces around comparison operators (>=, >, <, !=) in the constraint string | |
$constraint = preg_replace('/([><!]=?)\s*/', '$1', $constraint); | |
// Convert "* >" to ">" and other normalizations. | |
$constraint = preg_replace('/\* ?([><!]=?)\s*/', '$1', $constraint); | |
$constraint = str_replace('. *', '.*', $constraint); | |
// If it ends with ".", add a "*". | |
// This effects 615 projects as of 2025-03-24. | |
// @see rinsvent/data2dto | |
if (str_ends_with($constraint, '.')) { | |
$constraint = substr($constraint, 0, -1); | |
} | |
// Handle dev/rc suffixes | |
if (str_ends_with($constraint, '-dev') || str_ends_with($constraint, '-rc')) { | |
return true; // Simplified for this example | |
} | |
// Replace .x with .* | |
$constraint = str_ireplace('.x', '.*', $constraint); | |
// Strip leading 'v' | |
$constraint = preg_replace('/([><=~^]+)?v/i', '$1', $constraint); | |
// Handle wildcards | |
if (strpos($constraint, '*') !== false) { | |
$pattern = str_replace('.', '\.', $constraint); | |
$pattern = str_replace('*', '(\d+){1,2}', $pattern); | |
return preg_match('/^' . $pattern . '/', $version) === 1; | |
} | |
// Handle caret (^) | |
if (strpos($constraint, '^') === 0) { | |
$baseVersion = substr($constraint, 1); | |
$baseVersion = $this->ensure2Dots($baseVersion); | |
if (preg_match('/^0\.(\d+)/', $baseVersion, $matches)) { | |
$minor = (int)$matches[1]; | |
$nextMinor = "0." . ($minor + 1) . ".0"; | |
return version_compare($version, $baseVersion, '>=') && | |
version_compare($version, $nextMinor, '<'); | |
} elseif (preg_match('/^(\d+)/', $baseVersion, $matches)) { | |
$major = (int)$matches[0]; | |
$nextMajor = ($major + 1) . ".0.0"; | |
return version_compare($version, $baseVersion, '>=') && | |
version_compare($version, $nextMajor, '<'); | |
} | |
} | |
// Handle tilde (~) | |
if (strpos($constraint, '~') === 0) { | |
$baseVersion = substr($constraint, 1); | |
$parts = explode('.', $baseVersion); | |
$major = (int)($parts[0] ?? 0); | |
$minor = (int)($parts[1] ?? 0); | |
$nextMinor = "$major." . ($minor + 1) . ".0"; | |
$baseVersion = $this->ensure2Dots($baseVersion); | |
return version_compare($version, $baseVersion, '>=') && | |
version_compare($version, $nextMinor, '<'); | |
} | |
// Handle comparison operators (>, >=, <, <=, =) | |
if (preg_match('/^([><=]+)(\d+(\.\d+)?(\.\d+)?)/', $constraint, $matches)) { | |
$operator = $matches[1]; | |
$compareVersion = $this->ensure2Dots($matches[2]); | |
return version_compare($version, $compareVersion, $operator); | |
} | |
// Handle exact version | |
if (preg_match('/^\d+(\.\d+)?(\.\d+)?$/', $constraint)) { | |
$constraint = $this->ensure2Dots($constraint); | |
return version_compare($version, $constraint, '=='); | |
} | |
return false; // Unknown constraint format | |
} | |
/** | |
* Matches a single condition against a version. | |
* | |
* @param string $condition Single condition (e.g., '>=2.2'). | |
* @param string $version Version to check against. | |
* | |
* @return bool True if the version satisfies the condition, false otherwise. | |
*/ | |
private function matchesSingleCondition($condition, $version): bool | |
{ | |
if (preg_match('/^([><=!]=?|<=|>=)\s*(\d+(\.\d+(\.\d+)?)?)$/', $condition, $matches)) { | |
$operator = $matches[1]; | |
$versionToCompare = $matches[2]; | |
// Normalize single number to use proper version format | |
if (preg_match('/^\d+$/', $versionToCompare)) { | |
$versionToCompare .= '.0.0'; | |
} elseif (preg_match('/^\d+\.\d+$/', $versionToCompare)) { | |
$versionToCompare .= '.0'; | |
} | |
return version_compare($version, $versionToCompare, $operator); | |
} | |
// Additional checks here if needed... | |
return false; | |
} | |
/** | |
* Generate version(s) for a given composer version constraint. | |
* | |
* For each part of a compound constraint (parts separated by "|"), | |
* this method generates a candidate version that is either valid (if | |
* $matches is true) or invalid (if $matches is false) according to that part. | |
* | |
* @param string $constraint A composer version constraint (may be compound). | |
* @param bool $matches Whether to generate a version that matches the constraint. | |
* | |
* @return array|int An array of candidate versions or 0 if none could be generated. | |
*/ | |
public function generateVersionForConstraint(string $constraint, bool $matches = true): array|int | |
{ | |
$orParts = array_map('trim', explode('|', $constraint)); | |
$results = []; | |
foreach ($orParts as $part) { | |
if (empty($part)) { | |
continue; | |
} | |
// Split by AND operator (,) | |
$andParts = array_map('trim', explode(',', $part)); | |
$candidate = null; | |
// Handle each AND condition to find a base version | |
foreach ($andParts as $andPart) { | |
$andCandidate = $this->generateCandidateForSingleConstraint($andPart, $matches); | |
if ($candidate === null) { | |
$candidate = $andCandidate; | |
} else { | |
// Adjust candidate to satisfy all AND conditions | |
$candidate = $this->adjustCandidateForAnd($candidate, $andPart, $matches); | |
} | |
} | |
if ($candidate && $this->versionSatisfies($part, $candidate) === $matches) { | |
$results[$part] = $candidate; | |
} else { | |
// If adjustment fails, try an opposite candidate | |
$opposite = $this->generateOppositeCandidate($part, !$matches); | |
if ($this->versionSatisfies($part, $opposite) === $matches) { | |
$results[$part] = $opposite; | |
} | |
} | |
} | |
return empty($results) ? 0 : $results; | |
} | |
private function generateCandidateForSingleConstraint(string $constraint, bool $matches): string | |
{ | |
// Normalize hyphen ranges first | |
$constraint = $this->normalizeHyphenRanges($constraint); | |
// Remove spaces around comparison operators (>=, >, <, !=) in the constraint string | |
$constraint = preg_replace('/([><!-]=?)\s*/', '$1', $constraint); | |
// Convert "* >" to ">" and other normalizations. | |
$constraint = preg_replace('/\* ?([><!]=?)\s*/', '$1', $constraint); | |
$constraint = str_replace('. *', '.*', $constraint); | |
// Split the constraint into parts if it contains multiple conditions (e.g., "^3 <3.30") | |
$parts = preg_split('/\s+/', trim($constraint)); | |
if (count($parts) > 1) { | |
return $this->handleCompoundConstraint($parts, $matches); | |
} | |
// Handle single constraint | |
$operator = ''; | |
if (preg_match('/^([<>=!~^]+)/', $constraint, $matchesOperator)) { | |
$operator = $matchesOperator[1]; | |
$constraint = preg_replace('/^[<>=!~^]+\s*/', '', $constraint); | |
} | |
if (strpos($constraint, '*') !== false) { | |
return $matches ? str_replace('*', '1', $constraint) : str_replace('*', '0', $constraint); | |
} | |
$base = $this->ensure2Dots($constraint); | |
switch ($operator) { | |
case '^': | |
$parts = explode('.', $base); | |
if ($matches) { | |
return $base; // e.g., 3.0.0 for ^3 | |
} | |
return ((int)$parts[0] + 1) . '.0.0'; // e.g., 4.0.0 for non-matching ^3 | |
case '<': | |
if ($matches) { | |
return $this->decrementVersion($base); // One step below base | |
} | |
return $base; // Equal to base is non-matching for strict < | |
case '>=': | |
if ($matches) { | |
return $base; // Base satisfies >= | |
} | |
return $this->decrementVersion($base); // Below base | |
default: | |
return $base; // Exact version | |
} | |
} | |
private function handleCompoundConstraint(array $parts, bool $matches): string | |
{ | |
// For "^3 <3.30" | |
$minVersion = null; | |
$maxVersion = null; | |
foreach ($parts as $part) { | |
if (preg_match('/^([<>=!~^]+)(.*)/', $part, $match)) { | |
$operator = $match[1]; | |
$version = $this->ensure2Dots($match[2]); | |
if ($operator === '^') { | |
$minVersion = $version; // e.g., 3.0.0 | |
$maxVersion = ((int)explode('.', $version)[0] + 1) . '.0.0'; // e.g., 4.0.0 | |
} elseif ($operator === '<') { | |
$maxVersion = $version; // e.g., 3.30.0 | |
} elseif ($operator === '>=') { | |
$minVersion = $version; | |
} | |
} | |
} | |
if ($matches) { | |
// Return a version in the range, e.g., minVersion or slightly above | |
if ($maxVersion === null) { | |
// dd($matches, $parts); | |
$a = 1; | |
} | |
return $minVersion ?? $this->decrementVersion($maxVersion); | |
} else { | |
// Return a version outside the range, e.g., below min or at/above max | |
if ($minVersion && $this->versionCompare($minVersion, $maxVersion) < 0) { | |
return $this->decrementVersion($minVersion); // Below min | |
} | |
return $maxVersion; // At or above max | |
} | |
} | |
private function decrementVersion(string $version): string | |
{ | |
$parts = explode('.', $version); | |
$parts[2] = (int)$parts[2] - 1; | |
if ($parts[2] < 0) { | |
$parts[2] = 999; | |
$parts[1] = (int)$parts[1] - 1; | |
if ($parts[1] < 0) { | |
$parts[1] = 99; | |
$parts[0] = (int)$parts[0] - 1; | |
} | |
} | |
return implode('.', $parts); | |
} | |
private function versionCompare(string $v1, string $v2): int | |
{ | |
return version_compare($v1, $v2); | |
} | |
private function adjustCandidateForAnd(string $candidate, string $constraint, bool $matches): string | |
{ | |
$parts = explode('.', $candidate); | |
if (preg_match('/^>=(\d+(\.\d+)?(\.\d+)?)/', $constraint, $matches)) { | |
$minVersion = $this->ensure2Dots($matches[1]); | |
if (version_compare($candidate, $minVersion, '<')) { | |
return $minVersion; | |
} | |
} elseif (preg_match('/^<(\d+(\.\d+)?(\.\d+)?)/', $constraint, $matches)) { | |
$maxVersion = $this->ensure2Dots($matches[1]); | |
if (version_compare($candidate, $maxVersion, '>=')) { | |
$parts[2] = (int)$parts[2] - 1; // Decrease patch | |
return implode('.', $parts); | |
} | |
} elseif (strpos($constraint, '^') === 0) { | |
$base = $this->ensure2Dots(substr($constraint, 1)); | |
if (version_compare($candidate, $base, '<')) { | |
return $base; | |
} | |
} elseif (strpos($constraint, '~') === 0) { | |
$base = $this->ensure2Dots(substr($constraint, 1)); | |
if (version_compare($candidate, $base, '<')) { | |
return $base; | |
} | |
} | |
return $candidate; | |
} | |
/** | |
* Generate a candidate version by replacing wildcards and handling constraints. | |
* | |
* Examples: | |
* - "5.0.*@stable" becomes "5.0.1" | |
* - "~5.5" becomes "5.5.0" | |
* - "^7.2.1" becomes "7.2.1" | |
* - ">3" becomes "3.0.0" | |
* - "<4" becomes "4.0.0" | |
* | |
* @param string $constraintPart A single part of the constraint. | |
* | |
* @return string A candidate version string. | |
*/ | |
private function generateValidCandidate(string $constraintPart): string { | |
// Remove comparison operators (>, <, >=, <=, =, ==, !=) | |
$candidate = preg_replace('/^[<>=!~^]+\s*/', '', $constraintPart); | |
// Remove any stability flags (e.g., "@stable", "@beta", etc.). | |
$candidate = preg_replace('/\@[a-z]+$/i', '', $candidate); | |
// Replace 'x' with '*'. | |
$candidate = str_ireplace('x', '*', $candidate); | |
// Replace wildcards with a candidate number. | |
$candidate = str_replace('*', '1', $candidate); | |
// If it ends with a ., remove it... | |
if (str_ends_with($candidate, '.')) { | |
$candidate = substr($candidate, 0, -1); | |
} | |
// If the candidate has only a major version, add minor and patch. | |
if (preg_match('/^\d+$/', $candidate)) { | |
$candidate .= '.0.0'; | |
} | |
// If the candidate has only major.minor, add patch. | |
elseif (preg_match('/^\d+\.\d+$/', $candidate)) { | |
$candidate .= '.0'; | |
} | |
// Ensure we have at least 3 parts in the version | |
$parts = explode('.', $candidate); | |
while (count($parts) < 3) { | |
$parts[] = '0'; | |
} | |
$candidate = implode('.', $parts); | |
return $candidate; | |
} | |
/** | |
* Generate a candidate version that is expected to be the opposite (valid or invalid) | |
* relative to the constraint. | |
* | |
* This implementation simply modifies the major version number. | |
* | |
* @param string $constraintPart A single part of the constraint. | |
* @param bool $shouldMatch If true, generate a candidate that matches; otherwise, one that doesn't. | |
* | |
* @return string A candidate version string. | |
*/ | |
private function generateOppositeCandidate(string $constraintPart, bool $shouldMatch): string { | |
$validCandidate = $this->generateValidCandidate($constraintPart); | |
// Try to extract the major, minor, and patch parts. | |
if (preg_match('/^(\d+)\.(\d+)\.(\d+)/', $validCandidate, $matches)) { | |
$major = (int)$matches[1]; | |
$minor = $matches[2]; | |
$patch = $matches[3]; | |
if ($shouldMatch) { | |
// If we want a matching version, return the valid candidate. | |
return $validCandidate; | |
} else { | |
// To generate an invalid candidate, change the major version. | |
// For example, if the candidate is "5.0.1", return "4.0.1". | |
$newMajor = ($major === 0) ? 1 : $major - 1; | |
return $newMajor . '.' . $minor . '.' . $patch; | |
} | |
} elseif (preg_match('/^(\d+)\.(\d+)/', $validCandidate, $matches)) { | |
// Fallback if we only have major and minor parts. | |
$major = (int)$matches[1]; | |
$minor = $matches[2]; | |
if ($shouldMatch) { | |
return $validCandidate; | |
} else { | |
$newMajor = ($major === 0) ? 1 : $major - 1; | |
return $newMajor . '.' . $minor . '.0'; | |
} | |
} | |
// Final fallback: if unable to parse, return a hardcoded candidate. | |
return $shouldMatch ? '1.0.0' : '0.0.1'; | |
} | |
/** | |
* Generate valid versions for any Composer constraint, including complex multi-constraints | |
* | |
* @param string $constraint The composer constraint | |
* @return array An array of valid versions that satisfy the constraint parts | |
*/ | |
function generateValidVersionsForConstraint(string $constraint): array { | |
$versionParser = new VersionParser(); | |
$validVersions = []; | |
// Split by OR operators (both || and | are supported in Composer) | |
$parts = preg_split('/\s*\|\|\s*|\s*\|\s*/', $constraint); | |
foreach ($parts as $part) { | |
$part = trim($part); | |
if (empty($part)) { | |
continue; | |
} | |
try { | |
$parsedConstraint = $versionParser->parseConstraints($part); | |
$version = null; | |
// Handle exact version | |
if (preg_match('/^\d+\.\d+\.\d+$/', $part)) { | |
$version = $part; | |
} | |
// Handle wildcard | |
elseif (strpos($part, '*') !== false) { | |
$version = str_replace('*', '0', $part); | |
if (substr($part, -1) === '*') { | |
$version = rtrim($version, '.'); | |
} | |
// Ensure it has three segments | |
$versionParts = explode('.', $version); | |
while (count($versionParts) < 3) { | |
$versionParts[] = '0'; | |
} | |
$version = implode('.', $versionParts); | |
} | |
// Handle ^X | |
elseif (preg_match('/^\^(\d+)$/', $part, $matches)) { | |
$version = $matches[1] . '.0.0'; | |
} | |
// Handle ~X | |
elseif (preg_match('/^~(\d+)$/', $part, $matches)) { | |
$version = $matches[1] . '.0.0'; | |
} | |
// Handle ^X.Y | |
elseif (preg_match('/^\^(\d+\.\d+)$/', $part, $matches)) { | |
$version = $matches[1] . '.0'; | |
} | |
// Handle ~X.Y | |
elseif (preg_match('/^~(\d+\.\d+)$/', $part, $matches)) { | |
$version = $matches[1] . '.0'; | |
} | |
// Handle ^X.Y.Z | |
elseif (preg_match('/^\^(\d+\.\d+\.\d+)$/', $part, $matches)) { | |
$version = $matches[1]; | |
} | |
// Handle ~X.Y.Z | |
elseif (preg_match('/^~(\d+\.\d+\.\d+)$/', $part, $matches)) { | |
$version = $matches[1]; | |
} | |
// Handle >=X.Y | |
elseif (preg_match('/^>=(\d+(\.\d+(\.\d+)?)?)$/', $part, $matches)) { | |
$version = $matches[1]; | |
$versionParts = explode('.', $version); | |
while (count($versionParts) < 3) { | |
$versionParts[] = '0'; | |
} | |
$version = implode('.', $versionParts); | |
} | |
// Handle >X.Y | |
elseif (preg_match('/^>(\d+(\.\d+(\.\d+)?)?)$/', $part, $matches)) { | |
$version = $matches[1]; | |
$versionParts = explode('.', $version); | |
while (count($versionParts) < 3) { | |
$versionParts[] = '0'; | |
} | |
$versionParts[2] = (int)$versionParts[2] + 1; | |
$version = implode('.', $versionParts); | |
} | |
// Handle <X.Y | |
elseif (preg_match('/^<(\d+(\.\d+(\.\d+)?)?)$/', $part, $matches)) { | |
$version = $matches[1]; | |
$versionParts = explode('.', $version); | |
while (count($versionParts) < 3) { | |
$versionParts[] = '0'; | |
} | |
if ($versionParts[2] > '0') { | |
$versionParts[2] = (int)$versionParts[2] - 1; | |
} elseif ($versionParts[1] > '0') { | |
$versionParts[1] = (int)$versionParts[1] - 1; | |
$versionParts[2] = '9'; | |
} else { | |
$versionParts[0] = (int)$versionParts[0] - 1; | |
$versionParts[1] = '9'; | |
$versionParts[2] = '9'; | |
} | |
$version = implode('.', $versionParts); | |
} | |
// Handle <=X.Y | |
elseif (preg_match('/^<=(\d+(\.\d+(\.\d+)?)?)$/', $part, $matches)) { | |
$version = $matches[1]; | |
$versionParts = explode('.', $version); | |
while (count($versionParts) < 3) { | |
$versionParts[] = '0'; | |
} | |
$version = implode('.', $versionParts); | |
} | |
// For complex constraints, test some candidates | |
else { | |
$testVersions = []; | |
// Extract version numbers from the constraint | |
preg_match_all('/\d+(\.\d+)*/', $part, $matches); | |
foreach ($matches[0] as $match) { | |
$versionParts = explode('.', $match); | |
while (count($versionParts) < 3) { | |
$versionParts[] = '0'; | |
} | |
// Generate variants | |
$base = implode('.', $versionParts); | |
$testVersions[] = $base; | |
$testVersions[] = $versionParts[0] . '.' . $versionParts[1] . '.' . ((int)$versionParts[2] + 1); | |
$testVersions[] = $versionParts[0] . '.' . ((int)$versionParts[1] + 1) . '.0'; | |
} | |
// Test each candidate | |
foreach ($testVersions as $testVersion) { | |
try { | |
$normalizedVersion = $versionParser->normalize($testVersion); | |
$versionConstraint = new Constraint('==', $normalizedVersion); | |
if ($parsedConstraint->matches($versionConstraint)) { | |
$version = $testVersion; | |
break; | |
} | |
} catch (\Exception $e) { | |
continue; | |
} | |
} | |
} | |
// Verify the generated version is valid for this constraint part | |
if ($version !== null) { | |
$normalizedVersion = $versionParser->normalize($version); | |
$versionConstraint = new Constraint('==', $normalizedVersion); | |
if ($parsedConstraint->matches($versionConstraint)) { | |
$validVersions[$part] = $version; | |
} | |
} | |
} catch (\Exception $e) { | |
// Skip parts that can't be parsed or matched | |
continue; | |
} | |
} | |
return $validVersions; | |
} | |
/** | |
* Converts "5 - 6" to ">= 5.0.0 < 6.0.0". | |
*/ | |
private function normalizeHyphenRanges(string $constraint): string | |
{ | |
// Handle "X - Y" ranges by converting to ">=X <Y" | |
return preg_replace_callback( | |
'/(\d+(?:\.\d+)*)\s*-\s*(\d+(?:\.\d+)*)/', | |
function ($matches) { | |
$lower = $this->ensure2Dots($matches[1]); | |
$upper = $this->ensure2Dots($matches[2]); | |
return ">=$lower <$upper"; | |
}, | |
$constraint | |
); | |
} | |
public function testComplexComposerConstraints() | |
{ | |
$constraints = [ | |
'>1.1.8', | |
'* >=4', | |
'5 - 6', | |
'> 3', | |
'^3 <3.30', | |
'5.7.', | |
'v3.x', | |
"^3", | |
"^2 <3", | |
'^0.', | |
'~3.', | |
"2.x-dev", | |
"2.x", | |
"2.X", | |
"^V2.0", | |
"V2.0", | |
"^v2.0", | |
"v2.0", | |
"~6", | |
"^7", | |
">4", | |
"<2", | |
"3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", | |
"5.0.* || 5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.* || 7.*", | |
"~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", | |
"^7.2.1", "^7.2" | |
]; | |
foreach ($constraints as $constraint) { | |
$versionsToMatch = $this->generateVersionForConstraint($constraint, true); | |
if (empty($versionsToMatch)) { | |
$versionsToMatch = $this->generateValidVersionsForConstraint($constraint); | |
} | |
foreach ($versionsToMatch as $constraint => $version) { | |
self::assertTrue($this->versionSatisfies($constraint, $version), "The constraint '$constraint' didn't validate against '$version'."); | |
} | |
// dd($versionsToMatch); | |
// $versionsToMiss = $this->generateVersionForConstraint($constraint, false); | |
// foreach ($versionsToMiss as $constraint => $version) { | |
// self::assertFalse($this->matchesVersion($constraint, $version)); | |
// } | |
} | |
} | |
public function testAllComposerConstraints(): void | |
{ | |
// dd(file_get_contents( __DIR__ . '/constraints.json')); | |
$constraints = json_decode(file_get_contents(__DIR__ . '/constraints.json')); | |
//dd($constraints); | |
$constraints = array_reverse($constraints); | |
$constraints = array_chunk($constraints, 20); | |
foreach ($constraints as $i => $localSet) { | |
++$i; | |
// $localSet = [ '^2 <3']; | |
// $localSet = [ '^3' ]; | |
// $localSet = [ '^3 <3.30' ]; | |
$localSet = [ '>1.1.8' ]; | |
// dump([$localSet, "Chunk #$i"]); | |
$this->doTestAllComposerConstraints($localSet); | |
break; | |
if ($i >= 400) { | |
dump("Chunk #$i: Continue??"); | |
//sleep(1); | |
usleep(50000); | |
//fgets(STDIN); | |
} | |
} | |
} | |
/** | |
* Test all constraints against PHP versions | |
*/ | |
private function doTestAllComposerConstraints(array $constraints): void | |
{ | |
$totalTests = 0; | |
$errors = []; | |
// Test each constraint against each PHP version | |
foreach ($constraints as $constraint) { | |
// Skip invalid constraints | |
if (empty($constraint) || !is_string($constraint)) { | |
continue; | |
} | |
// If it isn't a valid composer constraint, go ahead and skip the test. | |
if ($this->isValidVersionConstraint($constraint) === false) { | |
dump("====== INVALID CONSTRAINT: $constraint ======"); | |
file_put_contents('invalid-constraints.log', "$constraint\n", FILE_APPEND); | |
continue; | |
} | |
try { | |
$validVersions = $this->generateVersionForConstraint($constraint); | |
foreach ($validVersions as $version) { | |
$totalTests++; | |
// Call our version constraint checker | |
$result = $this->versionSatisfies($constraint, $version); | |
// Since we don't have a reference implementation to compare against, | |
// we're just ensuring the function runs without errors. | |
// In a real test, you might compare against Composer's actual implementation. | |
$this->assertIsBool($result, "Result for constraint '$constraint' against $version should be boolean"); | |
} | |
} catch (\Exception $e) { | |
$errors[] = "Error testing constraint '$constraint': " . $e->getMessage(); | |
} | |
} | |
// Report any errors | |
if (!empty($errors)) { | |
file_put_contents('test-errors.log', implode("\n", $errors) . "\n", FILE_APPEND); | |
} | |
// $this->assertEmpty($errors, "Encountered " . count($errors) . " errors: " . implode(", ", $errors)); | |
$this->addToAssertionCount($totalTests); | |
//echo "Successfully tested {$totalTests} constraint/version combinations."; | |
} | |
/** | |
* Test specific known constraints (validation test) | |
* | |
* @return void | |
*/ | |
public function testKnownConstraints() | |
{ | |
// Test cases with expected results | |
$testCases = [ | |
["8.*", "8.0", true], | |
["8.*", "8.4", true], | |
["8.*", "7.2", false], | |
["8.0.*", "8.0", true], | |
["^7.2|8.0.*", "8.0", true], | |
["^7.2|8.0.*", "7.1", false], | |
["^7.2|8.0.*", "7.2", true], | |
["^7.2|8.0.*", "7.4", true], | |
["^7.2|8.0.*", "8.1", false], | |
[">=5.6 <8.0", "5.6", true], | |
[">=5.6 <8.0", "7.4", true], | |
[">=5.6 <8.0", "8.0", false], | |
]; | |
foreach ($testCases as $index => [$constraint, $phpVersion, $expected]) { | |
$result = $this->versionSatisfies($constraint, $phpVersion); | |
$this->assertSame( | |
$expected, | |
$result, | |
"Test case #{$index}: expected constraint '{$constraint}' to " . | |
($expected ? "match" : "not match") . " PHP {$phpVersion}" | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment