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
21 changes: 21 additions & 0 deletions bits_helpers/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ def topological_sort(specs):
edges = [(pkg, dep) for pkg, dep in edges if dep != current_package]
# ...but keep blocking those that still depend on other stuff!
leaves.extend(new_leaves - {pkg for pkg, _ in edges})
# If we have any edges left, we have a cycle
if edges:
# Find a cycle by following dependencies
cycle = []
start = edges[0][0] # Start with any remaining package
current = start
max_iter = 10000 # Prevent infinite loops
while max_iter > 0:
max_iter -= 1
cycle.append(current)
# Find what current depends on
for pkg, dep in edges:
if pkg == current:
current = dep
break
if current in cycle: # We found a cycle
cycle = cycle[cycle.index(current):] # Trim to just the cycle
dieOnError(True, "Dependency cycle detected: " + " -> ".join(cycle + [cycle[0]]))
if current == start: # We've gone full circle
raise RuntimeError("Internal error: cycle detection failed")
assert False, "Unreachable error: cycle detection failed"


def resolve_store_path(architecture, spec_hash):
Expand Down
37 changes: 36 additions & 1 deletion tests/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
# Assuming you are using the mock library to ... mock things
from unittest.mock import patch

from bits_helpers.utilities import doDetectArch, filterByArchitectureDefaults, disabledByArchitectureDefaults
from bits_helpers.utilities import doDetectArch, filterByArchitectureDefaults, disabledByArchitectureDefaults, getPkgDirs
from bits_helpers.utilities import Hasher
from bits_helpers.utilities import asList
from bits_helpers.utilities import prunePaths
from bits_helpers.utilities import resolve_version
from bits_helpers.utilities import topological_sort
from bits_helpers.utilities import resolveFilename, resolveDefaultsFilename
import bits_helpers
import os
import string

Expand Down Expand Up @@ -272,6 +274,39 @@ def test_dont_drop_packages(self) -> None:
self.assertEqual(frozenset(specs.keys()),
frozenset(topological_sort(specs)))

def test_cycle(self) -> None:
"""Test that dependency cycles are detected and reported."""
specs = {
"A": {"package": "A", "requires": ["B"]},
"B": {"package": "B", "requires": ["C"]},
"C": {"package": "C", "requires": ["D"]},
"D": {"package": "D", "requires": ["A"]}
}
with patch.object(alibuild_helpers.log, 'error') as mock_error:
with self.assertRaises(SystemExit) as cm:
list(topological_sort(specs))
self.assertEqual(cm.exception.code, 1)
mock_error.assert_called_once_with("%s", "Dependency cycle detected: A -> B -> C -> D -> A")

def test_empty_set(self) -> None:
"""Test that an empty set of packages is handled correctly."""
self.assertEqual([], list(topological_sort({})))

def test_single_package(self) -> None:
"""Test that a single package with no dependencies is handled correctly."""
self.assertEqual(["A"], list(topological_sort({
"A": {"package": "A", "requires": []}
})))

def test_independent_packages(self) -> None:
"""Test that packages with no dependencies between them are handled correctly."""
result = list(topological_sort({
"A": {"package": "A", "requires": []},
"B": {"package": "B", "requires": []},
"C": {"package": "C", "requires": []}
}))
self.assertEqual(set(["A", "B", "C"]), set(result))
self.assertEqual(3, len(result))

if __name__ == '__main__':
unittest.main()
Loading