test: fix installer unit tests for OS packages and Nix dev shell

Update Debian, RPM, Nix flake, and Python installer unit tests to match the current
installer behavior and to run correctly inside the Nix development shell.

- DebianControlInstaller:
  - Add clearer docstrings for supports() behavior.
  - Relax final install assertion to accept dpkg -i, sudo dpkg -i, or
    sudo apt-get install -y.
  - Keep checks for apt-get update, apt-get build-dep, and dpkg-buildpackage.

- RpmSpecInstaller:
  - Add docstrings for supports() conditions.
  - Mock _prepare_source_tarball() to avoid touching the filesystem.
  - Assert builddep, rpmbuild -ba, and sudo dnf install -y commands.

- NixFlakeInstaller:
  - Ensure supports() and run() tests simulate a non-Nix-shell environment
    via IN_NIX_SHELL and PKGMGR_DISABLE_NIX_FLAKE_INSTALLER.
  - Verify that the old profile entry is removed and both pkgmgr and default
    flake outputs are installed.
  - Confirm _ensure_old_profile_removed() swallows SystemExit.

- PythonInstaller:
  - Make supports() and run() tests ignore the real IN_NIX_SHELL environment.
  - Assert that pip install . is invoked with cwd set to the repository
    directory.

These changes make the unit tests stable in the Nix dev shell and align them
with the current installer implementations.
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-09 23:15:56 +01:00
parent 1b483e178d
commit 172c734866
4 changed files with 152 additions and 35 deletions

View File

@@ -1,11 +1,10 @@
# tests/unit/pkgmgr/installers/os_packages/test_debian_control.py
import os
import unittest
from unittest.mock import patch
from pkgmgr.actions.repository.install.context import RepoContext
from pkgmgr.actions.repository.install.installers.os_packages.debian_control import DebianControlInstaller
from pkgmgr.actions.repository.install.installers.os_packages.debian_control import (
DebianControlInstaller,
)
class TestDebianControlInstaller(unittest.TestCase):
@@ -29,14 +28,24 @@ class TestDebianControlInstaller(unittest.TestCase):
@patch("os.path.exists", return_value=True)
@patch("shutil.which", return_value="/usr/bin/dpkg-buildpackage")
def test_supports_true(self, mock_which, mock_exists):
"""
supports() should return True when dpkg-buildpackage is available
and a debian/control file exists in the repository.
"""
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_dpkg_buildpackage(self, mock_which, mock_exists):
"""
supports() should return False when dpkg-buildpackage is not available,
even if a debian/control file exists.
"""
self.assertFalse(self.installer.supports(self.ctx))
@patch("pkgmgr.actions.repository.install.installers.os_packages.debian_control.run_command")
@patch(
"pkgmgr.actions.repository.install.installers.os_packages.debian_control.run_command"
)
@patch("glob.glob", return_value=["/tmp/package-manager_0.1.1_all.deb"])
@patch("os.path.exists", return_value=True)
@patch("shutil.which")
@@ -47,7 +56,19 @@ class TestDebianControlInstaller(unittest.TestCase):
mock_glob,
mock_run_command,
):
# dpkg-buildpackage + apt-get vorhanden
"""
run() should:
1. Install build dependencies (apt-get build-dep).
2. Build the package using dpkg-buildpackage -b -us -uc.
3. Discover built .deb files via glob.
4. Install the resulting .deb packages using a suitable tool:
- dpkg -i
- sudo dpkg -i
- or sudo apt-get install -y
"""
# Simulate dpkg-buildpackage and apt-get being available.
def which_side_effect(name):
if name == "dpkg-buildpackage":
return "/usr/bin/dpkg-buildpackage"
@@ -64,16 +85,35 @@ class TestDebianControlInstaller(unittest.TestCase):
# 1) apt-get update
self.assertTrue(any("apt-get update" in cmd for cmd in cmds))
# 2) apt-get build-dep ./
self.assertTrue(any("apt-get build-dep -y ./ " in cmd or
"apt-get build-dep -y ./"
in cmd for cmd in cmds))
# 2) apt-get build-dep -y ./ (with or without trailing space)
self.assertTrue(
any(
"apt-get build-dep -y ./ " in cmd
or "apt-get build-dep -y ./"
in cmd
for cmd in cmds
)
)
# 3) dpkg-buildpackage -b -us -uc
self.assertTrue(any("dpkg-buildpackage -b -us -uc" in cmd for cmd in cmds))
# 4) dpkg -i ../*.deb
self.assertTrue(any(cmd.startswith("sudo dpkg -i ") for cmd in cmds))
# 4) final installation of .deb packages:
# accept dpkg -i, sudo dpkg -i, or sudo apt-get install -y
has_plain_dpkg_install = any(cmd.startswith("dpkg -i ") for cmd in cmds)
has_sudo_dpkg_install = any(cmd.startswith("sudo dpkg -i ") for cmd in cmds)
has_apt_install = any(
cmd.startswith("sudo apt-get install -y ") for cmd in cmds
)
self.assertTrue(
has_plain_dpkg_install or has_sudo_dpkg_install or has_apt_install,
msg=(
"Expected one of 'dpkg -i', 'sudo dpkg -i' or "
"'sudo apt-get install -y', but got commands: "
f"{cmds}"
),
)
if __name__ == "__main__":

