Files
sjnetPKG/tests/test_cli.py
T
jhkaktus f1250881f4 Implement sjnetpkg 0.1.0, a Git-based package manager
Packages are ordinary Git repositories carrying a sjnetpkg.toml, so sjnetpkg
only needs to resolve, clone, track and run them instead of inventing an
archive format or build system. This adds the v0.1 CLI (install from registry
or Git URL, remove, list/inspect, registry update, run) on top of the Python
standard library, together with a test suite.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-09-24 16:51:29 +02:00

266 lines
9.9 KiB
Python

from __future__ import annotations
import contextlib
import io
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from sjnetpkg import cli, config
GIT_AVAILABLE = shutil.which("git") is not None
def run_cli(*args: str) -> tuple[int, str, str]:
"""Invoke the CLI, capturing stdout/stderr and the exit code."""
stdout, stderr = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
code = cli.main(list(args))
return code, stdout.getvalue(), stderr.getvalue()
def write_package(root: Path, directory: str, manifest: str, files: dict[str, str] | None = None) -> Path:
target = root / directory
target.mkdir(parents=True, exist_ok=True)
(target / "sjnetpkg.toml").write_text(manifest, encoding="utf-8")
for name, content in (files or {}).items():
(target / name).write_text(content, encoding="utf-8")
return target
def make_git_repo(path: Path, manifest: str, files: dict[str, str] | None = None) -> Path:
write_package(path.parent, path.name, manifest, files)
subprocess.run(["git", "init", "-q", str(path)], check=True)
subprocess.run(["git", "-C", str(path), "add", "-A"], check=True)
subprocess.run(
[
"git", "-C", str(path),
"-c", "user.email=test@example.com",
"-c", "user.name=Test",
"commit", "-q", "-m", "init",
],
check=True,
)
return path
class CliTestCase(unittest.TestCase):
"""Base class that isolates the sjnetpkg home directory."""
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
self.home = self.root / ".sjnetpkg"
patcher = mock.patch.dict(os.environ, {"SJNETPKG_HOME": str(self.home)})
patcher.start()
self.addCleanup(patcher.stop)
self.paths = config.get_paths()
def tearDown(self) -> None:
self._tmp.cleanup()
def install_fixture(self, name: str = "cool-tool", manifest: str | None = None,
files: dict[str, str] | None = None) -> Path:
manifest = manifest or f'name = "{name}"\nversion = "1.0.0"\n'
repo = make_git_repo(self.root / "repos" / name, manifest, files)
code, _, err = run_cli("-U", repo.as_uri())
self.assertEqual(code, 0, err)
return repo
class VersionAndHelpTests(CliTestCase):
def test_version(self) -> None:
code, out, _ = run_cli("--version")
self.assertEqual(code, 0)
self.assertEqual(out.strip(), "sjnetpkg 0.1.0")
def test_help(self) -> None:
code, out, _ = run_cli("--help")
self.assertEqual(code, 0)
self.assertIn("Usage:", out)
for line in ("sjnetpkg -S <package>", "sjnetpkg -Sy", "sjnetpkg run <package>"):
self.assertIn(line, out)
def test_no_arguments_is_a_usage_error(self) -> None:
code, _, err = run_cli()
self.assertEqual(code, 2)
self.assertIn("Usage:", err)
def test_unknown_command(self) -> None:
code, _, err = run_cli("frobnicate")
self.assertEqual(code, 2)
self.assertIn("unknown command", err)
class QueryTests(CliTestCase):
def test_list_without_packages(self) -> None:
code, out, _ = run_cli("-Q")
self.assertEqual(code, 0)
self.assertIn("No packages installed.", out)
def test_list_alignment(self) -> None:
config.ensure_home(self.paths)
write_package(self.paths.pkg, "cool-tool", 'name = "cool-tool"\nversion = "1.0.0"\n')
write_package(self.paths.pkg, "another-tool", 'name = "another-tool"\nversion = "2.4.1"\n')
code, out, _ = run_cli("-Q")
self.assertEqual(code, 0)
lines = out.strip().splitlines()
self.assertEqual(len(lines), 2)
self.assertRegex(lines[0], r"^another-tool\s+2\.4\.1$")
self.assertRegex(lines[1], r"^cool-tool\s+1\.0\.0$")
# The versions are printed in the same column.
self.assertEqual(lines[0].index("2.4.1"), lines[1].index("1.0.0"))
def test_info_for_unknown_package(self) -> None:
code, _, err = run_cli("-Q", "missing")
self.assertEqual(code, 1)
self.assertIn("package 'missing' is not installed.", err)
@unittest.skipUnless(GIT_AVAILABLE, "git is required for these tests")
class InstallTests(CliTestCase):
def test_install_from_url(self) -> None:
repo = self.install_fixture()
target = self.home / "pkg" / "cool-tool"
self.assertTrue((target / ".git").is_dir())
self.assertTrue((target / "sjnetpkg.toml").is_file())
def test_install_reports_package(self) -> None:
repo = make_git_repo(
self.root / "repos" / "cool-tool",
'name = "cool-tool"\nversion = "1.0.0"\n',
)
code, out, _ = run_cli("-U", repo.as_uri())
self.assertEqual(code, 0)
self.assertIn("Installed cool-tool 1.0.0", out)
def test_second_install_is_rejected(self) -> None:
repo = self.install_fixture()
code, _, err = run_cli("-U", repo.as_uri())
self.assertEqual(code, 1)
self.assertIn("package 'cool-tool' is already installed.", err)
def test_invalid_url_is_rejected(self) -> None:
code, _, err = run_cli("-U", "not-a-url")
self.assertEqual(code, 1)
self.assertIn("invalid git URL", err)
def test_missing_manifest_does_not_leave_a_directory(self) -> None:
repo_dir = self.root / "repos" / "empty-tool"
repo_dir.mkdir(parents=True)
(repo_dir / "README.md").write_text("no manifest here")
subprocess.run(["git", "init", "-q", str(repo_dir)], check=True)
subprocess.run(["git", "-C", str(repo_dir), "add", "-A"], check=True)
subprocess.run(
["git", "-C", str(repo_dir), "-c", "user.email=t@e", "-c", "user.name=T",
"commit", "-q", "-m", "init"],
check=True,
)
code, _, err = run_cli("-U", repo_dir.as_uri())
self.assertEqual(code, 1)
self.assertIn("sjnetpkg.toml", err)
self.assertFalse((self.home / "pkg" / "empty-tool").exists())
@unittest.skipUnless(GIT_AVAILABLE, "git is required for these tests")
class RegistryInstallTests(CliTestCase):
def _write_registry(self, url: str, name: str = "cool-tool") -> None:
config.ensure_home(self.paths)
self.paths.registry.write_text(
f'[packages.{name}]\nrepo = "{url}"\n', encoding="utf-8"
)
def test_install_from_registry(self) -> None:
repo = make_git_repo(
self.root / "repos" / "cool-tool",
'name = "cool-tool"\nversion = "1.0.0"\n',
)
self._write_registry(repo.as_uri())
code, out, err = run_cli("-S", "cool-tool")
self.assertEqual(code, 0, err)
self.assertIn("Installed cool-tool 1.0.0", out)
def test_unknown_package(self) -> None:
self._write_registry("https://example.com/other.git", name="other")
code, _, err = run_cli("-S", "cool-tool")
self.assertEqual(code, 1)
self.assertIn("package 'cool-tool' was not found.", err)
def test_missing_local_registry(self) -> None:
code, _, err = run_cli("-S", "cool-tool")
self.assertEqual(code, 1)
self.assertIn("no local registry found", err)
def test_update_registry(self) -> None:
source = self.root / "remote-registry.toml"
source.write_text('[packages.cool-tool]\nrepo = "https://example.com/cool-tool.git"\n')
config.ensure_home(self.paths)
self.paths.config.write_text(f'registry = "{source.as_uri()}"\n', encoding="utf-8")
code, out, _ = run_cli("-Sy")
self.assertEqual(code, 0)
self.assertIn("Registry updated", out)
self.assertTrue(self.paths.registry.is_file())
@unittest.skipUnless(GIT_AVAILABLE, "git is required for these tests")
class RunAndRemoveTests(CliTestCase):
def test_run_returns_process_exit_code(self) -> None:
manifest = (
'name = "cool-tool"\nversion = "1.0.0"\n\n'
f'[run]\ncommand = \'"{sys.executable}" main.py\'\n'
)
self.install_fixture(manifest=manifest, files={"main.py": "import sys\nsys.exit(7)\n"})
code, _, _ = run_cli("run", "cool-tool")
self.assertEqual(code, 7)
def test_run_uses_package_directory_as_working_directory(self) -> None:
manifest = (
'name = "cool-tool"\nversion = "1.0.0"\n\n'
f'[run]\ncommand = \'"{sys.executable}" check_cwd.py\'\n'
)
script = (
"import pathlib, sys\n"
"sys.exit(0 if pathlib.Path('sjnetpkg.toml').exists() else 3)\n"
)
self.install_fixture(manifest=manifest, files={"check_cwd.py": script})
code, _, _ = run_cli("run", "cool-tool")
self.assertEqual(code, 0)
def test_run_without_run_command(self) -> None:
self.install_fixture()
code, _, err = run_cli("run", "cool-tool")
self.assertEqual(code, 1)
self.assertIn("does not define a run command", err)
def test_run_unknown_package(self) -> None:
code, _, err = run_cli("run", "missing")
self.assertEqual(code, 1)
self.assertIn("package 'missing' is not installed.", err)
def test_info_and_remove(self) -> None:
self.install_fixture()
code, out, _ = run_cli("-Q", "cool-tool")
self.assertEqual(code, 0)
self.assertIn("Name: cool-tool", out)
self.assertIn("Version: 1.0.0", out)
self.assertIn("Repository:", out)
code, out, _ = run_cli("-R", "cool-tool")
self.assertEqual(code, 0)
self.assertIn("Removed cool-tool", out)
self.assertFalse((self.home / "pkg" / "cool-tool").exists())
def test_remove_unknown_package(self) -> None:
code, _, err = run_cli("-R", "missing")
self.assertEqual(code, 1)
self.assertIn("package 'missing' is not installed.", err)
if __name__ == "__main__":
unittest.main()