[projects/git-slug] Add pytest test suite
arekm
arekm at pld-linux.org
Tue May 12 15:08:10 CEST 2026
commit 8a1cf682c54bb71ce69b3ae43b73c6f20e2abb2d
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Mon Apr 6 19:37:05 2026 +0200
Add pytest test suite
.gitignore | 5 +
README.md | 43 ++++++
pyproject.toml | 4 +
tests/conftest.py | 19 +++
tests/test_cli.py | 154 +++++++++++++++++++++
tests/test_config_defaults.py | 91 +++++++++++++
tests/test_failure_paths.py | 302 ++++++++++++++++++++++++++++++++++++++++++
tests/test_gitrepo.py | 116 ++++++++++++++++
tests/test_passthrough.py | 99 ++++++++++++++
tests/test_refsdata.py | 90 +++++++++++++
tests/test_slug_commands.py | 199 ++++++++++++++++++++++++++++
11 files changed, 1122 insertions(+)
---
diff --git a/.gitignore b/.gitignore
index 551698e..9b2a450 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
MANIFEST
+.coverage
+.pytest_cache/
+__pycache__/
+git_slug/__pycache__/
+tests/__pycache__/
diff --git a/README.md b/README.md
index b766340..6079882 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,49 @@ python3 setup.py install
The package is also available as `git-core-slug` RPM in PLD Linux.
+## Development
+
+Install test dependencies:
+
+```sh
+python3 -m pip install pytest pytest-cov
+```
+
+Run the test suite:
+
+```sh
+pytest
+```
+
+Run the suite with coverage:
+
+```sh
+pytest --cov=slug --cov=git_slug --cov-report=term-missing
+```
+
+Run a focused subset while working on one area:
+
+```sh
+pytest tests/test_cli.py
+pytest tests/test_slug_commands.py
+```
+
+The test suite is intentionally lightweight:
+
+- `pytest` is the primary framework.
+- `pytest-cov` is used only for coverage reporting.
+- Most tests are unit-style and use mocking rather than real PLD infrastructure.
+- The suite does not require live SSH access or the centralized Refs repo.
+
+Current test coverage is split roughly by responsibility:
+
+- `tests/test_cli.py` checks argv splitting, top-level dispatch, help/version, and parser exit codes.
+- `tests/test_config_defaults.py` checks git-config and hardcoded default precedence.
+- `tests/test_passthrough.py` checks git passthrough command assembly and output/error handling.
+- `tests/test_slug_commands.py` checks `update`, `clone`, `list`, and `init` command wiring.
+- `tests/test_failure_paths.py` checks common error paths that previously regressed.
+- `tests/test_refsdata.py` and `tests/test_gitrepo.py` cover small helper-module behavior.
+
## Setup
Add your name and email to git configuration:
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..47743ce
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,4 @@
+[tool.pytest.ini_options]
+minversion = "8.0"
+testpaths = ["tests"]
+addopts = "-ra"
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..c9545e3
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,19 @@
+import argparse
+
+import pytest
+
+
+ at pytest.fixture
+def make_options():
+ def _make(**overrides):
+ values = {
+ "packagesdir": "/pkgs",
+ "jobs": 2,
+ "quiet": False,
+ "pattern": ["*"],
+ "had_errors": False,
+ }
+ values.update(overrides)
+ return argparse.Namespace(**values)
+
+ return _make
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..a86ce63
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,154 @@
+import argparse
+
+import pytest
+
+import slug
+
+
+ at pytest.mark.parametrize(
+ ("argv", "expected"),
+ [
+ (["pull", "-q"], ([], "pull", ["-q"])),
+ (["-q", "pull"], (["-q"], "pull", [])),
+ (["-j4", "pull"], (["-j", "4"], "pull", [])),
+ (["-j", "4", "pull"], (["-j", "4"], "pull", [])),
+ (["-d/tmp/pld", "pull"], (["-d", "/tmp/pld"], "pull", [])),
+ (["--jobs=8", "pull"], (["--jobs=8"], "pull", [])),
+ (
+ ["--pattern", "perl-*", "--pattern=python-*", "status"],
+ (["--pattern", "perl-*", "--pattern=python-*"], "status", []),
+ ),
+ (["--help"], (["--help"], None, [])),
+ (["pull", "--help"], ([], "pull", ["--help"])),
+ (["-qj4", "pull"], (["-q", "-j", "4"], "pull", [])),
+ (["-x", "pull"], ([], "-x", ["pull"])),
+ (["--unknown", "pull"], ([], "--unknown", ["pull"])),
+ ],
+)
+def test_split_args(argv, expected):
+ assert slug.split_args(argv) == expected
+
+
+def test_main_top_level_help_exits_zero(monkeypatch, capsys):
+ monkeypatch.setattr(slug.sys, "argv", ["slug.py", "--help"])
+
+ with pytest.raises(SystemExit) as exc:
+ slug.main()
+
+ assert exc.value.code == 0
+ assert "Run git commands across PLD package repositories" in capsys.readouterr().out
+
+
+def test_main_without_command_prints_help_and_exits_one(monkeypatch, capsys):
+ monkeypatch.setattr(slug.sys, "argv", ["slug.py"])
+ monkeypatch.setattr(slug, "apply_defaults", lambda options: None)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.main()
+
+ assert exc.value.code == 1
+ assert "usage:" in capsys.readouterr().err
+
+
+def test_main_dispatches_slug_command(monkeypatch):
+ calls = []
+
+ def fake_apply_defaults(options):
+ options.packagesdir = "/pkgs"
+ options.jobs = 2
+ options.quiet = False
+ options.pattern = ["perl-*"]
+
+ def fake_handler(options, args):
+ calls.append((options.pattern, args))
+
+ monkeypatch.setattr(slug.sys, "argv", ["slug.py", "--pattern", "perl-*", "update", "--new"])
+ monkeypatch.setattr(slug, "apply_defaults", fake_apply_defaults)
+ monkeypatch.setitem(slug.SLUG_COMMANDS, "update", fake_handler)
+
+ slug.main()
+
+ assert calls == [(["perl-*"], ["--new"])]
+
+
+def test_main_passthrough_help_execs_git_once(monkeypatch):
+ captured = {}
+
+ def fake_apply_defaults(options):
+ options.packagesdir = "/pkgs"
+ options.jobs = 2
+ options.quiet = False
+ options.pattern = ["*"]
+
+ def fake_execvp(binary, argv):
+ captured["binary"] = binary
+ captured["argv"] = argv
+ raise SystemExit(0)
+
+ monkeypatch.setattr(slug.sys, "argv", ["slug.py", "pull", "--help"])
+ monkeypatch.setattr(slug, "apply_defaults", fake_apply_defaults)
+ monkeypatch.setattr(slug.os, "execvp", fake_execvp)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.main()
+
+ assert exc.value.code == 0
+ assert captured == {"binary": "git", "argv": ["git", "pull", "--help"]}
+
+
+def test_main_passthrough_command_calls_dispatcher(monkeypatch):
+ captured = {}
+
+ def fake_apply_defaults(options):
+ options.packagesdir = "/pkgs"
+ options.jobs = 4
+ options.quiet = True
+ options.pattern = ["python-*"]
+
+ def fake_passthrough(options, command, git_args):
+ captured["packagesdir"] = options.packagesdir
+ captured["jobs"] = options.jobs
+ captured["quiet"] = options.quiet
+ captured["pattern"] = options.pattern
+ captured["command"] = command
+ captured["git_args"] = git_args
+
+ monkeypatch.setattr(slug.sys, "argv", ["slug.py", "-q", "--pattern", "python-*", "status", "--branch"])
+ monkeypatch.setattr(slug, "apply_defaults", fake_apply_defaults)
+ monkeypatch.setattr(slug, "passthrough_command", fake_passthrough)
+
+ slug.main()
+
+ assert captured == {
+ "packagesdir": "/pkgs",
+ "jobs": 4,
+ "quiet": True,
+ "pattern": ["python-*"],
+ "command": "status",
+ "git_args": ["--branch"],
+ }
+
+
+def test_slug_argument_parser_errors_exit_one(capsys):
+ parser = slug.SlugArgumentParser(prog="git pld")
+ parser.add_argument("-j", "--jobs", type=int)
+
+ with pytest.raises(SystemExit) as exc:
+ parser.parse_args(["-j"])
+
+ assert exc.value.code == 1
+ assert "expected one argument" in capsys.readouterr().err
+
+
+def test_apply_defaults_can_fill_namespace_created_like_argparse(monkeypatch):
+ options = argparse.Namespace()
+ monkeypatch.setattr(slug, "git_config_get", lambda key: None)
+ monkeypatch.setattr(slug, "default_packagesdir", lambda: "/fallback")
+ monkeypatch.setattr(slug, "cpu_count", lambda: 3)
+
+ slug.apply_defaults(options)
+
+ assert options.packagesdir == "/fallback"
+ assert options.jobs == 3
+ assert options.quiet is False
+ assert options.pattern == ["*"]
diff --git a/tests/test_config_defaults.py b/tests/test_config_defaults.py
new file mode 100644
index 0000000..6d9723d
--- /dev/null
+++ b/tests/test_config_defaults.py
@@ -0,0 +1,91 @@
+import argparse
+from types import SimpleNamespace
+
+import slug
+
+
+def test_apply_defaults_prefers_cli_over_config(monkeypatch):
+ options = argparse.Namespace(
+ packagesdir="/cli/packages",
+ jobs=9,
+ quiet=True,
+ pattern=["perl-*"],
+ )
+
+ monkeypatch.setattr(slug, "git_config_get", lambda key: {
+ "PLD.packagesdir": "/cfg/packages",
+ "PLD.jobs": "4",
+ }.get(key))
+
+ slug.apply_defaults(options)
+
+ assert options.packagesdir == "/cli/packages"
+ assert options.jobs == 9
+ assert options.quiet is True
+ assert options.pattern == ["perl-*"]
+
+
+def test_apply_defaults_reads_git_config_for_missing_values(monkeypatch):
+ options = argparse.Namespace()
+
+ monkeypatch.setattr(slug, "git_config_get", lambda key: {
+ "PLD.packagesdir": "~/cfg/packages",
+ "PLD.jobs": "7",
+ }.get(key))
+ monkeypatch.setattr(slug, "default_packagesdir", lambda: "/fallback")
+ monkeypatch.setattr(slug, "cpu_count", lambda: 2)
+
+ slug.apply_defaults(options)
+
+ assert options.packagesdir.endswith("/cfg/packages")
+ assert options.jobs == 7
+ assert options.quiet is False
+ assert options.pattern == ["*"]
+
+
+def test_get_command_config_uses_builtin_when_unconfigured(monkeypatch):
+ monkeypatch.setattr(slug, "git_config_get", lambda key: None)
+
+ assert slug.get_command_config("pull") == [
+ "pull.rebase=true",
+ "pull.autostash=true",
+ ]
+ assert slug.get_command_config("merge") == []
+
+
+def test_get_command_config_uses_shlex_split_for_user_config(monkeypatch):
+ monkeypatch.setattr(
+ slug,
+ "git_config_get",
+ lambda key: "status.short=true 'status.branch=true'",
+ )
+
+ assert slug.get_command_config("status") == [
+ "status.short=true",
+ "status.branch=true",
+ ]
+
+
+def test_get_command_config_allows_empty_string_to_disable_defaults(monkeypatch):
+ monkeypatch.setattr(slug, "git_config_get", lambda key: "")
+
+ assert slug.get_command_config("fetch") == []
+
+
+def test_git_config_get_returns_none_on_nonzero_status(monkeypatch):
+ monkeypatch.setattr(
+ slug.subprocess,
+ "run",
+ lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout=""),
+ )
+
+ assert slug.git_config_get("PLD.jobs") is None
+
+
+def test_git_config_get_returns_none_when_git_is_missing(monkeypatch):
+ def fake_run(*args, **kwargs):
+ raise FileNotFoundError()
+
+ monkeypatch.setattr(slug.subprocess, "run", fake_run)
+
+ assert slug.git_config_get("PLD.jobs") is None
diff --git a/tests/test_failure_paths.py b/tests/test_failure_paths.py
new file mode 100644
index 0000000..dc3fe25
--- /dev/null
+++ b/tests/test_failure_paths.py
@@ -0,0 +1,302 @@
+import collections
+from types import SimpleNamespace
+
+import pytest
+
+import slug
+
+
+class FakeRepo:
+ def __init__(self, wtree="/repos/pkg"):
+ self.wtree = wtree
+ self.gdir = wtree + "/.git"
+
+
+def test_fetch_package_returns_fetch_error_on_git_failure(capsys, make_options):
+ class BrokenRepo(FakeRepo):
+ def check_remote(self, ref):
+ return "old-sha"
+
+ def fetch(self, fetchlist, depth):
+ raise slug.GitRepoError("fetch exploded")
+
+ result = slug.fetch_package(
+ BrokenRepo("/repos/perl-Broken"),
+ {"refs/heads/master": "new-sha"},
+ make_options(depth=0),
+ )
+
+ assert isinstance(result, slug._FetchError)
+ assert "perl-Broken: fetch exploded" in capsys.readouterr().err
+
+
+def test_fetch_package_returns_none_when_repo_is_up_to_date(make_options):
+ class CleanRepo(FakeRepo):
+ def check_remote(self, ref):
+ return "same-sha"
+
+ def fetch(self, fetchlist, depth):
+ raise AssertionError("fetch should not be called")
+
+ result = slug.fetch_package(
+ CleanRepo("/repos/perl-Clean"),
+ {"refs/heads/master": "same-sha"},
+ make_options(depth=0),
+ )
+
+ assert result is None
+
+
+def test_fetch_package_returns_repo_when_fetch_emits_stderr(capsys, make_options):
+ class WarningRepo(FakeRepo):
+ def check_remote(self, ref):
+ return "old-sha"
+
+ def fetch(self, fetchlist, depth):
+ return (b"", b"warning line\n")
+
+ repo = WarningRepo("/repos/perl-Warn")
+ result = slug.fetch_package(
+ repo,
+ {"refs/heads/master": "new-sha"},
+ make_options(depth=0),
+ )
+
+ assert result is repo
+ assert "perl-Warn: warning line" in capsys.readouterr().err
+
+
+def test_initpackage_returns_none_on_repo_init_error(monkeypatch, make_options, capsys):
+ class BrokenGitRepo:
+ def __init__(self, path):
+ self.path = path
+
+ def init(self, remotepull, remotepush):
+ raise slug.GitRepoError("init failed")
+
+ monkeypatch.setattr(slug, "GitRepo", BrokenGitRepo)
+
+ result = slug.initpackage("perl-Broken", make_options())
+
+ assert result is None
+ assert "error: failed to init perl-Broken: init failed" in capsys.readouterr().err
+
+
+def test_createpackage_returns_false_when_server_create_fails(monkeypatch, make_options, capsys):
+ monkeypatch.setattr(
+ slug.subprocess,
+ "run",
+ lambda argv: SimpleNamespace(returncode=1),
+ )
+
+ assert slug.createpackage("perl-Broken", make_options()) is False
+ assert "error: failed to create perl-Broken on server" in capsys.readouterr().err
+
+
+def test_getrefs_exits_on_remote_refs_error(monkeypatch, capsys):
+ def fake_refs(*args):
+ raise slug.RemoteRefsError("heads", "git://repo")
+
+ monkeypatch.setattr(slug, "GitArchiveRefsData", fake_refs)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.getrefs(["master"], ["perl-*"])
+
+ assert exc.value.code == 1
+ assert "fatal: problem with file heads in repository git://repo" in capsys.readouterr().err
+
+
+def test_getrefs_exits_on_no_matches(monkeypatch, capsys):
+ def fake_refs(*args):
+ raise slug.NoMatchedRepos()
+
+ monkeypatch.setattr(slug, "GitArchiveRefsData", fake_refs)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.getrefs(["master"], ["perl-*"])
+
+ assert exc.value.code == 1
+ assert "fatal: no matching package has been found" in capsys.readouterr().err
+
+
+def test_run_worker_filters_none_results(monkeypatch, make_options):
+ state = {}
+
+ class FakePool:
+ def __init__(self, jobs, initializer):
+ state["jobs"] = jobs
+ state["initializer"] = initializer
+ state["closed"] = False
+ state["joined"] = False
+
+ def starmap(self, function, args):
+ state["args"] = list(args)
+ return [None, "repo-a", None, "repo-b"]
+
+ def close(self):
+ state["closed"] = True
+
+ def join(self):
+ state["joined"] = True
+
+ monkeypatch.setattr(slug, "WorkerPool", FakePool)
+
+ result = slug.run_worker(object(), make_options(jobs=7), [("a",), ("b",)])
+
+ assert result == ["repo-a", "repo-b"]
+ assert state["jobs"] == 7
+ assert state["initializer"] is slug.pool_worker_init
+ assert state["closed"] is True
+ assert state["joined"] is True
+ assert state["args"] == [("a",), ("b",)]
+
+
+def test_run_worker_handles_keyboard_interrupt(monkeypatch, make_options, capsys):
+ state = {}
+
+ class FakePool:
+ def __init__(self, jobs, initializer):
+ state["jobs"] = jobs
+
+ def starmap(self, function, args):
+ raise KeyboardInterrupt()
+
+ def terminate(self):
+ state["terminated"] = True
+
+ def join(self):
+ state["joined"] = True
+
+ monkeypatch.setattr(slug, "WorkerPool", FakePool)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.run_worker(object(), make_options(jobs=3), [])
+
+ assert exc.value.code == 1
+ assert state["terminated"] is True
+ assert state["joined"] is True
+ assert "Keyboard interrupt received, finishing..." in capsys.readouterr().err
+
+
+def test_fetch_packages_clone_mode_only_fetches_new_repos(monkeypatch, make_options, capsys):
+ refs = SimpleNamespace(
+ heads={
+ "pkg-new": {"refs/heads/master": "sha-new"},
+ "pkg-old": {"refs/heads/master": "sha-old"},
+ }
+ )
+ captured = {}
+
+ class FakeGitRepo:
+ def __init__(self, path):
+ self.path = path
+ self.wtree = path
+
+ def fake_isdir(path):
+ return path.endswith("pkg-old/.git")
+
+ def fake_run_worker(function, options, args):
+ items = list(args)
+ if function is slug.initpackage:
+ captured["init_args"] = items
+ return ["pkg-new"]
+ if function is slug.fetch_package:
+ captured["fetch_args"] = items
+ return ["updated-pkg-new"]
+ raise AssertionError("unexpected worker function")
+
+ monkeypatch.setattr(slug, "getrefs", lambda *args: refs)
+ monkeypatch.setattr(slug.os.path, "isdir", fake_isdir)
+ monkeypatch.setattr(slug, "run_worker", fake_run_worker)
+ monkeypatch.setattr(slug, "GitRepo", FakeGitRepo)
+
+ options = make_options(
+ branch=["master"],
+ repopattern=["pkg-*"],
+ newpkgs=True,
+ prune=False,
+ depth=0,
+ omitexisting=True,
+ )
+
+ result = slug.fetch_packages(options)
+
+ assert result == ["updated-pkg-new"]
+ assert options.had_errors is False
+ assert "Read remotes data" in capsys.readouterr().err
+ assert captured["init_args"] == [("pkg-new", options)]
+ assert len(captured["fetch_args"]) == 1
+ assert captured["fetch_args"][0][0].path == "/pkgs/pkg-new"
+ assert captured["fetch_args"][0][1] == {"refs/heads/master": "sha-new"}
+
+
+def test_fetch_packages_marks_errors_on_partial_init_failures(monkeypatch, make_options):
+ refs = SimpleNamespace(
+ heads={
+ "pkg-a": {"refs/heads/master": "sha-a"},
+ "pkg-b": {"refs/heads/master": "sha-b"},
+ }
+ )
+
+ monkeypatch.setattr(slug, "getrefs", lambda *args: refs)
+ monkeypatch.setattr(slug.os.path, "isdir", lambda path: False)
+ monkeypatch.setattr(slug, "GitRepo", lambda path: SimpleNamespace(wtree=path, gdir=path + "/.git"))
+
+ def fake_run_worker(function, options, args):
+ if function is slug.initpackage:
+ return ["only-one-success"]
+ if function is slug.fetch_package:
+ return []
+ raise AssertionError("unexpected worker function")
+
+ monkeypatch.setattr(slug, "run_worker", fake_run_worker)
+
+ options = make_options(
+ branch=["master"],
+ repopattern=["pkg-*"],
+ newpkgs=True,
+ prune=False,
+ depth=0,
+ omitexisting=False,
+ )
+
+ slug.fetch_packages(options)
+
+ assert options.had_errors is True
+
+
+def test_fetch_packages_marks_errors_on_prune_delete_failure(monkeypatch, make_options, capsys):
+ refs_initial = SimpleNamespace(heads={"keep": {"refs/heads/master": "sha-keep"}})
+ refs_full = SimpleNamespace(heads=collections.defaultdict(dict, {"keep": {"refs/heads/master": "sha-keep"}}))
+ calls = []
+
+ def fake_getrefs(*args):
+ calls.append(args)
+ return refs_initial if len(calls) == 1 else refs_full
+
+ monkeypatch.setattr(slug, "getrefs", fake_getrefs)
+ monkeypatch.setattr(slug, "GitRepo", lambda path: SimpleNamespace(wtree=path, gdir=path + "/.git"))
+ monkeypatch.setattr(slug, "run_worker", lambda function, options, args: [])
+ monkeypatch.setattr(slug, "find_git_repos", lambda packagesdir, patterns: ["/pkgs/orphan"])
+
+ def fake_rmtree(path):
+ raise OSError("permission denied")
+
+ monkeypatch.setattr(slug.shutil, "rmtree", fake_rmtree)
+
+ options = make_options(
+ branch=["master"],
+ repopattern=["*"],
+ newpkgs=False,
+ prune=True,
+ depth=0,
+ omitexisting=False,
+ )
+
+ result = slug.fetch_packages(options)
+
+ assert result == []
+ assert options.had_errors is True
+ output = capsys.readouterr().err
+ assert "warning: removing /pkgs/orphan" in output
+ assert "error: failed to remove /pkgs/orphan: permission denied" in output
diff --git a/tests/test_gitrepo.py b/tests/test_gitrepo.py
new file mode 100644
index 0000000..283b572
--- /dev/null
+++ b/tests/test_gitrepo.py
@@ -0,0 +1,116 @@
+from types import SimpleNamespace
+
+import pytest
+
+import git_slug.gitrepo as gitrepo_module
+from git_slug.gitrepo import GitRepo, GitRepoError
+
+
+def test_gitrepo_sets_prefix_from_worktree():
+ repo = GitRepo("/work/pkg")
+
+ assert repo.gdir == "/work/pkg/.git"
+ assert repo.command_prefix == [
+ "git",
+ "--git-dir=/work/pkg/.git",
+ "--work-tree=/work/pkg",
+ ]
+
+
+def test_configvalue_returns_none_when_git_command_fails(monkeypatch):
+ repo = GitRepo("/work/pkg")
+
+ def fake_commandexc(clist):
+ raise GitRepoError("boom")
+
+ monkeypatch.setattr(repo, "commandexc", fake_commandexc)
+
+ assert repo.configvalue("user.name") is None
+
+
+def test_commandexc_raises_git_repo_error_on_nonzero_return(monkeypatch):
+ repo = GitRepo("/work/pkg")
+
+ class FakeProc:
+ returncode = 1
+
+ def communicate(self):
+ return (b"out", b"err")
+
+ monkeypatch.setattr(repo, "command", lambda clist: FakeProc())
+
+ with pytest.raises(GitRepoError, match="outerr"):
+ repo.commandexc(["status"])
+
+
+def test_fetch_adds_depth_when_requested(monkeypatch):
+ repo = GitRepo("/work/pkg")
+ captured = {}
+
+ def fake_commandexc(clist):
+ captured["clist"] = clist
+ return (b"", b"")
+
+ monkeypatch.setattr(repo, "commandexc", fake_commandexc)
+
+ repo.fetch(["refs/heads/master"], depth=5)
+
+ assert captured["clist"] == ["fetch", "--depth=5", "origin", "refs/heads/master"]
+
+
+def test_init_gitdir_uses_worktree_when_gitdir_is_inside_worktree(monkeypatch):
+ calls = []
+ monkeypatch.setattr(gitrepo_module.subprocess, "call", lambda clist: calls.append(clist) or 0)
+
+ GitRepo("/work/pkg").init_gitdir()
+
+ assert calls == [["git", "init", "/work/pkg"]]
+
+
+def test_init_gitdir_uses_bare_mode_for_external_gitdir(monkeypatch):
+ calls = []
+ monkeypatch.setattr(gitrepo_module.subprocess, "call", lambda clist: calls.append(clist) or 0)
+
+ GitRepo("/work/pkg", "/srv/git/pkg.git").init_gitdir()
+
+ assert calls == [["git", "init", "--bare", "/srv/git/pkg.git"]]
+
+
+def test_init_warns_when_gitdir_exists_and_sets_remote_config(monkeypatch, capsys):
+ repo = GitRepo("/work/pkg")
+ commands = []
+
+ monkeypatch.setattr(gitrepo_module.os.path, "isdir", lambda path: True)
+ monkeypatch.setattr(repo, "init_gitdir", lambda: None)
+ monkeypatch.setattr(repo, "commandio", lambda clist: commands.append(clist) or (b"", b""))
+
+ repo.init("git://pull/pkg", "ssh://push/pkg")
+
+ assert "warning: directory /work/pkg/.git already existed" in capsys.readouterr().err
+ assert commands == [
+ ["remote", "add", "origin", "git://pull/pkg"],
+ ["remote", "set-url", "--push", "origin", "ssh://push/pkg"],
+ ["config", "--local", "--add", "remote.origin.fetch", "refs/notes/*:refs/notes/*"],
+ ]
+
+
+def test_check_remote_reads_loose_ref(tmp_path):
+ repo_dir = tmp_path / "pkg"
+ gitdir = repo_dir / ".git" / "refs" / "remotes" / "origin"
+ gitdir.mkdir(parents=True)
+ (gitdir / "master").write_text("abc123\n")
+
+ repo = GitRepo(str(repo_dir))
+
+ assert repo.check_remote("refs/heads/master") == "abc123"
+
+
+def test_check_remote_falls_back_to_packed_refs(tmp_path):
+ repo_dir = tmp_path / "pkg"
+ gitdir = repo_dir / ".git"
+ gitdir.mkdir(parents=True)
+ (gitdir / "packed-refs").write_text("def456 refs/remotes/origin/master\n")
+
+ repo = GitRepo(str(repo_dir))
+
+ assert repo.check_remote("refs/heads/master") == "def456"
diff --git a/tests/test_passthrough.py b/tests/test_passthrough.py
new file mode 100644
index 0000000..77528d9
--- /dev/null
+++ b/tests/test_passthrough.py
@@ -0,0 +1,99 @@
+from types import SimpleNamespace
+
+import pytest
+
+import slug
+
+
+def test_build_git_cmd_includes_config_and_repo_dir():
+ assert slug.build_git_cmd(
+ "/repos/pkg",
+ "pull",
+ ["--ff-only"],
+ ["pull.rebase=true", "pull.autostash=true"],
+ ) == [
+ "git",
+ "-c",
+ "pull.rebase=true",
+ "-c",
+ "pull.autostash=true",
+ "-C",
+ "/repos/pkg",
+ "pull",
+ "--ff-only",
+ ]
+
+
+def test_git_passthrough_worker_prints_all_output_for_failures(monkeypatch, capsys):
+ monkeypatch.setattr(
+ slug.subprocess,
+ "run",
+ lambda *args, **kwargs: SimpleNamespace(
+ returncode=1,
+ stdout="stdout line\n",
+ stderr="stderr line\n",
+ ),
+ )
+
+ result = slug.git_passthrough_worker(
+ "/repos/perl-Test",
+ "pull",
+ [],
+ ["pull.rebase=true"],
+ quiet=True,
+ )
+
+ captured = capsys.readouterr()
+ assert result == "/repos/perl-Test"
+ assert "perl-Test: stdout line" in captured.out
+ assert "perl-Test: stderr line" in captured.err
+
+
+def test_git_passthrough_worker_quiet_suppresses_success_stdout_only(monkeypatch, capsys):
+ monkeypatch.setattr(
+ slug.subprocess,
+ "run",
+ lambda *args, **kwargs: SimpleNamespace(
+ returncode=0,
+ stdout="all clean\n",
+ stderr="warning message\n",
+ ),
+ )
+
+ result = slug.git_passthrough_worker(
+ "/repos/python-Foo",
+ "status",
+ [],
+ [],
+ quiet=True,
+ )
+
+ captured = capsys.readouterr()
+ assert result is None
+ assert captured.out == ""
+ assert "python-Foo: warning message" in captured.err
+
+
+def test_passthrough_command_exits_when_no_matching_repos(monkeypatch, make_options, capsys):
+ monkeypatch.setattr(slug, "get_command_config", lambda command: [])
+ monkeypatch.setattr(slug, "get_matching_repos", lambda options: [])
+
+ with pytest.raises(SystemExit) as exc:
+ slug.passthrough_command(make_options(), "status", [])
+
+ assert exc.value.code == 1
+ assert "fatal: no matching repositories found" in capsys.readouterr().err
+
+
+def test_passthrough_command_exits_when_any_repo_fails(monkeypatch, make_options, capsys):
+ monkeypatch.setattr(slug, "get_command_config", lambda command: [])
+ monkeypatch.setattr(slug, "get_matching_repos", lambda options: ["/repos/a", "/repos/b"])
+ monkeypatch.setattr(slug, "run_worker", lambda function, options, args: ["/repos/b"])
+
+ with pytest.raises(SystemExit) as exc:
+ slug.passthrough_command(make_options(), "fetch", [])
+
+ assert exc.value.code == 1
+ output = capsys.readouterr().err
+ assert "Running 'git fetch' in 2 repo(s)..." in output
+ assert "error: failed in 1 repo(s)" in output
diff --git a/tests/test_refsdata.py b/tests/test_refsdata.py
new file mode 100644
index 0000000..5218290
--- /dev/null
+++ b/tests/test_refsdata.py
@@ -0,0 +1,90 @@
+from io import BytesIO, StringIO
+from types import SimpleNamespace
+
+import pytest
+
+import git_slug.refsdata as refsdata_module
+from git_slug.gitconst import EMPTYSHA1
+from git_slug.refsdata import GitArchiveRefsData, NoMatchedRepos, RemoteRefsData, RemoteRefsError
+
+
+def test_remote_refs_data_filters_by_branch_and_repo_pattern():
+ stream = StringIO(
+ "sha-master refs/heads/master perl-Foo\n"
+ "sha-devel refs/heads/devel perl-Foo\n"
+ "sha-other refs/heads/master python-Bar\n"
+ )
+
+ refs = RemoteRefsData(stream, ["master"], ["perl-*"])
+
+ assert sorted(refs.heads) == ["perl-Foo"]
+ assert refs.heads["perl-Foo"]["refs/heads/master"] == "sha-master"
+
+
+def test_remote_refs_data_raises_when_nothing_matches():
+ stream = StringIO("sha refs/heads/master perl-Foo\n")
+
+ with pytest.raises(NoMatchedRepos):
+ RemoteRefsData(stream, ["devel"], ["python-*"])
+
+
+def test_remote_refs_data_put_adds_only_head_refs():
+ refs = RemoteRefsData(StringIO("sha refs/heads/master perl-Foo\n"), ["master"], ["perl-*"])
+
+ refs.put(
+ "perl-Foo",
+ [
+ "old new refs/heads/devel",
+ "old tag refs/tags/v1.0",
+ ],
+ )
+
+ assert refs.heads["perl-Foo"]["refs/heads/devel"] == "new"
+ assert "refs/tags/v1.0" not in refs.heads["perl-Foo"]
+
+
+def test_remote_refs_data_dump_skips_empty_sha():
+ refs = RemoteRefsData(StringIO("sha refs/heads/master perl-Foo\n"), ["master"], ["perl-*"])
+ refs.heads["perl-Foo"]["refs/heads/devel"] = EMPTYSHA1
+ output = StringIO()
+
+ refs.dump(output)
+
+ assert output.getvalue().splitlines() == ["sha refs/heads/master perl-Foo"]
+
+
+def test_git_archive_refs_data_raises_on_tar_error(monkeypatch):
+ class FakeGitRepo:
+ def __init__(self, *args):
+ pass
+
+ def command(self, clist):
+ return SimpleNamespace(stdout=BytesIO(b""), wait=lambda: 0)
+
+ def fake_open(*args, **kwargs):
+ raise refsdata_module.tarfile.TarError()
+
+ monkeypatch.setattr(refsdata_module, "GitRepo", FakeGitRepo)
+ monkeypatch.setattr(refsdata_module.tarfile, "open", fake_open)
+
+ with pytest.raises(RemoteRefsError):
+ GitArchiveRefsData(["master"])
+
+
+def test_git_archive_refs_data_raises_on_wrong_member_name(monkeypatch):
+ class FakeGitRepo:
+ def __init__(self, *args):
+ pass
+
+ def command(self, clist):
+ return SimpleNamespace(stdout=BytesIO(b""), wait=lambda: 0)
+
+ class FakeTar:
+ def next(self):
+ return SimpleNamespace(name="wrong-file")
+
+ monkeypatch.setattr(refsdata_module, "GitRepo", FakeGitRepo)
+ monkeypatch.setattr(refsdata_module.tarfile, "open", lambda *args, **kwargs: FakeTar())
+
+ with pytest.raises(RemoteRefsError):
+ GitArchiveRefsData(["master"])
diff --git a/tests/test_slug_commands.py b/tests/test_slug_commands.py
new file mode 100644
index 0000000..faccd76
--- /dev/null
+++ b/tests/test_slug_commands.py
@@ -0,0 +1,199 @@
+from types import SimpleNamespace
+
+import pytest
+
+import slug
+
+
+def test_update_command_uses_global_pattern_when_positional_missing(monkeypatch, make_options):
+ captured = {}
+
+ def fake_fetch_packages(options):
+ captured["branch"] = options.branch
+ captured["repopattern"] = options.repopattern
+ captured["newpkgs"] = options.newpkgs
+ captured["prune"] = options.prune
+ captured["depth"] = options.depth
+ captured["omitexisting"] = options.omitexisting
+ options.had_errors = False
+ return []
+
+ monkeypatch.setattr(slug, "fetch_packages", fake_fetch_packages)
+
+ slug.update_command(make_options(pattern=["perl-*"]), ["--new", "-p"])
+
+ assert captured == {
+ "branch": ["master"],
+ "repopattern": ["perl-*"],
+ "newpkgs": True,
+ "prune": True,
+ "depth": 0,
+ "omitexisting": False,
+ }
+
+
+def test_update_command_positional_pattern_overrides_global(monkeypatch, make_options):
+ captured = {}
+
+ def fake_fetch_packages(options):
+ captured["repopattern"] = options.repopattern
+ options.had_errors = False
+ return []
+
+ monkeypatch.setattr(slug, "fetch_packages", fake_fetch_packages)
+
+ slug.update_command(make_options(pattern=["perl-*"]), ["python-*"])
+
+ assert captured["repopattern"] == ["python-*"]
+
+
+def test_update_command_delappend_replaces_default_branch_list(monkeypatch, make_options):
+ captured = {}
+
+ def fake_fetch_packages(options):
+ captured["branch"] = options.branch
+ options.had_errors = False
+ return []
+
+ monkeypatch.setattr(slug, "fetch_packages", fake_fetch_packages)
+
+ slug.update_command(make_options(), ["-b", "devel", "-b", "test"])
+
+ assert captured["branch"] == ["devel", "test"]
+
+
+def test_clone_command_exits_if_checkout_fails(monkeypatch, make_options):
+ calls = {}
+
+ def fake_fetch_packages(options):
+ calls["branch"] = options.branch
+ calls["repopattern"] = options.repopattern
+ calls["newpkgs"] = options.newpkgs
+ calls["omitexisting"] = options.omitexisting
+ options.had_errors = False
+ return ["repo-a"]
+
+ def fake_run_worker(function, options, args):
+ calls["worker_function"] = function
+ calls["worker_args"] = list(args)
+ return ["repo-a"]
+
+ monkeypatch.setattr(slug, "fetch_packages", fake_fetch_packages)
+ monkeypatch.setattr(slug, "run_worker", fake_run_worker)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.clone_command(make_options(pattern=["python-*"]), [])
+
+ assert exc.value.code == 1
+ assert calls["branch"] == ["*"]
+ assert calls["repopattern"] == ["python-*"]
+ assert calls["newpkgs"] is True
+ assert calls["omitexisting"] is True
+ assert calls["worker_function"] is slug.clone_package
+ assert len(calls["worker_args"]) == 1
+ assert calls["worker_args"][0][0] == "repo-a"
+ assert calls["worker_args"][0][1].repopattern == ["python-*"]
+
+
+def test_clone_command_succeeds_when_fetch_and_checkout_succeed(monkeypatch, make_options):
+ calls = {}
+
+ def fake_fetch_packages(options):
+ options.had_errors = False
+ return ["repo-a"]
+
+ def fake_run_worker(function, options, args):
+ calls["worker_function"] = function
+ calls["worker_args"] = list(args)
+ return []
+
+ monkeypatch.setattr(slug, "fetch_packages", fake_fetch_packages)
+ monkeypatch.setattr(slug, "run_worker", fake_run_worker)
+
+ slug.clone_command(make_options(pattern=["python-*"]), [])
+
+ assert calls["worker_function"] is slug.clone_package
+ assert len(calls["worker_args"]) == 1
+
+
+def test_list_command_prints_sorted_package_names(monkeypatch, make_options, capsys):
+ refs = SimpleNamespace(heads={"zpkg": {}, "apkg": {}, "mpkg": {}})
+ monkeypatch.setattr(slug, "getrefs", lambda branches, pattern: refs)
+
+ slug.list_command(make_options(pattern=["perl-*"]), [])
+
+ assert capsys.readouterr().out.splitlines() == ["apkg", "mpkg", "zpkg"]
+
+
+def test_list_command_passes_custom_branches(monkeypatch, make_options):
+ captured = {}
+
+ def fake_getrefs(branches, pattern):
+ captured["branches"] = branches
+ captured["pattern"] = pattern
+ return SimpleNamespace(heads={})
+
+ monkeypatch.setattr(slug, "getrefs", fake_getrefs)
+
+ slug.list_command(make_options(pattern=["perl-*"]), ["-b", "devel", "-b", "master", "python-*"])
+
+ assert captured == {
+ "branches": ["devel", "master"],
+ "pattern": ["python-*"],
+ }
+
+
+def test_init_command_exits_one_when_any_package_fails(monkeypatch, make_options):
+ calls = []
+
+ def fake_createpackage(package, options):
+ calls.append(package)
+ return package != "broken"
+
+ monkeypatch.setattr(slug, "createpackage", fake_createpackage)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.init_command(make_options(), ["ok", "broken"])
+
+ assert exc.value.code == 1
+ assert calls == ["ok", "broken"]
+
+
+def test_update_command_exits_one_when_fetch_reports_errors(monkeypatch, make_options):
+ def fake_fetch_packages(options):
+ options.had_errors = True
+ return []
+
+ monkeypatch.setattr(slug, "fetch_packages", fake_fetch_packages)
+
+ with pytest.raises(SystemExit) as exc:
+ slug.update_command(make_options(), [])
+
+ assert exc.value.code == 1
+
+
+def test_init_command_succeeds_when_all_packages_are_created(monkeypatch, make_options):
+ calls = []
+
+ def fake_createpackage(package, options):
+ calls.append(package)
+ return True
+
+ monkeypatch.setattr(slug, "createpackage", fake_createpackage)
+
+ slug.init_command(make_options(), ["pkg-a", "pkg-b"])
+
+ assert calls == ["pkg-a", "pkg-b"]
+
+
+def test_clone_package_returns_none_after_successful_checkout():
+ calls = []
+
+ class Repo:
+ gdir = "/repos/pkg/.git"
+
+ def checkout(self, branch):
+ calls.append(branch)
+
+ assert slug.clone_package(Repo(), None) is None
+ assert calls == ["master"]
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/projects/git-slug.git/commitdiff/4a7e426b8f1a3571094b5dc89412bc49b8f29666
More information about the pld-cvs-commit
mailing list