[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