mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: add upgrade prompt to version update check
When a newer release is found, prompt the user to upgrade in-place. Resolves the git repo root via `git rev-parse --show-toplevel` before running `git pull && make clean && make && make install`, so the upgrade works correctly whether hate_crack is run from source or installed into site-packages. Ctrl-C and EOF at the prompt continue normally.
This commit is contained in:
@@ -836,6 +836,40 @@ def check_for_updates():
|
||||
f"\n Update available: {latest} (current: {local_base})."
|
||||
f"\n See https://github.com/trustedsec/hate_crack/releases\n"
|
||||
)
|
||||
try:
|
||||
answer = input(" Upgrade now? [y/N] ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
return
|
||||
if answer == "y":
|
||||
import subprocess
|
||||
|
||||
print()
|
||||
# Find the actual git repo root - _repo_root may point to
|
||||
# site-packages when installed rather than the source checkout.
|
||||
git_root_result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
cwd=_repo_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if git_root_result.returncode != 0:
|
||||
print(
|
||||
"\n Could not find a git repository to upgrade from."
|
||||
"\n Run manually: git pull && make clean && make && make install\n"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
repo_root = git_root_result.stdout.strip()
|
||||
result = subprocess.run(
|
||||
"git pull && make clean && make && make install",
|
||||
shell=True,
|
||||
cwd=repo_root,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("\n Upgrade complete. Please restart hate_crack.\n")
|
||||
else:
|
||||
print("\n Upgrade failed. Check the output above for errors.\n")
|
||||
raise SystemExit(0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+110
-3
@@ -1,7 +1,7 @@
|
||||
"""Tests for the startup version check feature."""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestCheckForUpdates:
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
):
|
||||
), patch("builtins.input", return_value="n"):
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
@@ -99,7 +99,7 @@ class TestCheckForUpdates:
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
):
|
||||
), patch("builtins.input", return_value="n"):
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
@@ -119,3 +119,110 @@ class TestCheckForUpdates:
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Update available" not in output
|
||||
|
||||
def test_upgrade_declined_does_not_run_make(self, hc_module):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = {"tag_name": "v99.0.0"}
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
), patch("builtins.input", return_value="n"), patch(
|
||||
"subprocess.run"
|
||||
) as mock_run:
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
mock_run.assert_not_called()
|
||||
|
||||
def test_upgrade_accepted_runs_make_and_exits(self, hc_module, capsys):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = {"tag_name": "v99.0.0"}
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
git_root_proc = MagicMock()
|
||||
git_root_proc.returncode = 0
|
||||
git_root_proc.stdout = "/fake/repo\n"
|
||||
|
||||
make_proc = MagicMock()
|
||||
make_proc.returncode = 0
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
), patch("builtins.input", return_value="y"), patch(
|
||||
"subprocess.run", side_effect=[git_root_proc, make_proc]
|
||||
) as mock_run, pytest.raises(
|
||||
SystemExit
|
||||
):
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
assert mock_run.call_count == 2
|
||||
make_cmd = mock_run.call_args_list[1][0][0]
|
||||
assert "git pull" in make_cmd
|
||||
assert "make install" in make_cmd
|
||||
assert mock_run.call_args_list[1][1]["cwd"] == "/fake/repo"
|
||||
output = capsys.readouterr().out
|
||||
assert "Upgrade complete" in output
|
||||
|
||||
def test_upgrade_failure_prints_error(self, hc_module, capsys):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = {"tag_name": "v99.0.0"}
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
git_root_proc = MagicMock()
|
||||
git_root_proc.returncode = 0
|
||||
git_root_proc.stdout = "/fake/repo\n"
|
||||
|
||||
make_proc = MagicMock()
|
||||
make_proc.returncode = 1
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
), patch("builtins.input", return_value="y"), patch(
|
||||
"subprocess.run", side_effect=[git_root_proc, make_proc]
|
||||
), pytest.raises(
|
||||
SystemExit
|
||||
):
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Upgrade failed" in output
|
||||
|
||||
def test_upgrade_no_git_repo_prints_manual_instructions(self, hc_module, capsys):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = {"tag_name": "v99.0.0"}
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
git_root_proc = MagicMock()
|
||||
git_root_proc.returncode = 128
|
||||
git_root_proc.stdout = ""
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
), patch("builtins.input", return_value="y"), patch(
|
||||
"subprocess.run", return_value=git_root_proc
|
||||
), pytest.raises(
|
||||
SystemExit
|
||||
):
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Run manually" in output
|
||||
|
||||
def test_upgrade_prompt_ctrl_c_continues(self, hc_module, capsys):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = {"tag_name": "v99.0.0"}
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
with patch.object(hc_module, "requests") as mock_requests, patch.object(
|
||||
hc_module, "REQUESTS_AVAILABLE", True
|
||||
), patch("builtins.input", side_effect=KeyboardInterrupt), patch(
|
||||
"subprocess.run"
|
||||
) as mock_run:
|
||||
mock_requests.get.return_value = mock_resp
|
||||
hc_module.check_for_updates()
|
||||
|
||||
mock_run.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user