* Treat remote tags as the source of truth by force-fetching tags from *origin* * Update preview output to reflect the real fetch behavior * Align unit tests with the new forced tag fetch command https://chatgpt.com/share/693bdfc3-b8b4-800f-8adc-b1dc63c56a89
199 lines
7.4 KiB
Python
199 lines
7.4 KiB
Python
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()
|