Add recursive capability resolver, integration tests, and GitHub workflow (see: https://chatgpt.com/share/6936abc9-87cc-800f-97e6-f7429fb1a910)

This commit is contained in:
Kevin Veen-Birkenbach
2025-12-08 11:43:39 +01:00
parent 775c30149c
commit f641b95d81
7 changed files with 990 additions and 4 deletions

View File

View File

@@ -0,0 +1,116 @@
# Capability Resolution & Installer Shadowing
## Layer Hierarchy
```
┌───────────────────────────┐ Highest layer
│ OS-PACKAGES │ (PKGBUILD / debian / rpm)
└───────────▲───────────────┘
│ shadows lower layers
┌───────────┴───────────────┐
│ NIX │ (flake.nix)
└───────────▲───────────────┘
│ shadows lower layers
┌───────────┴───────────────┐
│ PYTHON │ (pyproject.toml)
└───────────▲───────────────┘
│ shadows lower layers
┌───────────┴───────────────┐
│ MAKEFILE │ (Makefile)
└────────────────────────────┘ Lowest layer
```
---
## Scenario Matrix
| Scenario | Makefile | Python | Nix | OS-Pkgs | Test Name |
| -------------------------- | -------- | ------ | --- | ------- | ----------------------------- |
| 1) Only Makefile | ✔ | | | | `only_makefile` |
| 2) Python + Makefile | ✔ | ✔ | | | `python_and_makefile` |
| 3) Python shadows Makefile | ✗ | ✔ | | | `python_shadows_makefile` |
| 4) Nix shadows Py & MF | ✗ | ✗ | ✔ | | `nix_shadows_python_makefile` |
| 5) OS-Pkgs shadow all | ✗ | ✗ | ✗ | ✔ | `os_packages_shadow_all` |
Legend:
✔ = installer runs
✗ = installer skipped (shadowed by upper layer)
= no such layer present
---
## What the Integration Test Confirms
**Goal:** Validate that the capability-shadowing mechanism correctly determines *which installers actually run* for a given repository layout.
### 1) Only Makefile
* Makefile provides `make-install`.
* No higher layers → MakefileInstaller runs.
### 2) Python + Makefile
* Python provides `python-runtime`.
* Makefile additionally provides `make-install`.
* No capability overlap → both installers run.
### 3) Python shadows Makefile
* Python also provides `make-install`.
* Makefiles capability is fully covered → MakefileInstaller is skipped.
### 4) Nix shadows Python & Makefile
* Nix provides all capabilities below it.
* Only NixInstaller runs.
### 5) OS-Packages shadow all
* PKGBUILD/debian/rpm provide all capabilities.
* Only the corresponding OS package installer runs.
---
## Capability Processing Flowchart
```
┌────────────────────────────┐
│ Start │
└───────────────┬────────────┘
provided_capabilities = ∅
┌──────────────────────────────────────────────┐
│ For each installer in layer order (low→high) │
└───────────────────┬──────────────────────────┘
supports(ctx)?│
┌─────────┴──────────┐
│ no │
│ → skip installer │
└─────────┬──────────┘
│ yes
caps = detect_capabilities(layer)
caps ⊆ provided_capabilities ?
┌─────────────┬─────────────┐
│ yes │ no │
│ skip │ run installer│
└─────────────┴──────────────┘
provided_capabilities = caps
┌────────────────────────┐
│ End of installer list │
└────────────────────────┘
```
---
## Core Principle (one sentence)
**A layer only executes if it provides at least one capability not already guaranteed by any higher layer.**

View File

