test(unit): update NixFlakeInstaller tests for new run_command-based logic

- Adapt DummyCtx to include quiet and force_update flags
- Replace os.system mocking with run_command/subprocess mocks
- Align assertions with new Nix install/upgrade output
- Keep coverage for mandatory vs optional output handling

https://chatgpt.com/share/693db645-c420-800f-b921-9d5c0356d0ac
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-13 19:53:34 +01:00
parent f2970adbb2
commit 1e5d6d3eee

View File

@@ -5,15 +5,18 @@
Unit tests for NixFlakeInstaller using unittest (no pytest). Unit tests for NixFlakeInstaller using unittest (no pytest).
Covers: Covers:
- Successful installation (exit_code == 0) - Successful installation (returncode == 0)
- Mandatory failure → SystemExit with correct code - Mandatory failure → SystemExit with correct code
- Optional failure (pkgmgr default) → no raise, but warning - Optional failure (pkgmgr default) → no raise, but warning
- supports() behavior incl. PKGMGR_DISABLE_NIX_FLAKE_INSTALLER - supports() behavior incl. PKGMGR_DISABLE_NIX_FLAKE_INSTALLER
""" """
from __future__ import annotations
import io import io
import os import os
import shutil import shutil
import subprocess
import tempfile import tempfile
import unittest import unittest
from contextlib import redirect_stdout from contextlib import redirect_stdout
@@ -25,10 +28,19 @@ from pkgmgr.actions.install.installers.nix_flake import NixFlakeInstaller
class DummyCtx: class DummyCtx:
"""Minimal context object to satisfy NixFlakeInstaller.run() / supports().""" """Minimal context object to satisfy NixFlakeInstaller.run() / supports()."""
def __init__(self, identifier: str, repo_dir: str, preview: bool = False): def __init__(
self,
identifier: str,
repo_dir: str,
preview: bool = False,
quiet: bool = False,
force_update: bool = False,
):
self.identifier = identifier self.identifier = identifier
self.repo_dir = repo_dir self.repo_dir = repo_dir
self.preview = preview self.preview = preview
self.quiet = quiet
self.force_update = force_update
class TestNixFlakeInstaller(unittest.TestCase): class TestNixFlakeInstaller(unittest.TestCase):
@@ -44,161 +56,162 @@ class TestNixFlakeInstaller(unittest.TestCase):
os.environ.pop("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER", None) os.environ.pop("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER", None)
def tearDown(self) -> None: def tearDown(self) -> None:
# Cleanup temporary directory
if os.path.isdir(self._tmpdir): if os.path.isdir(self._tmpdir):
shutil.rmtree(self._tmpdir, ignore_errors=True) shutil.rmtree(self._tmpdir, ignore_errors=True)
def _enable_nix_in_module(self, which_patch): @staticmethod
def _cp(code: int) -> subprocess.CompletedProcess:
# stdout/stderr are irrelevant here, but keep shape realistic
return subprocess.CompletedProcess(args=["nix"], returncode=code, stdout="", stderr="")
@staticmethod
def _enable_nix_in_module(which_patch) -> None:
"""Ensure shutil.which('nix') in nix_flake module returns a path.""" """Ensure shutil.which('nix') in nix_flake module returns a path."""
which_patch.return_value = "/usr/bin/nix" which_patch.return_value = "/usr/bin/nix"
def test_nix_flake_run_success(self): def test_nix_flake_run_success(self) -> None:
""" """
When os.system returns a successful exit code, the installer When run_command returns success (returncode 0), installer
should report success and not raise. should report success and not raise.
""" """
ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir) ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir)
installer = NixFlakeInstaller() installer = NixFlakeInstaller()
buf = io.StringIO() buf = io.StringIO()
with patch( with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock, patch(
"pkgmgr.actions.install.installers.nix_flake.shutil.which" "pkgmgr.actions.install.installers.nix_flake.subprocess.run"
) as which_mock, patch( ) as subproc_mock, patch(
"pkgmgr.actions.install.installers.nix_flake.os.system" "pkgmgr.actions.install.installers.nix_flake.run_command"
) as system_mock, redirect_stdout(buf): ) as run_cmd_mock, redirect_stdout(buf):
self._enable_nix_in_module(which_mock) self._enable_nix_in_module(which_mock)
# Simulate os.system returning success (exit code 0) # For profile list JSON (used only on failure paths, but keep deterministic)
system_mock.return_value = 0 subproc_mock.return_value = subprocess.CompletedProcess(
args=["nix", "profile", "list", "--json"],
returncode=0,
stdout='{"elements": []}',
stderr="",
)
# Install succeeds
run_cmd_mock.return_value = self._cp(0)
# Sanity: supports() must be True
self.assertTrue(installer.supports(ctx)) self.assertTrue(installer.supports(ctx))
installer.run(ctx) installer.run(ctx)
out = buf.getvalue() out = buf.getvalue()
self.assertIn("[INFO] Running: nix profile install", out) self.assertIn("[nix] install: nix profile install", out)
self.assertIn("Nix flake output 'default' successfully installed.", out) self.assertIn("[nix] output 'default' successfully installed.", out)
# Ensure the nix command was actually invoked run_cmd_mock.assert_called_with(
system_mock.assert_called_with( f"nix profile install {self.repo_dir}#default",
f"nix profile install {self.repo_dir}#default" cwd=self.repo_dir,
preview=False,
allow_failure=True,
) )
def test_nix_flake_run_mandatory_failure_raises(self): def test_nix_flake_run_mandatory_failure_raises(self) -> None:
""" """
For a generic repository (identifier not pkgmgr/package-manager), For a generic repository, 'default' is mandatory.
`default` is mandatory and a non-zero exit code should raise SystemExit A non-zero return code must raise SystemExit with that code.
with the real exit code (e.g. 1, not 256).
""" """
ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir) ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir)
installer = NixFlakeInstaller() installer = NixFlakeInstaller()
buf = io.StringIO() buf = io.StringIO()
with patch( with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock, patch(
"pkgmgr.actions.install.installers.nix_flake.shutil.which" "pkgmgr.actions.install.installers.nix_flake.subprocess.run"
) as which_mock, patch( ) as subproc_mock, patch(
"pkgmgr.actions.install.installers.nix_flake.os.system" "pkgmgr.actions.install.installers.nix_flake.run_command"
) as system_mock, redirect_stdout(buf): ) as run_cmd_mock, redirect_stdout(buf):
self._enable_nix_in_module(which_mock) self._enable_nix_in_module(which_mock)
# Simulate os.system returning encoded status for exit code 1 # No indices available (empty list)
# os.system encodes exit code as (exit_code << 8) subproc_mock.return_value = subprocess.CompletedProcess(
system_mock.return_value = 1 << 8 args=["nix", "profile", "list", "--json"],
returncode=0,
stdout='{"elements": []}',
stderr="",
)
# First install fails, retry fails -> should raise SystemExit(1)
run_cmd_mock.side_effect = [self._cp(1), self._cp(1)]
self.assertTrue(installer.supports(ctx)) self.assertTrue(installer.supports(ctx))
with self.assertRaises(SystemExit) as cm: with self.assertRaises(SystemExit) as cm:
installer.run(ctx) installer.run(ctx)
# The real exit code should be 1 (not 256)
self.assertEqual(cm.exception.code, 1) self.assertEqual(cm.exception.code, 1)
out = buf.getvalue() out = buf.getvalue()
self.assertIn("[INFO] Running: nix profile install", out) self.assertIn("[nix] install: nix profile install", out)
self.assertIn("[Error] Failed to install Nix flake output 'default'", out) self.assertIn("[ERROR] Failed to install Nix flake output 'default' (exit 1)", out)
self.assertIn("[Error] Command exited with code 1", out)
def test_nix_flake_run_optional_failure_does_not_raise(self): def test_nix_flake_run_optional_failure_does_not_raise(self) -> None:
""" """
For the package-manager repository, the 'default' output is optional. For pkgmgr/package-manager repositories:
Failure to install it must not raise, but should log a warning instead. - 'pkgmgr' output is mandatory
- 'default' output is optional
Failure of optional output must not raise.
""" """
ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir) ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir)
installer = NixFlakeInstaller() installer = NixFlakeInstaller()
calls = []
def fake_system(cmd: str) -> int:
calls.append(cmd)
# First call (pkgmgr) → success
if len(calls) == 1:
return 0
# Second call (default) → failure (exit code 1 encoded)
return 1 << 8
buf = io.StringIO() buf = io.StringIO()
with patch( with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock, patch(
"pkgmgr.actions.install.installers.nix_flake.shutil.which" "pkgmgr.actions.install.installers.nix_flake.subprocess.run"
) as which_mock, patch( ) as subproc_mock, patch(
"pkgmgr.actions.install.installers.nix_flake.os.system", "pkgmgr.actions.install.installers.nix_flake.run_command"
side_effect=fake_system, ) as run_cmd_mock, redirect_stdout(buf):
), redirect_stdout(buf):
self._enable_nix_in_module(which_mock) self._enable_nix_in_module(which_mock)
# No indices available (empty list)
subproc_mock.return_value = subprocess.CompletedProcess(
args=["nix", "profile", "list", "--json"],
returncode=0,
stdout='{"elements": []}',
stderr="",
)
# pkgmgr install ok; default fails twice (initial + retry)
run_cmd_mock.side_effect = [self._cp(0), self._cp(1), self._cp(1)]
self.assertTrue(installer.supports(ctx)) self.assertTrue(installer.supports(ctx))
# Optional failure must NOT raise # Must NOT raise despite optional failure
installer.run(ctx) installer.run(ctx)
out = buf.getvalue() out = buf.getvalue()
# Both outputs should have been mentioned # Should announce both outputs
self.assertIn( self.assertIn("ensuring outputs: pkgmgr, default", out)
"attempting to install profile outputs: pkgmgr, default", out
)
# First output ("pkgmgr") succeeded # First output ok
self.assertIn( self.assertIn("[nix] output 'pkgmgr' successfully installed.", out)
"Nix flake output 'pkgmgr' successfully installed.", out
)
# Second output ("default") failed but did not raise # Second output failed but no raise
self.assertIn( self.assertIn("[ERROR] Failed to install Nix flake output 'default' (exit 1)", out)
"[Error] Failed to install Nix flake output 'default'", out self.assertIn("[WARNING] Continuing despite failure of optional output 'default'.", out)
)
self.assertIn("[Error] Command exited with code 1", out)
self.assertIn(
"Continuing despite failure to install optional output 'default'.",
out,
)
# Ensure we actually called os.system twice (pkgmgr and default) # Verify run_command was called for both outputs (default twice due to retry)
self.assertEqual(len(calls), 2) expected_calls = [
self.assertIn( (f"nix profile install {self.repo_dir}#pkgmgr",),
f"nix profile install {self.repo_dir}#pkgmgr", (f"nix profile install {self.repo_dir}#default",),
calls[0], (f"nix profile install {self.repo_dir}#default",),
) ]
self.assertIn( actual_cmds = [c.args[0] for c in run_cmd_mock.call_args_list]
f"nix profile install {self.repo_dir}#default", self.assertEqual(actual_cmds, [e[0] for e in expected_calls])
calls[1],
)
def test_nix_flake_supports_respects_disable_env(self): def test_nix_flake_supports_respects_disable_env(self) -> None:
""" """
PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 must disable the installer, PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 must disable the installer,
even if flake.nix exists and nix is available. even if flake.nix exists and nix is available.
""" """
ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir) ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir, quiet=False)
installer = NixFlakeInstaller() installer = NixFlakeInstaller()
with patch( with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock:
"pkgmgr.actions.install.installers.nix_flake.shutil.which"
) as which_mock:
self._enable_nix_in_module(which_mock) self._enable_nix_in_module(which_mock)
os.environ["PKGMGR_DISABLE_NIX_FLAKE_INSTALLER"] = "1" os.environ["PKGMGR_DISABLE_NIX_FLAKE_INSTALLER"] = "1"
self.assertFalse(installer.supports(ctx)) self.assertFalse(installer.supports(ctx))