mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 11:27:43 +08:00
181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
"""
|
||
本地 CLI 更新遇到源码改动时的确认与清理测试。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import importlib.util
|
||
import subprocess
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts" / "local_setup.py"
|
||
|
||
|
||
def load_local_setup_module():
|
||
"""
|
||
加载独立的本地安装脚本模块,避免共享全局配置。
|
||
"""
|
||
module_name = f"moviepilot_local_setup_update_{uuid.uuid4().hex}"
|
||
spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH)
|
||
assert spec is not None and spec.loader is not None
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
def test_dirty_update_clears_tracked_changes_after_confirmation(monkeypatch):
|
||
"""
|
||
确认清理后应重置已跟踪源码改动,供后续 Git 更新继续。
|
||
"""
|
||
module = load_local_setup_module()
|
||
prompts: list[tuple[str, bool]] = []
|
||
commands: list[list[str]] = []
|
||
monkeypatch.setattr(module, "_git_output", lambda *_args: " M app/example.py\n")
|
||
monkeypatch.setattr(
|
||
module,
|
||
"_prompt_yes_no",
|
||
lambda label, default: prompts.append((label, default)) or True,
|
||
)
|
||
monkeypatch.setattr(
|
||
module,
|
||
"run",
|
||
lambda command, cwd=None: commands.append(command),
|
||
)
|
||
|
||
module._ensure_git_clean()
|
||
|
||
assert prompts == [
|
||
(
|
||
"检测到当前仓库有未提交的源码改动:app/example.py,是否清空本地改动并继续更新",
|
||
False,
|
||
)
|
||
]
|
||
assert commands == [["git", "reset", "--hard", "HEAD"]]
|
||
|
||
|
||
def test_dirty_update_stops_when_local_change_cleanup_is_rejected(monkeypatch):
|
||
"""
|
||
拒绝清理时应取消更新且不执行任何 Git 写操作。
|
||
"""
|
||
module = load_local_setup_module()
|
||
commands: list[list[str]] = []
|
||
monkeypatch.setattr(module, "_git_output", lambda *_args: " M app/example.py\n")
|
||
monkeypatch.setattr(module, "_prompt_yes_no", lambda *_args, **_kwargs: False)
|
||
monkeypatch.setattr(
|
||
module,
|
||
"run",
|
||
lambda command, cwd=None: commands.append(command),
|
||
)
|
||
|
||
with pytest.raises(RuntimeError, match="已取消更新"):
|
||
module._ensure_git_clean()
|
||
|
||
assert commands == []
|
||
|
||
|
||
def test_dirty_update_stops_without_an_interactive_input(monkeypatch):
|
||
"""
|
||
没有可用输入时应取消更新,而不是默认清除源码改动。
|
||
"""
|
||
module = load_local_setup_module()
|
||
commands: list[list[str]] = []
|
||
monkeypatch.setattr(module, "_git_output", lambda *_args: " M app/example.py\n")
|
||
|
||
def raise_eof(*_args, **_kwargs):
|
||
"""
|
||
模拟没有可用终端输入。
|
||
"""
|
||
raise EOFError
|
||
|
||
monkeypatch.setattr(module, "_prompt_yes_no", raise_eof)
|
||
monkeypatch.setattr(
|
||
module,
|
||
"run",
|
||
lambda command, cwd=None: commands.append(command),
|
||
)
|
||
|
||
with pytest.raises(RuntimeError, match="不支持交互确认"):
|
||
module._ensure_git_clean()
|
||
|
||
assert commands == []
|
||
|
||
|
||
@pytest.fixture
|
||
def moved_tag_repository(tmp_path):
|
||
"""
|
||
用临时本地仓库模拟远端重建同名标签,不访问外部网络。
|
||
"""
|
||
remote = tmp_path / "remote"
|
||
checkout = tmp_path / "checkout"
|
||
remote.mkdir()
|
||
|
||
def git(path, *args):
|
||
"""
|
||
隔离 Git 身份和签名配置,执行测试仓库操作。
|
||
"""
|
||
return subprocess.check_output(
|
||
["git", "-c", "user.name=CLI Test", "-c", "user.email=cli@example.test",
|
||
"-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", "-C", str(path), *args],
|
||
text=True,
|
||
).strip()
|
||
|
||
git(remote, "init", "-b", "v3")
|
||
git(remote, "commit", "--allow-empty", "-m", "initial")
|
||
git(remote, "tag", "v3.1.0")
|
||
old_sha = git(remote, "rev-parse", "HEAD")
|
||
git(tmp_path, "clone", str(remote), str(checkout))
|
||
git(checkout, "tag", "local-only")
|
||
git(remote, "commit", "--allow-empty", "-m", "rebuild")
|
||
git(remote, "tag", "--force", "v3.1.0")
|
||
return checkout, git, old_sha, git(remote, "rev-parse", "HEAD")
|
||
|
||
|
||
@pytest.mark.parametrize("ref", ["latest", "v3", "v3.1.0"])
|
||
def test_update_overwrites_moved_remote_tag(monkeypatch, moved_tag_repository, ref):
|
||
"""
|
||
同名标签随远端更新,同时保留远端没有的本地标签。
|
||
"""
|
||
checkout, git, old_sha, new_sha = moved_tag_repository
|
||
module = load_local_setup_module()
|
||
monkeypatch.setattr(module, "ROOT", checkout)
|
||
|
||
assert module._update_backend_ref(ref) == ("v3" if ref == "latest" else ref)
|
||
|
||
assert git(checkout, "rev-parse", "v3.1.0") == new_sha
|
||
assert git(checkout, "rev-parse", "HEAD") == new_sha
|
||
assert git(checkout, "rev-parse", "local-only") == old_sha
|
||
|
||
|
||
def test_update_moved_tag_does_not_reset_diverged_branch(monkeypatch, moved_tag_repository):
|
||
"""
|
||
强制同步标签不得覆盖分叉分支上的本地提交。
|
||
"""
|
||
checkout, git, _, new_sha = moved_tag_repository
|
||
git(checkout, "commit", "--allow-empty", "-m", "local work")
|
||
local_sha = git(checkout, "rev-parse", "HEAD")
|
||
module = load_local_setup_module()
|
||
monkeypatch.setattr(module, "ROOT", checkout)
|
||
|
||
with pytest.raises(subprocess.CalledProcessError):
|
||
module._update_backend_ref("latest")
|
||
|
||
assert git(checkout, "rev-parse", "HEAD") == local_sha
|
||
assert git(checkout, "rev-parse", "v3.1.0") == new_sha
|
||
|
||
|
||
def test_offline_update_preserves_local_tag(monkeypatch, moved_tag_repository):
|
||
"""
|
||
离线更新只使用已验证的本地标签,即使远端已更换标签。
|
||
"""
|
||
checkout, git, old_sha, _ = moved_tag_repository
|
||
git(checkout, "remote", "remove", "origin")
|
||
module = load_local_setup_module()
|
||
monkeypatch.setattr(module, "ROOT", checkout)
|
||
|
||
assert module._update_backend_ref("v3.1.0", fetch=False) == "v3.1.0"
|
||
|
||
assert git(checkout, "rev-parse", "HEAD") == old_sha
|