diff --git a/tests/integration/test_mirror_commands.py b/tests/integration/test_mirror_commands.py index 5d57568..b65ebcc 100644 --- a/tests/integration/test_mirror_commands.py +++ b/tests/integration/test_mirror_commands.py @@ -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()) diff --git a/tests/unit/pkgmgr/actions/branch/test_close_branch.py b/tests/unit/pkgmgr/actions/branch/test_close_branch.py index 250843b..7fcede6 100644 --- a/tests/unit/pkgmgr/actions/branch/test_close_branch.py +++ b/tests/unit/pkgmgr/actions/branch/test_close_branch.py @@ -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() diff --git a/tests/unit/pkgmgr/actions/branch/test_drop_branch.py b/tests/unit/pkgmgr/actions/branch/test_drop_branch.py index f85902c..ff851c3 100644 --- a/tests/unit/pkgmgr/actions/branch/test_drop_branch.py +++ b/tests/unit/pkgmgr/actions/branch/test_drop_branch.py @@ -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() diff --git a/tests/unit/pkgmgr/actions/branch/test_open_branch.py b/tests/unit/pkgmgr/actions/branch/test_open_branch.py index edbd043..d6ed474 100644 --- a/tests/unit/pkgmgr/actions/branch/test_open_branch.py +++ b/tests/unit/pkgmgr/actions/branch/test_open_branch.py @@ -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=""): diff --git a/tests/unit/pkgmgr/actions/test_changelog.py b/tests/unit/pkgmgr/actions/test_changelog.py index 2d985c2..cbcc7e7 100644 --- a/tests/unit/pkgmgr/actions/test_changelog.py +++ b/tests/unit/pkgmgr/actions/test_changelog.py @@ -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) diff --git a/tests/unit/pkgmgr/core/test_git_utils.py b/tests/unit/pkgmgr/core/test_git_utils.py index 46739b0..08ea844 100644 --- a/tests/unit/pkgmgr/core/test_git_utils.py +++ b/tests/unit/pkgmgr/core/test_git_utils.py @@ -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)