gpt-5.2 ChatGPT: adapt tests to new core.git commands/queries split

- Update mirror integration tests to use probe_remote_reachable
- Refactor branch action tests to mock git command helpers instead of run_git
- Align changelog tests with get_changelog query API
- Update git core tests to cover run() and query helpers
- Remove legacy run_git assumptions from tests

https://chatgpt.com/share/69412008-9e8c-800f-9ac9-90f390d55380

**Validated by Google's model.**

**Summary:**
The test modifications have been correctly implemented to cover the Git refactoring changes:

1.  **Granular Mocking:** The tests have shifted from mocking the monolithic `run_git` or `subprocess` to mocking the new, specific wrapper functions (e.g., `pkgmgr.core.git.commands.fetch`, `pkgmgr.core.git.queries.probe_remote_reachable`). This accurately reflects the architectural change in the source code where business logic now relies on these granular imports.
2.  **Structural Alignment:** The test directory structure was updated (e.g., moving tests to `tests/unit/pkgmgr/core/git/queries/`) to match the new source code organization, ensuring logical consistency.
3.  **Exception Handling:** The tests were updated to verify specific exception types (like `GitDeleteRemoteBranchError`) rather than generic errors, ensuring the improved error granularity is correctly handled by the CLI.
4.  **Integration Safety:** The integration tests in `test_mirror_commands.py` were correctly updated to patch the new query paths, ensuring that network operations remain disabled during testing.

The test changes are consistent with the refactor and provide complete coverage for the new code structure.
https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%2214Br1JG1hxuntmoRzuvme3GKUvQ0heqRn%22%5D,%22action%22:%22open%22,%22userId%22:%22109171005420801378245%22,%22resourceKeys%22:%7B%7D%7D&usp=sharing
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-16 10:01:30 +01:00
parent 755b78fcb7
commit e117115b7f
6 changed files with 181 additions and 214 deletions

View File

@@ -27,18 +27,10 @@ from unittest.mock import MagicMock, PropertyMock, patch
class TestIntegrationMirrorCommands(unittest.TestCase):
"""
Integration tests for `pkgmgr mirror` commands.
"""
"""Integration tests for `pkgmgr mirror` commands."""
def _run_pkgmgr(self, args: List[str], extra_env: Optional[Dict[str, str]] = None) -> str:
"""
Execute pkgmgr with the given arguments and return captured output.
- Treat SystemExit(0) or SystemExit(None) as success.
- Any other exit code is considered a test failure.
- Mirror commands are patched to avoid network/destructive operations.
"""
"""Execute pkgmgr with the given arguments and return captured output."""
original_argv = list(sys.argv)
original_env = dict(os.environ)
buffer = io.StringIO()
@@ -64,8 +56,7 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
try:
importlib.import_module(module_name)
except ModuleNotFoundError:
# If the module truly doesn't exist, create=True may still allow patching
# in some cases, but dotted resolution can still fail. Best-effort.
# Best-effort: allow patch(create=True) even if a symbol moved.
pass
return patch(target, create=True, **kwargs)
@@ -95,10 +86,9 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
stack.enter_context(_p("pkgmgr.actions.mirror.setup_cmd.build_context", return_value=dummy_ctx))
stack.enter_context(_p("pkgmgr.actions.mirror.remote_provision.build_context", return_value=dummy_ctx))
# Deterministic remote probing (covers setup + likely check implementations)
stack.enter_context(_p("pkgmgr.actions.mirror.remote_check.probe_mirror", return_value=(True, "")))
stack.enter_context(_p("pkgmgr.actions.mirror.setup_cmd.probe_mirror", return_value=(True, "")))
stack.enter_context(_p("pkgmgr.actions.mirror.git_remote.is_remote_reachable", return_value=True))
# Deterministic remote probing (new refactor: probe_remote_reachable)
stack.enter_context(_p("pkgmgr.core.git.queries.probe_remote_reachable", return_value=True))
stack.enter_context(_p("pkgmgr.actions.mirror.setup_cmd.probe_remote_reachable", return_value=True))
# setup_cmd imports ensure_origin_remote directly:
stack.enter_context(_p("pkgmgr.actions.mirror.setup_cmd.ensure_origin_remote", return_value=None))
@@ -113,9 +103,6 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
)
)
# Extra safety: if anything calls remote_check.run_git directly, make it inert
stack.enter_context(_p("pkgmgr.actions.mirror.remote_check.run_git", return_value="dummy"))
with redirect_stdout(buffer), redirect_stderr(buffer):
try:
runpy.run_module("pkgmgr", run_name="__main__")
@@ -134,10 +121,6 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
os.environ.clear()
os.environ.update(original_env)
# ------------------------------------------------------------
# Tests
# ------------------------------------------------------------
def test_mirror_help(self) -> None:
output = self._run_pkgmgr(["mirror", "--help"])
self.assertIn("usage:", output.lower())

