1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""Tests for the actions TUI page."""
from __future__ import annotations
import sys
import time
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from pygit2 import Repository, init_repository
from pyratatui import Rect
from pygittools.tui.git_colors import init_tui_color_theme
from pygittools.tui.keys import key
from pygittools.tui.pages.actions import ActionsPage, _RunState, actions_page
from pygittools.tui.types import PageAction, PageContext
def _repo_with_actions(tmp_path: Path, actions_body: str) -> PageContext:
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
repo = init_repository(str(repo_dir), bare=False)
(repo_dir / "pyproject.toml").write_text(
f'[project]\nname = "demo"\nversion = "0"\n\n[tool.pgt]\nactions = {actions_body}\n',
encoding="utf-8",
)
venv_bin = repo_dir / ".venv" / "bin"
venv_bin.mkdir(parents=True)
(venv_bin / "python").symlink_to(sys.executable)
return PageContext(repo=repo, repo_path=Path(repo.path))
def test_actions_page_empty(tmp_path: Path) -> None:
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
repo = init_repository(str(repo_dir), bare=False)
ctx = PageContext(repo=repo, repo_path=Path(repo.path))
page = actions_page()
page.on_enter(ctx)
assert "No pyproject" in page.status_text() or "No [tool.pgt]" in page.status_text()
page.draw(MagicMock(), Rect(0, 0, 80, 20))
assert page.handle_key(key("Esc")).action == PageAction.POP
@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_actions_page_lists_and_run(tmp_path: Path) -> None:
ctx = _repo_with_actions(tmp_path, '[{ name = "ok", script = "ok.py" }]')
init_tui_color_theme(ctx.repo)
workdir = Path(ctx.repo.workdir)
(workdir / "ok.py").write_text("print('done')\n", encoding="utf-8")
page = ActionsPage()
page.on_enter(ctx)
assert "Enter run" in page.status_text()
page.draw(MagicMock(), Rect(0, 0, 80, 20))
# select the action row (index 1; 0 is Run all)
page.handle_key(key("Down"))
page.handle_key(key("Enter"))
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
page.on_idle()
with page._lock:
if page._active is None and page._states.get(("default", "ok")) is not _RunState.IDLE:
break
time.sleep(0.05)
assert page._states[("default", "ok")].name == "SUCCESS"
assert page._messages[("default", "ok")] == "done"
page.draw(MagicMock(), Rect(0, 0, 80, 20))
@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_actions_page_run_all_stops_on_failure(tmp_path: Path) -> None:
ctx = _repo_with_actions(
tmp_path,
"""[
{ name = "bad", script = "bad.py" },
{ name = "good", script = "good.py" },
]""",
)
init_tui_color_theme(ctx.repo)
workdir = Path(ctx.repo.workdir)
(workdir / "bad.py").write_text("import sys\nsys.exit(1)\n", encoding="utf-8")
(workdir / "good.py").write_text("print('should-not-run')\n", encoding="utf-8")
page = ActionsPage()
page.on_enter(ctx)
page.handle_key(key("a"))
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
page.on_idle()
with page._lock:
if page._active is None:
break
time.sleep(0.05)
assert page._states[("default", "bad")] is _RunState.FAILURE
assert page._states[("default", "good")] is _RunState.IDLE
assert ("default", "good") not in page._messages
@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_actions_page_opens_from_factory(tmp_path: Path) -> None:
ctx = _repo_with_actions(tmp_path, "[]")
page = actions_page()
page.on_enter(ctx)
assert page.title == "actions"
assert isinstance(ctx.repo, Repository)
assert "No actions configured" in page.status_text()
def _repo_dir(tmp_path: Path) -> tuple[PageContext, Path]:
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
repo = init_repository(str(repo_dir), bare=False)
return PageContext(repo=repo, repo_path=Path(repo.path)), repo_dir
def test_actions_page_cycles_sets(tmp_path: Path) -> None:
ctx, repo_dir = _repo_dir(tmp_path)
(repo_dir / "pyproject.toml").write_text(
'[project]\nname = "demo"\n\n[tool.pgt]\nactions = [{ name = "lint", script = "lint.py" }]\n',
encoding="utf-8",
)
(repo_dir / "actions.toml").write_text(
'[release]\nactions = [{ name = "build", script = "build.py" }]\n',
encoding="utf-8",
)
page = ActionsPage()
page.on_enter(ctx)
assert "Tab/Shift-Tab" in page.status_text()
assert [label for _, label in page._rows()] == ["Run all", "lint"]
page.handle_key(key("Down"))
page.handle_key(key("Tab"))
assert [label for _, label in page._rows()] == ["Run all", "build"]
assert page._list_state.selected == 0
page.handle_key(key("Tab", shift=True))
assert [label for _, label in page._rows()] == ["Run all", "lint"]
page.handle_key(key("BackTab"))
assert [label for _, label in page._rows()] == ["Run all", "build"]
page.handle_key(key("Left"))
assert [label for _, label in page._rows()] == ["Run all", "lint"]
page.handle_key(key("Right"))
assert [label for _, label in page._rows()] == ["Run all", "build"]
page.draw(MagicMock(), Rect(0, 0, 80, 20))
def test_actions_page_run_all_uses_current_set(tmp_path: Path) -> None:
ctx, repo_dir = _repo_dir(tmp_path)
(repo_dir / "actions.toml").write_text(
"""
[first]
actions = [{ name = "one", script = "one.py" }]
[second]
actions = [{ name = "two", script = "two.py" }]
""",
encoding="utf-8",
)
page = ActionsPage()
page.on_enter(ctx)
page.handle_key(key("Tab"))
page.handle_key(key("a"))
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
page.on_idle()
with page._lock:
if page._active is None:
break
time.sleep(0.05)
assert page._states[("first", "one")] is _RunState.IDLE
assert page._states[("second", "two")] is _RunState.FAILURE
assert "Script not found" in page._messages[("second", "two")]
def test_actions_page_bad_actions_toml(tmp_path: Path) -> None:
ctx, repo_dir = _repo_dir(tmp_path)
(repo_dir / "actions.toml").write_text("quality = 1\n", encoding="utf-8")
page = ActionsPage()
page.on_enter(ctx)
assert "unexpected key" in page.status_text()
assert page.handle_key(key("Esc")).action == PageAction.POP