Refine command resolution and symlink creation (see ChatGPT conversation: https://chatgpt.com/share/6936be2d-952c-800f-a1cd-7ce5438014ff)

This commit is contained in:
Kevin Veen-Birkenbach
2025-12-08 13:02:05 +01:00
parent f641b95d81
commit 15f3c1bcba
9 changed files with 800 additions and 59 deletions

View File

@@ -0,0 +1,102 @@
# tests/unit/pkgmgr/test_create_ink.py
import unittest
from unittest.mock import patch
import pkgmgr.create_ink as create_ink_module
class TestCreateInk(unittest.TestCase):
@patch("pkgmgr.create_ink.get_repo_dir")
@patch("pkgmgr.create_ink.get_repo_identifier")
def test_create_ink_skips_when_no_command(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
):
repo = {} # no 'command' key
mock_get_repo_identifier.return_value = "test-id"
mock_get_repo_dir.return_value = "/repos/test-id"
with patch("pkgmgr.create_ink.os.makedirs") as mock_makedirs, \
patch("pkgmgr.create_ink.os.symlink") as mock_symlink, \
patch("pkgmgr.create_ink.os.chmod") as mock_chmod:
create_ink_module.create_ink(
repo=repo,
repositories_base_dir="/repos",
bin_dir="/bin",
all_repos=[repo],
quiet=True,
preview=False,
)
mock_makedirs.assert_not_called()
mock_symlink.assert_not_called()
mock_chmod.assert_not_called()
@patch("pkgmgr.create_ink.get_repo_dir")
@patch("pkgmgr.create_ink.get_repo_identifier")
def test_create_ink_preview_only(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
):
repo = {"command": "/repos/test-id/main.py"}
mock_get_repo_identifier.return_value = "test-id"
mock_get_repo_dir.return_value = "/repos/test-id"
with patch("pkgmgr.create_ink.os.makedirs") as mock_makedirs, \
patch("pkgmgr.create_ink.os.symlink") as mock_symlink, \
patch("pkgmgr.create_ink.os.chmod") as mock_chmod:
create_ink_module.create_ink(
repo=repo,
repositories_base_dir="/repos",
bin_dir="/bin",
all_repos=[repo],
quiet=True,
preview=True,
)
mock_makedirs.assert_not_called()
mock_symlink.assert_not_called()
mock_chmod.assert_not_called()
@patch("pkgmgr.create_ink.get_repo_dir")
@patch("pkgmgr.create_ink.get_repo_identifier")
def test_create_ink_creates_symlink_and_alias(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
):
repo = {
"command": "/repos/test-id/main.py",
"alias": "alias-id",
}
mock_get_repo_identifier.return_value = "test-id"
mock_get_repo_dir.return_value = "/repos/test-id"
with patch("pkgmgr.create_ink.os.makedirs") as mock_makedirs, \
patch("pkgmgr.create_ink.os.symlink") as mock_symlink, \
patch("pkgmgr.create_ink.os.chmod") as mock_chmod, \
patch("pkgmgr.create_ink.os.path.exists", return_value=False), \
patch("pkgmgr.create_ink.os.path.islink", return_value=False), \
patch("pkgmgr.create_ink.os.remove") as mock_remove, \
patch("pkgmgr.create_ink.os.path.realpath", side_effect=lambda p: p):
create_ink_module.create_ink(
repo=repo,
repositories_base_dir="/repos",
bin_dir="/bin",
all_repos=[repo],
quiet=True,
preview=False,
)
# main link + alias link
self.assertEqual(mock_symlink.call_count, 2)
mock_makedirs.assert_called_once()
mock_chmod.assert_called_once()
mock_remove.assert_not_called()
if __name__ == "__main__":
unittest.main()

View File

@@ -11,7 +11,7 @@ from pkgmgr.installers.base import BaseInstaller
class DummyInstaller(BaseInstaller):
"""Simple installer for testing orchestration."""
layer = None # keine speziellen Capabilities
layer = None # no specific capabilities
def __init__(self):
self.calls = []
@@ -26,6 +26,7 @@ class DummyInstaller(BaseInstaller):
class TestInstallReposOrchestration(unittest.TestCase):
@patch("pkgmgr.install_repos.create_ink")
@patch("pkgmgr.install_repos.resolve_command_for_repo")
@patch("pkgmgr.install_repos.verify_repository")
@patch("pkgmgr.install_repos.get_repo_dir")
@patch("pkgmgr.install_repos.get_repo_identifier")
@@ -36,6 +37,7 @@ class TestInstallReposOrchestration(unittest.TestCase):
mock_get_repo_identifier,
mock_get_repo_dir,
mock_verify_repository,
mock_resolve_command_for_repo,
mock_create_ink,
):
repo1 = {"name": "repo1"}
@@ -50,6 +52,9 @@ class TestInstallReposOrchestration(unittest.TestCase):
# Simulate verification success: (ok, errors, commit, key)
mock_verify_repository.return_value = (True, [], "commit", "key")
# Resolve commands for both repos so create_ink will be called
mock_resolve_command_for_repo.side_effect = ["/bin/cmd1", "/bin/cmd2"]
# Ensure directories exist (no cloning)
with patch("os.path.exists", return_value=True):
dummy_installer = DummyInstaller()
@@ -75,6 +80,7 @@ class TestInstallReposOrchestration(unittest.TestCase):
self.assertEqual(dummy_installer.calls, ["id1", "id2"])
self.assertEqual(mock_create_ink.call_count, 2)
self.assertEqual(mock_verify_repository.call_count, 2)
self.assertEqual(mock_resolve_command_for_repo.call_count, 2)
@patch("pkgmgr.install_repos.verify_repository")
@patch("pkgmgr.install_repos.get_repo_dir")
@@ -100,6 +106,7 @@ class TestInstallReposOrchestration(unittest.TestCase):
dummy_installer = DummyInstaller()
with patch("os.path.exists", return_value=True), \
patch("pkgmgr.install_repos.create_ink") as mock_create_ink, \
patch("pkgmgr.install_repos.resolve_command_for_repo") as mock_resolve_cmd, \
patch("builtins.input", return_value="n"):
old_installers = install_module.INSTALLERS
install_module.INSTALLERS = [dummy_installer]
@@ -121,6 +128,7 @@ class TestInstallReposOrchestration(unittest.TestCase):
# No installer run and no create_ink when user declines
self.assertEqual(dummy_installer.calls, [])
mock_create_ink.assert_not_called()
mock_resolve_cmd.assert_not_called()
if __name__ == "__main__":

