mirror of
https://github.com/kiviktnm/decman.git
synced 2026-09-19 12:08:28 +00:00
Rename packages to plugins
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
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"] == {str(cp1)}
|
||||
assert store["custom_packages_for_module"]["mod2"] == {str(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, dbsiglevel, dbpath) -> 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, None, None)
|
||||
|
||||
def fake_pm_ctor(
|
||||
commands,
|
||||
print_highlights,
|
||||
keywords,
|
||||
dbsiglevel,
|
||||
dbpath,
|
||||
) -> 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_traceback() -> None:
|
||||
traceback_called.append(True)
|
||||
|
||||
monkeypatch.setattr(aur_plugin.output, "print_error", fake_print_error)
|
||||
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 errors_logged)
|
||||
assert traceback_called
|
||||
@@ -0,0 +1,743 @@
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
from decman.plugins.aur import package as pkg_mod
|
||||
from decman.plugins.aur.error import AurRPCError, PKGBUILDParseError
|
||||
from decman.plugins.aur.package import (
|
||||
CustomPackage,
|
||||
PackageInfo,
|
||||
PackageSearch,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def silence_output(monkeypatch):
|
||||
# Avoid real I/O / prompts in tests by default
|
||||
monkeypatch.setattr(pkg_mod.output, "print_debug", lambda *a, **k: None)
|
||||
monkeypatch.setattr(pkg_mod.output, "print_summary", lambda *a, **k: None)
|
||||
monkeypatch.setattr(
|
||||
pkg_mod.output,
|
||||
"prompt_number",
|
||||
lambda *a, **k: 1, # safe default
|
||||
)
|
||||
|
||||
|
||||
# --- PackageInfo -----------------------------------------------------------
|
||||
|
||||
|
||||
def test_packageinfo_requires_exactly_one_source():
|
||||
with pytest.raises(ValueError, match="cannot be None"):
|
||||
PackageInfo(pkgname="a", pkgbase="a", version="1.0")
|
||||
|
||||
with pytest.raises(ValueError, match="cannot be set"):
|
||||
PackageInfo(
|
||||
pkgname="a",
|
||||
pkgbase="a",
|
||||
version="1.0",
|
||||
git_url="git://example",
|
||||
pkgbuild_directory="/tmp",
|
||||
)
|
||||
|
||||
|
||||
class DummyPacman:
|
||||
def __init__(self, installable: set[str]):
|
||||
self._installable = installable
|
||||
self.calls: list[str] = []
|
||||
|
||||
def is_installable(self, name: str) -> bool:
|
||||
self.calls.append(name)
|
||||
return name in self._installable
|
||||
|
||||
|
||||
def _make_pkg_for_deps() -> PackageInfo:
|
||||
return PackageInfo(
|
||||
pkgname="pkg",
|
||||
pkgbase="pkg",
|
||||
version="1.0",
|
||||
git_url="git://example",
|
||||
dependencies=("native>=1", "foreign=2"),
|
||||
make_dependencies=("make-native", "make-foreign>=3"),
|
||||
check_dependencies=("check-foreign<4", "check-native"),
|
||||
)
|
||||
|
||||
|
||||
def test_packageinfo_foreign_and_native_dependencies_are_split_and_stripped():
|
||||
pacman = DummyPacman(
|
||||
{
|
||||
"native>=1",
|
||||
"make-native",
|
||||
"check-native",
|
||||
}
|
||||
)
|
||||
pkg = _make_pkg_for_deps()
|
||||
|
||||
assert pkg.native_dependencies(pacman) == ["native"]
|
||||
assert pkg.foreign_dependencies(pacman) == ["foreign"]
|
||||
assert pkg.native_make_dependencies(pacman) == ["make-native"]
|
||||
assert pkg.foreign_make_dependencies(pacman) == ["make-foreign"]
|
||||
assert pkg.native_check_dependencies(pacman) == ["check-native"]
|
||||
assert pkg.foreign_check_dependencies(pacman) == ["check-foreign"]
|
||||
|
||||
|
||||
# --- CustomPackage ---------------------------------------------------------
|
||||
|
||||
|
||||
def test_custompackage_requires_exactly_one_source():
|
||||
with pytest.raises(ValueError, match="cannot be None"):
|
||||
CustomPackage("pkg", git_url=None, pkgbuild_directory=None)
|
||||
|
||||
with pytest.raises(ValueError, match="cannot be set"):
|
||||
CustomPackage("pkg", git_url="git://example", pkgbuild_directory="/tmp")
|
||||
|
||||
|
||||
class DummyCommands:
|
||||
"""Minimal stub; only here so type checks pass where needed."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"srcinfo, expected_version",
|
||||
[
|
||||
(
|
||||
"""
|
||||
pkgbase = foo
|
||||
pkgver = 1.2.3
|
||||
pkgrel = 4
|
||||
pkgname = foo
|
||||
""",
|
||||
"1.2.3-4",
|
||||
),
|
||||
(
|
||||
"""
|
||||
pkgbase = foo
|
||||
pkgver = 1.2.3
|
||||
pkgrel = 4
|
||||
epoch = 2
|
||||
pkgname = foo
|
||||
""",
|
||||
"2:1.2.3-4",
|
||||
),
|
||||
(
|
||||
"""
|
||||
pkgbase = foo
|
||||
pkgver = 1.2.3
|
||||
pkgname = foo
|
||||
""",
|
||||
"1.2.3",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parse_srcinfo_version_handling(srcinfo: str, expected_version: str) -> None:
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory="/dummy")
|
||||
|
||||
info = pkg._parse_srcinfo(srcinfo)
|
||||
|
||||
assert info.pkgname == "foo"
|
||||
assert info.pkgbase == "foo"
|
||||
assert info.version == expected_version
|
||||
|
||||
|
||||
def test_parse_srcinfo_single_package_dependencies() -> None:
|
||||
srcinfo = """
|
||||
pkgbase = foo
|
||||
pkgver = 1.2.3
|
||||
pkgrel = 1
|
||||
depends = bar>=1.0
|
||||
makedepends = baz
|
||||
checkdepends = qux
|
||||
|
||||
pkgname = foo
|
||||
"""
|
||||
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory="/dummy")
|
||||
|
||||
info = pkg._parse_srcinfo(srcinfo)
|
||||
|
||||
assert info.dependencies == ("bar>=1.0",)
|
||||
assert info.make_dependencies == ("baz",)
|
||||
assert info.check_dependencies == ("qux",)
|
||||
|
||||
|
||||
def test_parse_srcinfo_split_package_uses_only_target_pkg_dependencies(monkeypatch) -> None:
|
||||
# Ensure arch-specific keys match
|
||||
monkeypatch.setattr(pkg_mod.config, "arch", "x86_64", raising=False)
|
||||
|
||||
srcinfo = """
|
||||
pkgbase = clion
|
||||
pkgver = 2025.3
|
||||
pkgrel = 1
|
||||
makedepends = rsync
|
||||
depends = base-dep
|
||||
depends_x86_64 = base-arch-dep
|
||||
|
||||
pkgname = clion
|
||||
depends = libdbusmenu-glib
|
||||
depends_x86_64 = clion-arch-dep
|
||||
checkdepends = clion-check
|
||||
|
||||
pkgname = clion-jre
|
||||
depends = jre-dep
|
||||
makedepends = jre-make
|
||||
|
||||
pkgname = clion-cmake
|
||||
depends = cmake-dep
|
||||
"""
|
||||
|
||||
pkg = CustomPackage(pkgname="clion", git_url=None, pkgbuild_directory="/dummy")
|
||||
|
||||
info = pkg._parse_srcinfo(srcinfo)
|
||||
|
||||
# version
|
||||
assert info.pkgbase == "clion"
|
||||
assert info.version == "2025.3-1"
|
||||
|
||||
# base deps + target pkg deps (including arch-specific)
|
||||
assert info.dependencies == (
|
||||
"base-dep",
|
||||
"base-arch-dep",
|
||||
"libdbusmenu-glib",
|
||||
"clion-arch-dep",
|
||||
)
|
||||
|
||||
# only base and target pkg makedepends
|
||||
assert info.make_dependencies == ("rsync",)
|
||||
|
||||
# base + target pkg checkdepends
|
||||
assert info.check_dependencies == ("clion-check",)
|
||||
|
||||
|
||||
def test_parse_srcinfo_arch_specific_ignored_for_other_arch(monkeypatch) -> None:
|
||||
# Different arch → *_x86_64 keys should be ignored
|
||||
monkeypatch.setattr(pkg_mod.config, "arch", "aarch64", raising=False)
|
||||
|
||||
srcinfo = """
|
||||
pkgbase = foo
|
||||
pkgver = 1.0
|
||||
pkgrel = 1
|
||||
depends_x86_64 = base-arch-dep
|
||||
|
||||
pkgname = foo
|
||||
depends = common-dep
|
||||
depends_x86_64 = pkg-arch-dep
|
||||
"""
|
||||
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory="/dummy")
|
||||
|
||||
info = pkg._parse_srcinfo(srcinfo)
|
||||
|
||||
# Only common deps, no *_x86_64 because arch != x86_64
|
||||
assert info.dependencies == ("common-dep",)
|
||||
|
||||
|
||||
def test_parse_srcinfo_missing_required_fields_raises() -> None:
|
||||
# Missing pkgbase
|
||||
srcinfo_no_pkgbase = """
|
||||
pkgver = 1.0
|
||||
pkgrel = 1
|
||||
pkgname = foo
|
||||
"""
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory="/dummy")
|
||||
|
||||
with pytest.raises(PKGBUILDParseError) as excinfo:
|
||||
pkg._parse_srcinfo(srcinfo_no_pkgbase)
|
||||
assert "pkgbase/pkgver" in str(excinfo.value)
|
||||
|
||||
# Missing pkgver
|
||||
srcinfo_no_pkgver = """
|
||||
pkgbase = foo
|
||||
pkgname = foo
|
||||
"""
|
||||
|
||||
with pytest.raises(PKGBUILDParseError) as excinfo2:
|
||||
pkg._parse_srcinfo(srcinfo_no_pkgver)
|
||||
assert "pkgbase/pkgver" in str(excinfo2.value)
|
||||
|
||||
|
||||
def test_parse_srcinfo_missing_target_pkg_raises() -> None:
|
||||
srcinfo = """
|
||||
pkgbase = foo
|
||||
pkgver = 1.0
|
||||
pkgrel = 1
|
||||
pkgname = other
|
||||
"""
|
||||
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory="/dummy")
|
||||
|
||||
with pytest.raises(PKGBUILDParseError) as excinfo:
|
||||
pkg._parse_srcinfo(srcinfo)
|
||||
|
||||
msg = str(excinfo.value)
|
||||
assert "Package foo not found in SRCINFO" in msg
|
||||
assert "other" in msg # listed in present packages
|
||||
|
||||
|
||||
def test_srcinfo_from_pkgbuild_directory_missing_dir_raises(tmp_path: pathlib.Path) -> None:
|
||||
missing = tmp_path / "does-not-exist"
|
||||
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory=str(missing))
|
||||
|
||||
with pytest.raises(PKGBUILDParseError) as excinfo:
|
||||
pkg._srcinfo_from_pkgbuild_directory(DummyCommands())
|
||||
|
||||
msg = str(excinfo.value)
|
||||
assert "does not exist or is not a directory" in msg
|
||||
|
||||
|
||||
def test_srcinfo_from_pkgbuild_directory_missing_pkgbuild_raises(tmp_path: pathlib.Path) -> None:
|
||||
path = tmp_path / "pkgdir"
|
||||
path.mkdir()
|
||||
|
||||
pkg = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory=str(path))
|
||||
|
||||
with pytest.raises(PKGBUILDParseError) as excinfo:
|
||||
pkg._srcinfo_from_pkgbuild_directory(DummyCommands())
|
||||
|
||||
msg = str(excinfo.value)
|
||||
assert "No PKGBUILD found" in msg
|
||||
|
||||
|
||||
def test_custom_package_equality_and_hash() -> None:
|
||||
a1 = CustomPackage(
|
||||
pkgname="foo", git_url="https://example.com/repo.git", pkgbuild_directory=None
|
||||
)
|
||||
a2 = CustomPackage(
|
||||
pkgname="foo", git_url="https://example.com/repo.git", pkgbuild_directory=None
|
||||
)
|
||||
b = CustomPackage(pkgname="foo", git_url=None, pkgbuild_directory="/some/path")
|
||||
|
||||
assert a1 == a2
|
||||
assert hash(a1) == hash(a2)
|
||||
|
||||
assert a1 != b
|
||||
assert hash(a1) != hash(b)
|
||||
|
||||
|
||||
def test_custom_package_str_git_and_directory() -> None:
|
||||
git_pkg = CustomPackage(
|
||||
pkgname="foo",
|
||||
git_url="https://example.com/repo.git",
|
||||
pkgbuild_directory=None,
|
||||
)
|
||||
dir_pkg = CustomPackage(
|
||||
pkgname="foo",
|
||||
git_url=None,
|
||||
pkgbuild_directory="/some/path",
|
||||
)
|
||||
|
||||
assert "pkgname=foo" in str(git_pkg)
|
||||
assert "git_url=https://example.com/repo.git" in str(git_pkg)
|
||||
|
||||
assert "pkgname=foo" in str(dir_pkg)
|
||||
assert "pkgbuild_directory=/some/path" in str(dir_pkg)
|
||||
|
||||
|
||||
# --- PackageSearch: caching ------------------------------------------------
|
||||
|
||||
|
||||
def _make_pkg(name: str = "pkg") -> PackageInfo:
|
||||
return PackageInfo(
|
||||
pkgname=name,
|
||||
pkgbase=name,
|
||||
version="1.0",
|
||||
git_url=f"git://example/{name}",
|
||||
provides=("virt-" + name,),
|
||||
dependencies=("dep",),
|
||||
make_dependencies=(),
|
||||
check_dependencies=(),
|
||||
)
|
||||
|
||||
|
||||
def test_add_custom_pkg_caches_package():
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("foo")
|
||||
|
||||
search.add_custom_pkg(pkg)
|
||||
|
||||
assert pkg in search._custom_packages
|
||||
assert search._package_cache["foo"] is pkg
|
||||
assert search._all_providers_cache["virt-foo"] == ["foo"]
|
||||
|
||||
|
||||
def test_try_caching_packages_skips_already_cached(monkeypatch):
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("foo")
|
||||
search._cache_pkg(pkg)
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_get(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
raise AssertionError("requests.get should not be called")
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
search.try_caching_packages(["foo"])
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_try_caching_packages_caches_from_aur(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {
|
||||
"type": "success",
|
||||
"results": [
|
||||
{
|
||||
"Name": "bar",
|
||||
"PackageBase": "bar-base",
|
||||
"Version": "2.0",
|
||||
"Depends": ["dep1"],
|
||||
"MakeDepends": ["make1"],
|
||||
"CheckDepends": ["check1"],
|
||||
"Provides": ["virt-bar"],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
search.try_caching_packages(["bar"])
|
||||
|
||||
assert "bar" in search._package_cache
|
||||
info = search._package_cache["bar"]
|
||||
assert isinstance(info, PackageInfo)
|
||||
assert search._all_providers_cache["virt-bar"] == ["bar"]
|
||||
|
||||
|
||||
def test_try_caching_packages_aur_returns_error(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {"type": "error", "error": "boom"}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
with pytest.raises(AurRPCError):
|
||||
search.try_caching_packages(["bar"])
|
||||
|
||||
|
||||
def test_try_caching_packages_request_exception_raises_aur_error(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
class DummyError(pkg_mod.requests.RequestException):
|
||||
pass
|
||||
|
||||
def fake_get(url, timeout):
|
||||
raise DummyError("boom")
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
with pytest.raises(AurRPCError):
|
||||
search.try_caching_packages(["bar"])
|
||||
|
||||
|
||||
# --- PackageSearch: get_package_info --------------------------------------
|
||||
|
||||
|
||||
def test_get_package_info_returns_from_cache():
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("foo")
|
||||
search._cache_pkg(pkg)
|
||||
|
||||
result = search.get_package_info("foo")
|
||||
assert result is pkg
|
||||
|
||||
|
||||
def test_get_package_info_returns_custom_package_if_not_cached():
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("foo")
|
||||
search._custom_packages.append(pkg)
|
||||
|
||||
result = search.get_package_info("foo")
|
||||
|
||||
assert result is pkg
|
||||
assert search._package_cache["foo"] is pkg
|
||||
|
||||
|
||||
def test_get_package_info_aur_not_found_returns_none(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {"type": "success", "resultcount": 0, "results": []}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
result = search.get_package_info("foo")
|
||||
assert result is None
|
||||
assert "foo" not in search._package_cache
|
||||
|
||||
|
||||
def test_get_package_info_aur_success_caches_and_returns(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {
|
||||
"type": "success",
|
||||
"resultcount": 1,
|
||||
"results": [
|
||||
{
|
||||
"Name": "foo",
|
||||
"PackageBase": "foo-base",
|
||||
"Version": "1.2",
|
||||
"Depends": ["dep1"],
|
||||
"MakeDepends": ["make1"],
|
||||
"CheckDepends": ["check1"],
|
||||
"Provides": ["virt-foo"],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
result = search.get_package_info("foo")
|
||||
assert isinstance(result, PackageInfo)
|
||||
assert result.pkgname == "foo"
|
||||
assert search._package_cache["foo"] is result
|
||||
|
||||
|
||||
def test_get_package_info_aur_returns_error(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {"type": "error", "error": "boom"}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
with pytest.raises(AurRPCError):
|
||||
search.get_package_info("foo")
|
||||
|
||||
|
||||
def test_get_package_info_request_exception_raises_aur_error(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
class DummyError(pkg_mod.requests.RequestException):
|
||||
pass
|
||||
|
||||
def fake_get(url, timeout):
|
||||
raise DummyError("boom")
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
with pytest.raises(AurRPCError):
|
||||
search.get_package_info("foo")
|
||||
|
||||
|
||||
# --- PackageSearch: find_provider -----------------------------------------
|
||||
|
||||
|
||||
def test_find_provider_uses_selected_providers_cache():
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("foo")
|
||||
search._selected_providers_cache["dep"] = pkg
|
||||
|
||||
result = search.find_provider("dep")
|
||||
assert result is pkg
|
||||
|
||||
|
||||
def test_find_provider_exact_name_match(monkeypatch):
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("dep")
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
assert name == "dep"
|
||||
return pkg
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
result = search.find_provider("dep")
|
||||
assert result is pkg
|
||||
assert search._selected_providers_cache["dep"] is pkg
|
||||
|
||||
|
||||
def test_find_provider_single_known_provider(monkeypatch):
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("provider")
|
||||
search._all_providers_cache["dep"] = ["provider"]
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
if name == "dep":
|
||||
return None
|
||||
assert name == "provider"
|
||||
return pkg
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
result = search.find_provider("dep")
|
||||
assert result is pkg
|
||||
assert search._selected_providers_cache["dep"] is pkg
|
||||
|
||||
|
||||
def test_find_provider_aur_search_not_found(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
# Exact name match should fail
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {"type": "success", "resultcount": 0, "results": []}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
result = search.find_provider("dep")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_find_provider_aur_search_single_result(monkeypatch):
|
||||
search = PackageSearch()
|
||||
pkg = _make_pkg("provider")
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
# first call for stripped_dependency -> None
|
||||
if name == "dep":
|
||||
return None
|
||||
assert name == "provider"
|
||||
return pkg
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {
|
||||
"type": "success",
|
||||
"resultcount": 1,
|
||||
"results": [{"Name": "provider"}],
|
||||
}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
result = search.find_provider("dep")
|
||||
assert result is pkg
|
||||
|
||||
|
||||
def test_find_provider_aur_search_multiple_results_calls_choose_provider(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
# no exact match
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {
|
||||
"type": "success",
|
||||
"resultcount": 2,
|
||||
"results": [{"Name": "a"}, {"Name": "b"}],
|
||||
}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
sentinel = object()
|
||||
|
||||
def fake_choose(dep, providers, where):
|
||||
assert dep == "dep"
|
||||
assert providers == ["a", "b"]
|
||||
assert where == "AUR"
|
||||
return sentinel
|
||||
|
||||
monkeypatch.setattr(search, "_choose_provider", fake_choose)
|
||||
|
||||
result = search.find_provider("dep")
|
||||
assert result is sentinel
|
||||
|
||||
|
||||
def test_find_provider_aur_search_error(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
def fake_get(url, timeout):
|
||||
class Resp:
|
||||
def json(self):
|
||||
return {"type": "error", "error": "boom"}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
with pytest.raises(AurRPCError):
|
||||
search.find_provider("dep")
|
||||
|
||||
|
||||
def test_find_provider_aur_search_request_exception_raises_aur_error(monkeypatch):
|
||||
search = PackageSearch()
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
class DummyError(pkg_mod.requests.RequestException):
|
||||
pass
|
||||
|
||||
def fake_get(url, timeout):
|
||||
raise DummyError("boom")
|
||||
|
||||
monkeypatch.setattr(pkg_mod.requests, "get", fake_get)
|
||||
|
||||
with pytest.raises(AurRPCError):
|
||||
search.find_provider("dep")
|
||||
|
||||
|
||||
# --- PackageSearch: _choose_provider --------------------------------------
|
||||
|
||||
|
||||
def test_choose_provider_prompts_and_caches(monkeypatch):
|
||||
search = PackageSearch()
|
||||
providers = ["a", "b", "c"]
|
||||
selected_pkg = _make_pkg("b")
|
||||
|
||||
# override prompt to select "2" (provider "b")
|
||||
monkeypatch.setattr(
|
||||
pkg_mod.output,
|
||||
"prompt_number",
|
||||
lambda *a, **k: 2,
|
||||
)
|
||||
|
||||
def fake_get_package_info(name: str):
|
||||
assert name == "b"
|
||||
return selected_pkg
|
||||
|
||||
monkeypatch.setattr(search, "get_package_info", fake_get_package_info)
|
||||
|
||||
result = search._choose_provider("dep", providers, "AUR")
|
||||
assert result is selected_pkg
|
||||
assert search._selected_providers_cache["dep"] is selected_pkg
|
||||
@@ -0,0 +1,87 @@
|
||||
import pytest
|
||||
from decman.plugins.aur.error import DependencyCycleError
|
||||
from decman.plugins.aur.resolver import DepGraph, ForeignPackage
|
||||
|
||||
|
||||
def test_add_dependency():
|
||||
graph = DepGraph()
|
||||
|
||||
graph.add_requirement("A", None)
|
||||
graph.add_requirement("B1", "A")
|
||||
graph.add_requirement("B2", "A")
|
||||
graph.add_requirement("C", "B1")
|
||||
|
||||
assert "B1" in graph.package_nodes["A"].children
|
||||
assert "B2" in graph.package_nodes["A"].children
|
||||
assert "C" in graph.package_nodes["B1"].children
|
||||
|
||||
|
||||
def test_cyclic_dependency_raises():
|
||||
graph = DepGraph()
|
||||
|
||||
graph.add_requirement("A", None)
|
||||
graph.add_requirement("B", "A")
|
||||
graph.add_requirement("C", "B")
|
||||
|
||||
with pytest.raises(DependencyCycleError):
|
||||
graph.add_requirement("A", "C")
|
||||
|
||||
|
||||
def _build_graph_for_outer_deps() -> DepGraph:
|
||||
graph = DepGraph()
|
||||
|
||||
# Roots
|
||||
graph.add_requirement("A", None)
|
||||
graph.add_requirement("V", None)
|
||||
|
||||
# Level B
|
||||
graph.add_requirement("B1", "A")
|
||||
graph.add_requirement("B2", "A")
|
||||
graph.add_requirement("B3", "A")
|
||||
|
||||
# Extra dependency B1 -> B2
|
||||
graph.add_requirement("B1", "B2")
|
||||
|
||||
# Level C
|
||||
graph.add_requirement("C1", "B1")
|
||||
graph.add_requirement("C2", "B1")
|
||||
|
||||
# Level D + cycle-ish edges
|
||||
graph.add_requirement("D", "C1")
|
||||
graph.add_requirement("C2", "D")
|
||||
|
||||
# Foreign packages and their foreign deps
|
||||
defs = {
|
||||
"V": [],
|
||||
"A": ["B1", "B2", "B3", "C1", "C2", "D"],
|
||||
"B1": ["C1", "C2", "D"],
|
||||
"B2": ["B1", "C1", "C2", "D"],
|
||||
"B3": [],
|
||||
"C1": ["D", "C2"],
|
||||
"C2": [],
|
||||
"D": ["C2"],
|
||||
}
|
||||
|
||||
for name, deps in defs.items():
|
||||
pkg = ForeignPackage(name)
|
||||
pkg.add_foreign_dependency_packages(deps)
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
def _assert_outer_dep_names(graph: DepGraph, expected: set[str]) -> None:
|
||||
result = graph.get_and_remove_outer_dep_pkgs()
|
||||
names = {pkg.name for pkg in result}
|
||||
assert names == expected
|
||||
|
||||
|
||||
def test_get_and_remove_outer_deps_sequence():
|
||||
graph = _build_graph_for_outer_deps()
|
||||
|
||||
_assert_outer_dep_names(graph, {"C2", "B3", "V"})
|
||||
_assert_outer_dep_names(graph, {"D"})
|
||||
_assert_outer_dep_names(graph, {"C1"})
|
||||
_assert_outer_dep_names(graph, {"B1"})
|
||||
_assert_outer_dep_names(graph, {"B2"})
|
||||
_assert_outer_dep_names(graph, {"A"})
|
||||
_assert_outer_dep_names(graph, set())
|
||||
@@ -0,0 +1,313 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from decman.plugins import pacman as pacman_plugin
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dep,expected",
|
||||
[
|
||||
("foo", "foo"),
|
||||
("foo=1.0", "foo"),
|
||||
("bar>=2", "bar"),
|
||||
("baz<3", "baz"),
|
||||
("multi=1.0-2", "multi"),
|
||||
],
|
||||
)
|
||||
def test_strip_dependency(dep, expected):
|
||||
assert pacman_plugin.strip_dependency(dep) == expected
|
||||
|
||||
|
||||
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, packages: set[str]) -> None:
|
||||
self.name = name
|
||||
self._changed = False
|
||||
self._packages = packages
|
||||
|
||||
|
||||
def test_process_modules_collects_packages_and_marks_changed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pacman = pacman_plugin.Pacman()
|
||||
store = FakeStore()
|
||||
mod1 = FakeModule("mod1", {"pkg1", "pkg2"})
|
||||
mod2 = FakeModule("mod2", {"pkg3"})
|
||||
|
||||
def fake_run_method_with_attribute(mod: FakeModule, attr: str) -> set[str]:
|
||||
assert attr == "__pacman__packages__"
|
||||
return mod._packages
|
||||
|
||||
monkeypatch.setattr(
|
||||
pacman_plugin.plugins,
|
||||
"run_method_with_attribute",
|
||||
fake_run_method_with_attribute,
|
||||
)
|
||||
|
||||
pacman.process_modules(store, {mod1, mod2})
|
||||
|
||||
# packages collected
|
||||
assert pacman.packages == {"pkg1", "pkg2", "pkg3"}
|
||||
# stored mapping per module
|
||||
assert store["packages_for_module"]["mod1"] == {"pkg1", "pkg2"}
|
||||
assert store["packages_for_module"]["mod2"] == {"pkg3"}
|
||||
# modules marked changed (first run)
|
||||
assert mod1._changed is True
|
||||
assert mod2._changed is True
|
||||
|
||||
|
||||
def test_apply_dry_run_computes_sets_and_does_not_call_pacman(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pacman = pacman_plugin.Pacman()
|
||||
store = FakeStore()
|
||||
|
||||
# Desired state
|
||||
pacman.packages = {"keep-explicit", "new-pkg"}
|
||||
|
||||
# Fake PacmanInterface returned by plugin module
|
||||
class FakePM:
|
||||
def __init__(
|
||||
self, commands, print_highlights, keywords, database_signature_level, database_path
|
||||
) -> None: # noqa: D401
|
||||
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
|
||||
self.upgrade_called = False
|
||||
self.install_called_with: set[str] | None = None
|
||||
|
||||
def get_native_explicit(self) -> set[str]:
|
||||
# keep-explicit (in desired), old-explicit (to demote/remove)
|
||||
return {"keep-explicit", "old-explicit"}
|
||||
|
||||
def get_foreign_explicit(self) -> set[str]:
|
||||
# foreign-package protects its deps
|
||||
return {"foreign-pkg"}
|
||||
|
||||
def get_native_orphans(self) -> set[str]:
|
||||
# orphan-explicit is also candidate
|
||||
return {"orphan-explicit"}
|
||||
|
||||
def get_dependants(self, pkg: str) -> set[str]:
|
||||
# old-explicit has a foreign dependant -> demote to dep
|
||||
# orphan-explicit has no dependants -> remove
|
||||
if pkg == "old-explicit":
|
||||
return {"foreign-pkg"}
|
||||
if pkg == "orphan-explicit":
|
||||
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
|
||||
|
||||
def upgrade(self) -> None:
|
||||
self.upgrade_called = True
|
||||
|
||||
def install(self, pkgs: set[str]) -> None:
|
||||
self.install_called_with = pkgs
|
||||
|
||||
fake_pm = FakePM(None, None, None, None, None)
|
||||
|
||||
def fake_pm_ctor(
|
||||
commands, print_highlights, keywords, database_signature_level, database_path
|
||||
) -> FakePM:
|
||||
# constructor used in Pacman.apply
|
||||
fake_pm.commands = commands
|
||||
fake_pm.print_highlights = print_highlights
|
||||
fake_pm.keywords = keywords
|
||||
return fake_pm
|
||||
|
||||
monkeypatch.setattr(pacman_plugin, "PacmanInterface", fake_pm_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(pacman_plugin.output, "print_list", fake_print_list)
|
||||
monkeypatch.setattr(pacman_plugin.output, "print_summary", fake_print_summary)
|
||||
|
||||
ok = pacman.apply(store, dry_run=True)
|
||||
|
||||
assert ok is True
|
||||
|
||||
# to_remove = (native | orphans) - desired
|
||||
# = {keep-explicit, old-explicit} ∪ {orphan-explicit} - {keep-explicit, new-pkg}
|
||||
# = {old-explicit, orphan-explicit}
|
||||
#
|
||||
# old-explicit has foreign dependant -> demoted to dep
|
||||
# orphan-explicit has no dependants -> removed
|
||||
|
||||
# printed lists (titles and contents)
|
||||
titles = [t for t, _ in printed_lists]
|
||||
assert "Removing pacman packages:" in titles
|
||||
assert "Setting previously explicitly installed packages as dependencies:" in titles
|
||||
assert "Installing pacman packages:" in titles
|
||||
|
||||
# find lists by title
|
||||
remove_list = next(items for t, items in printed_lists if "Removing pacman packages:" in t)
|
||||
demote_list = next(
|
||||
items
|
||||
for t, items in printed_lists
|
||||
if "Setting previously explicitly installed packages as dependencies:" in t
|
||||
)
|
||||
install_list = next(items for t, items in printed_lists if "Installing pacman packages:" in t)
|
||||
|
||||
assert remove_list == ["orphan-explicit"]
|
||||
assert demote_list == ["old-explicit"]
|
||||
# to_install = desired - currently_installed_native
|
||||
# = {keep-explicit, new-pkg} - {keep-explicit, old-explicit}
|
||||
# = {new-pkg}
|
||||
assert install_list == ["new-pkg"]
|
||||
|
||||
# Upgrade summary printed even in dry-run
|
||||
assert any("Upgrading packages." in s for s in printed_summaries)
|
||||
|
||||
# No mutating calls in dry-run
|
||||
assert fake_pm.remove_called_with is None
|
||||
assert fake_pm.set_as_deps_called_with is None
|
||||
assert fake_pm.upgrade_called is False
|
||||
assert fake_pm.install_called_with is None
|
||||
|
||||
|
||||
def test_apply_returns_false_on_command_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
pacman = pacman_plugin.Pacman()
|
||||
store = FakeStore()
|
||||
pacman.packages = set()
|
||||
|
||||
class FailingPM:
|
||||
def __init__(self, *args, **kwargs) -> None: # noqa: D401
|
||||
pass
|
||||
|
||||
def get_native_explicit(self) -> set[str]:
|
||||
raise pacman_plugin.errors.CommandFailedError(["get_native_explicit"], "boom")
|
||||
|
||||
monkeypatch.setattr(pacman_plugin, "PacmanInterface", FailingPM)
|
||||
|
||||
errors_logged: list[str] = []
|
||||
continuations: list[str] = []
|
||||
traceback_called = []
|
||||
|
||||
def fake_print_error(msg: str) -> None:
|
||||
errors_logged.append(msg)
|
||||
|
||||
def fake_print_traceback() -> None:
|
||||
traceback_called.append(True)
|
||||
|
||||
def fake_print_continuation(msg: str) -> None:
|
||||
continuations.append(msg)
|
||||
|
||||
monkeypatch.setattr(pacman_plugin.output, "print_error", fake_print_error)
|
||||
monkeypatch.setattr(pacman_plugin.output, "print_traceback", fake_print_traceback)
|
||||
monkeypatch.setattr(pacman_plugin.output, "print_continuation", fake_print_continuation)
|
||||
|
||||
ok = pacman.apply(store, dry_run=False)
|
||||
|
||||
assert ok is False
|
||||
assert any("Pacman command exited with an unexpected" in msg for msg in errors_logged)
|
||||
assert any("boom" in msg for msg in continuations)
|
||||
assert traceback_called # at least once
|
||||
|
||||
|
||||
def test_ignored_packages_are_not_removed_or_installed(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
pacman = pacman_plugin.Pacman()
|
||||
store = FakeStore()
|
||||
|
||||
# Desired state: "already" and "new" should be managed normally.
|
||||
# "ignored-installed" is currently installed but not desired -> would normally be removed.
|
||||
# "ignored-uninstalled" is desired but not installed -> would normally be installed.
|
||||
pacman.packages = {"already", "new", "ignored-uninstalled"}
|
||||
pacman.ignored_packages = {"ignored-installed", "ignored-uninstalled"}
|
||||
|
||||
class FakePM:
|
||||
def __init__(
|
||||
self, commands, print_highlights, keywords, database_signature_level, database_path
|
||||
) -> None: # noqa: D401
|
||||
self.commands = commands
|
||||
self.print_highlights = print_highlights
|
||||
self.keywords = keywords
|
||||
|
||||
self.remove_called_with: set[str] | None = None
|
||||
self.install_called_with: set[str] | None = None
|
||||
self.set_as_deps_called_with: set[str] | None = None
|
||||
self.upgrade_called = False
|
||||
|
||||
def get_native_explicit(self) -> set[str]:
|
||||
# currently installed explicit packages
|
||||
return {"ignored-installed", "already"}
|
||||
|
||||
def get_foreign_explicit(self) -> set[str]:
|
||||
return set()
|
||||
|
||||
def get_native_orphans(self) -> set[str]:
|
||||
return set()
|
||||
|
||||
def get_dependants(self, pkg: str) -> set[str]:
|
||||
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
|
||||
|
||||
def upgrade(self) -> None:
|
||||
self.upgrade_called = True
|
||||
|
||||
def install(self, pkgs: set[str]) -> None:
|
||||
self.install_called_with = pkgs
|
||||
|
||||
fake_pm = FakePM(None, None, None, None, None)
|
||||
|
||||
def fake_pm_ctor(
|
||||
commands, print_highlights, keywords, database_signature_level, database_path
|
||||
) -> FakePM:
|
||||
fake_pm.commands = commands
|
||||
fake_pm.print_highlights = print_highlights
|
||||
fake_pm.keywords = keywords
|
||||
return fake_pm
|
||||
|
||||
monkeypatch.setattr(pacman_plugin, "PacmanInterface", fake_pm_ctor)
|
||||
|
||||
printed_lists: list[tuple[str, list[str]]] = []
|
||||
|
||||
def fake_print_list(title: str, items: list[str]) -> None:
|
||||
printed_lists.append((title, items))
|
||||
|
||||
# don't care about summaries here
|
||||
monkeypatch.setattr(pacman_plugin.output, "print_list", fake_print_list)
|
||||
monkeypatch.setattr(pacman_plugin.output, "print_summary", lambda *_args, **_kw: None)
|
||||
|
||||
ok = pacman.apply(store, dry_run=False)
|
||||
|
||||
assert ok is True
|
||||
|
||||
# Ignored packages must never be passed to remove() or install()
|
||||
assert (
|
||||
fake_pm.remove_called_with is None or "ignored-installed" not in fake_pm.remove_called_with
|
||||
)
|
||||
assert fake_pm.install_called_with is not None
|
||||
assert "ignored-uninstalled" not in fake_pm.install_called_with
|
||||
|
||||
# Also ensure the printed install list doesn't contain ignored packages
|
||||
install_items = next(
|
||||
items for title, items in printed_lists if "Installing pacman packages:" in title
|
||||
)
|
||||
assert "ignored-uninstalled" not in install_items
|
||||
# "new" is the only package that should be installed in this scenario
|
||||
assert install_items == ["new"]
|
||||
Reference in New Issue
Block a user