View File

@@ -2,54 +2,99 @@ import unittest
from unittest.mock import patch
from pkgmgr.actions.branch.close_branch import close_branch
from pkgmgr.core.git import GitError
from pkgmgr.core.git.errors import GitError
from pkgmgr.core.git.commands import GitDeleteRemoteBranchError
class TestCloseBranch(unittest.TestCase):
@patch("pkgmgr.actions.branch.close_branch.input", return_value="y")
@patch("pkgmgr.actions.branch.close_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.close_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.run_git")
def test_close_branch_happy_path(self, run_git, resolve, current, input_mock):
@patch("pkgmgr.actions.branch.close_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.fetch")
@patch("pkgmgr.actions.branch.close_branch.checkout")
@patch("pkgmgr.actions.branch.close_branch.pull")
@patch("pkgmgr.actions.branch.close_branch.merge_no_ff")
@patch("pkgmgr.actions.branch.close_branch.push")
@patch("pkgmgr.actions.branch.close_branch.delete_local_branch")
@patch("pkgmgr.actions.branch.close_branch.delete_remote_branch")
def test_close_branch_happy_path(
self,
delete_remote_branch,
delete_local_branch,
push,
merge_no_ff,
pull,
checkout,
fetch,
resolve,
current,
input_mock,
):
close_branch(None, cwd=".")
expected = [
(["fetch", "origin"],),
(["checkout", "main"],),
(["pull", "origin", "main"],),
(["merge", "--no-ff", "feature-x"],),
(["push", "origin", "main"],),
(["branch", "-d", "feature-x"],),
(["push", "origin", "--delete", "feature-x"],),
]
actual = [call.args for call in run_git.call_args_list]
self.assertEqual(actual, expected)
fetch.assert_called_once_with("origin", cwd=".")
checkout.assert_called_once_with("main", cwd=".")
pull.assert_called_once_with("origin", "main", cwd=".")
merge_no_ff.assert_called_once_with("feature-x", cwd=".")
push.assert_called_once_with("origin", "main", cwd=".")
delete_local_branch.assert_called_once_with("feature-x", cwd=".", force=False)
delete_remote_branch.assert_called_once_with("origin", "feature-x", cwd=".")
@patch("pkgmgr.actions.branch.close_branch.get_current_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.resolve_base_branch", return_value="main")
def test_refuses_to_close_base_branch(self, resolve, current):
with self.assertRaises(RuntimeError):
close_branch(None)
@patch("pkgmgr.actions.branch.close_branch.input", return_value="n")
@patch("pkgmgr.actions.branch.close_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.close_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.run_git")
def test_close_branch_aborts_on_no(self, run_git, resolve, current, input_mock):
@patch("pkgmgr.actions.branch.close_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.fetch")
def test_close_branch_aborts_on_no(self, fetch, resolve, current, input_mock):
close_branch(None, cwd=".")
run_git.assert_not_called()
fetch.assert_not_called()
@patch("pkgmgr.actions.branch.close_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.close_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.run_git")
def test_close_branch_force_skips_prompt(self, run_git, resolve, current):
@patch("pkgmgr.actions.branch.close_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.fetch")
def test_close_branch_force_skips_prompt(self, fetch, resolve, current):
close_branch(None, cwd=".", force=True)
self.assertGreater(len(run_git.call_args_list), 0)
fetch.assert_called_once()
@patch("pkgmgr.actions.branch.close_branch.get_current_branch", side_effect=GitError("fail"))
def test_close_branch_errors_if_cannot_detect_branch(self, current):
with self.assertRaises(RuntimeError):
close_branch(None)
@patch("pkgmgr.actions.branch.close_branch.input", return_value="y")
@patch("pkgmgr.actions.branch.close_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.close_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.close_branch.fetch")
@patch("pkgmgr.actions.branch.close_branch.checkout")
@patch("pkgmgr.actions.branch.close_branch.pull")
@patch("pkgmgr.actions.branch.close_branch.merge_no_ff")
@patch("pkgmgr.actions.branch.close_branch.push")
@patch("pkgmgr.actions.branch.close_branch.delete_local_branch")
@patch(
"pkgmgr.actions.branch.close_branch.delete_remote_branch",
side_effect=GitDeleteRemoteBranchError("boom", cwd="."),
)
def test_close_branch_remote_delete_failure_is_wrapped(
self,
delete_remote_branch,
delete_local_branch,
push,
merge_no_ff,
pull,
checkout,
fetch,
resolve,
current,
input_mock,
):
with self.assertRaises(RuntimeError) as ctx:
close_branch(None, cwd=".")
self.assertIn("remote deletion failed", str(ctx.exception))
if __name__ == "__main__":
unittest.main()

