"""Tests for ops.gittool 模块 (@px.tool 注册验证).""" from __future__ import annotations import subprocess from pathlib import Path from unittest.mock import patch import pytest import pyflowx as px import pyflowx.ops.gittool from pyflowx.ops import gittool from pyflowx.tools import _TOOL_REGISTRY, get_tool # ---------------------------------------------------------------------- # # @px.tool 注册验证 # ---------------------------------------------------------------------- # class TestGittoolRegistration: """``gittool`` 模块 ``@px.tool`` 注册验证.""" def test_gittool_registered(self) -> None: """gittool 应在 _TOOL_REGISTRY 中注册.""" assert "gittool" in _TOOL_REGISTRY def test_visible_subcommands(self) -> None: """可见子命令: a, c, i, isub, p, pl.""" subs = px.list_subcommands("gittool") assert "a" in subs assert "c" in subs assert "i" in subs assert "isub" in subs assert "p" in subs assert "pl" in subs def test_clean_is_hidden(self) -> None: """clean 应为 hidden (不在可见子命令中).""" subs = px.list_subcommands("gittool") assert "clean" not in subs def test_clean_hidden_included_with_flag(self) -> None: """include_hidden=True 时 clean 应出现.""" subs = px.list_subcommands("gittool", include_hidden=True) assert "clean" in subs def test_c_needs_clean(self) -> None: """c 应依赖 clean.""" spec = get_tool("gittool", "c") assert "clean" in spec.needs def test_clean_has_cmd(self) -> None: """clean 应有 cmd.""" spec = get_tool("gittool", "clean") assert spec.cmd is not None assert "git" in spec.cmd assert "clean" in spec.cmd assert "-xfd" in spec.cmd def test_clean_cmd_includes_excludes(self) -> None: """clean cmd 应包含排除项.""" spec = get_tool("gittool", "clean") assert spec.cmd is not None assert "-e" in spec.cmd assert ".venv" in spec.cmd def test_c_has_cmd(self) -> None: """c 应有 cmd.""" spec = get_tool("gittool", "c") assert spec.cmd == ("git", "status", "--porcelain") def test_p_has_cmd(self) -> None: """p 应有 cmd.""" spec = get_tool("gittool", "p") assert spec.cmd == ("git", "push") def test_pl_has_cmd(self) -> None: """pl 应有 cmd.""" spec = get_tool("gittool", "pl") assert spec.cmd == ("git", "pull") # ---------------------------------------------------------------------- # # 辅助函数验证 (not_has_git_repo / has_files) # ---------------------------------------------------------------------- # class TestGittoolHelpers: """``gittool`` 模块辅助函数验证.""" def test_not_has_git_repo_true(self, tmp_path: Path) -> None: """无 .git 目录时 not_has_git_repo 应返回 True.""" with patch.object(Path, "cwd", return_value=tmp_path): assert gittool.not_has_git_repo() is True def test_not_has_git_repo_false(self, tmp_path: Path) -> None: """有 .git 目录时 not_has_git_repo 应返回 False.""" (tmp_path / ".git").mkdir() with patch.object(Path, "cwd", return_value=tmp_path): assert gittool.not_has_git_repo() is False def test_has_files_false_when_no_changes(self, monkeypatch: pytest.MonkeyPatch) -> None: """无未提交更改时 has_files 应返回 False.""" class _FakeResult: stdout = "" monkeypatch.setattr("subprocess.run", lambda *_, **__: _FakeResult()) assert gittool.has_files() is False def test_has_files_true_when_changes(self, monkeypatch: pytest.MonkeyPatch) -> None: """有未提交更改时 has_files 应返回 True.""" class _FakeResult: stdout = " M test.txt\n" monkeypatch.setattr("subprocess.run", lambda *_, **__: _FakeResult()) assert gittool.has_files() is True def test_has_files_false_on_subprocess_error(self, monkeypatch: pytest.MonkeyPatch) -> None: """subprocess.SubprocessError 时 has_files 应返回 False.""" def _raise(*args: object, **kwargs: object) -> None: raise subprocess.SubprocessError("fail") monkeypatch.setattr("subprocess.run", _raise) assert gittool.has_files() is False def test_has_files_false_on_oserror(self, monkeypatch: pytest.MonkeyPatch) -> None: """OSError 时 has_files 应返回 False.""" def _raise(*args: object, **kwargs: object) -> None: raise OSError("fail") monkeypatch.setattr("subprocess.run", _raise) assert gittool.has_files() is False # ---------------------------------------------------------------------- # # git_add_commit / git_init_add_commit 函数体验证 # ---------------------------------------------------------------------- # class TestGittoolCommit: """``gittool`` 模块提交函数验证.""" def test_git_add_commit_no_changes( self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: """无更改时 git_add_commit 应打印提示且不执行 git 命令.""" monkeypatch.setattr(gittool, "has_files", lambda: False) calls: list[list[str]] = [] def _capture(*args: object, **kwargs: object) -> None: calls.append(args[0]) # type: ignore[arg-type] monkeypatch.setattr("subprocess.run", _capture) gittool.git_add_commit() assert calls == [] assert "没有文件需要提交" in capsys.readouterr().out def test_git_add_commit_with_changes(self, monkeypatch: pytest.MonkeyPatch) -> None: """有更改时 git_add_commit 应执行 git add + git commit.""" monkeypatch.setattr(gittool, "has_files", lambda: True) calls: list[list[str]] = [] def _capture(*args: object, **kwargs: object) -> None: calls.append(args[0]) # type: ignore[arg-type] monkeypatch.setattr("subprocess.run", _capture) gittool.git_add_commit(message="custom msg") assert calls == [["git", "add", "."], ["git", "commit", "-m", "custom msg"]] def test_git_init_add_commit_init_and_commit(self, monkeypatch: pytest.MonkeyPatch) -> None: """无仓库且有更改时 git_init_add_commit 应 git init + add + commit.""" monkeypatch.setattr(gittool, "not_has_git_repo", lambda: True) monkeypatch.setattr(gittool, "has_files", lambda: True) calls: list[list[str]] = [] def _capture(*args: object, **kwargs: object) -> None: calls.append(args[0]) # type: ignore[arg-type] monkeypatch.setattr("subprocess.run", _capture) gittool.git_init_add_commit() assert calls == [["git", "init"], ["git", "add", "."], ["git", "commit", "-m", "init commit"]] def test_git_init_add_commit_init_no_changes( self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: """无仓库且无更改时 git_init_add_commit 应 git init + 打印提示.""" monkeypatch.setattr(gittool, "not_has_git_repo", lambda: True) monkeypatch.setattr(gittool, "has_files", lambda: False) calls: list[list[str]] = [] def _capture(*args: object, **kwargs: object) -> None: calls.append(args[0]) # type: ignore[arg-type] monkeypatch.setattr("subprocess.run", _capture) gittool.git_init_add_commit() assert calls == [["git", "init"]] assert "没有文件需要提交" in capsys.readouterr().out def test_git_init_add_commit_no_init_with_changes(self, monkeypatch: pytest.MonkeyPatch) -> None: """有仓库且有更改时 git_init_add_commit 应 add + commit (不 init).""" monkeypatch.setattr(gittool, "not_has_git_repo", lambda: False) monkeypatch.setattr(gittool, "has_files", lambda: True) calls: list[list[str]] = [] def _capture(*args: object, **kwargs: object) -> None: calls.append(args[0]) # type: ignore[arg-type] monkeypatch.setattr("subprocess.run", _capture) gittool.git_init_add_commit() assert calls == [["git", "add", "."], ["git", "commit", "-m", "init commit"]] def test_git_init_add_commit_no_init_no_changes( self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: """有仓库且无更改时 git_init_add_commit 应仅打印提示 (不 init, 不 add/commit).""" monkeypatch.setattr(gittool, "not_has_git_repo", lambda: False) monkeypatch.setattr(gittool, "has_files", lambda: False) calls: list[list[str]] = [] def _capture(*args: object, **kwargs: object) -> None: calls.append(args[0]) # type: ignore[arg-type] monkeypatch.setattr("subprocess.run", _capture) gittool.git_init_add_commit() assert calls == [] assert "没有文件需要提交" in capsys.readouterr().out # ---------------------------------------------------------------------- # # init_sub_dirs 函数验证 # ---------------------------------------------------------------------- # class TestGittoolInitSubDirs: """``init_sub_dirs`` 函数验证.""" def test_init_sub_dirs_runs_graph_for_each_subdir( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """init_sub_dirs 应为每个子目录构建并运行图 (文件被跳过).""" (tmp_path / "subdir1").mkdir() (tmp_path / "subdir2").mkdir() (tmp_path / "file.txt").write_text("test") monkeypatch.setattr(Path, "cwd", lambda: tmp_path) runs: list[object] = [] monkeypatch.setattr(gittool.px, "run", runs.append) gittool.init_sub_dirs() assert len(runs) == 2 def test_init_sub_dirs_no_subdirs( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """无子目录时 init_sub_dirs 不应运行任何图.""" (tmp_path / "file.txt").write_text("test") monkeypatch.setattr(Path, "cwd", lambda: tmp_path) runs: list[object] = [] monkeypatch.setattr(gittool.px, "run", runs.append) gittool.init_sub_dirs() assert runs == []