diff --git a/fedora_revdep_check.py b/fedora_revdep_check.py index 92072a5..8e419b1 100755 --- a/fedora_revdep_check.py +++ b/fedora_revdep_check.py @@ -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: @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 1ada7e0..3346a8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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): """ diff --git a/tests/fixtures/mock_packages.py b/tests/fixtures/mock_packages.py index 4a18e80..faf3281 100644 --- a/tests/fixtures/mock_packages.py +++ b/tests/fixtures/mock_packages.py @@ -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) diff --git a/tests/integration/test_simulation.py b/tests/integration/test_simulation.py index 260fd23..a3c110a 100644 --- a/tests/integration/test_simulation.py +++ b/tests/integration/test_simulation.py @@ -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 diff --git a/tests/unit/test_version_operations.py b/tests/unit/test_version_operations.py index 8009927..640e7b2 100644 --- a/tests/unit/test_version_operations.py +++ b/tests/unit/test_version_operations.py @@ -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),