Skip to content

Commit f5e9a50

Browse files
committed
Report already broken packages as a separate category
Fixes: #2
1 parent cabf3d1 commit f5e9a50

7 files changed

Lines changed: 455 additions & 18 deletions

File tree

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,33 @@ fedora-revdep-check python-requests 2.32.0 --repo fedora-40 --repo fedora-40-sou
3939

4040
## Output
4141

42-
When conflicts are found:
42+
When conflicts are found, they are categorized into new problems and already-broken packages:
4343

4444
```
4545
These packages would FTBFS:
4646
jupyter-server: python3dist(jupyterlab) < 4.7
4747
4848
These packages would FTI:
4949
python3-jupyter-client-8.0.0-1.fc44: python3dist(jupyterlab) >= 4.0, < 4.7
50+
51+
These packages already FTBFS (not a new problem):
52+
some-package: python3dist(jupyterlab) < 3.0
53+
54+
These packages already FTI (not a new problem):
55+
python3-old-package-1.0.0-1.fc44: python3dist(jupyterlab) < 3.0
5056
```
5157

5258
- **FTBFS**: Fail To Build From Source (source packages that won't build)
5359
- **FTI**: Fail To Install (binary packages that won't install)
5460

61+
The tool distinguishes between:
62+
- **New problems**: Packages that currently work but would break with the update
63+
- **Already broken**: Packages that already fail with the current version in repos (not caused by the update)
64+
5565
## Exit Codes
5666

57-
- `0` - No conflicts detected
58-
- `1` - Conflicts found or error occurred
67+
- `0` - No new conflicts detected (already-broken packages don't cause non-zero exit)
68+
- `1` - New conflicts found or error occurred
5969
- `130` - Interrupted by user
6070

6171
## How It Works

fedora_revdep_check.py

100644100755
Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env python3
12
"""
23
Fedora Reverse Dependency Checker
34
@@ -406,14 +407,27 @@ def _check_requirement_conflict(
406407
if constraints:
407408
for constraint in constraints:
408409
if not self._version_satisfies(new_version, constraint['op'], constraint['version']):
410+
# New version fails - now check if current version also fails
411+
# to determine if this is a new problem or already broken
412+
current_version_also_fails = False
413+
414+
# Get current version from prov_info_list
415+
for pkg, prov_str, prov_version in prov_info_list:
416+
# Use the package version if provide doesn't have its own version
417+
current_ver = prov_version if prov_version else pkg.get_version()
418+
if not self._version_satisfies(current_ver, constraint['op'], constraint['version']):
419+
current_version_also_fails = True
420+
break
421+
409422
return {
410423
'rdep_package': f"{rdep_pkg.get_name()}-{rdep_pkg.get_version()}-{rdep_pkg.get_release()}",
411424
'rdep_source': rdep_pkg.get_source_name(),
412425
'rdep_arch': rdep_pkg.get_arch(),
413426
'requirement': req_str,
414427
'provide_name': prov_name,
415428
'new_version': new_version,
416-
'failed_constraint': f"{prov_name} {constraint['op']} {constraint['version']}"
429+
'failed_constraint': f"{prov_name} {constraint['op']} {constraint['version']}",
430+
'already_broken': current_version_also_fails
417431
}
418432

419433
return None
@@ -479,30 +493,58 @@ def print_results(self, results: Dict):
479493
if self.verbose:
480494
print(f"No conflicts detected for {results['srpm_name']} {results['new_version']}")
481495
else:
482-
# Separate conflicts by type
483-
ftbfs_conflicts = []
484-
fti_conflicts = []
496+
# Separate conflicts by type and whether they're new or already broken
497+
ftbfs_new = []
498+
ftbfs_already_broken = []
499+
fti_new = []
500+
fti_already_broken = []
485501

486502
for conflict in conflicts:
487503
is_source = conflict['rdep_arch'] == 'src'
504+
is_already_broken = conflict.get('already_broken', False)
505+
488506
if is_source:
489-
ftbfs_conflicts.append(conflict)
507+
if is_already_broken:
508+
ftbfs_already_broken.append(conflict)
509+
else:
510+
ftbfs_new.append(conflict)
490511
else:
491-
fti_conflicts.append(conflict)
512+
if is_already_broken:
513+
fti_already_broken.append(conflict)
514+
else:
515+
fti_new.append(conflict)
492516

493-
# Print FTBFS section
494-
if ftbfs_conflicts:
517+
# Print new FTBFS conflicts
518+
if ftbfs_new:
495519
print("These packages would FTBFS:")
496-
for conflict in ftbfs_conflicts:
520+
for conflict in ftbfs_new:
497521
package_name = conflict['rdep_source']
498522
print(f" {package_name}: {conflict['failed_constraint']}")
499523

500-
# Print FTI section
501-
if fti_conflicts:
502-
if ftbfs_conflicts:
524+
# Print new FTI conflicts
525+
if fti_new:
526+
if ftbfs_new:
503527
print() # Empty line between sections
504528
print("These packages would FTI:")
505-
for conflict in fti_conflicts:
529+
for conflict in fti_new:
530+
package_name = conflict['rdep_package']
531+
print(f" {package_name}: {conflict['failed_constraint']}")
532+
533+
# Print already broken FTBFS packages
534+
if ftbfs_already_broken:
535+
if ftbfs_new or fti_new:
536+
print() # Empty line between sections
537+
print("These packages already FTBFS (not a new problem):")
538+
for conflict in ftbfs_already_broken:
539+
package_name = conflict['rdep_source']
540+
print(f" {package_name}: {conflict['failed_constraint']}")
541+
542+
# Print already broken FTI packages
543+
if fti_already_broken:
544+
if ftbfs_new or fti_new or ftbfs_already_broken:
545+
print() # Empty line between sections
546+
print("These packages already FTI (not a new problem):")
547+
for conflict in fti_already_broken:
506548
package_name = conflict['rdep_package']
507549
print(f" {package_name}: {conflict['failed_constraint']}")
508550

@@ -536,9 +578,15 @@ def main():
536578
results = checker.simulate_version_change(args.srpm_name, args.new_version)
537579
checker.print_results(results)
538580

539-
# Exit with error code if conflicts found
581+
# Exit with error code if NEW conflicts found (not already-broken packages)
540582
if results.get('conflicts'):
541-
sys.exit(1)
583+
# Check if there are any new conflicts (not already broken)
584+
has_new_conflicts = any(
585+
not conflict.get('already_broken', False)
586+
for conflict in results['conflicts']
587+
)
588+
if has_new_conflicts:
589+
sys.exit(1)
542590

543591
except KeyboardInterrupt:
544592
print("\n\nInterrupted by user.")

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
create_bundled_provides_scenario,
3333
create_multi_binary_scenario,
3434
create_same_srpm_dependency_scenario,
35+
create_already_broken_scenario,
3536
)
3637
from fedora_revdep_check import FedoraRevDepChecker # noqa: E402
3738

@@ -113,6 +114,12 @@ def same_srpm_dep_base():
113114
return create_same_srpm_dependency_scenario()
114115

115116

117+
@pytest.fixture
118+
def already_broken_base():
119+
"""Provide a mock DNF base with already-broken and new conflict scenarios."""
120+
return create_already_broken_scenario()
121+
122+
116123
@pytest.fixture
117124
def checker_instance(mock_dnf_base):
118125
"""

tests/e2e/test_full_workflow.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,99 @@ def mock_init(self, verbose=False, base=None, repos=None):
140140

141141
# Outputs should be identical (deterministic)
142142
assert output1.getvalue() == output2.getvalue()
143+
144+
def test_main_exit_code_zero_for_already_broken_only(self, monkeypatch, mock_dnf_base, capsys):
145+
"""Test that main() exits with 0 when only already-broken packages are found."""
146+
monkeypatch.setattr('sys.argv', ['fedora-revdep-check', 'pytest', '8.0.0'])
147+
148+
original_init = FedoraRevDepChecker.__init__
149+
150+
def mock_init(self, verbose=False, base=None, repos=None):
151+
original_init(self, verbose=verbose, base=mock_dnf_base if base is None else base, repos=repos)
152+
153+
# Mock simulate_version_change to return only already-broken conflicts
154+
original_simulate = FedoraRevDepChecker.simulate_version_change
155+
156+
def mock_simulate(self, srpm_name, new_version):
157+
return {
158+
'srpm_name': srpm_name,
159+
'new_version': new_version,
160+
'binary_packages': ['python3-pytest-7.0.0-1.fc40'],
161+
'conflicts': [
162+
{
163+
'rdep_package': 'oldpkg-1.0.0-1.fc40',
164+
'rdep_source': 'oldpkg',
165+
'rdep_arch': 'src',
166+
'requirement': 'python3dist(pytest) < 5.0',
167+
'provide_name': 'python3dist(pytest)',
168+
'new_version': new_version,
169+
'failed_constraint': 'python3dist(pytest) < 5.0',
170+
'already_broken': True
171+
}
172+
]
173+
}
174+
175+
monkeypatch.setattr(FedoraRevDepChecker, '__init__', mock_init)
176+
monkeypatch.setattr(FedoraRevDepChecker, 'simulate_version_change', mock_simulate)
177+
178+
# main() should exit with 0 (no NEW conflicts)
179+
main()
180+
181+
# Check output contains already-broken message
182+
captured = capsys.readouterr()
183+
assert 'already FTBFS (not a new problem)' in captured.out
184+
185+
def test_main_exit_code_one_for_mixed_conflicts(self, monkeypatch, mock_dnf_base, capsys):
186+
"""Test that main() exits with 1 when there are new conflicts mixed with already-broken."""
187+
monkeypatch.setattr('sys.argv', ['fedora-revdep-check', 'pytest', '8.0.0'])
188+
189+
original_init = FedoraRevDepChecker.__init__
190+
191+
def mock_init(self, verbose=False, base=None, repos=None):
192+
original_init(self, verbose=verbose, base=mock_dnf_base if base is None else base, repos=repos)
193+
194+
# Mock simulate_version_change to return mixed conflicts
195+
def mock_simulate(self, srpm_name, new_version):
196+
return {
197+
'srpm_name': srpm_name,
198+
'new_version': new_version,
199+
'binary_packages': ['python3-pytest-7.0.0-1.fc40'],
200+
'conflicts': [
201+
# New conflict
202+
{
203+
'rdep_package': 'newpkg-1.0.0-1.fc40',
204+
'rdep_source': 'newpkg',
205+
'rdep_arch': 'src',
206+
'requirement': 'python3dist(pytest) < 8.0',
207+
'provide_name': 'python3dist(pytest)',
208+
'new_version': new_version,
209+
'failed_constraint': 'python3dist(pytest) < 8.0',
210+
'already_broken': False
211+
},
212+
# Already broken
213+
{
214+
'rdep_package': 'oldpkg-1.0.0-1.fc40',
215+
'rdep_source': 'oldpkg',
216+
'rdep_arch': 'src',
217+
'requirement': 'python3dist(pytest) < 5.0',
218+
'provide_name': 'python3dist(pytest)',
219+
'new_version': new_version,
220+
'failed_constraint': 'python3dist(pytest) < 5.0',
221+
'already_broken': True
222+
}
223+
]
224+
}
225+
226+
monkeypatch.setattr(FedoraRevDepChecker, '__init__', mock_init)
227+
monkeypatch.setattr(FedoraRevDepChecker, 'simulate_version_change', mock_simulate)
228+
229+
# main() should exit with 1 (NEW conflicts found)
230+
with pytest.raises(SystemExit) as exc_info:
231+
main()
232+
233+
assert exc_info.value.code == 1
234+
235+
# Check output contains both sections
236+
captured = capsys.readouterr()
237+
assert 'These packages would FTBFS:' in captured.out
238+
assert 'already FTBFS (not a new problem)' in captured.out

tests/fixtures/mock_packages.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,75 @@ def create_same_srpm_dependency_scenario():
541541
]
542542

543543
return MockBase(packages=packages)
544+
545+
546+
def create_already_broken_scenario():
547+
"""
548+
Create a scenario with both new conflicts and already-broken packages.
549+
550+
Scenario:
551+
- library 4.0.0 currently installed
552+
- old-package requires library < 3.0 (already broken with current 4.0.0)
553+
- new-package requires library < 5.0 (will break when upgrading to 5.0.0)
554+
"""
555+
packages = [
556+
# Library package
557+
MockPackage(
558+
name='library',
559+
version='4.0.0',
560+
release='1.fc40',
561+
arch='noarch',
562+
source_name='library',
563+
provides=[
564+
'library',
565+
'library = 4.0.0-1.fc40',
566+
'python3dist(library) = 4.0.0',
567+
]
568+
),
569+
# Old package (already broken)
570+
MockPackage(
571+
name='python3-old-package',
572+
version='1.0.0',
573+
release='1.fc40',
574+
arch='noarch',
575+
source_name='old-package',
576+
requires=[
577+
'python3dist(library) < 3.0', # Already fails with 4.0.0
578+
]
579+
),
580+
# Old package SRPM (for FTBFS testing)
581+
MockPackage(
582+
name='old-package',
583+
version='1.0.0',
584+
release='1.fc40',
585+
arch='src',
586+
source_name='old-package',
587+
requires=[
588+
'python3dist(library) < 3.0', # Already fails with 4.0.0
589+
]
590+
),
591+
# New package (will break with 5.0.0)
592+
MockPackage(
593+
name='python3-new-package',
594+
version='1.0.0',
595+
release='1.fc40',
596+
arch='noarch',
597+
source_name='new-package',
598+
requires=[
599+
'python3dist(library) < 5.0', # Will fail with 5.0.0
600+
]
601+
),
602+
# New package SRPM (for FTBFS testing)
603+
MockPackage(
604+
name='new-package',
605+
version='1.0.0',
606+
release='1.fc40',
607+
arch='src',
608+
source_name='new-package',
609+
requires=[
610+
'python3dist(library) < 5.0', # Will fail with 5.0.0
611+
]
612+
),
613+
]
614+
615+
return MockBase(packages=packages)

tests/integration/test_simulation.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,37 @@ def test_simulate_version_change_same_srpm_dependency(self, same_srpm_dep_base):
153153
# python3-external-tool requires micropipenv < 1.12, so it should still be satisfied
154154
# (1.11.0 < 1.12 is True)
155155
assert len(results['conflicts']) == 0
156+
157+
def test_simulate_version_change_already_broken_package(self, already_broken_base):
158+
"""Test that already-broken packages are properly marked."""
159+
checker = FedoraRevDepChecker(verbose=False, base=already_broken_base)
160+
161+
# Upgrading library from 4.0.0 to 5.0.0
162+
# old-package requires library < 3.0, so it's already broken with 4.0.0
163+
# new-package requires library < 5.0, so it will break with 5.0.0
164+
results = checker.simulate_version_change('library', '5.0.0')
165+
166+
assert 'error' not in results
167+
# Should have 4 conflicts: 2 FTBFS (src) + 2 FTI (noarch) for each package
168+
assert len(results['conflicts']) == 4
169+
170+
# Separate conflicts into already broken and new
171+
old_pkg_conflicts = []
172+
new_pkg_conflicts = []
173+
for conflict in results['conflicts']:
174+
if 'old-package' in conflict['rdep_source']:
175+
old_pkg_conflicts.append(conflict)
176+
elif 'new-package' in conflict['rdep_source']:
177+
new_pkg_conflicts.append(conflict)
178+
179+
# old-package should have 2 conflicts (FTBFS and FTI), both already broken
180+
assert len(old_pkg_conflicts) == 2
181+
for conflict in old_pkg_conflicts:
182+
assert conflict['already_broken'] is True
183+
assert 'python3dist(library) < 3.0' in conflict['failed_constraint']
184+
185+
# new-package should have 2 conflicts (FTBFS and FTI), both new problems
186+
assert len(new_pkg_conflicts) == 2
187+
for conflict in new_pkg_conflicts:
188+
assert conflict['already_broken'] is False
189+
assert 'python3dist(library) < 5.0' in conflict['failed_constraint']

0 commit comments

Comments
 (0)