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
24 changes: 23 additions & 1 deletion fedora_revdep_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ def simulate_version_change(self, srpm_name: str, new_version: str) -> Dict:
'binary_packages': []
}

# If new_version doesn't include an epoch, inherit it from current packages
if ':' not in new_version:
# Get epoch from first package (all packages from same SRPM should have same epoch)
current_epoch = binary_packages[0].get_epoch()
if current_epoch and current_epoch != '0':
new_version = f"{current_epoch}:{new_version}"
if self.verbose:
print(f"No epoch specified in new version, using epoch {current_epoch} from current package")
print(f"Testing with version: {new_version}\n")

if self.verbose:
print(f"Found {len(binary_packages)} binary package(s) from {srpm_name}:")
for pkg in binary_packages:
Expand Down Expand Up @@ -406,7 +416,19 @@ def _check_requirement_conflict(
# Check if new version satisfies all constraints
if constraints:
for constraint in constraints:
if not self._version_satisfies(new_version, constraint['op'], constraint['version']):
# Determine which new version to use based on whether the provide uses epochs
# Check if any current provide has an epoch in its version
provide_uses_epoch = False
for pkg, prov_str, prov_version in prov_info_list:
current_ver = prov_version if prov_version else pkg.get_version()
if ':' in current_ver:
provide_uses_epoch = True
break

# Use appropriate version: with or without epoch depending on provide format
version_to_check = new_version if provide_uses_epoch else new_version.split(':', 1)[-1]

if not self._version_satisfies(version_to_check, constraint['op'], constraint['version']):
# New version fails - now check if current version also fails
# to determine if this is a new problem or already broken
current_version_also_fails = False
Expand Down
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
create_multi_binary_scenario,
create_same_srpm_dependency_scenario,
create_already_broken_scenario,
create_epoch_package_scenario,
create_epoch_with_dist_provides_scenario,
)
from fedora_revdep_check import FedoraRevDepChecker # noqa: E402

Expand Down Expand Up @@ -120,6 +122,18 @@ def already_broken_base():
return create_already_broken_scenario()


@pytest.fixture
def epoch_package_base():
"""Provide a mock DNF base with packages that have epochs."""
return create_epoch_package_scenario()


@pytest.fixture
def epoch_with_dist_provides_base():
"""Provide a mock DNF base with packages that have both RPM and dist provides."""
return create_epoch_with_dist_provides_scenario()


@pytest.fixture
def checker_instance(mock_dnf_base):
"""
Expand Down
94 changes: 94 additions & 0 deletions tests/fixtures/mock_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,3 +613,97 @@ def create_already_broken_scenario():
]

return MockBase(packages=packages)


def create_epoch_package_scenario():
"""
Create a scenario with packages that have epochs.

Scenario:
- sphinx 1:8.0.0 currently installed (epoch 1)
- reverse-dep requires sphinx >= 1:8.0.0
- Upgrading to 9.0.0 without specifying epoch should inherit epoch 1
- So 1:9.0.0 should satisfy the requirement (not 0:9.0.0 which would fail)
"""
packages = [
# Sphinx package with epoch 1
MockPackage(
name='python3-sphinx',
version='8.0.0',
release='1.fc40',
arch='noarch',
source_name='sphinx',
epoch='1',
provides=[
'python3-sphinx',
'python3-sphinx = 1:8.0.0-1.fc40',
'python3dist(sphinx) = 8.0.0',
]
),
# Reverse dependency requiring >= 1:8.0.0
MockPackage(
name='python3-docs',
version='1.0.0',
release='1.fc40',
arch='noarch',
source_name='python-docs',
requires=[
'python3-sphinx >= 1:8.0.0',
]
),
]

return MockBase(packages=packages)


def create_epoch_with_dist_provides_scenario():
"""
Create a scenario with packages that have both RPM and dist provides.

