mirror of
https://github.com/kiviktnm/decman.git
synced 2026-09-19 12:08:28 +00:00
384 lines
12 KiB
Python
384 lines
12 KiB
Python
import typing
|
|
from unittest.mock import MagicMock
|
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
|
|
import pytest
|
|
from decman.plugins.aur.commands import AurCommands
|
|
from decman.plugins.aur.fpm import ForeignPackageManager
|
|
from decman.plugins.aur.package import PackageInfo, PackageSearch
|
|
|
|
|
|
class FakeAurPacmanInterface:
|
|
def __init__(self) -> None:
|
|
self.installed_native: set[str] = set()
|
|
self.installed_foreign: dict[str, str] = {}
|
|
self.explicitly_installed: set[str] = set()
|
|
self.not_installable: set[str] = set()
|
|
self.installed_files: list[str] = [] # To track what install_files() actually does
|
|
self.provided_pkgs: set[str] = set()
|
|
|
|
def get_native_explicit(self) -> set[str]:
|
|
return self.installed_native.intersection(self.explicitly_installed)
|
|
|
|
def get_native_orphans(self) -> set[str]:
|
|
return set()
|
|
|
|
def get_foreign_explicit(self) -> set[str]:
|
|
return set(self.installed_foreign.keys()).intersection(self.explicitly_installed)
|
|
|
|
def get_dependants(self, package: str) -> set[str]:
|
|
return set()
|
|
|
|
def set_as_dependencies(self, packages: set[str]):
|
|
self.explicitly_installed.difference_update(packages)
|
|
|
|
def install(self, packages: set[str]):
|
|
self.installed_native.update(packages)
|
|
self.explicitly_installed.update(packages)
|
|
|
|
def upgrade(self):
|
|
pass
|
|
|
|
def is_provided_by_installed(self, dependency: str) -> bool:
|
|
return dependency in self.provided_pkgs
|
|
|
|
def get_all_packages(self) -> set[str]:
|
|
return self.installed_native | self.installed_foreign.keys()
|
|
|
|
def filter_installed_packages(self, deps: set[str]) -> set[str]:
|
|
out = set()
|
|
for d in deps:
|
|
if not self.is_provided_by_installed(d) and d not in self.get_all_packages():
|
|
out.add(d)
|
|
return out
|
|
|
|
def remove(self, packages: set[str]):
|
|
self.installed_native.difference_update(packages)
|
|
for p in packages:
|
|
self.installed_foreign.pop(p, None)
|
|
self.explicitly_installed.difference_update(packages)
|
|
|
|
def get_foreign_orphans(self) -> set[str]:
|
|
return set()
|
|
|
|
def is_installable(self, pkg: str) -> bool:
|
|
return pkg not in self.not_installable
|
|
|
|
def get_versioned_foreign_packages(self) -> list[tuple[str, str]]:
|
|
return list(self.installed_foreign.items())
|
|
|
|
def install_dependencies(self, deps: set[str]):
|
|
self.installed_native.update(deps)
|
|
|
|
def install_files(self, files: list[str], as_explicit: set[str]):
|
|
self.installed_files.extend(files)
|
|
|
|
for file in files:
|
|
self.installed_foreign[file] = "file"
|
|
|
|
for pkg in as_explicit:
|
|
self.explicitly_installed.add(pkg)
|
|
|
|
|
|
class FakeStore:
|
|
def __init__(self) -> None:
|
|
self._store: dict[str, typing.Any] = {}
|
|
|
|
def __getitem__(self, key: str) -> typing.Any:
|
|
return self._store[key]
|
|
|
|
def __setitem__(self, key: str, value: typing.Any) -> None:
|
|
self._store[key] = value
|
|
|
|
def get(self, key: str, default: typing.Any = None) -> typing.Any:
|
|
return self._store.get(key, default)
|
|
|
|
def ensure(self, key: str, default: typing.Any = None):
|
|
if key not in self._store:
|
|
self._store[key] = default
|
|
|
|
def __enter__(self) -> "FakeStore":
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def save(self) -> None:
|
|
pass
|
|
|
|
def __repr__(self) -> str:
|
|
return repr(self._store)
|
|
|
|
|
|
class MockAurServer:
|
|
def __init__(self) -> None:
|
|
self.db: dict[str, dict] = {} # Maps pkgname -> raw JSON result dict
|
|
|
|
def seed(self, packages: list[PackageInfo]):
|
|
for pkg in packages:
|
|
# Reconstruct the raw JSON structure expected by PackageSearch
|
|
entry = {
|
|
"Name": pkg.pkgname,
|
|
"PackageBase": pkg.pkgbase or pkg.pkgname,
|
|
"Version": pkg.version,
|
|
"Description": "Mock Description",
|
|
"URL": "https://example.com",
|
|
"Depends": pkg.dependencies,
|
|
"MakeDepends": pkg.make_dependencies,
|
|
"CheckDepends": pkg.check_dependencies,
|
|
"Provides": pkg.provides,
|
|
# Add other fields if your class relies on them
|
|
}
|
|
self.db[pkg.pkgname] = entry
|
|
|
|
def handle_request(self, url, *args, **kwargs):
|
|
parsed = urlparse(url)
|
|
path = parsed.path
|
|
query = parse_qs(parsed.query)
|
|
|
|
results = []
|
|
|
|
# --- Handle: Multi-info query (.../info?arg[]=pkg1&arg[]=pkg2) ---
|
|
if "/rpc/v5/info" in path and "arg[]" in query:
|
|
requested_names = query["arg[]"]
|
|
for name in requested_names:
|
|
if name in self.db:
|
|
results.append(self.db[name])
|
|
|
|
# --- Handle: Single info query (.../rpc/v5/info/pkgname) ---
|
|
elif "/rpc/v5/info/" in path:
|
|
# Extract package name from end of path
|
|
pkg_name = path.split("/")[-1]
|
|
if pkg_name in self.db:
|
|
results.append(self.db[pkg_name])
|
|
|
|
# --- Handle: Search providers (.../rpc/v5/search/dep?by=provides) ---
|
|
elif "/rpc/v5/search/" in path and query.get("by") == ["provides"]:
|
|
search_term = path.split("/")[-1]
|
|
search_term = unquote(search_term)
|
|
|
|
# Linear search through DB for 'Provides'
|
|
for entry in self.db.values():
|
|
if search_term in entry.get("Provides", []):
|
|
results.append(entry)
|
|
# Also match if the package name itself matches the provider request
|
|
elif entry["Name"] == search_term:
|
|
results.append(entry)
|
|
|
|
# Construct the response object
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"version": 5,
|
|
"type": "multiinfo",
|
|
"resultcount": len(results),
|
|
"results": results,
|
|
}
|
|
|
|
return mock_response
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_aur(mocker):
|
|
server = MockAurServer()
|
|
mocker.patch("requests.get", side_effect=server.handle_request)
|
|
return server
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_pacman(mocker):
|
|
pacman = FakeAurPacmanInterface()
|
|
return pacman
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_fpm(mocker, mock_aur, mock_pacman):
|
|
mock_builder_cls = mocker.patch("decman.plugins.aur.fpm.PackageBuilder")
|
|
mock_builder_instance = mock_builder_cls.return_value
|
|
mock_builder_instance.__enter__.return_value = mock_builder_instance
|
|
mock_builder_instance.__exit__.return_value = None
|
|
|
|
# NOTE: find_latest_cached_package must return a tuple, otherwise
|
|
# the 'assert built_pkg is not None' line in install() will fail.
|
|
def mock_find_cached(store, package):
|
|
# return just the package, so that mock pacman can get the package name from the 'file' name
|
|
return ("1.0.0", package)
|
|
|
|
mocker.patch("decman.plugins.aur.fpm.find_latest_cached_package", side_effect=mock_find_cached)
|
|
mocker.patch("decman.plugins.aur.fpm.add_package_to_cache", return_value=None)
|
|
|
|
# handle prompts automatically
|
|
mocker.patch("decman.core.output.prompt_confirm", return_value=True)
|
|
|
|
store = FakeStore()
|
|
search = PackageSearch()
|
|
commands = AurCommands()
|
|
mgr = ForeignPackageManager(
|
|
store=store, # type: ignore
|
|
pacman=mock_pacman,
|
|
search=search,
|
|
commands=commands,
|
|
pkg_cache_dir="/tmp/cache",
|
|
build_dir="/tmp/build",
|
|
makepkg_user="nobody",
|
|
)
|
|
|
|
return mgr
|
|
|
|
|
|
def test_remove_pacman_deps_provided_by_foreign_packages(
|
|
mock_fpm, mock_aur, mock_pacman: FakeAurPacmanInterface
|
|
):
|
|
mock_pacman.not_installable |= {"kwin-hifps", "qt6-base-hifps", "syncthingtray-qt6"}
|
|
pkgs = [
|
|
PackageInfo(
|
|
pkgbase="kwin-hifps",
|
|
pkgname="kwin-hifps",
|
|
version="1",
|
|
git_url="...",
|
|
dependencies=("qt6-base-hifps",),
|
|
),
|
|
PackageInfo(
|
|
pkgbase="qt6-base-hifps",
|
|
pkgname="qt6-base-hifps",
|
|
version="1",
|
|
git_url="...",
|
|
provides=("qt6-base",),
|
|
),
|
|
PackageInfo(
|
|
pkgbase="syncthingtray-qt6",
|
|
pkgname="syncthingtray-qt6",
|
|
version="1",
|
|
git_url="...",
|
|
dependencies=("qt6-base",),
|
|
),
|
|
]
|
|
mock_aur.seed(pkgs)
|
|
|
|
mock_fpm.install(["kwin-hifps", "syncthingtray-qt6"])
|
|
|
|
assert len(mock_pacman.installed_files) == 3
|
|
assert mock_pacman.explicitly_installed == {"kwin-hifps", "syncthingtray-qt6"}
|
|
assert "qt6-base" not in mock_pacman.installed_native
|
|
|
|
|
|
def test_remove_pacman_deps_provided_by_already_installed_foreign_packages(
|
|
mock_fpm, mock_aur, mock_pacman: FakeAurPacmanInterface
|
|
):
|
|
mock_pacman.not_installable |= {"kwin-hifps", "qt6-base-hifps", "syncthingtray-qt6"}
|
|
pkgs = [
|
|
PackageInfo(
|
|
pkgbase="kwin-hifps",
|
|
pkgname="kwin-hifps",
|
|
version="1",
|
|
git_url="...",
|
|
dependencies=("qt6-base-hifps",),
|
|
),
|
|
PackageInfo(
|
|
pkgbase="qt6-base-hifps",
|
|
pkgname="qt6-base-hifps",
|
|
version="1",
|
|
git_url="...",
|
|
provides=("qt6-base",),
|
|
),
|
|
PackageInfo(
|
|
pkgbase="syncthingtray-qt6",
|
|
pkgname="syncthingtray-qt6",
|
|
version="1",
|
|
git_url="...",
|
|
dependencies=("qt6-base",),
|
|
),
|
|
]
|
|
mock_pacman.installed_foreign = {
|
|
"kwin-hifps": "1",
|
|
"qt6-base-hifps": "1",
|
|
}
|
|
mock_pacman.explicitly_installed.add("kwin-hifps")
|
|
mock_pacman.provided_pkgs.add("qt6-base")
|
|
mock_aur.seed(pkgs)
|
|
|
|
mock_fpm.install(["syncthingtray-qt6"])
|
|
|
|
assert len(mock_pacman.installed_files) == 1
|
|
assert mock_pacman.explicitly_installed == {"kwin-hifps", "syncthingtray-qt6"}
|
|
assert "qt6-base" not in mock_pacman.installed_native
|
|
|
|
|
|
def test_install_simple_package(
|
|
mock_fpm, mock_pacman: FakeAurPacmanInterface, mock_aur: MockAurServer
|
|
):
|
|
mock_pacman.not_installable.add("foo")
|
|
pkg = PackageInfo(
|
|
pkgbase="foo",
|
|
pkgname="foo",
|
|
version="100.0.0",
|
|
git_url="...",
|
|
)
|
|
mock_aur.seed([pkg])
|
|
|
|
mock_fpm.install(["foo"])
|
|
|
|
assert len(mock_pacman.installed_files) == 1
|
|
assert "foo" in mock_pacman.installed_files[0]
|
|
assert "foo" in mock_pacman.explicitly_installed
|
|
assert "foo" in mock_pacman.installed_foreign
|
|
|
|
|
|
def test_upgrade_foreign_package(mock_fpm, mock_pacman, mock_aur):
|
|
mock_pacman.not_installable.add("my-app")
|
|
mock_pacman.installed_foreign = {"my-app": "1.0"}
|
|
mock_pacman.explicitly_installed = {"my-app"}
|
|
|
|
pkg = PackageInfo(
|
|
pkgbase="my-app",
|
|
pkgname="my-app",
|
|
version="2.0",
|
|
git_url="...",
|
|
)
|
|
mock_aur.seed([pkg])
|
|
|
|
mock_fpm.upgrade()
|
|
|
|
assert len(mock_pacman.installed_files) == 1
|
|
assert "my-app" in mock_pacman.installed_foreign
|
|
assert "my-app" in mock_pacman.installed_files[0]
|
|
|
|
|
|
def test_upgrade_skips_current_package(mock_fpm, mock_pacman, mock_aur):
|
|
mock_pacman.not_installable.add("stable-app")
|
|
mock_pacman.installed_foreign = {"stable-app": "5.0"}
|
|
mock_pacman.explicitly_installed = {"stable-app"}
|
|
|
|
pkg = PackageInfo(
|
|
pkgbase="stable-app",
|
|
pkgname="stable-app",
|
|
version="5.0",
|
|
git_url="...",
|
|
)
|
|
mock_aur.seed([pkg])
|
|
|
|
mock_fpm.upgrade()
|
|
|
|
assert len(mock_pacman.installed_files) == 0
|
|
|
|
|
|
def test_install_resolves_dependencies(mock_fpm, mock_pacman, mock_aur):
|
|
mock_pacman.not_installable |= {"lib-helper", "main-app"}
|
|
pkg_dep = PackageInfo(pkgbase="lib-helper", pkgname="lib-helper", version="1.5", git_url="...")
|
|
pkg_main = PackageInfo(
|
|
pkgbase="main-app",
|
|
pkgname="main-app",
|
|
version="2.0",
|
|
dependencies=("lib-helper",),
|
|
git_url="...",
|
|
)
|
|
|
|
mock_aur.seed([pkg_dep, pkg_main])
|
|
|
|
mock_fpm.install(["main-app"])
|
|
|
|
assert len(mock_pacman.installed_files) == 2
|
|
assert "main-app" in mock_pacman.explicitly_installed
|
|
assert "main-app" in mock_pacman.installed_files
|
|
assert "lib-helper" in mock_pacman.installed_files
|