mirror of
https://github.com/kiviktnm/decman.git
synced 2026-09-19 12:08:28 +00:00
Add AUR plugin
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from decman.plugins import aur as aur_plugin
|
||||
|
||||
|
||||
class FakeStore(dict):
|
||||
def ensure(self, key: str, default: Any) -> None:
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
|
||||
|
||||
class FakeModule:
|
||||
def __init__(self, name: str, aur_pkgs: set[str], custom_pkgs: set[Any]) -> None:
|
||||
self.name = name
|
||||
self._changed = False
|
||||
self._aur_pkgs = aur_pkgs
|
||||
self._custom_pkgs = custom_pkgs
|
||||
|
||||
|
||||
class FakeCustomPackage:
|
||||
def __init__(self, pkgname: str) -> None:
|
||||
self.pkgname = pkgname
|
||||
|
||||
def __hash__(self) -> int: # needed because instances go into sets
|
||||
return hash(self.pkgname)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, FakeCustomPackage) and self.pkgname == other.pkgname
|
||||
|
||||
def parse(self, commands: Any) -> str:
|
||||
# Whatever ForeignPackageManager expects; we just need something to feed into add_custom_pkg
|
||||
return f"parsed-{self.pkgname}"
|
||||
|
||||
|
||||
def test_process_modules_collects_aur_and_custom_packages_and_marks_changed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
aur = aur_plugin.AUR()
|
||||
store = FakeStore()
|
||||
|
||||
cp1 = FakeCustomPackage("custom1")
|
||||
cp2 = FakeCustomPackage("custom2")
|
||||
|
||||
mod1 = FakeModule("mod1", {"aur1", "aur2"}, {cp1})
|
||||
mod2 = FakeModule("mod2", {"aur3"}, {cp2})
|
||||
|
||||
def fake_run_method_with_attribute(mod: FakeModule, attr: str):
|
||||
if attr == "__aur__packages__":
|
||||
return mod._aur_pkgs
|
||||
if attr == "__custom__packages__":
|
||||
return mod._custom_pkgs
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
aur_plugin.plugins, "run_method_with_attribute", fake_run_method_with_attribute
|
||||
)
|
||||
|
||||
aur.process_modules(store, {mod1, mod2})
|
||||
|
||||
# union of all aur/custom packages collected
|
||||
assert aur.packages == {"aur1", "aur2", "aur3"}
|
||||
assert aur.custom_packages == {cp1, cp2}
|
||||
|
||||
# stored per-module
|
||||
assert store["aur_packages_for_module"]["mod1"] == {"aur1", "aur2"}
|
||||
assert store["aur_packages_for_module"]["mod2"] == {"aur3"}
|
||||
assert store["custom_packages_for_module"]["mod1"] == {cp1}
|
||||
assert store["custom_packages_for_module"]["mod2"] == {cp2}
|
||||
|
||||
# first run: modules marked changed
|
||||
assert mod1._changed is True
|
||||
assert mod2._changed is True
|
||||
|
||||
|
||||
def test_apply_respects_ignored_packages_and_protects_their_dependencies(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
aur = aur_plugin.AUR()
|
||||
store = FakeStore()
|
||||
|
||||
# Desired AUR/custom state
|
||||
aur.packages = {"desired-aur"}
|
||||
cp = FakeCustomPackage("custom-aur")
|
||||
aur.custom_packages = {cp}
|
||||
|
||||
# Ignored foreign package (installed) and an ignored but *uninstalled* package
|
||||
aur.ignored_packages = {"ignored-aur", "ignored-not-installed"}
|
||||
|
||||
# Fake PackageSearch
|
||||
class FakePackageSearch:
|
||||
def __init__(self, timeout: int) -> None:
|
||||
self.timeout = timeout
|
||||
self.added: list[Any] = []
|
||||
|
||||
def add_custom_pkg(self, parsed: Any) -> None:
|
||||
self.added.append(parsed)
|
||||
|
||||
monkeypatch.setattr(aur_plugin, "PackageSearch", FakePackageSearch)
|
||||
monkeypatch.setattr(aur_plugin.os, "makedirs", lambda *x, **kw: None)
|
||||
|
||||
# Fake pacman interface for foreign/native info
|
||||
class FakePM:
|
||||
def __init__(self, commands, print_highlights, keywords) -> None:
|
||||
self.commands = commands
|
||||
self.print_highlights = print_highlights
|
||||
self.keywords = keywords
|
||||
|
||||
self.remove_called_with: set[str] | None = None
|
||||
self.set_as_deps_called_with: set[str] | None = None
|
||||
|
||||
def get_native_explicit(self) -> set[str]:
|
||||
# no natives needed for this scenario
|
||||
return set()
|
||||
|
||||
def get_foreign_explicit(self) -> set[str]:
|
||||
# All explicitly installed foreign packages:
|
||||
# - ignored-aur (ignored, must stay and protect deps)
|
||||
# - dep-of-ignored (candidate; has ignored dependant)
|
||||
# - orphan-foreign (candidate; no dependants)
|
||||
return {"ignored-aur", "dep-of-ignored", "orphan-foreign"}
|
||||
|
||||
def get_foreign_orphans(self) -> set[str]:
|
||||
# orphan-foreign also considered orphan
|
||||
return {"orphan-foreign"}
|
||||
|
||||
def get_dependants(self, pkg: str) -> set[str]:
|
||||
if pkg == "dep-of-ignored":
|
||||
# ignored-aur depends on dep-of-ignored -> must demote, not remove
|
||||
return {"ignored-aur"}
|
||||
if pkg == "orphan-foreign":
|
||||
return set()
|
||||
return set()
|
||||
|
||||
def remove(self, pkgs: set[str]) -> None:
|
||||
self.remove_called_with = pkgs
|
||||
|
||||
def set_as_dependencies(self, pkgs: set[str]) -> None:
|
||||
self.set_as_deps_called_with = pkgs
|
||||
|
||||
fake_pm = FakePM(None, None, None)
|
||||
|
||||
def fake_pm_ctor(commands, print_highlights, keywords) -> FakePM:
|
||||
fake_pm.commands = commands
|
||||
fake_pm.print_highlights = print_highlights
|
||||
fake_pm.keywords = keywords
|
||||
return fake_pm
|
||||
|
||||
monkeypatch.setattr(aur_plugin, "AurPacmanInterface", fake_pm_ctor)
|
||||
|
||||
# Fake ForeignPackageManager
|
||||
class FakeFPM:
|
||||
def __init__(
|
||||
self,
|
||||
store_arg,
|
||||
pm_arg,
|
||||
package_search_arg,
|
||||
commands_arg,
|
||||
cache_dir,
|
||||
build_dir,
|
||||
makepkg_user,
|
||||
) -> None:
|
||||
self.store = store_arg
|
||||
self.pm = pm_arg
|
||||
self.package_search = package_search_arg
|
||||
self.commands = commands_arg
|
||||
self.cache_dir = cache_dir
|
||||
self.build_dir = build_dir
|
||||
self.makepkg_user = makepkg_user
|
||||
|
||||
self.upgrade_args: tuple[bool, bool, set[str]] | None = None
|
||||
self.install_called_with: list[str] | None = None
|
||||
|
||||
def upgrade(self, upgrade_devel: bool, force: bool, ignored: set[str]) -> None:
|
||||
self.upgrade_args = (upgrade_devel, force, ignored)
|
||||
|
||||
def install(self, pkgs: list[str], force: bool = False) -> None:
|
||||
# store as set to ignore ordering
|
||||
self.install_called_with = pkgs
|
||||
|
||||
fake_fpm = FakeFPM(None, None, None, None, None, None, None)
|
||||
|
||||
def fake_fpm_ctor(
|
||||
store_arg, pm_arg, package_search_arg, commands_arg, cache_dir, build_dir, makepkg_user
|
||||
):
|
||||
fake_fpm.store = store_arg
|
||||
fake_fpm.pm = pm_arg
|
||||
fake_fpm.package_search = package_search_arg
|
||||
fake_fpm.commands = commands_arg
|
||||
fake_fpm.cache_dir = cache_dir
|
||||
fake_fpm.build_dir = build_dir
|
||||
fake_fpm.makepkg_user = makepkg_user
|
||||
return fake_fpm
|
||||
|
||||
monkeypatch.setattr(aur_plugin, "ForeignPackageManager", fake_fpm_ctor)
|
||||
|
||||
printed_lists: list[tuple[str, list[str]]] = []
|
||||
printed_summaries: list[str] = []
|
||||
|
||||
def fake_print_list(title: str, items: list[str]) -> None:
|
||||
printed_lists.append((title, items))
|
||||
|
||||
def fake_print_summary(msg: str) -> None:
|
||||
printed_summaries.append(msg)
|
||||
|
||||
monkeypatch.setattr(aur_plugin.output, "print_list", fake_print_list)
|
||||
monkeypatch.setattr(aur_plugin.output, "print_summary", fake_print_summary)
|
||||
|
||||
# Use params to test flag propagation into upgrade/install
|
||||
ok = aur.apply(store, dry_run=False, params=["aur-upgrade-devel", "aur-force"])
|
||||
|
||||
assert ok is True
|
||||
|
||||
# Removal / demotion logic:
|
||||
#
|
||||
# custom_package_names = {"custom-aur"}
|
||||
# currently_installed_foreign = {"ignored-aur", "dep-of-ignored", "orphan-foreign"}
|
||||
# orphans = {"orphan-foreign"}
|
||||
#
|
||||
# to_remove candidates:
|
||||
# (foreign | orphans) - desired - custom - ignored
|
||||
# = {"ignored-aur", "dep-of-ignored", "orphan-foreign"} ∪ {"orphan-foreign"}
|
||||
# - {"desired-aur"} - {"custom-aur"} - {"ignored-aur"}
|
||||
# = {"dep-of-ignored", "orphan-foreign"}
|
||||
#
|
||||
# dependants_to_keep includes ignored installed foreign -> dep-of-ignored is demoted, orphan-foreign removed.
|
||||
|
||||
assert fake_pm.remove_called_with == {"orphan-foreign"}
|
||||
assert fake_pm.set_as_deps_called_with == {"dep-of-ignored"}
|
||||
|
||||
# Ensure ignored packages were not removed
|
||||
assert "ignored-aur" not in (fake_pm.remove_called_with or set())
|
||||
|
||||
# Upgrade called with flags and ignored set
|
||||
assert fake_fpm.upgrade_args == (True, True, aur.ignored_packages)
|
||||
|
||||
# to_install = (packages | custom_names) - installed_foreign - ignored
|
||||
# = {"desired-aur", "custom-aur"} - {"ignored-aur", "dep-of-ignored", "orphan-foreign"}
|
||||
# - {"ignored-aur", "ignored-not-installed"}
|
||||
# = {"desired-aur", "custom-aur"}
|
||||
assert set(fake_fpm.install_called_with or []) == {"desired-aur", "custom-aur"}
|
||||
# ignored packages must not be installed
|
||||
assert "ignored-aur" not in (fake_fpm.install_called_with or [])
|
||||
assert "ignored-not-installed" not in (fake_fpm.install_called_with or [])
|
||||
|
||||
# Also check the printed lists mirror this
|
||||
titles = [t for t, _ in printed_lists]
|
||||
assert "Removing foreign packages:" in titles
|
||||
assert "Setting previously explicitly installed foreign packages as dependencies:" in titles
|
||||
assert "Installing foreign packages:" in titles
|
||||
|
||||
remove_list = next(items for t, items in printed_lists if "Removing foreign packages:" in t)
|
||||
demote_list = next(
|
||||
items
|
||||
for t, items in printed_lists
|
||||
if "Setting previously explicitly installed foreign packages as dependencies:" in t
|
||||
)
|
||||
install_list = next(items for t, items in printed_lists if "Installing foreign packages:" in t)
|
||||
|
||||
assert remove_list == ["orphan-foreign"]
|
||||
assert demote_list == ["dep-of-ignored"]
|
||||
# Order of install_list is deterministic because sorted() is used
|
||||
assert install_list == ["custom-aur", "desired-aur"]
|
||||
assert any("Upgrading foreign packages." in s for s in printed_summaries)
|
||||
|
||||
|
||||
def test_apply_returns_false_on_aur_rpc_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
aur = aur_plugin.AUR()
|
||||
store = FakeStore()
|
||||
|
||||
# Force PackageSearch to fail immediately
|
||||
class FailingPackageSearch:
|
||||
def __init__(self, timeout: int) -> None:
|
||||
raise aur_plugin.AurRPCError("RPC down", "url")
|
||||
|
||||
monkeypatch.setattr(aur_plugin, "PackageSearch", FailingPackageSearch)
|
||||
monkeypatch.setattr(aur_plugin.os, "makedirs", lambda *x, **kw: None)
|
||||
|
||||
errors_logged: list[str] = []
|
||||
continuations: list[str] = []
|
||||
traceback_called: list[bool] = []
|
||||
|
||||
def fake_print_error(msg: str) -> None:
|
||||
errors_logged.append(msg)
|
||||
|
||||
def fake_print_continuation(msg: str) -> None:
|
||||
continuations.append(msg)
|
||||
|
||||
def fake_print_traceback() -> None:
|
||||
traceback_called.append(True)
|
||||
|
||||
monkeypatch.setattr(aur_plugin.output, "print_error", fake_print_error)
|
||||
monkeypatch.setattr(aur_plugin.output, "print_continuation", fake_print_continuation)
|
||||
monkeypatch.setattr(aur_plugin.output, "print_traceback", fake_print_traceback)
|
||||
|
||||
ok = aur.apply(store, dry_run=False)
|
||||
|
||||
assert ok is False
|
||||
assert any("AUR RPC" in msg or "fetch data from AUR RPC" in msg for msg in errors_logged)
|
||||
assert any("RPC down" in msg for msg in continuations)
|
||||
assert traceback_called
|
||||
Reference in New Issue
Block a user