327 lines
11 KiB
Python
327 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Integration tests for recursive capability resolution and installer shadowing.
|
|
|
|
These tests verify that, given different repository layouts (Makefile, pyproject,
|
|
flake.nix, PKGBUILD), only the expected installers are executed based on the
|
|
capabilities provided by higher layers.
|
|
|
|
Layer order (strongest → weakest):
|
|
|
|
OS-PACKAGES > NIX > PYTHON > MAKEFILE
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from typing import List, Sequence, Tuple
|
|
from unittest.mock import patch
|
|
|
|
import pkgmgr.actions.install as install_mod
|
|
from pkgmgr.actions.install import install_repos
|
|
from pkgmgr.actions.install.installers.makefile import MakefileInstaller
|
|
from pkgmgr.actions.install.installers.nix_flake import NixFlakeInstaller
|
|
from pkgmgr.actions.install.installers.os_packages.arch_pkgbuild import (
|
|
ArchPkgbuildInstaller,
|
|
)
|
|
from pkgmgr.actions.install.installers.python import PythonInstaller
|
|
|
|
|
|
InstallerSpec = Tuple[str, object]
|
|
|
|
|
|
class TestRecursiveCapabilitiesIntegration(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.tmp_root = tempfile.mkdtemp(prefix="pkgmgr-recursive-caps-")
|
|
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)
|
|
|
|
# ------------------------------------------------------------------ helpers
|
|
|
|
def _new_repo(self) -> str:
|
|
"""
|
|
Create a fresh temporary repo directory under self.tmp_root.
|
|
"""
|
|
return tempfile.mkdtemp(prefix="repo-", dir=self.tmp_root)
|
|
|
|
def _run_with_installers(
|
|
self,
|
|
repo_dir: str,
|
|
installers: Sequence[InstallerSpec],
|
|
selected_repos=None,
|
|
) -> List[str]:
|
|
"""
|
|
Run install_repos() with a custom INSTALLERS list and capture which
|
|
installer labels actually run.
|
|
|
|
We override each installer's supports() to always return True and
|
|
override run() to append its label to called_installers.
|
|
"""
|
|
if selected_repos is None:
|
|
repo = {"repository": "dummy"}
|
|
selected_repos = [repo]
|
|
all_repos = [repo]
|
|
else:
|
|
all_repos = selected_repos
|
|
|
|
called_installers: List[str] = []
|
|
|
|
patched_installers = []
|
|
for label, inst in installers:
|
|
def always_supports(self, ctx):
|
|
return True
|
|
|
|
def make_run(label_name: str):
|
|
def _run(self, ctx):
|
|
called_installers.append(label_name)
|
|
return _run
|
|
|
|
inst.supports = always_supports.__get__(inst, inst.__class__) # type: ignore[assignment]
|
|
inst.run = make_run(label).__get__(inst, inst.__class__) # type: ignore[assignment]
|
|
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, "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="ssh",
|
|
update_dependencies=False,
|
|
)
|
|
|
|
return called_installers
|
|
|
|
# ----------------------------------------------------------------- scenarios
|
|
|
|
def test_only_makefile_installer_runs(self) -> None:
|
|
"""
|
|
With only a Makefile present, only the MakefileInstaller should run.
|
|
"""
|
|
repo_dir = self._new_repo()
|
|
|
|
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
|
|
f.write("install:\n\t@echo 'make install'\n")
|
|
|
|
mk_inst = MakefileInstaller()
|
|
installers: Sequence[InstallerSpec] = [("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.",
|
|
)
|
|
|
|
def test_python_and_makefile_both_run_when_caps_disjoint(self) -> None:
|
|
"""
|
|
If Python and Makefile have disjoint capabilities, both installers run.
|
|
"""
|
|
repo_dir = self._new_repo()
|
|
|
|
# pyproject.toml without any explicit "make install" hint
|
|
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
|
|
f.write("name = 'dummy'\n")
|
|
|
|
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
|
|
f.write("install:\n\t@echo 'make install'\n")
|
|
|
|
py_inst = PythonInstaller()
|
|
mk_inst = MakefileInstaller()
|
|
installers: Sequence[InstallerSpec] = [
|
|
("python", py_inst),
|
|
("makefile", mk_inst),
|
|
]
|
|
|
|
called = self._run_with_installers(repo_dir, installers)
|
|
|
|
self.assertEqual(
|
|
called,
|
|
["python", "makefile"],
|
|
"PythonInstaller and MakefileInstaller should both run when their "
|
|
"capabilities are disjoint.",
|
|
)
|
|
|
|
def test_python_shadows_makefile_when_pyproject_mentions_make_install(self) -> None:
|
|
"""
|
|
If the Python layer advertises a 'make-install' capability (pyproject
|
|
explicitly hints at 'make install'), the Makefile layer must be skipped.
|
|
"""
|
|
repo_dir = self._new_repo()
|
|
|
|
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
|
|
f.write(
|
|
"name = 'dummy'\n"
|
|
"\n"
|
|
"# Hint for MakeInstallCapability on layer 'python'\n"
|
|
"make install\n"
|
|
)
|
|
|
|
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
|
|
f.write("install:\n\t@echo 'make install'\n")
|
|
|
|
py_inst = PythonInstaller()
|
|
mk_inst = MakefileInstaller()
|
|
installers: Sequence[InstallerSpec] = [
|
|
("python", py_inst),
|
|
("makefile", mk_inst),
|
|
]
|
|
|
|
called = self._run_with_installers(repo_dir, installers)
|
|
|
|
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.",
|
|
)
|
|
|
|
def test_nix_shadows_python_and_makefile(self) -> None:
|
|
"""
|
|
If a Nix flake advertises both python-runtime and make-install
|
|
capabilities, Python and Makefile installers must be skipped.
|
|
"""
|
|
repo_dir = self._new_repo()
|
|
|
|
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
|
|
f.write("name = 'dummy'\n")
|
|
|
|
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
|
|
f.write("install:\n\t@echo 'make install'\n")
|
|
|
|
with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f:
|
|
f.write(
|
|
' 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: Sequence[InstallerSpec] = [
|
|
("nix", nix_inst),
|
|
("python", py_inst),
|
|
("makefile", mk_inst),
|
|
]
|
|
|
|
called = self._run_with_installers(repo_dir, installers)
|
|
|
|
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.",
|
|
)
|
|
|
|
def test_os_packages_shadow_nix_python_and_makefile(self) -> None:
|
|
"""
|
|
If an OS package layer (PKGBUILD) advertises all capabilities,
|
|
all lower layers (Nix, Python, Makefile) must be skipped.
|
|
"""
|
|
repo_dir = self._new_repo()
|
|
|
|
with open(os.path.join(repo_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
|
|
f.write("name = 'dummy'\n")
|
|
|
|
with open(os.path.join(repo_dir, "Makefile"), "w", encoding="utf-8") as f:
|
|
f.write("install:\n\t@echo 'make install'\n")
|
|
|
|
with open(os.path.join(repo_dir, "flake.nix"), "w", encoding="utf-8") as f:
|
|
f.write(
|
|
' description = "integration test flake";\n'
|
|
"}\n"
|
|
"\n"
|
|
"buildPythonApplication something\n"
|
|
"make install\n"
|
|
)
|
|
|
|
with open(os.path.join(repo_dir, "PKGBUILD"), "w", encoding="utf-8") as f:
|
|
f.write(
|
|
"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: Sequence[InstallerSpec] = [
|
|
("os-packages", os_inst),
|
|
("nix", nix_inst),
|
|
("python", py_inst),
|
|
("makefile", mk_inst),
|
|
]
|
|
|
|
called = self._run_with_installers(repo_dir, installers)
|
|
|
|
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()
|