from __future__ import annotations import unittest from unittest.mock import patch from pkgmgr.core.git import GitError from pkgmgr.actions.release.git_ops import ( ensure_clean_and_synced, is_highest_version_tag, run_git_command, update_latest_tag, ) class TestRunGitCommand(unittest.TestCase): @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_run_git_command_success(self, mock_run) -> None: run_git_command("git status") mock_run.assert_called_once() args, kwargs = mock_run.call_args self.assertIn("git status", args[0]) self.assertTrue(kwargs.get("check")) self.assertTrue(kwargs.get("capture_output")) self.assertTrue(kwargs.get("text")) @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_run_git_command_failure_raises_git_error(self, mock_run) -> None: from subprocess import CalledProcessError mock_run.side_effect = CalledProcessError( returncode=1, cmd="git status", output="stdout", stderr="stderr", ) with self.assertRaises(GitError): run_git_command("git status") class TestEnsureCleanAndSynced(unittest.TestCase): def _fake_run(self, cmd: str, *args, **kwargs): class R: def __init__(self, stdout: str = "", stderr: str = "", returncode: int = 0): self.stdout = stdout self.stderr = stderr self.returncode = returncode # upstream detection if "git rev-parse --abbrev-ref --symbolic-full-name @{u}" in cmd: return R(stdout="origin/main") # fetch/pull should be invoked in real mode if cmd == "git fetch --prune --tags": return R(stdout="") if cmd == "git pull --ff-only": return R(stdout="Already up to date.") return R(stdout="") @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_ensure_clean_and_synced_preview_does_not_run_git_commands(self, mock_run) -> None: def fake(cmd: str, *args, **kwargs): class R: def __init__(self, stdout: str = ""): self.stdout = stdout self.stderr = "" self.returncode = 0 if "git rev-parse --abbrev-ref --symbolic-full-name @{u}" in cmd: return R(stdout="origin/main") return R(stdout="") mock_run.side_effect = fake ensure_clean_and_synced(preview=True) called_cmds = [c.args[0] for c in mock_run.call_args_list] self.assertTrue(any("git rev-parse" in c for c in called_cmds)) self.assertFalse(any(c == "git fetch --prune --tags" for c in called_cmds)) self.assertFalse(any(c == "git pull --ff-only" for c in called_cmds)) @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_ensure_clean_and_synced_no_upstream_skips(self, mock_run) -> None: def fake(cmd: str, *args, **kwargs): class R: def __init__(self, stdout: str = ""): self.stdout = stdout self.stderr = "" self.returncode = 0 if "git rev-parse --abbrev-ref --symbolic-full-name @{u}" in cmd: return R(stdout="") # no upstream return R(stdout="") mock_run.side_effect = fake ensure_clean_and_synced(preview=False) called_cmds = [c.args[0] for c in mock_run.call_args_list] self.assertTrue(any("git rev-parse" in c for c in called_cmds)) self.assertFalse(any(c == "git fetch --prune --tags" for c in called_cmds)) self.assertFalse(any(c == "git pull --ff-only" for c in called_cmds)) @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_ensure_clean_and_synced_real_runs_fetch_and_pull(self, mock_run) -> None: mock_run.side_effect = self._fake_run ensure_clean_and_synced(preview=False) called_cmds = [c.args[0] for c in mock_run.call_args_list] self.assertIn("git fetch origin --prune --tags --force", called_cmds) self.assertIn("git pull --ff-only", called_cmds) class TestIsHighestVersionTag(unittest.TestCase): @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_is_highest_version_tag_no_tags_true(self, mock_run) -> None: def fake(cmd: str, *args, **kwargs): class R: def __init__(self, stdout: str = ""): self.stdout = stdout self.stderr = "" self.returncode = 0 if "git tag --list" in cmd and "'v*'" in cmd: return R(stdout="") # no tags return R(stdout="") mock_run.side_effect = fake self.assertTrue(is_highest_version_tag("v1.0.0")) # ensure at least the list command was queried called_cmds = [c.args[0] for c in mock_run.call_args_list] self.assertTrue(any("git tag --list" in c for c in called_cmds)) @patch("pkgmgr.actions.release.git_ops.subprocess.run") def test_is_highest_version_tag_compares_sort_v(self, mock_run) -> None: """ This test is aligned with the CURRENT implementation: return tag >= latest which is a *string comparison*, not a semantic version compare. Therefore, a candidate like v1.2.0 is lexicographically >= v1.10.0 (because '2' > '1' at the first differing char after 'v1.'). """ def fake(cmd: str, *args, **kwargs): class R: def __init__(self, stdout: str = ""): self.stdout = stdout self.stderr = "" self.returncode = 0 if cmd.strip() == "git tag --list 'v*'": return R(stdout="v1.0.0\nv1.2.0\nv1.10.0\n") if "git tag --list 'v*'" in cmd and "sort -V" in cmd and "tail -n1" in cmd: return R(stdout="v1.10.0") return R(stdout="") mock_run.side_effect = fake # With the current implementation (string >=), both of these are True. self.assertTrue(is_highest_version_tag("v1.10.0")) self.assertTrue(is_highest_version_tag("v1.2.0")) # And a clearly lexicographically smaller candidate should be False. # Example: "v1.0.0" < "v1.10.0" self.assertFalse(is_highest_version_tag("v1.0.0")) # Ensure both capture commands were executed called_cmds = [c.args[0] for c in mock_run.call_args_list] self.assertTrue(any(cmd == "git tag --list 'v*'" for cmd in called_cmds)) self.assertTrue(any("sort -V" in cmd and "tail -n1" in cmd for cmd in called_cmds)) class TestUpdateLatestTag(unittest.TestCase): @patch("pkgmgr.actions.release.git_ops.run_git_command") def test_update_latest_tag_preview_does_not_call_git(self, mock_run_git_command) -> None: update_latest_tag("v1.2.3", preview=True) mock_run_git_command.assert_not_called() @patch("pkgmgr.actions.release.git_ops.run_git_command") def test_update_latest_tag_real_calls_git(self, mock_run_git_command) -> None: update_latest_tag("v1.2.3", preview=False) calls = [c.args[0] for c in mock_run_git_command.call_args_list] self.assertIn( 'git tag -f -a latest v1.2.3^{} -m "Floating latest tag for v1.2.3"', calls, ) self.assertIn("git push origin latest --force", calls) if __name__ == "__main__": unittest.main()