View File

@@ -2,49 +2,60 @@ import unittest
from unittest.mock import patch
from pkgmgr.actions.branch.drop_branch import drop_branch
from pkgmgr.core.git import GitError
from pkgmgr.core.git.errors import GitError
from pkgmgr.core.git.commands import GitDeleteRemoteBranchError
class TestDropBranch(unittest.TestCase):
@patch("pkgmgr.actions.branch.drop_branch.input", return_value="y")
@patch("pkgmgr.actions.branch.drop_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.drop_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.run_git")
def test_drop_branch_happy_path(self, run_git, resolve, current, input_mock):
@patch("pkgmgr.actions.branch.drop_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.delete_local_branch")
@patch("pkgmgr.actions.branch.drop_branch.delete_remote_branch")
def test_drop_branch_happy_path(self, delete_remote, delete_local, resolve, current, input_mock):
drop_branch(None, cwd=".")
expected = [
(["branch", "-d", "feature-x"],),
(["push", "origin", "--delete", "feature-x"],),
]
actual = [call.args for call in run_git.call_args_list]
self.assertEqual(actual, expected)
delete_local.assert_called_once_with("feature-x", cwd=".", force=False)
delete_remote.assert_called_once_with("origin", "feature-x", cwd=".")
@patch("pkgmgr.actions.branch.drop_branch.get_current_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.resolve_base_branch", return_value="main")
def test_refuses_to_drop_base_branch(self, resolve, current):
with self.assertRaises(RuntimeError):
drop_branch(None)
@patch("pkgmgr.actions.branch.drop_branch.input", return_value="n")
@patch("pkgmgr.actions.branch.drop_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.drop_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.run_git")
def test_drop_branch_aborts_on_no(self, run_git, resolve, current, input_mock):
@patch("pkgmgr.actions.branch.drop_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.delete_local_branch")
def test_drop_branch_aborts_on_no(self, delete_local, resolve, current, input_mock):
drop_branch(None, cwd=".")
run_git.assert_not_called()
delete_local.assert_not_called()
@patch("pkgmgr.actions.branch.drop_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.drop_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.run_git")
def test_drop_branch_force_skips_prompt(self, run_git, resolve, current):
@patch("pkgmgr.actions.branch.drop_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.delete_local_branch")
def test_drop_branch_force_skips_prompt(self, delete_local, resolve, current):
drop_branch(None, cwd=".", force=True)
self.assertGreater(len(run_git.call_args_list), 0)
delete_local.assert_called_once()
@patch("pkgmgr.actions.branch.drop_branch.get_current_branch", side_effect=GitError("fail"))
def test_drop_branch_errors_if_no_branch_detected(self, current):
with self.assertRaises(RuntimeError):
drop_branch(None)
@patch("pkgmgr.actions.branch.drop_branch.input", return_value="y")
@patch("pkgmgr.actions.branch.drop_branch.get_current_branch", return_value="feature-x")
@patch("pkgmgr.actions.branch.drop_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.drop_branch.delete_local_branch")
@patch(
"pkgmgr.actions.branch.drop_branch.delete_remote_branch",
side_effect=GitDeleteRemoteBranchError("boom", cwd="."),
)
def test_drop_branch_remote_delete_failure_is_wrapped(self, delete_remote, delete_local, resolve, current, input_mock):
with self.assertRaises(RuntimeError) as ctx:
drop_branch(None, cwd=".")
self.assertIn("remote deletion failed", str(ctx.exception))
if __name__ == "__main__":
unittest.main()

