""" Integration tests for installer selection across repository layouts. Layer order (strongest → weakest): OS-PACKAGES > NIX > PYTHON > MAKEFILE """ import os import shutil import tempfile import unittest from collections.abc import Sequence 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 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_only_the_first_supported_hook_runs(self) -> None: """A repository with two usable hooks is installed by the first one.""" 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"], "Only the first supported hook may install the repository.", ) 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()