jw-pkg/test/unit/python/jw/pkg/lib/version/test.py
Jan Lindemann c23a2e4ab2
lib.version.Dependency.constraint_str(): Delimit by comma
A multi-boundary constraint is rendered as 'foo >= 1.2.3-45 foo < 1.2.4',
which is invalid: the RPM spec template writes the Requires: line from it
verbatim, and RPM entries are comma-separated, so the second clause is
swallowed into one bad entry. Debian's format_depends() normalizes runs of
whitespace to commas, so it tolerates the space join, but the comma is the
only delimiter that is correct for both.

Join the boundary clauses with ', ' instead of a space.

Also:

- Drop the stray trailing comma in the version_boundaries() call.
- Fix the untemplated docstring, which describes the opposite of the code:
  untemplated = True (the default) resolves the macros, untemplated = False
  keeps them as written.

Tests written by AI.

Signed-off-by: Jan Lindemann <jan@janware.com>
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1
2026-09-15 21:43:47 +02:00

253 lines
8.8 KiB
Python

from jw.pkg.lib.version import Component, Dependency, Syntax, Version
class FakeApp:
"""Minimal stand-in for jw.pkg.App providing get_version()"""
class Error(Exception):
pass
def __init__(self, versions: dict[str, str]) -> None:
self.__versions = versions
def get_version(self, project: str) -> str:
if project not in self.__versions:
raise self.Error(f"Can't get version of project {project}")
return self.__versions[project]
app = FakeApp({'foo': '1.2.3-45', 'bar': '0.9.1-2'})
# Name only
d = Dependency('foo')
assert d.base_name == 'foo'
assert d.full_name == 'foo'
assert d.version_boundaries() == []
assert d.constraint_str() == 'foo'
# Subpackage suffixes are stripped from base_name
d = Dependency('foo-dev')
assert d.base_name == 'foo'
assert d.full_name == 'foo-dev'
d = Dependency('foo-devel')
assert d.base_name == 'foo'
assert d.full_name == 'foo-devel'
d = Dependency('foo-run')
assert d.base_name == 'foo'
assert d.full_name == 'foo-run'
# A package literally named 'dev' keeps its name
d = Dependency('dev')
assert d.base_name == 'dev'
assert d.full_name == 'dev'
# Operators with and without whitespace
for op in ['=', '<', '<=', '>', '>=']:
d = Dependency(f'foo {op} 1.0')
assert d.base_name == 'foo'
assert d.full_name == 'foo'
b = d.version_boundaries()
assert len(b) == 1
assert b[0].op == op
assert b[0].version.id(False) == '1.0'
d = Dependency(f'foo{op}1.0')
assert d.constraint_str(untemplated = False) == f'foo {op} 1.0'
# Without a lookup, literals render as written and macros fail
d = Dependency('foo-devel >= 2.0')
assert d.constraint_str() == 'foo-devel >= 2.0'
try:
Dependency('foo = VERSION').constraint_str()
assert False, 'Should have raised'
except Version.Error:
pass
# The lookup resolves the macros
d = Dependency('foo = VERSION', app.get_version)
assert d.constraint_str() == 'foo = 1.2.3'
d = Dependency('foo = VERSION', app.get_version)
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3'
# Only the VERSION macro loses its revision, VERSION-REVISION and
# literals keep it
d = Dependency('foo = VERSION-REVISION', app.get_version)
assert d.constraint_str() == 'foo = 1.2.3-45'
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3-45'
d = Dependency('foo = 1.2.3-45')
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3-45'
# untemplated, the macro has no revision to strip
d = Dependency('foo = VERSION', app.get_version)
assert d.constraint_str(untemplated=False, include_revision=False) == \
'foo = VERSION'
# untemplated=False keeps the specifiers as written
d = Dependency('foo = VERSION', app.get_version)
assert d.constraint_str(untemplated = False) == 'foo = VERSION'
# A bare REVISION is rejected on the grounds that is assumed that
# the user wanted VERSION instead
try:
Dependency('foo = REVISION', app.get_version).constraint_str()
assert False, 'Should have raised'
except Dependency.Error:
pass
# The lookup uses the base name, not the subpackage name
d = Dependency('foo-devel = VERSION', app.get_version)
assert d.constraint_str() == 'foo-devel = 1.2.3'
# Unknown projects fail when untemplating
try:
Dependency('baz = VERSION', app.get_version).constraint_str()
assert False, 'Should have raised'
except FakeApp.Error:
pass
# The lookup is cached: the mapper is called once
calls: list[str] = []
def mapper(project: str) -> str:
calls.append(project)
return '1.2.3-45'
d = Dependency('foo = VERSION', mapper)
assert d.constraint_str() == 'foo = 1.2.3'
assert d.constraint_str() == 'foo = 1.2.3'
assert calls == ['foo']
# as_range expands a non-full boundary into the range it spans
d = Dependency('foo = VERSION-REVISION', app.get_version)
assert d.constraint_str(as_range = True) == 'foo = 1.2.3-45'
d = Dependency('foo >= VERSION-REVISION', app.get_version)
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3-45'
d = Dependency('foo > 1.0.0-259', app.get_version)
assert d.constraint_str(as_range = True) == 'foo > 1.0.0-259'
# A raw render keeps the macro as written
d = Dependency('foo = VERSION-REVISION', app.get_version)
constraint = d.constraint_str(untemplated = False, as_range = True)
assert constraint == 'foo = VERSION-REVISION'
# A full version stays exact; a non-full one expands
d = Dependency('foo = VERSION', app.get_version)
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3, foo < 1.2.4'
d = Dependency('foo >= VERSION', app.get_version)
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3, foo < 1.2.4'
# '<' and '<=' are not expanded
d = Dependency('foo <= VERSION', app.get_version)
assert d.constraint_str(as_range = True) == 'foo <= 1.2.3'
# The bound steps a micro, so a tripartite core expands
d = Dependency('foo = 1.2.3', app.get_version)
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3, foo < 1.2.4'
# Default syntax is SEM_VER
d = Dependency('foo < 2.0')
assert d.constraint_str(untemplated = False) == 'foo < 2.0'
# DEBIAN converts strict inequalities, other operators pass through
d = Dependency('foo < 2.0')
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo << 2.0'
d = Dependency('foo > 1.0')
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo >> 1.0'
d = Dependency('foo <= 1.0')
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo <= 1.0'
d = Dependency('foo = 1.0')
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo = 1.0'
# NAMES_ONLY drops the version
d = Dependency('foo-devel >= 1.0')
assert d.constraint_str(Syntax.NAMES_ONLY) == 'foo-devel'
# no_subpackages strips the suffix from the name
d = Dependency('foo-devel >= 1.0')
assert d.constraint_str(no_subpackages = True, untemplated = False) == 'foo >= 1.0'
# quote wraps the result
d = Dependency('foo-devel >= 1.0')
assert d.constraint_str(quote = '"', untemplated = False) == '"foo-devel >= 1.0"'
# Version parts
v = Version('foo', '1.2.3-45', app.get_version)
assert v.major == 1
assert v.minor == 2
assert v.micro == 3
assert v.core(True) == '1.2.3'
assert v.revision(True) == '45'
assert str(v.next_binary_compatible) == '1.2.3-46'
assert str(v.next_binary_incompatible) == '1.2.4'
assert str(v.next_source_incompatible) == '1.3.0'
assert v.parts_str(Component.ID, True) == '1.2.3-45'
assert v.parts_str(Component.CORE, True) == '1.2.3'
assert v.parts_str(Component.MAJOR | Component.MINOR, True) == '1.2'
# next_binary_compatible steps to the next revision, dropping any suffix
for spec, expected in [
('1.2.3-4', '1.2.3-5'),
('1.2.3-4blah', '1.2.3-5'),
('1.2.3-4.myvariant', '1.2.3-5'),
('1.2.3-4-broken', '1.2.3-5'),
('1.0.0-259', '1.0.0-260'),
]:
v = Version('foo', spec)
assert str(v.next_binary_compatible) == expected, spec
# A macro resolves before the increment
v = Version('foo', 'VERSION-REVISION', app.get_version)
assert str(v.next_binary_compatible) == '1.2.3-46'
# A revision without a leading digit cannot be incremented
for spec in ['1.2.3-rc1', '1.2.3']:
v = Version('foo', spec)
try:
v.next_binary_compatible
assert False, f'Should have raised for {spec!r}'
except Version.Error:
pass
# constructor
d = Dependency('foo-devel = 1.0')
assert d.base_name == 'foo'
assert d.full_name == 'foo-devel'
assert d.constraint_str(untemplated = False) == 'foo-devel = 1.0'
# parse_deps_spec with a comma-separated string
deps = Dependency.parse_deps_spec('foo = 1.0, bar-devel >= 2.0, baz')
assert len(deps) == 3
assert [d.full_name for d in deps] == ['foo', 'bar-devel', 'baz']
assert deps[1].base_name == 'bar'
assert deps[1].constraint_str(untemplated = False) == 'bar-devel >= 2.0'
assert deps[2].constraint_str() == 'baz'
# parse_deps_spec passes the lookup to the created packages
deps = Dependency.parse_deps_spec('foo = VERSION, bar', app.get_version)
assert deps[0].constraint_str() == 'foo = 1.2.3'
assert deps[1].constraint_str() == 'bar'
# Malformed specs are rejected when parsed
try:
Dependency('foo = 1.0 > 2.0').full_name
assert False, 'Should have raised'
except Dependency.Error:
pass
# Empty names and versions are rejected
for bad in ['', 'foo =', ' = 1.0']:
try:
Dependency(bad).full_name
assert False, f'Should have raised for {bad!r}'
except Dependency.Error:
pass
# Operators outside the supported set are rejected when parsed,
# not folded into the package name or deferred to expansion
for bad in [
'foo ~= 1.0',
'foo~=1.0',
'foo ~ 1.0',
'foo != 1.0',
'foo!=1.0',
'foo == 1.0',
'foo==1.0',
'foo === 1.0',
'foo << 1.0',
'foo >> 1.0',
]:
try:
Dependency(bad).full_name
assert False, f'Should have raised for {bad!r}'
except Dependency.Error as e:
assert 'unsupported operator' in str(e)
# str
assert str(Dependency('foo-devel >= 1.0')) == 'foo-devel >= 1.0'
assert str(Dependency('foo')) == 'foo'
print('All lib.version tests passed')