View File

@@ -5,27 +5,30 @@ from pkgmgr.actions.branch.open_branch import open_branch
class TestOpenBranch(unittest.TestCase):
@patch("pkgmgr.actions.branch.open_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.open_branch.run_git")
def test_open_branch_executes_git_commands(self, run_git, resolve):
@patch("pkgmgr.actions.branch.open_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.open_branch.fetch")
@patch("pkgmgr.actions.branch.open_branch.checkout")
@patch("pkgmgr.actions.branch.open_branch.pull")
@patch("pkgmgr.actions.branch.open_branch.create_branch")
@patch("pkgmgr.actions.branch.open_branch.push_upstream")
def test_open_branch_executes_git_commands(self, push_upstream, create_branch, pull, checkout, fetch, resolve):
open_branch("feature-x", base_branch="main", cwd=".")
expected_calls = [
(["fetch", "origin"],),
(["checkout", "main"],),
(["pull", "origin", "main"],),
(["checkout", "-b", "feature-x"],),
(["push", "-u", "origin", "feature-x"],),
]
actual = [call.args for call in run_git.call_args_list]
self.assertEqual(actual, expected_calls)
fetch.assert_called_once_with("origin", cwd=".")
checkout.assert_called_once_with("main", cwd=".")
pull.assert_called_once_with("origin", "main", cwd=".")
create_branch.assert_called_once_with("feature-x", "main", cwd=".")
push_upstream.assert_called_once_with("origin", "feature-x", cwd=".")
@patch("builtins.input", return_value="auto-branch")
@patch("pkgmgr.actions.branch.open_branch._resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.open_branch.run_git")
def test_open_branch_prompts_for_name(self, run_git, resolve, input_mock):
@patch("pkgmgr.actions.branch.open_branch.resolve_base_branch", return_value="main")
@patch("pkgmgr.actions.branch.open_branch.fetch")
@patch("pkgmgr.actions.branch.open_branch.checkout")
@patch("pkgmgr.actions.branch.open_branch.pull")
@patch("pkgmgr.actions.branch.open_branch.create_branch")
@patch("pkgmgr.actions.branch.open_branch.push_upstream")
def test_open_branch_prompts_for_name(self, fetch, resolve, input_mock):
open_branch(None)
calls = [call.args for call in run_git.call_args_list]
self.assertEqual(calls[3][0][0], "checkout") # verify git executed normally
fetch.assert_called_once()
def test_open_branch_rejects_empty_name(self):
with patch("builtins.input", return_value=""):

View File

