Skip to content
151 changes: 151 additions & 0 deletions tests/test_the_test/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,154 @@ def check(declaration: str, tested_version: str, *, should_be_inside: bool):
check(declaration, "3.4.5", should_be_inside=True)
check(declaration, "3.9.9", should_be_inside=True)
check(declaration, "4.0.0", should_be_inside=True)


def test_semver():
assert ComponentVersion("nodejs", "v2.7.0-rc.4") > ComponentVersion("nodejs", "v2.7.0-dev")
assert Version("2.7.0-rc.4") not in CustomSpec("<2.7.0-dev")
assert ComponentVersion("nodejs", "v2.7.0-a") < ComponentVersion("nodejs", "v2.7.0-dev")
assert Version("2.7.0-a") not in CustomSpec(">2.7.0-dev")


@pytest.mark.parametrize(
("spec", "version", "expected"),
[
# Equality operator
("1.0.0", "1.0.0", True),
("1.0.0", "1.0.1", False),
("1.0.0", "0.9.9", False),
("1.0.0", "1.0.0-alpha", False),
("1.0.0-alpha", "1.0.0-alpha", True),
("1.0.0-alpha", "1.0.0-beta", False),
("1.0.0-alpha", "1.0.0", False),
("1.0.0-alpha.1", "1.0.0-alpha.1", True),
("1.0.0-alpha.1", "1.0.0-alpha.2", False),
# Basic operators without prerelease
(">1.0.0", "1.0.1", True),
(">1.0.0", "1.0.0", False),
(">1.0.0", "0.9.9", False),
(">=1.0.0", "1.0.0", True),
(">=1.0.0", "0.9.9", False),
("<2.0.0", "1.9.9", True),
("<2.0.0", "2.0.0", False),
("<=2.0.0", "2.0.0", True),
("<=2.0.0", "2.0.1", False),
# Prereleases included in non-prerelease specs (different major.minor.patch)
(">=1.0.0", "1.0.1-alpha", True),
("<2.0.0", "1.9.9-beta", True),
(">1.0.0", "1.0.1-rc.1", True),
# Prerelease has lower precedence than its normal version (semver rule 11.3)
(">=1.0.0", "1.0.0-alpha", False),
(">1.0.0", "1.0.0-rc.1", False),
("<1.0.0", "1.0.0-alpha", True),
# GT/GTE with prerelease target: lexical prerelease comparison
(">2.7.0-dev", "2.7.0", True),
(">2.7.0-dev", "2.8.0", True),
(">2.7.0-dev", "2.7.0-rc.4", True),
(">2.7.0-dev", "2.7.0-dev", False),
(">2.7.0-dev", "2.7.0-alpha", False),
(">2.7.0-dev", "2.6.0", False),
(">=2.7.0-dev", "2.7.0", True),
(">=2.7.0-dev", "2.7.0-dev", True),
(">=2.7.0-dev", "2.7.0-rc.4", True),
(">=2.7.0-dev", "2.7.0-alpha", False),
(">=2.7.0-dev", "2.6.9", False),
# LT/LTE with prerelease target: lexical prerelease comparison
("<2.7.0-dev", "2.7.0-alpha", True),
("<2.7.0-dev", "2.7.0-beta", True),
("<2.7.0-dev", "2.6.0", True),
("<2.7.0-dev", "2.7.0-dev", False),
("<2.7.0-dev", "2.7.0-rc.4", False),
("<2.7.0-dev", "2.7.0", False),
("<=2.7.0-dev", "2.7.0-dev", True),
("<=2.7.0-dev", "2.7.0-alpha", True),
("<=2.7.0-dev", "2.7.0-rc.4", False),
("<=2.7.0-dev", "2.7.0", False),
# Semver precedence chain (semver rule 11.4)
(">1.0.0-alpha", "1.0.0-alpha.1", True),
(">1.0.0-alpha.1", "1.0.0-alpha.beta", True),
(">1.0.0-alpha.beta", "1.0.0-beta", True),
(">1.0.0-beta", "1.0.0-beta.2", True),
(">1.0.0-beta.2", "1.0.0-beta.11", True),
(">1.0.0-beta.11", "1.0.0-rc.1", True),
(">1.0.0-rc.1", "1.0.0", True),
# Numeric prerelease identifiers compared numerically, not lexically (semver rule 11.4.1)
("<1.0.0-beta.11", "1.0.0-beta.2", True),
# Numeric identifiers have lower precedence than non-numeric (semver rule 11.4.3)
(">1.0.0-1", "1.0.0-alpha", True),
# AND clauses (space-separated)
(">=1.0.0 <2.0.0", "1.5.0", True),
(">=1.0.0 <2.0.0", "2.0.0", False),
(">=1.0.0 <2.0.0", "0.9.0", False),
(">=1.0.0 <2.0.0", "1.5.0-beta", True),
# OR clauses (||)
(">=1.0.0 <2.0.0 || >=3.0.0", "1.5.0", True),
(">=1.0.0 <2.0.0 || >=3.0.0", "3.0.0", True),
(">=1.0.0 <2.0.0 || >=3.0.0", "2.5.0", False),
# Caret (^) ranges: ^1.2.3 means >=1.2.3 <2.0.0
("^1.2.3", "1.2.3", True),
("^1.2.3", "1.9.9", True),
("^1.2.3", "1.2.2", False),
("^1.2.3", "2.0.0", False),
("^1.2.3", "1.5.0-alpha", True),
("^1.2.3", "1.2.3-alpha", False),
("^1.2.3", "2.0.0-alpha", False),
# Caret with prerelease target: ^1.2.3-beta means >=1.2.3-beta <2.0.0-0
("^1.2.3-beta", "1.2.3-alpha", False),
("^1.2.3-beta", "1.2.3-beta", True),
("^1.2.3-beta", "1.2.3-rc.1", True),
("^1.2.3-beta", "1.2.3", True),
("^1.2.3-beta", "1.9.9", True),
("^1.2.3-beta", "2.0.0", False),
# Caret with 0.x: ^0.2.3 means >=0.2.3 <0.3.0-0 (minor is locked)
("^0.2.3", "0.2.3", True),
("^0.2.3", "0.2.9", True),
("^0.2.3", "0.2.3-alpha", False),
("^0.2.3", "0.2.4-alpha", True),
("^0.2.3", "0.3.0", False),
("^0.2.3", "0.3.0-alpha", False),
# Caret with 0.0.x: ^0.0.3 means >=0.0.3 <0.0.4-0 (patch is locked)
("^0.0.3", "0.0.3", True),
("^0.0.3", "0.0.3-alpha", False),
("^0.0.3", "0.0.4", False),
("^0.0.3", "0.0.4-alpha", False),
# Caret with 0.0.x and prerelease: ^0.0.3-beta means >=0.0.3-beta <0.0.4-0
("^0.0.3-beta", "0.0.3-alpha", False),
("^0.0.3-beta", "0.0.3-beta", True),
("^0.0.3-beta", "0.0.3-rc.1", True),
("^0.0.3-beta", "0.0.3", True),
("^0.0.3-beta", "0.0.4", False),
# Hyphen ranges: 1.0.0 - 2.0.0 means >=1.0.0 <=2.0.0
("1.0.0 - 2.0.0", "1.5.0", True),
("1.0.0 - 2.0.0", "1.0.0", True),
("1.0.0 - 2.0.0", "2.0.0", True),
("1.0.0 - 2.0.0", "2.0.1", False),
("1.0.0 - 2.0.0", "0.9.9", False),
("1.0.0 - 2.0.0", "1.0.0-alpha", False),
("1.0.0 - 2.0.0", "1.0.1-alpha", True),
("1.0.0 - 2.0.0", "2.0.0-rc.1", True),
("1.0.0 - 2.0.0", "2.0.1-alpha", False),
# Hyphen with prerelease lower bound
("1.0.0-beta - 2.0.0", "1.0.0-alpha", False),
("1.0.0-beta - 2.0.0", "1.0.0-beta", True),
("1.0.0-beta - 2.0.0", "1.0.0-rc.1", True),
("1.0.0-beta - 2.0.0", "1.0.0", True),
# Hyphen with prerelease upper bound
("1.0.0 - 2.0.0-rc.1", "2.0.0-alpha", True),
("1.0.0 - 2.0.0-rc.1", "2.0.0-rc.1", True),
("1.0.0 - 2.0.0-rc.1", "2.0.0-rc.2", False),
("1.0.0 - 2.0.0-rc.1", "2.0.0", False),
("1.0.0 - 2.0.0-rc.1", "1.9.9", True),
# Hyphen with prerelease on both bounds
("1.0.0-alpha - 2.0.0-beta", "1.0.0-alpha", True),
("1.0.0-alpha - 2.0.0-beta", "1.0.0", True),
("1.0.0-alpha - 2.0.0-beta", "2.0.0-alpha", True),
("1.0.0-alpha - 2.0.0-beta", "2.0.0-beta", True),
("1.0.0-alpha - 2.0.0-beta", "2.0.0-rc.1", False),
("1.0.0-alpha - 2.0.0-beta", "2.0.0", False),
("1.0.0-alpha - 2.0.0-beta", "0.9.9", False),
],
)
def test_semver_ranges(spec: str, version: str, *, expected: bool) -> None:
result = Version(version) in CustomSpec(spec)
assert result == expected, f"Version('{version}') in CustomSpec('{spec}') = {result}, expected {expected}"
60 changes: 60 additions & 0 deletions utils/manifest/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,75 @@
from .const import TestDeclaration