@@ -0,0 +1,409 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Integration tests for the recursive / layered capability handling in pkgmgr.
We focus on the interaction between:
- MakefileInstaller (layer: "makefile")
- PythonInstaller (layer: "python")
- NixFlakeInstaller (layer: "nix")
- ArchPkgbuildInstaller (layer: "os-packages")
The core idea:
- Each installer declares logical capabilities for its layer via
discover_capabilities() and the global CAPABILITY_MATCHERS.
- install_repos() tracks which capabilities have already been provided
by earlier installers (in INSTALLERS order).
- If an installer only provides capabilities that are already covered
by previous installers, it is skipped.
These tests use *real* capability detection (based on repo files like
flake.nix, pyproject.toml, Makefile, PKGBUILD), but patch the installers'
run() methods so that no real external commands are executed.
Scenarios:
1. Only Makefile with install target
→ MakefileInstaller runs, all good.
2. Python + Makefile (no "make install" in pyproject.toml)
→ PythonInstaller provides only python-runtime
→ MakefileInstaller provides make-install
→ Both run, since their capabilities are disjoint.
3. Python + Makefile (pyproject.toml mentions "make install")
→ PythonInstaller provides {python-runtime, make-install}
→ MakefileInstaller provides {make-install}
→ MakefileInstaller is skipped (capabilities already covered).
4. Nix + Python + Makefile
- flake.nix hints:
* buildPythonApplication (python-runtime)
* make install (make-install)
→ NixFlakeInstaller provides {python-runtime, make-install, nix-flake}
→ PythonInstaller and MakefileInstaller are skipped.
5. OS packages + Nix + Python + Makefile
- PKGBUILD contains:
* "pip install ." (python-runtime via os-packages)
* "make install" (make-install via os-packages)
* "nix profile" (nix-flake via os-packages)
→ ArchPkgbuildInstaller provides all capabilities
→ All lower layers are skipped.
"""
import os
import shutil
import tempfile
import unittest
from unittest.mock import patch
import pkgmgr.install_repos as install_mod
from pkgmgr.install_repos import install_repos
from pkgmgr.installers.nix_flake import NixFlakeInstaller
from pkgmgr.installers.python import PythonInstaller
from pkgmgr.installers.makefile import MakefileInstaller
from pkgmgr.installers.os_packages.arch_pkgbuild import ArchPkgbuildInstaller
class TestRecursiveCapabilitiesIntegration(unittest.TestCase):
def setUp(self) -> None:
# Temporary base directory for this test class
self.tmp_root = tempfile.mkdtemp(prefix="pkgmgr-integration-")
self.bin_dir = os.path.join(self.tmp_root, "bin")
os.makedirs(self.bin_dir, exist_ok=True)
def tearDown(self) -> None:
shutil.rmtree(self.tmp_root)
# ------------------------------------------------------------------
# Helper: create a new repo directory for a scenario
# ------------------------------------------------------------------
def _new_repo(self) -> str:
repo_dir = tempfile.mkdtemp(prefix="repo-", dir=self.tmp_root)
return repo_dir
# ------------------------------------------------------------------
# Helper: run install_repos() with a custom installer list
# and record which installers actually ran.
# ------------------------------------------------------------------
def _run_with_installers(self, repo_dir: str, installers, selected_repos=None):
"""
Run install_repos() with a given INSTALLERS list and a single
dummy repo; return the list of installer labels that actually ran.
The installers' supports() are forced to True so that only the
capability-shadowing logic decides whether they are skipped.
The installers' run() methods are patched to avoid real commands.
"""
if selected_repos is None:
repo = {}
selected_repos = [repo]
all_repos = [repo]
else:
all_repos = selected_repos
called_installers: list[str] = []
# Prepare patched instances with recording run() and always-supports.
patched_installers = []
for label, inst in installers:
def always_supports(self, ctx):
return True
def make_run(label_name):
def _run(self, ctx):
called_installers.append(label_name)
return _run
inst.supports = always_supports.__get__(inst, inst.__class__)
inst.run = make_run(label).__get__(inst, inst.__class__)
patched_installers.append(inst)
with patch.object(install_mod, "INSTALLERS", patched_installers), \
patch.object(install_mod, "get_repo_identifier", return_value="dummy-repo"), \
patch.object(install_mod, "get_repo_dir", return_value=repo_dir), \
patch.object(install_mod, "verify_repository", return_value=(True, [], None, None)), \
patch.object(install_mod, "create_ink"), \
patch.object(install_mod, "clone_repos"):
install_repos(
selected_repos=selected_repos,
repositories_base_dir=self.tmp_root,
bin_dir=self.bin_dir,
all_repos=all_repos,
no_verification=True,
preview=False,
quiet=False,
clone_mode="shallow",
update_dependencies=False,
)
return called_installers
# ------------------------------------------------------------------
# Scenario 1: Only Makefile with install target
# ------------------------------------------------------------------
def test_only_makefile_installer_runs(self) -> None:
repo_dir = self._new_repo()
# Makefile: detect a real 'install' target for makefile layer.
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
f.write("install:\n\t@echo 'installing from Makefile'\n")
mk_inst = MakefileInstaller()
installers = [("makefile", mk_inst)]
called = self._run_with_installers(repo_dir, installers)
self.assertEqual(
called,
["makefile"],
"With only a Makefile, the MakefileInstaller should run exactly once.",
)
# ------------------------------------------------------------------
# Scenario 2: Python + Makefile, but pyproject.toml does NOT mention 'make install'
# → capabilities are disjoint, both installers should run.
# ------------------------------------------------------------------
def test_python_and_makefile_both_run_when_caps_disjoint(self) -> None:
repo_dir = self._new_repo()
# pyproject.toml: basic Python project, no 'make install' string.
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
f.write(
"[project]\n"
"name = 'dummy'\n"
)
# Makefile: install target for makefile layer.
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
f.write("install:\n\t@echo 'installing from Makefile'\n")
py_inst = PythonInstaller()
mk_inst = MakefileInstaller()
# Order: Python first, then Makefile
installers = [
("python", py_inst),
("makefile", mk_inst),
]
called = self._run_with_installers(repo_dir, installers)
# Both should have run because:
# - Python provides {python-runtime}
# - Makefile provides {make-install}
self.assertEqual(
called,
["python", "makefile"],
"PythonInstaller and MakefileInstaller should both run when their capabilities are disjoint.",
)
# ------------------------------------------------------------------
# Scenario 3: Python + Makefile, pyproject.toml mentions 'make install'
# → PythonInstaller provides {python-runtime, make-install}
# MakefileInstaller only {make-install}
# → MakefileInstaller must be skipped.
# ------------------------------------------------------------------
def test_python_shadows_makefile_when_pyproject_mentions_make_install(self) -> None:
repo_dir = self._new_repo()
# pyproject.toml: Python project with 'make install' hint.
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
f.write(
"[project]\n"
"name = 'dummy'\n"
"\n"
"# Hint for MakeInstallCapability on layer 'python'\n"
"make install\n"
)
# Makefile: install target, but should be shadowed by Python.
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
f.write("install:\n\t@echo 'installing from Makefile'\n")
py_inst = PythonInstaller()
mk_inst = MakefileInstaller()
installers = [
("python", py_inst),
("makefile", mk_inst),
]
called = self._run_with_installers(repo_dir, installers)
# Python should run, Makefile should be skipped because its only
# capability (make-install) is already provided by Python.
self.assertIn("python", called, "PythonInstaller should have run.")
self.assertNotIn(
"makefile",
called,
"MakefileInstaller should be skipped because its 'make-install' capability "
"is already provided by Python.",
)
# ------------------------------------------------------------------
# Scenario 4: Nix + Python + Makefile
# flake.nix provides python-runtime + make-install + nix-flake
# → Nix shadows both Python and Makefile.
# ------------------------------------------------------------------
def test_nix_shadows_python_and_makefile(self) -> None:
repo_dir = self._new_repo()
# pyproject.toml: generic Python project
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
f.write(
"[project]\n"
"name = 'dummy'\n"
)
# Makefile: install target
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
f.write("install:\n\t@echo 'installing from Makefile'\n")
# flake.nix: hints for both python-runtime and make-install on layer 'nix'
with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f:
f.write(
"{\n"
' description = "integration test flake";\n'
"}\n"
"\n"
"# Hint for PythonRuntimeCapability on layer 'nix'\n"
"buildPythonApplication something\n"
"\n"
"# Hint for MakeInstallCapability on layer 'nix'\n"
"make install\n"
)
nix_inst = NixFlakeInstaller()
py_inst = PythonInstaller()
mk_inst = MakefileInstaller()
installers = [
("nix", nix_inst),
("python", py_inst),
("makefile", mk_inst),
]
called = self._run_with_installers(repo_dir, installers)
# Nix must run, Python and Makefile must be skipped:
# - Nix provides {python-runtime, make-install, nix-flake}
# - Python provides {python-runtime}
# - Makefile provides {make-install}
self.assertIn("nix", called, "NixFlakeInstaller should have run.")
self.assertNotIn(
"python",
called,
"PythonInstaller should be skipped because its python-runtime capability "
"is already provided by Nix.",
)
self.assertNotIn(
"makefile",
called,
"MakefileInstaller should be skipped because its make-install capability "
"is already provided by Nix.",
)
# ------------------------------------------------------------------
# Scenario 5: OS packages + Nix + Python + Makefile
# PKGBUILD provides python-runtime + make-install + nix-flake
# → ArchPkgbuildInstaller shadows everything below.
# ------------------------------------------------------------------
def test_os_packages_shadow_nix_python_and_makefile(self) -> None:
repo_dir = self._new_repo()
# pyproject.toml: enough to signal a Python project
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
f.write(
"[project]\n"
"name = 'dummy'\n"
)
# Makefile: install target
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
f.write("install:\n\t@echo 'installing from Makefile'\n")
# flake.nix: as before
with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f:
f.write(
"{\n"
' description = "integration test flake";\n'
"}\n"
"\n"
"buildPythonApplication something\n"
"make install\n"
)
# PKGBUILD: contains patterns for all three capabilities on layer 'os-packages':
# - "pip install ." → python-runtime
# - "make install" → make-install
# - "nix profile" → nix-flake
with open(os.path.join(repo_dir, "PKGBUILD"), "w", encoding="utf-8") as f:
f.write(
"pkgname=dummy\n"
"pkgver=0.1\n"
"pkgrel=1\n"
"pkgdesc='dummy pkg for integration test'\n"
"arch=('any')\n"
"source=()\n"
"sha256sums=()\n"
"\n"
"build() {\n"
" echo 'build phase'\n"
"}\n"
"\n"
"package() {\n"
" echo 'install via pip and make and nix'\n"
" pip install .\n"
" make install\n"
" nix profile list || true\n"
"}\n"
)
os_inst = ArchPkgbuildInstaller()
nix_inst = NixFlakeInstaller()
py_inst = PythonInstaller()
mk_inst = MakefileInstaller()
installers = [
("os-packages", os_inst),
("nix", nix_inst),
("python", py_inst),
("makefile", mk_inst),
]
called = self._run_with_installers(repo_dir, installers)
# ArchPkgbuildInstaller must run, and everything below must be skipped:
# - os-packages provides {python-runtime, make-install, nix-flake}
# - nix provides {python-runtime, make-install, nix-flake}
# - python provides {python-runtime}
# - makefile provides {make-install}
self.assertIn("os-packages", called, "ArchPkgbuildInstaller should have run.")
self.assertNotIn(
"nix",
called,
"NixFlakeInstaller should be skipped because all its capabilities "
"are already provided by os-packages.",
)
self.assertNotIn(
"python",
called,
"PythonInstaller should be skipped because python-runtime is already "
"provided by os-packages.",
)
self.assertNotIn(
"makefile",
called,
"MakefileInstaller should be skipped because make-install is already "
"provided by os-packages.",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -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()