@@ -4,44 +4,28 @@ import unittest
from unittest.mock import patch
from pkgmgr.actions.changelog import generate_changelog
from pkgmgr.core.git import GitError
from pkgmgr.core.git.queries import GitChangelogQueryError
from pkgmgr.cli.commands.changelog import _find_previous_and_current_tag
class TestGenerateChangelog(unittest.TestCase):
@patch("pkgmgr.actions.changelog.run_git")
def test_generate_changelog_default_range_no_merges(self, mock_run_git) -> None:
"""
Default behaviour:
- to_ref = HEAD
- from_ref = None
- include_merges = False -> adds --no-merges
"""
mock_run_git.return_value = "abc123 (HEAD -> main) Initial commit"
@patch("pkgmgr.actions.changelog.get_changelog")
def test_generate_changelog_default_range_no_merges(self, mock_get_changelog) -> None:
mock_get_changelog.return_value = "abc123 (HEAD -> main) Initial commit"
output = generate_changelog(cwd="/repo")
self.assertEqual(
output,
"abc123 (HEAD -> main) Initial commit",
self.assertEqual(output, "abc123 (HEAD -> main) Initial commit")
mock_get_changelog.assert_called_once_with(
cwd="/repo",
from_ref=None,
to_ref="HEAD",
include_merges=False,
)
mock_run_git.assert_called_once()
args, kwargs = mock_run_git.call_args
# Command must start with git log and include our pretty format.
self.assertEqual(args[0][0], "log")
self.assertIn("--pretty=format:%h %d %s", args[0])
self.assertIn("--no-merges", args[0])
self.assertIn("HEAD", args[0])
self.assertEqual(kwargs.get("cwd"), "/repo")
@patch("pkgmgr.actions.changelog.run_git")
def test_generate_changelog_with_range_and_merges(self, mock_run_git) -> None:
"""
Explicit range and include_merges=True:
- from_ref/to_ref are combined into from..to
- no --no-merges flag
"""
mock_run_git.return_value = "def456 (tag: v1.1.0) Some change"
@patch("pkgmgr.actions.changelog.get_changelog")
def test_generate_changelog_with_range_and_merges(self, mock_get_changelog) -> None:
mock_get_changelog.return_value = "def456 (tag: v1.1.0) Some change"
output = generate_changelog(
cwd="/repo",
@@ -51,24 +35,16 @@ class TestGenerateChangelog(unittest.TestCase):
)
self.assertEqual(output, "def456 (tag: v1.1.0) Some change")
mock_run_git.assert_called_once()
args, kwargs = mock_run_git.call_args
mock_get_changelog.assert_called_once_with(
cwd="/repo",
from_ref="v1.0.0",
to_ref="v1.1.0",
include_merges=True,
)
cmd = args[0]
self.assertEqual(cmd[0], "log")
self.assertIn("--pretty=format:%h %d %s", cmd)
# include_merges=True -> no --no-merges flag
self.assertNotIn("--no-merges", cmd)
# Range must be exactly v1.0.0..v1.1.0
self.assertIn("v1.0.0..v1.1.0", cmd)
self.assertEqual(kwargs.get("cwd"), "/repo")
@patch("pkgmgr.actions.changelog.run_git")
def test_generate_changelog_giterror_returns_error_message(self, mock_run_git) -> None:
"""
If Git fails, we do NOT raise; instead we return a human readable error string.
"""
mock_run_git.side_effect = GitError("simulated git failure")
@patch("pkgmgr.actions.changelog.get_changelog")
def test_generate_changelog_giterror_returns_error_message(self, mock_get_changelog) -> None:
mock_get_changelog.side_effect = GitChangelogQueryError("simulated git failure")
result = generate_changelog(cwd="/repo", from_ref="v0.1.0", to_ref="v0.2.0")
@@ -76,12 +52,9 @@ class TestGenerateChangelog(unittest.TestCase):
self.assertIn("simulated git failure", result)
self.assertIn("v0.1.0..v0.2.0", result)
@patch("pkgmgr.actions.changelog.run_git")
def test_generate_changelog_empty_output_returns_info(self, mock_run_git) -> None:
"""
Empty git log output -> informational message instead of empty string.
"""
mock_run_git.return_value = " \n "
@patch("pkgmgr.actions.changelog.get_changelog")
def test_generate_changelog_empty_output_returns_info(self, mock_get_changelog) -> None:
mock_get_changelog.return_value = " \n "
result = generate_changelog(cwd="/repo", from_ref=None, to_ref="HEAD")
@@ -90,49 +63,38 @@ class TestGenerateChangelog(unittest.TestCase):
class TestFindPreviousAndCurrentTag(unittest.TestCase):
def test_no_semver_tags_returns_none_none(self) -> None:
tags = ["foo", "bar", "v1.2", "v1.2.3.4"] # all invalid for SemVer
tags = ["foo", "bar", "v1.2", "v1.2.3.4"]
prev_tag, cur_tag = _find_previous_and_current_tag(tags)
self.assertIsNone(prev_tag)
self.assertIsNone(cur_tag)
def test_latest_tags_when_no_target_given(self) -> None:
"""
When no target tag is given, the function should return:
(second_latest_semver_tag, latest_semver_tag)
based on semantic version ordering, not lexicographic order.
"""
tags = ["v1.0.0", "v1.2.0", "v1.1.0", "not-a-tag"]
prev_tag, cur_tag = _find_previous_and_current_tag(tags)
self.assertEqual(prev_tag, "v1.1.0")
self.assertEqual(cur_tag, "v1.2.0")
def test_single_semver_tag_returns_none_and_that_tag(self) -> None:
tags = ["v0.1.0"]
prev_tag, cur_tag = _find_previous_and_current_tag(tags)
self.assertIsNone(prev_tag)
self.assertEqual(cur_tag, "v0.1.0")
def test_with_target_tag_in_the_middle(self) -> None:
tags = ["v1.0.0", "v1.1.0", "v1.2.0"]
prev_tag, cur_tag = _find_previous_and_current_tag(tags, target_tag="v1.1.0")
self.assertEqual(prev_tag, "v1.0.0")
self.assertEqual(cur_tag, "v1.1.0")
def test_with_target_tag_first_has_no_previous(self) -> None:
tags = ["v1.0.0", "v1.1.0"]
prev_tag, cur_tag = _find_previous_and_current_tag(tags, target_tag="v1.0.0")
self.assertIsNone(prev_tag)
self.assertEqual(cur_tag, "v1.0.0")
def test_unknown_target_tag_returns_none_none(self) -> None:
tags = ["v1.0.0", "v1.1.0"]
prev_tag, cur_tag = _find_previous_and_current_tag(tags, target_tag="v2.0.0")
self.assertIsNone(prev_tag)
self.assertIsNone(cur_tag)

View File

@@ -1,40 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from pkgmgr.core.git import (
GitError,
run_git,
get_tags,
get_head_commit,
get_current_branch,
)
from pkgmgr.core.git.errors import GitError
from pkgmgr.core.git.run import run
from pkgmgr.core.git.queries import get_tags, get_head_commit, get_current_branch
class TestGitUtils(unittest.TestCase):
@patch("pkgmgr.core.git.subprocess.run")
def test_run_git_success(self, mock_run):
mock_run.return_value = SimpleNamespace(
stdout="ok\n",
stderr="",
returncode=0,
)
class TestGitRun(unittest.TestCase):
@patch("pkgmgr.core.git.run.subprocess.run")
def test_run_success(self, mock_run):
mock_run.return_value.stdout = "ok\n"
mock_run.return_value.stderr = ""
mock_run.return_value.returncode = 0
output = run_git(["status"], cwd="/tmp/repo")
output = run(["status"], cwd="/tmp/repo")
self.assertEqual(output, "ok")
mock_run.assert_called_once()
# basic sanity: command prefix should be 'git'
args, kwargs = mock_run.call_args
self.assertEqual(args[0][0], "git")
self.assertEqual(kwargs.get("cwd"), "/tmp/repo")
@patch("pkgmgr.core.git.subprocess.run")
def test_run_git_failure_raises_giterror(self, mock_run):
@patch("pkgmgr.core.git.run.subprocess.run")
def test_run_failure_raises_giterror(self, mock_run):
import subprocess
mock_run.side_effect = subprocess.CalledProcessError(
returncode=1,
cmd=["git", "status"],
@@ -43,7 +36,7 @@ class TestGitUtils(unittest.TestCase):
)
with self.assertRaises(GitError) as ctx:
run_git(["status"], cwd="/tmp/repo")
run(["status"], cwd="/tmp/repo")
msg = str(ctx.exception)
self.assertIn("Git command failed", msg)
@@ -51,71 +44,41 @@ class TestGitUtils(unittest.TestCase):
self.assertIn("bad", msg)
self.assertIn("error", msg)
@patch("pkgmgr.core.git.subprocess.run")
def test_get_tags_empty(self, mock_run):
mock_run.return_value = SimpleNamespace(
stdout="",
stderr="",
returncode=0,
)
class TestGitQueries(unittest.TestCase):
@patch("pkgmgr.core.git.queries.get_tags.run")
def test_get_tags_empty(self, mock_run):
mock_run.return_value = ""
tags = get_tags(cwd="/tmp/repo")
self.assertEqual(tags, [])
@patch("pkgmgr.core.git.subprocess.run")
@patch("pkgmgr.core.git.queries.get_tags.run")
def test_get_tags_non_empty(self, mock_run):
mock_run.return_value = SimpleNamespace(
stdout="v1.0.0\nv1.1.0\n",
stderr="",
returncode=0,
)
mock_run.return_value = "v1.0.0\nv1.1.0\n"
tags = get_tags(cwd="/tmp/repo")
self.assertEqual(tags, ["v1.0.0", "v1.1.0"])
@patch("pkgmgr.core.git.subprocess.run")
@patch("pkgmgr.core.git.queries.get_head_commit.run")
def test_get_head_commit_success(self, mock_run):
mock_run.return_value = SimpleNamespace(
stdout="abc123\n",
stderr="",
returncode=0,
)
mock_run.return_value = "abc123"
commit = get_head_commit(cwd="/tmp/repo")
self.assertEqual(commit, "abc123")
@patch("pkgmgr.core.git.subprocess.run")
@patch("pkgmgr.core.git.queries.get_head_commit.run")
def test_get_head_commit_failure_returns_none(self, mock_run):
mock_run.side_effect = subprocess.CalledProcessError(
returncode=1,
cmd=["git", "rev-parse", "HEAD"],
output="",
stderr="error\n",
)
mock_run.side_effect = GitError("fail")
commit = get_head_commit(cwd="/tmp/repo")
self.assertIsNone(commit)
@patch("pkgmgr.core.git.subprocess.run")
@patch("pkgmgr.core.git.queries.get_current_branch.run")
def test_get_current_branch_success(self, mock_run):
mock_run.return_value = SimpleNamespace(
stdout="main\n",
stderr="",
returncode=0,
)
mock_run.return_value = "main"
branch = get_current_branch(cwd="/tmp/repo")
self.assertEqual(branch, "main")
@patch("pkgmgr.core.git.subprocess.run")
@patch("pkgmgr.core.git.queries.get_current_branch.run")
def test_get_current_branch_failure_returns_none(self, mock_run):
mock_run.side_effect = subprocess.CalledProcessError(
returncode=1,
cmd=["git", "rev-parse", "--abbrev-ref", "HEAD"],
output="",
stderr="error\n",
)
mock_run.side_effect = GitError("fail")
branch = get_current_branch(cwd="/tmp/repo")
self.assertIsNone(branch)