View File

@@ -0,0 +1,166 @@
# tests/unit/pkgmgr/test_resolve_command.py
import unittest
from unittest.mock import patch
import pkgmgr.resolve_command as resolve_command_module
class TestResolveCommandForRepo(unittest.TestCase):
def test_explicit_command_wins(self):
repo = {"command": "/custom/cmd"}
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, "/custom/cmd")
@patch("pkgmgr.resolve_command.shutil.which", return_value="/usr/bin/tool")
def test_system_binary_returns_none_and_no_error(self, mock_which):
repo = {}
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
# System binary → no link
self.assertIsNone(result)
@patch("pkgmgr.resolve_command.os.access")
@patch("pkgmgr.resolve_command.os.path.exists")
@patch("pkgmgr.resolve_command.shutil.which", return_value=None)
@patch("pkgmgr.resolve_command.os.path.expanduser", return_value="/fakehome")
def test_nix_profile_binary(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
No system/PATH binary, but a Nix profile binary exists:
→ must return the Nix binary path.
"""
repo = {}
fake_home = "/fakehome"
nix_path = f"{fake_home}/.nix-profile/bin/tool"
def fake_exists(path):
# Only the Nix binary exists
return path == nix_path
def fake_access(path, mode):
# Only the Nix binary is executable
return path == nix_path
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, nix_path)
@patch("pkgmgr.resolve_command.os.access")
@patch("pkgmgr.resolve_command.os.path.exists")
@patch("pkgmgr.resolve_command.os.path.expanduser", return_value="/home/user")
@patch("pkgmgr.resolve_command.shutil.which", return_value="/home/user/.local/bin/tool")
def test_non_system_binary_on_path(
self,
mock_which,
mock_expanduser,
mock_exists,
mock_access,
):
"""
No system (/usr) binary and no Nix binary, but a non-system
PATH binary exists (e.g. venv or ~/.local/bin):
→ must return that PATH binary.
"""
repo = {}
non_system_path = "/home/user/.local/bin/tool"
nix_candidate = "/home/user/.nix-profile/bin/tool"
def fake_exists(path):
# Only the non-system PATH binary "exists".
return path == non_system_path
def fake_access(path, mode):
# Only the non-system PATH binary is executable.
return path == non_system_path
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, non_system_path)
@patch("pkgmgr.resolve_command.os.access")
@patch("pkgmgr.resolve_command.os.path.exists")
@patch("pkgmgr.resolve_command.shutil.which", return_value=None)
@patch("pkgmgr.resolve_command.os.path.expanduser", return_value="/fakehome")
def test_fallback_to_main_py(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
No system/non-system PATH binary, no Nix binary, but main.py exists:
→ must fall back to main.py in the repo.
"""
repo = {}
main_py = "/repos/tool/main.py"
def fake_exists(path):
return path == main_py
def fake_access(path, mode):
return path == main_py
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, main_py)
@patch("pkgmgr.resolve_command.os.access", return_value=False)
@patch("pkgmgr.resolve_command.os.path.exists", return_value=False)
@patch("pkgmgr.resolve_command.shutil.which", return_value=None)
@patch("pkgmgr.resolve_command.os.path.expanduser", return_value="/fakehome")
def test_no_command_results_in_system_exit(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
Nothing available at any layer:
→ must raise SystemExit with a descriptive error message.
"""
repo = {}
with self.assertRaises(SystemExit) as cm:
resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
msg = str(cm.exception)
self.assertIn("No executable command could be resolved for repository 'tool'", msg)
if __name__ == "__main__":
unittest.main()