Refine command resolution and symlink creation (see ChatGPT conversation: https://chatgpt.com/share/6936be2d-952c-800f-a1cd-7ce5438014ff)
This commit is contained in:
102
tests/unit/pkgmgr/test_create_ink.py
Normal file
102
tests/unit/pkgmgr/test_create_ink.py
Normal 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()
|
||||
@@ -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__":
|
||||
|
||||
166
tests/unit/pkgmgr/test_resolve_command.py
Normal file
166
tests/unit/pkgmgr/test_resolve_command.py
Normal 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()
|
||||
Reference in New Issue
Block a user