Scenario:
- sphinx 1:8.0.0 currently installed (epoch 1)
- Provides both python3-sphinx (with epoch) and python3dist(sphinx) (without epoch)
- reverse-dep-rpm requires python3-sphinx >= 1:8.0.0 (RPM provide with epoch)
- reverse-dep-dist requires python3dist(sphinx) < 10~~ (dist provide without epoch)
- Upgrading to 9.1.0 should satisfy both (use epoch for RPM, not for dist)
"""
packages = [
# Sphinx package with epoch 1
MockPackage(
name='python3-sphinx',
version='8.0.0',
release='1.fc40',
arch='noarch',
source_name='sphinx',
epoch='1',
provides=[
'python3-sphinx',
'python3-sphinx = 1:8.0.0-1.fc40',
'python3dist(sphinx) = 8.0.0',
]
),
# Reverse dependency requiring RPM package with epoch
MockPackage(
name='python3-docs',
version='1.0.0',
release='1.fc40',
arch='noarch',
source_name='python-docs',
requires=[
'python3-sphinx >= 1:8.0.0',
]
),
# Reverse dependency requiring dist provide without epoch
MockPackage(
name='python3-myst-parser',
version='5.0.0',
release='1.fc40',
arch='noarch',
source_name='python-myst-parser',
requires=[
'python3dist(sphinx) < 10~~',
]
),
]

return MockBase(packages=packages)
37 changes: 37 additions & 0 deletions tests/integration/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,40 @@ def test_simulate_version_change_already_broken_package(self, already_broken_bas
for conflict in new_pkg_conflicts:
assert conflict['already_broken'] is False
assert 'python3dist(library) < 5.0' in conflict['failed_constraint']

def test_simulate_version_change_inherits_epoch(self, epoch_package_base):
"""Test that new version without epoch inherits epoch from current package."""
checker = FedoraRevDepChecker(verbose=False, base=epoch_package_base)

# Current package is 1:8.0.0, upgrading to 9.0.0 (without epoch)
# should be interpreted as 1:9.0.0, not 0:9.0.0
# reverse-dep requires >= 1:8.0.0, so 1:9.0.0 should satisfy it
results = checker.simulate_version_change('sphinx', '9.0.0')

assert 'error' not in results
# The new_version should be transformed to include epoch
assert results['new_version'] == '1:9.0.0'

# Should have no conflicts (1:9.0.0 >= 1:8.0.0)
assert len(results['conflicts']) == 0

def test_simulate_version_change_epoch_only_for_rpm_provides(self, epoch_with_dist_provides_base):
"""Test that epoch is only used for RPM provides, not for dist provides."""
checker = FedoraRevDepChecker(verbose=False, base=epoch_with_dist_provides_base)

# Current package is 1:8.0.0 with both RPM and dist provides
# Upgrading to 9.1.0 should:
# - Use 1:9.1.0 for RPM package provides (with epoch)
# - Use 9.1.0 for dist provides (without epoch)
# reverse-dep-rpm requires python3-sphinx >= 1:8.0.0 (should be satisfied)
# reverse-dep-dist requires python3dist(sphinx) < 10~~ (should be satisfied)
results = checker.simulate_version_change('sphinx', '9.1.0')

assert 'error' not in results
assert results['new_version'] == '1:9.1.0'

# Should have no conflicts
# Both requirements should be satisfied:
# - 1:9.1.0 >= 1:8.0.0 (True)
# - 9.1.0 < 10~~ (True)
assert len(results['conflicts']) == 0
12 changes: 12 additions & 0 deletions tests/unit/test_version_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ def test_version_satisfies_invalid_operator(self, checker):
assert result is False


@pytest.mark.parametrize("version,op,required,expected", [
# Test that new version without epoch is correctly compared
# to requirements with epoch (after epoch is inherited)
("1:9.1.0", ">=", "1:8.2.0", True), # Should satisfy after inheriting epoch 1
("1:9.1.0", ">=", "8.2.0", True), # Epoch 1 > epoch 0
])
def test_version_satisfies_inherited_epoch(self, checker, version, op, required, expected):
"""Test version comparison when epoch is inherited from current package."""
result = checker._version_satisfies(version, op, required)
assert result == expected, f"{version} {op} {required} should be {expected}"


@pytest.mark.parametrize("version,op,required,expected", [
# Double tilde (very pre-release)
("4.7.0~~alpha", "<", "4.7.0~beta", True),
Expand Down