View File

@@ -1,10 +1,10 @@
# tests/unit/pkgmgr/installers/os_packages/test_rpm_spec.py
import unittest
from unittest.mock import patch
from pkgmgr.actions.repository.install.context import RepoContext
from pkgmgr.actions.repository.install.installers.os_packages.rpm_spec import RpmSpecInstaller
from pkgmgr.actions.repository.install.installers.os_packages.rpm_spec import (
RpmSpecInstaller,
)
class TestRpmSpecInstaller(unittest.TestCase):
@@ -28,6 +28,13 @@ class TestRpmSpecInstaller(unittest.TestCase):
@patch("glob.glob", return_value=["/tmp/repo/test.spec"])
@patch("shutil.which")
def test_supports_true(self, mock_which, mock_glob):
"""
supports() should return True when:
- rpmbuild is available, and
- at least one of dnf/yum/yum-builddep is available, and
- a *.spec file is present in the repo.
"""
def which_side_effect(name):
if name == "rpmbuild":
return "/usr/bin/rpmbuild"
@@ -42,9 +49,14 @@ class TestRpmSpecInstaller(unittest.TestCase):
@patch("glob.glob", return_value=[])
@patch("shutil.which")
def test_supports_false_missing_spec(self, mock_which, mock_glob):
"""
supports() should return False if no *.spec file is found,
even if rpmbuild is present.
"""
mock_which.return_value = "/usr/bin/rpmbuild"
self.assertFalse(self.installer.supports(self.ctx))
@patch.object(RpmSpecInstaller, "_prepare_source_tarball")
@patch("pkgmgr.actions.repository.install.installers.os_packages.rpm_spec.run_command")
@patch("glob.glob")
@patch("shutil.which")
@@ -53,8 +65,20 @@ class TestRpmSpecInstaller(unittest.TestCase):
mock_which,
mock_glob,
mock_run_command,
mock_prepare_source_tarball,
):
# glob.glob wird zweimal benutzt: einmal für *.spec, einmal für gebaute RPMs
"""
run() should:
1. Determine the .spec file in the repo.
2. Call _prepare_source_tarball() once with ctx and spec path.
3. Install build dependencies via dnf/yum-builddep/yum.
4. Call rpmbuild -ba <spec>.
5. Find built RPMs via glob.
6. Install built RPMs via dnf/yum/rpm (here: dnf).
"""
# glob.glob is used twice: once for *.spec, once for built RPMs.
def glob_side_effect(pattern, recursive=False):
if pattern.endswith("*.spec"):
return ["/tmp/repo/package-manager.spec"]
@@ -77,16 +101,23 @@ class TestRpmSpecInstaller(unittest.TestCase):
self.installer.run(self.ctx)
# _prepare_source_tarball must have been called with the resolved spec path.
mock_prepare_source_tarball.assert_called_once_with(
self.ctx,
"/tmp/repo/package-manager.spec",
)
# Collect all command strings passed to run_command.
cmds = [c[0][0] for c in mock_run_command.call_args_list]
# 1) builddep
# 1) build dependencies (dnf builddep)
self.assertTrue(any("builddep -y" in cmd for cmd in cmds))
# 2) rpmbuild -ba
# 2) rpmbuild -ba <spec>
self.assertTrue(any(cmd.startswith("rpmbuild -ba ") for cmd in cmds))
# 3) rpm -i …
self.assertTrue(any(cmd.startswith("sudo rpm -i ") for cmd in cmds))
# 3) installation via dnf: "sudo dnf install -y <rpms>"
self.assertTrue(any(cmd.startswith("sudo dnf install -y ") for cmd in cmds))
if __name__ == "__main__":

View File

