Add recursive capability resolver, integration tests, and GitHub workflow (see: https://chatgpt.com/share/6936abc9-87cc-800f-97e6-f7429fb1a910)
This commit is contained in:
@@ -8,21 +8,32 @@ from pkgmgr.capabilities import (
|
||||
PythonRuntimeCapability,
|
||||
MakeInstallCapability,
|
||||
NixFlakeCapability,
|
||||
CapabilityMatcher,
|
||||
detect_capabilities,
|
||||
resolve_effective_capabilities,
|
||||
LAYER_ORDER,
|
||||
)
|
||||
|
||||
|
||||
class DummyCtx:
|
||||
"""Minimal RepoContext stub with just repo_dir."""
|
||||
|
||||
def __init__(self, repo_dir: str):
|
||||
self.repo_dir = repo_dir
|
||||
|
||||
|
||||
class TestCapabilities(unittest.TestCase):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests for individual capability detectors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCapabilitiesDetectors(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ctx = DummyCtx("/tmp/repo")
|
||||
|
||||
@patch("pkgmgr.capabilities.os.path.exists")
|
||||
def test_python_runtime_python_layer_pyproject(self, mock_exists):
|
||||
"""PythonRuntimeCapability: python layer is provided if pyproject.toml exists."""
|
||||
cap = PythonRuntimeCapability()
|
||||
|
||||
def exists_side_effect(path):
|
||||
@@ -32,10 +43,17 @@ class TestCapabilities(unittest.TestCase):
|
||||
|
||||
self.assertTrue(cap.applies_to_layer("python"))
|
||||
self.assertTrue(cap.is_provided(self.ctx, "python"))
|
||||
# Other layers should not be treated as python-runtime by this branch
|
||||
self.assertFalse(cap.is_provided(self.ctx, "nix"))
|
||||
self.assertFalse(cap.is_provided(self.ctx, "os-packages"))
|
||||
|
||||
@patch("pkgmgr.capabilities._read_text_if_exists")
|
||||
@patch("pkgmgr.capabilities.os.path.exists")
|
||||
def test_python_runtime_nix_layer_flake(self, mock_exists, mock_read):
|
||||
"""
|
||||
PythonRuntimeCapability: nix layer is provided if flake.nix contains
|
||||
Python-related patterns like buildPythonApplication.
|
||||
"""
|
||||
cap = PythonRuntimeCapability()
|
||||
|
||||
def exists_side_effect(path):
|
||||
@@ -54,6 +72,7 @@ class TestCapabilities(unittest.TestCase):
|
||||
read_data="install:\n\t echo 'installing'\n",
|
||||
)
|
||||
def test_make_install_makefile_layer(self, mock_file, mock_exists):
|
||||
"""MakeInstallCapability: makefile layer is provided if Makefile has an install target."""
|
||||
cap = MakeInstallCapability()
|
||||
|
||||
self.assertTrue(cap.applies_to_layer("makefile"))
|
||||
@@ -61,6 +80,7 @@ class TestCapabilities(unittest.TestCase):
|
||||
|
||||
@patch("pkgmgr.capabilities.os.path.exists")
|
||||
def test_nix_flake_capability_on_nix_layer(self, mock_exists):
|
||||
"""NixFlakeCapability: nix layer is provided if flake.nix exists."""
|
||||
cap = NixFlakeCapability()
|
||||
|
||||
def exists_side_effect(path):
|
||||
@@ -72,5 +92,283 @@ class TestCapabilities(unittest.TestCase):
|
||||
self.assertTrue(cap.is_provided(self.ctx, "nix"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummy capability matcher for resolver tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DummyCapability(CapabilityMatcher):
|
||||
"""
|
||||
Simple test capability that returns True/False based on a static mapping:
|
||||
|
||||
mapping = {
|
||||
"makefile": True,
|
||||
"python": False,
|
||||
"nix": True,
|
||||
...
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, mapping: dict[str, bool]):
|
||||
self.name = name
|
||||
self._mapping = mapping
|
||||
|
||||
def applies_to_layer(self, layer: str) -> bool:
|
||||
return layer in self._mapping
|
||||
|
||||
def is_provided(self, ctx: DummyCtx, layer: str) -> bool:
|
||||
# ctx is unused here; we are testing the resolution logic, not IO
|
||||
return self._mapping.get(layer, False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests for detect_capabilities (raw detection)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDetectCapabilities(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ctx = DummyCtx("/tmp/repo")
|
||||
|
||||
def test_detect_capabilities_with_dummy_matchers(self):
|
||||
"""
|
||||
detect_capabilities should aggregate all capabilities per layer
|
||||
based on the matchers' applies_to_layer/is_provided logic.
|
||||
"""
|
||||
layers = ["makefile", "python", "nix", "os-packages"]
|
||||
|
||||
dummy1 = DummyCapability(
|
||||
"cap-a",
|
||||
{
|
||||
"makefile": True,
|
||||
"python": False,
|
||||
"nix": True,
|
||||
},
|
||||
)
|
||||
dummy2 = DummyCapability(
|
||||
"cap-b",
|
||||
{
|
||||
"python": True,
|
||||
"os-packages": True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("pkgmgr.capabilities.CAPABILITY_MATCHERS", [dummy1, dummy2]):
|
||||
caps = detect_capabilities(self.ctx, layers)
|
||||
|
||||
self.assertEqual(
|
||||
caps,
|
||||
{
|
||||
"makefile": {"cap-a"},
|
||||
"python": {"cap-b"},
|
||||
"nix": {"cap-a"},
|
||||
"os-packages": {"cap-b"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests for resolve_effective_capabilities (bottom-up shadowing)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestResolveEffectiveCapabilities(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ctx = DummyCtx("/tmp/repo")
|
||||
|
||||
def test_bottom_up_shadowing_makefile_python_nix(self):
|
||||
"""
|
||||
Scenario:
|
||||
- makefile: provides make-install
|
||||
- python: provides python-runtime, make-install
|
||||
- nix: provides python-runtime, make-install, nix-flake
|
||||
- os-packages: none
|
||||
|
||||
Expected effective capabilities:
|
||||
- makefile: {}
|
||||
- python: {}
|
||||
- nix: {python-runtime, make-install, nix-flake}
|
||||
- os-packages: {}
|
||||
"""
|
||||
layers = ["makefile", "python", "nix", "os-packages"]
|
||||
|
||||
cap_make_install = DummyCapability(
|
||||
"make-install",
|
||||
{
|
||||
"makefile": True,
|
||||
"python": True,
|
||||
"nix": True,
|
||||
"os-packages": False,
|
||||
},
|
||||
)
|
||||
cap_python_runtime = DummyCapability(
|
||||
"python-runtime",
|
||||
{
|
||||
"makefile": False,
|
||||
"python": True,
|
||||
"nix": True,
|
||||
"os-packages": False,
|
||||
},
|
||||
)
|
||||
cap_nix_flake = DummyCapability(
|
||||
"nix-flake",
|
||||
{
|
||||
"makefile": False,
|
||||
"python": False,
|
||||
"nix": True,
|
||||
"os-packages": False,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pkgmgr.capabilities.CAPABILITY_MATCHERS",
|
||||
[cap_make_install, cap_python_runtime, cap_nix_flake],
|
||||
):
|
||||
effective = resolve_effective_capabilities(self.ctx, layers)
|
||||
|
||||
self.assertEqual(effective["makefile"], set())
|
||||
self.assertEqual(effective["python"], set())
|
||||
self.assertEqual(
|
||||
effective["nix"],
|
||||
{"python-runtime", "make-install", "nix-flake"},
|
||||
)
|
||||
self.assertEqual(effective["os-packages"], set())
|
||||
|
||||
def test_os_packages_shadow_all_lower_layers(self):
|
||||
"""
|
||||
Scenario:
|
||||
- python: provides python-runtime
|
||||
- nix: provides python-runtime
|
||||
- os-packages: provides python-runtime
|
||||
|
||||
Expected effective capabilities:
|
||||
- python: {}
|
||||
- nix: {}
|
||||
- os-packages: {python-runtime}
|
||||
"""
|
||||
layers = ["python", "nix", "os-packages"]
|
||||
|
||||
cap_python_runtime = DummyCapability(
|
||||
"python-runtime",
|
||||
{
|
||||
"python": True,
|
||||
"nix": True,
|
||||
"os-packages": True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pkgmgr.capabilities.CAPABILITY_MATCHERS",
|
||||
[cap_python_runtime],
|
||||
):
|
||||
effective = resolve_effective_capabilities(self.ctx, layers)
|
||||
|
||||
self.assertEqual(effective["python"], set())
|
||||
self.assertEqual(effective["nix"], set())
|
||||
self.assertEqual(effective["os-packages"], {"python-runtime"})
|
||||
|
||||
def test_capability_only_in_lowest_layer(self):
|
||||
"""
|
||||
If a capability is only provided by the lowest layer, it should remain
|
||||
attached to that layer as an effective capability.
|
||||
"""
|
||||
layers = ["makefile", "python", "nix"]
|
||||
|
||||
cap_only_make = DummyCapability(
|
||||
"make-install",
|
||||
{
|
||||
"makefile": True,
|
||||
"python": False,
|
||||
"nix": False,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("pkgmgr.capabilities.CAPABILITY_MATCHERS", [cap_only_make]):
|
||||
effective = resolve_effective_capabilities(self.ctx, layers)
|
||||
|
||||
self.assertEqual(effective["makefile"], {"make-install"})
|
||||
self.assertEqual(effective["python"], set())
|
||||
self.assertEqual(effective["nix"], set())
|
||||
|
||||
def test_capability_only_in_highest_layer(self):
|
||||
"""
|
||||
If a capability is only provided by the highest layer, it should appear
|
||||
only there as an effective capability.
|
||||
"""
|
||||
layers = ["makefile", "python", "nix"]
|
||||
|
||||
cap_only_nix = DummyCapability(
|
||||
"nix-flake",
|
||||
{
|
||||
"makefile": False,
|
||||
"python": False,
|
||||
"nix": True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("pkgmgr.capabilities.CAPABILITY_MATCHERS", [cap_only_nix]):
|
||||
effective = resolve_effective_capabilities(self.ctx, layers)
|
||||
|
||||
self.assertEqual(effective["makefile"], set())
|
||||
self.assertEqual(effective["python"], set())
|
||||
self.assertEqual(effective["nix"], {"nix-flake"})
|
||||
|
||||
def test_partial_layer_subset_order_respected(self):
|
||||
"""
|
||||
When passing a custom subset of layers, the resolver must respect
|
||||
that custom order for shadowing.
|
||||
|
||||
Scenario:
|
||||
- layers = ["python", "nix"]
|
||||
- both provide "python-runtime"
|
||||
|
||||
Expected:
|
||||
- python: {}
|
||||
- nix: {"python-runtime"}
|
||||
"""
|
||||
layers = ["python", "nix"]
|
||||
|
||||
cap_python_runtime = DummyCapability(
|
||||
"python-runtime",
|
||||
{
|
||||
"python": True,
|
||||
"nix": True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pkgmgr.capabilities.CAPABILITY_MATCHERS",
|
||||
[cap_python_runtime],
|
||||
):
|
||||
effective = resolve_effective_capabilities(self.ctx, layers)
|
||||
|
||||
self.assertEqual(effective["python"], set())
|
||||
self.assertEqual(effective["nix"], {"python-runtime"})
|
||||
|
||||
def test_default_layer_order_is_used_if_none_given(self):
|
||||
"""
|
||||
If no explicit layers are passed, resolve_effective_capabilities
|
||||
should use LAYER_ORDER.
|
||||
"""
|
||||
cap_dummy = DummyCapability(
|
||||
"dummy-cap",
|
||||
{
|
||||
# Only provide something on the highest default layer
|
||||
LAYER_ORDER[-1]: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pkgmgr.capabilities.CAPABILITY_MATCHERS",
|
||||
[cap_dummy],
|
||||
):
|
||||
effective = resolve_effective_capabilities(self.ctx)
|
||||
|
||||
# All lower layers must be empty; highest default layer must have the cap
|
||||
for layer in LAYER_ORDER[:-1]:
|
||||
self.assertEqual(effective[layer], set())
|
||||
self.assertEqual(effective[LAYER_ORDER[-1]], {"dummy-cap"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user