Add cross-distribution OS package installers (Arch PKGBUILD, Debian control, RPM spec) and restructure tests.

Remove deprecated AUR and Ansible requirements installers.
Introduce Nix init + wrapper scripts and full packaging (Arch/DEB/RPM).
Associated conversation: https://chatgpt.com/share/693476a8-b9f0-800f-8e0c-ea5151295ce2
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-06 19:32:31 +01:00
parent d6a7ce0aa0
commit aaf20da0a0
23 changed files with 658 additions and 634 deletions

View File

@@ -1,14 +1,14 @@
# tests/unit/pkgmgr/installers/test_pkgbuild.py
# tests/unit/pkgmgr/installers/os_packages/test_arch_pkgbuild.py
import os
import unittest
from unittest.mock import patch
from pkgmgr.context import RepoContext
from pkgmgr.installers.pkgbuild import PkgbuildInstaller
from pkgmgr.installers.os_packages.arch_pkgbuild import ArchPkgbuildInstaller
class TestPkgbuildInstaller(unittest.TestCase):
class TestArchPkgbuildInstaller(unittest.TestCase):
def setUp(self):
self.repo = {"name": "test-repo"}
self.ctx = RepoContext(
@@ -24,7 +24,7 @@ class TestPkgbuildInstaller(unittest.TestCase):
clone_mode="ssh",
update_dependencies=False,
)
self.installer = PkgbuildInstaller()
self.installer = ArchPkgbuildInstaller()
@patch("os.path.exists", return_value=True)
@patch("shutil.which", return_value="/usr/bin/pacman")
@@ -38,7 +38,7 @@ class TestPkgbuildInstaller(unittest.TestCase):
def test_supports_false_when_pkgbuild_missing(self, mock_which, mock_exists):
self.assertFalse(self.installer.supports(self.ctx))
@patch("pkgmgr.installers.pkgbuild.run_command")
@patch("pkgmgr.installers.os_packages.arch_pkgbuild.run_command")
@patch("subprocess.check_output", return_value="python\ngit\n")
@patch("os.path.exists", return_value=True)
@patch("shutil.which", return_value="/usr/bin/pacman")
@@ -47,14 +47,14 @@ class TestPkgbuildInstaller(unittest.TestCase):
):
self.installer.run(self.ctx)
# Check subprocess.check_output arguments (clean shell)
# subprocess.check_output call
args, kwargs = mock_check_output.call_args
cmd_list = args[0]
self.assertEqual(cmd_list[0], "bash")
self.assertIn("--noprofile", cmd_list)
self.assertIn("--norc", cmd_list)
# Check that pacman is called with the extracted packages
# pacman install command
cmd = mock_run_command.call_args[0][0]
self.assertTrue(cmd.startswith("sudo pacman -S --noconfirm "))
self.assertIn("python", cmd)

View File