from semantic_version.base import AllOf, Never, Range, Version


# semver module offers two spec engine :
# 1. SimpleSpec : not a good fit because it does not allows OR clause
# 2. NpmSpec : not a good fit because it disallow prerelease version by default (6.0.0-pre is not in ">=5.0.0")
# So we use a custom one, based on NPM spec, allowing pre-release versions
class _CustomParser(semver.NpmSpec.Parser):
# [CHANGED] Override range() to use PRERELEASE_ALWAYS policy on all clauses.
# The library defaults to PRERELEASE_SAMEPATCH which excludes prereleases
# from non-prerelease specs (e.g. 6.0.0-pre not in ">=5.0.0").
@classmethod
def range(cls, operator: Any, target: Any) -> semver.base.Range: # noqa: ANN401
return semver.base.Range(operator, target, prerelease_policy=semver.base.Range.PRERELEASE_ALWAYS)

# [CHANGED] Override parse() to fix prerelease handling.
# The library splits prerelease clauses into separate branches that break
# alphabetical ordering (e.g. 2.7.0-rc.4 incorrectly matches "<2.7.0-dev").
# Our version removes that split and uses clauses directly, relying on
# PRERELEASE_ALWAYS from range() for correct semver comparison.
@classmethod
def parse(cls, expression: str) -> AllOf:
result = Never()
# [IDENTICAL] Split on || for OR groups
groups = expression.split(cls.JOINER)
for raw_group in groups:
group = raw_group.strip() or ">=0.0.0"

