diff --git a/tests/unit/pkgmgr/actions/install/installers/test_nix_flake.py b/tests/unit/pkgmgr/actions/install/installers/test_nix_flake.py index c9299c0..ad891ee 100644 --- a/tests/unit/pkgmgr/actions/install/installers/test_nix_flake.py +++ b/tests/unit/pkgmgr/actions/install/installers/test_nix_flake.py @@ -5,15 +5,18 @@ Unit tests for NixFlakeInstaller using unittest (no pytest). Covers: -- Successful installation (exit_code == 0) +- Successful installation (returncode == 0) - Mandatory failure → SystemExit with correct code - Optional failure (pkgmgr default) → no raise, but warning - supports() behavior incl. PKGMGR_DISABLE_NIX_FLAKE_INSTALLER """ +from __future__ import annotations + import io import os import shutil +import subprocess import tempfile import unittest from contextlib import redirect_stdout @@ -25,10 +28,19 @@ from pkgmgr.actions.install.installers.nix_flake import NixFlakeInstaller class DummyCtx: """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.repo_dir = repo_dir self.preview = preview + self.quiet = quiet + self.force_update = force_update class TestNixFlakeInstaller(unittest.TestCase): @@ -44,161 +56,162 @@ class TestNixFlakeInstaller(unittest.TestCase): os.environ.pop("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER", None) def tearDown(self) -> None: - # Cleanup temporary directory if os.path.isdir(self._tmpdir): 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.""" 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. """ ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir) - installer = NixFlakeInstaller() buf = io.StringIO() - with patch( - "pkgmgr.actions.install.installers.nix_flake.shutil.which" - ) as which_mock, patch( - "pkgmgr.actions.install.installers.nix_flake.os.system" - ) as system_mock, redirect_stdout(buf): + with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock, patch( + "pkgmgr.actions.install.installers.nix_flake.subprocess.run" + ) as subproc_mock, patch( + "pkgmgr.actions.install.installers.nix_flake.run_command" + ) as run_cmd_mock, redirect_stdout(buf): self._enable_nix_in_module(which_mock) - # Simulate os.system returning success (exit code 0) - system_mock.return_value = 0 + # For profile list JSON (used only on failure paths, but keep deterministic) + 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)) - installer.run(ctx) out = buf.getvalue() - self.assertIn("[INFO] Running: nix profile install", out) - self.assertIn("Nix flake output 'default' successfully installed.", out) + self.assertIn("[nix] install: nix profile install", out) + self.assertIn("[nix] output 'default' successfully installed.", out) - # Ensure the nix command was actually invoked - system_mock.assert_called_with( - f"nix profile install {self.repo_dir}#default" + run_cmd_mock.assert_called_with( + 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), - `default` is mandatory and a non-zero exit code should raise SystemExit - with the real exit code (e.g. 1, not 256). + For a generic repository, 'default' is mandatory. + A non-zero return code must raise SystemExit with that code. """ ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir) installer = NixFlakeInstaller() buf = io.StringIO() - with patch( - "pkgmgr.actions.install.installers.nix_flake.shutil.which" - ) as which_mock, patch( - "pkgmgr.actions.install.installers.nix_flake.os.system" - ) as system_mock, redirect_stdout(buf): + with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock, patch( + "pkgmgr.actions.install.installers.nix_flake.subprocess.run" + ) as subproc_mock, patch( + "pkgmgr.actions.install.installers.nix_flake.run_command" + ) as run_cmd_mock, redirect_stdout(buf): self._enable_nix_in_module(which_mock) - # Simulate os.system returning encoded status for exit code 1 - # os.system encodes exit code as (exit_code << 8) - system_mock.return_value = 1 << 8 + # No indices available (empty list) + subproc_mock.return_value = subprocess.CompletedProcess( + 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)) - with self.assertRaises(SystemExit) as cm: installer.run(ctx) - # The real exit code should be 1 (not 256) self.assertEqual(cm.exception.code, 1) - out = buf.getvalue() - self.assertIn("[INFO] Running: nix profile install", out) - self.assertIn("[Error] Failed to install Nix flake output 'default'", out) - self.assertIn("[Error] Command exited with code 1", out) + self.assertIn("[nix] install: nix profile install", out) + self.assertIn("[ERROR] Failed to install Nix flake output 'default' (exit 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. - Failure to install it must not raise, but should log a warning instead. + For pkgmgr/package-manager repositories: + - 'pkgmgr' output is mandatory + - 'default' output is optional + Failure of optional output must not raise. """ ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir) 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() - with patch( - "pkgmgr.actions.install.installers.nix_flake.shutil.which" - ) as which_mock, patch( - "pkgmgr.actions.install.installers.nix_flake.os.system", - side_effect=fake_system, - ), redirect_stdout(buf): + with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock, patch( + "pkgmgr.actions.install.installers.nix_flake.subprocess.run" + ) as subproc_mock, patch( + "pkgmgr.actions.install.installers.nix_flake.run_command" + ) as run_cmd_mock, redirect_stdout(buf): 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)) - # Optional failure must NOT raise + # Must NOT raise despite optional failure installer.run(ctx) out = buf.getvalue() - # Both outputs should have been mentioned - self.assertIn( - "attempting to install profile outputs: pkgmgr, default", out - ) + # Should announce both outputs + self.assertIn("ensuring outputs: pkgmgr, default", out) - # First output ("pkgmgr") succeeded - self.assertIn( - "Nix flake output 'pkgmgr' successfully installed.", out - ) + # First output ok + self.assertIn("[nix] output 'pkgmgr' successfully installed.", out) - # Second output ("default") failed but did not raise - self.assertIn( - "[Error] Failed to install Nix flake output 'default'", out - ) - self.assertIn("[Error] Command exited with code 1", out) - self.assertIn( - "Continuing despite failure to install optional output 'default'.", - out, - ) + # Second output failed but no raise + self.assertIn("[ERROR] Failed to install Nix flake output 'default' (exit 1)", out) + self.assertIn("[WARNING] Continuing despite failure of optional output 'default'.", out) - # Ensure we actually called os.system twice (pkgmgr and default) - self.assertEqual(len(calls), 2) - self.assertIn( - f"nix profile install {self.repo_dir}#pkgmgr", - calls[0], - ) - self.assertIn( - f"nix profile install {self.repo_dir}#default", - calls[1], - ) + # Verify run_command was called for both outputs (default twice due to retry) + expected_calls = [ + (f"nix profile install {self.repo_dir}#pkgmgr",), + (f"nix profile install {self.repo_dir}#default",), + (f"nix profile install {self.repo_dir}#default",), + ] + actual_cmds = [c.args[0] for c in run_cmd_mock.call_args_list] + self.assertEqual(actual_cmds, [e[0] for e in expected_calls]) - 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, 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() - with patch( - "pkgmgr.actions.install.installers.nix_flake.shutil.which" - ) as which_mock: + with patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") as which_mock: self._enable_nix_in_module(which_mock) os.environ["PKGMGR_DISABLE_NIX_FLAKE_INSTALLER"] = "1" - self.assertFalse(installer.supports(ctx))