@@ -0,0 +1,66 @@
# tests/unit/pkgmgr/installers/os_packages/test_debian_control.py
import unittest
from unittest.mock import patch, mock_open
from pkgmgr.context import RepoContext
from pkgmgr.installers.os_packages.debian_control import DebianControlInstaller
class TestDebianControlInstaller(unittest.TestCase):
def setUp(self):
self.repo = {"name": "repo"}
self.ctx = RepoContext(
repo=self.repo,
identifier="id",
repo_dir="/tmp/repo",
repositories_base_dir="/tmp",
bin_dir="/bin",
all_repos=[self.repo],
no_verification=False,
preview=False,
quiet=False,
clone_mode="ssh",
update_dependencies=False,
)
self.installer = DebianControlInstaller()
@patch("os.path.exists", return_value=True)
@patch("shutil.which", return_value="/usr/bin/apt-get")
def test_supports_true(self, mock_which, mock_exists):
self.assertTrue(self.installer.supports(self.ctx))
@patch("os.path.exists", return_value=True)
@patch("shutil.which", return_value=None)
def test_supports_false_without_apt(self, mock_which, mock_exists):
self.assertFalse(self.installer.supports(self.ctx))
@patch("pkgmgr.installers.os_packages.debian_control.run_command")
@patch("builtins.open", new_callable=mock_open, read_data="""
Build-Depends: python3, git (>= 2.0)
Depends: curl | wget
""")
@patch("os.path.exists", return_value=True)
@patch("shutil.which", return_value="/usr/bin/apt-get")
def test_run_installs_parsed_packages(
self,
mock_which,
mock_exists,
mock_file,
mock_run_command
):
self.installer.run(self.ctx)
# First call: apt-get update
self.assertIn("apt-get update", mock_run_command.call_args_list[0][0][0])
# Second call: install packages
install_cmd = mock_run_command.call_args_list[1][0][0]
self.assertIn("apt-get install -y", install_cmd)
self.assertIn("python3", install_cmd)
self.assertIn("git", install_cmd)
self.assertIn("curl", install_cmd)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,60 @@
# tests/unit/pkgmgr/installers/os_packages/test_rpm_spec.py
import unittest
from unittest.mock import patch, mock_open
from pkgmgr.context import RepoContext
from pkgmgr.installers.os_packages.rpm_spec import RpmSpecInstaller
class TestRpmSpecInstaller(unittest.TestCase):
def setUp(self):
self.repo = {"name": "repo"}
self.ctx = RepoContext(
repo=self.repo,
identifier="id",
repo_dir="/tmp/repo",
repositories_base_dir="/tmp",
bin_dir="/bin",
all_repos=[self.repo],
no_verification=False,
preview=False,
quiet=False,
clone_mode="ssh",
update_dependencies=False,
)
self.installer = RpmSpecInstaller()
@patch("glob.glob", return_value=["/tmp/repo/test.spec"])
@patch("shutil.which", return_value="/usr/bin/dnf")
def test_supports_true(self, mock_which, mock_glob):
self.assertTrue(self.installer.supports(self.ctx))
@patch("glob.glob", return_value=[])
@patch("shutil.which", return_value="/usr/bin/dnf")
def test_supports_false_missing_spec(self, mock_which, mock_glob):
self.assertFalse(self.installer.supports(self.ctx))
@patch("pkgmgr.installers.os_packages.rpm_spec.run_command")
@patch("builtins.open", new_callable=mock_open, read_data="""
BuildRequires: python3-devel, git >= 2.0
Requires: curl
""")
@patch("glob.glob", return_value=["/tmp/repo/test.spec"])
@patch("shutil.which", return_value="/usr/bin/dnf")
@patch("os.path.exists", return_value=True)
def test_run_installs_parsed_dependencies(
self, mock_exists, mock_which, mock_glob, mock_file, mock_run_command
):
self.installer.run(self.ctx)
install_cmd = mock_run_command.call_args_list[0][0][0]
self.assertIn("dnf install -y", install_cmd)
self.assertIn("python3-devel", install_cmd)
self.assertIn("git", install_cmd)
self.assertIn("curl", install_cmd)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,168 +0,0 @@
# tests/unit/pkgmgr/installers/test_ansible_requirements.py
import os
import unittest
from unittest.mock import patch, mock_open
from pkgmgr.context import RepoContext
from pkgmgr.installers.ansible_requirements import AnsibleRequirementsInstaller
class TestAnsibleRequirementsInstaller(unittest.TestCase):
def setUp(self):
self.repo = {"name": "test-repo"}
self.ctx = RepoContext(
repo=self.repo,
identifier="test-id",
repo_dir="/tmp/repo",
repositories_base_dir="/tmp",
bin_dir="/bin",
all_repos=[self.repo],
no_verification=False,
preview=False,
quiet=False,
clone_mode="ssh",
update_dependencies=False,
)
self.installer = AnsibleRequirementsInstaller()
@patch("os.path.exists", return_value=True)
def test_supports_true_when_requirements_exist(self, mock_exists):
self.assertTrue(self.installer.supports(self.ctx))
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "requirements.yml"))
@patch("os.path.exists", return_value=False)
def test_supports_false_when_requirements_missing(self, mock_exists):
self.assertFalse(self.installer.supports(self.ctx))
@patch("pkgmgr.installers.ansible_requirements.run_command")
@patch("tempfile.NamedTemporaryFile")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
collections:
- name: community.docker
roles:
- src: geerlingguy.docker
""",
)
@patch("os.path.exists", return_value=True)
def test_run_installs_collections_and_roles(
self, mock_exists, mock_file, mock_tmp, mock_run_command
):
# Fake temp file name
mock_tmp().__enter__().name = "/tmp/req.yml"
self.installer.run(self.ctx)
cmds = [call[0][0] for call in mock_run_command.call_args_list]
self.assertIn(
"ansible-galaxy collection install -r /tmp/req.yml",
cmds,
)
self.assertIn(
"ansible-galaxy role install -r /tmp/req.yml",
cmds,
)
# --- Neue Tests für den Validator -------------------------------------
@patch("pkgmgr.installers.ansible_requirements.run_command")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
- not:
- a: mapping
""",
)
@patch("os.path.exists", return_value=True)
def test_run_raises_when_top_level_is_not_mapping(
self, mock_exists, mock_file, mock_run_command
):
# YAML ist eine Liste -> Validator soll fehlschlagen
with self.assertRaises(SystemExit):
self.installer.run(self.ctx)
mock_run_command.assert_not_called()
@patch("pkgmgr.installers.ansible_requirements.run_command")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
collections: community.docker
roles:
- src: geerlingguy.docker
""",
)
@patch("os.path.exists", return_value=True)
def test_run_raises_when_collections_is_not_list(
self, mock_exists, mock_file, mock_run_command
):
# collections ist ein String statt Liste -> invalid
with self.assertRaises(SystemExit):
self.installer.run(self.ctx)
mock_run_command.assert_not_called()
@patch("pkgmgr.installers.ansible_requirements.run_command")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
collections:
- name: community.docker
roles:
- version: "latest"
""",
)
@patch("os.path.exists", return_value=True)
def test_run_raises_when_role_mapping_has_no_name(
self, mock_exists, mock_file, mock_run_command
):
# roles-Eintrag ist Mapping ohne 'name' -> invalid
with self.assertRaises(SystemExit):
self.installer.run(self.ctx)
mock_run_command.assert_not_called()
@patch("pkgmgr.installers.ansible_requirements.run_command")
@patch("tempfile.NamedTemporaryFile")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
collections:
- name: community.docker
extra_key: should_be_ignored_but_warned
""",
)
@patch("os.path.exists", return_value=True)
def test_run_accepts_unknown_top_level_keys(
self, mock_exists, mock_file, mock_tmp, mock_run_command
):
"""
Unknown top-level keys (z.B. 'extra_key') sollen nur eine Warnung
auslösen, aber keine Validation-Exception.
"""
mock_tmp().__enter__().name = "/tmp/req.yml"
# Erwartung: kein SystemExit, run_command wird für collections aufgerufen
self.installer.run(self.ctx)
cmds = [call[0][0] for call in mock_run_command.call_args_list]
self.assertIn(
"ansible-galaxy collection install -r /tmp/req.yml",
cmds,
)
# Keine roles definiert -> kein role-install
self.assertNotIn(
"ansible-galaxy role install -r /tmp/req.yml",
cmds,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,97 +0,0 @@
# tests/unit/pkgmgr/installers/test_aur.py
import os
import unittest
from unittest.mock import patch, mock_open
from pkgmgr.context import RepoContext
from pkgmgr.installers.aur import AurInstaller, AUR_CONFIG_FILENAME
class TestAurInstaller(unittest.TestCase):
def setUp(self):
self.repo = {"name": "test-repo"}
self.ctx = RepoContext(
repo=self.repo,
identifier="test-id",
repo_dir="/tmp/repo",
repositories_base_dir="/tmp",
bin_dir="/bin",
all_repos=[self.repo],
no_verification=False,
preview=False,
quiet=False,
clone_mode="ssh",
update_dependencies=False,
)
self.installer = AurInstaller()
@patch("shutil.which", return_value="/usr/bin/pacman")
@patch("os.path.exists", return_value=True)
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
helper: yay
packages:
- aurutils
- name: some-aur-only-tool
reason: "Test tool"
""",
)
def test_supports_true_when_arch_and_aur_config_present(
self, mock_file, mock_exists, mock_which
):
self.assertTrue(self.installer.supports(self.ctx))
mock_which.assert_called_with("pacman")
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, AUR_CONFIG_FILENAME))
@patch("shutil.which", return_value=None)
def test_supports_false_when_not_arch(self, mock_which):
self.assertFalse(self.installer.supports(self.ctx))
@patch("shutil.which", return_value="/usr/bin/pacman")
@patch("os.path.exists", return_value=False)
def test_supports_false_when_no_config(self, mock_exists, mock_which):
self.assertFalse(self.installer.supports(self.ctx))
@patch("shutil.which", side_effect=lambda name: "/usr/bin/pacman" if name == "pacman" else "/usr/bin/yay")
@patch("pkgmgr.installers.aur.run_command")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="""
helper: yay
packages:
- aurutils
- some-aur-only-tool
""",
)
@patch("os.path.exists", return_value=True)
def test_run_installs_packages_with_helper(
self, mock_exists, mock_file, mock_run_command, mock_which
):
self.installer.run(self.ctx)
cmd = mock_run_command.call_args[0][0]
self.assertTrue(cmd.startswith("yay -S --noconfirm "))
self.assertIn("aurutils", cmd)
self.assertIn("some-aur-only-tool", cmd)
@patch("shutil.which", return_value="/usr/bin/pacman")
@patch(
"builtins.open",
new_callable=mock_open,
read_data="packages: []",
)
@patch("os.path.exists", return_value=True)
def test_run_skips_when_no_packages(
self, mock_exists, mock_file, mock_which
):
with patch("pkgmgr.installers.aur.run_command") as mock_run_command:
self.installer.run(self.ctx)
mock_run_command.assert_not_called()
if __name__ == "__main__":
unittest.main()