subclauses = []
# [IDENTICAL] Hyphen range handling
if cls.HYPHEN in group:
low, high = group.split(cls.HYPHEN, 2)
subclauses = cls.parse_simple(">=" + low) + cls.parse_simple("<=" + high)
else:
# [IDENTICAL] Block parsing and validation
blocks = group.split(" ")
for block in blocks:
if not cls.NPM_SPEC_BLOCK.match(block):
raise ValueError(f"Invalid NPM block in {expression!r}: {block!r}")
parsed = cls.parse_simple(block)
# [CHANGED] Caret upper bound: ^1.2.3 expands to >=1.2.3 <2.0.0.
# Replace <X.Y.Z with <X.Y.Z-0 so prereleases of the next major
# are excluded (e.g. 2.0.0-alpha not in ^1.2.3).
if block.startswith("^"):
for clause in parsed:
if clause.operator == Range.OP_LT and not clause.target.prerelease:
subclauses.append(
cls.range(
operator=Range.OP_LT,
target=Version(
major=clause.target.major,
minor=clause.target.minor,
patch=clause.target.patch,
prerelease=("0",),
),
)
)
else:
subclauses.append(clause)
else:
subclauses.extend(parsed)

# [CHANGED] Use subclauses directly instead of the library's prerelease split.
# The library separates prerelease/non-prerelease clauses and recombines them
# with extra bounds, which breaks direct prerelease comparison.
result |= AllOf(*subclauses)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve release match for exact prerelease declarations

Using result |= AllOf(*subclauses) drops the upstream parser’s prerelease/non-prerelease companion branch, which changes existing manifest behavior for exact prerelease pins: SemverRange("1.61.0-SNAPSHOT") stops matching 1.61.0 after this commit. Fresh evidence is in manifests/java.yml, which has inline 1.61.0-SNAPSHOT activations for telemetry tests; with this change, Java 1.61.0 is now treated as still missing_feature, so those tests can be skipped/incorrectly deactivated for the release build.

Useful? React with 👍 / 👎.


return result


class SemverRange(semver.NpmSpec):
Parser = _CustomParser
Expand Down
Loading