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>
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user