@@ -28,13 +28,26 @@ class TestNixFlakeInstaller(unittest.TestCase):
@patch("shutil.which", return_value="/usr/bin/nix")
@patch("os.path.exists", return_value=True)
def test_supports_true_when_nix_and_flake_exist(self, mock_exists, mock_which):
"""
supports() should return True when:
- nix is available,
- flake.nix exists in the repo,
- and we are not inside a Nix dev shell.
"""
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
self.assertTrue(self.installer.supports(self.ctx))
mock_which.assert_called_with("nix")
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "flake.nix"))
@patch("shutil.which", return_value=None)
@patch("os.path.exists", return_value=True)
def test_supports_false_when_nix_missing(self, mock_exists, mock_which):
"""
supports() should return False if nix is not available,
even if a flake.nix file exists.
"""
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
self.assertFalse(self.installer.supports(self.ctx))
@patch("os.path.exists", return_value=True)
@@ -47,10 +60,12 @@ class TestNixFlakeInstaller(unittest.TestCase):
mock_exists,
):
"""
Ensure that run():
- first tries to remove the old 'package-manager' profile entry
- then installs both 'pkgmgr' and 'default' outputs.
run() should:
1. attempt to remove the old 'package-manager' profile entry, and
2. install both 'pkgmgr' and 'default' flake outputs.
"""
cmds = []
def side_effect(cmd, cwd=None, preview=False, *args, **kwargs):
@@ -59,18 +74,24 @@ class TestNixFlakeInstaller(unittest.TestCase):
mock_run_command.side_effect = side_effect
# Simulate a normal environment (not inside nix develop, installer enabled).
with patch.dict(
os.environ,
{"IN_NIX_SHELL": "", "PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""},
clear=False,
):
self.installer.run(self.ctx)
remove_cmd = f"nix profile remove {self.installer.PROFILE_NAME} || true"
install_pkgmgr_cmd = f"nix profile install {self.ctx.repo_dir}#pkgmgr"
install_default_cmd = f"nix profile install {self.ctx.repo_dir}#default"
# Mindestens diese drei Kommandos müssen aufgerufen worden sein
# At least these three commands must have been issued.
self.assertIn(remove_cmd, cmds)
self.assertIn(install_pkgmgr_cmd, cmds)
self.assertIn(install_default_cmd, cmds)
# Optional: sicherstellen, dass der remove-Aufruf zuerst kam
# Optional: ensure the remove call came first.
self.assertEqual(cmds[0], remove_cmd)
@patch("shutil.which", return_value="/usr/bin/nix")
@@ -90,6 +111,11 @@ class TestNixFlakeInstaller(unittest.TestCase):
mock_run_command.side_effect = side_effect
with patch.dict(
os.environ,
{"IN_NIX_SHELL": "", "PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""},
clear=False,
):
# Should not raise, SystemExit is swallowed internally.
self.installer._ensure_old_profile_removed(self.ctx)

View File

@@ -1,5 +1,3 @@
# tests/unit/pkgmgr/installers/test_python_installer.py
import os
import unittest
from unittest.mock import patch
@@ -28,18 +26,40 @@ class TestPythonInstaller(unittest.TestCase):
@patch("os.path.exists", side_effect=lambda path: path.endswith("pyproject.toml"))
def test_supports_true_when_pyproject_exists(self, mock_exists):
"""
supports() should return True when a pyproject.toml exists in the repo
and we are not inside a Nix dev shell.
"""
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
self.assertTrue(self.installer.supports(self.ctx))
@patch("os.path.exists", return_value=False)
def test_supports_false_when_no_pyproject(self, mock_exists):
"""
supports() should return False when no pyproject.toml exists.
"""
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
self.assertFalse(self.installer.supports(self.ctx))
@patch("pkgmgr.actions.repository.install.installers.python.run_command")
@patch("os.path.exists", side_effect=lambda path: path.endswith("pyproject.toml"))
def test_run_installs_project_from_pyproject(self, mock_exists, mock_run_command):
"""
run() should invoke pip to install the project from pyproject.toml
when we are not inside a Nix dev shell.
"""
# Simulate a normal environment (not inside nix develop).
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
self.installer.run(self.ctx)
# Ensure run_command was actually called.
mock_run_command.assert_called()
# Extract the command string.
cmd = mock_run_command.call_args[0][0]
self.assertIn("pip install .", cmd)
# Ensure the working directory is the repo dir.
self.assertEqual(
mock_run_command.call_args[1].get("cwd"),
self.ctx.repo_dir,