-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_cli.py
More file actions
178 lines (147 loc) · 7.38 KB
/
Copy pathtest_cli.py
File metadata and controls
178 lines (147 loc) · 7.38 KB
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
"""Tests for devforge meta-package."""
from __future__ import annotations
from devforge import TOOLS, __version__
from devforge.cli import _is_tool_installed, app
from typer.testing import CliRunner
from unittest import mock
runner = CliRunner()
class TestVersion:
def test_version_flag(self):
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert "devforge" in result.stdout.lower()
assert __version__ in result.stdout
class TestToolsCommand:
def test_lists_all_tools(self):
result = runner.invoke(app, ["tools"])
assert result.exit_code == 0
for cmd in TOOLS:
assert cmd in result.stdout
def test_show_specific_tool(self):
result = runner.invoke(app, ["tools", "guard"])
assert result.exit_code == 0
assert "guard" in result.stdout
assert "api-contract-guardian" in result.stdout
def test_unknown_tool(self):
result = runner.invoke(app, ["tools", "nonexistent"])
assert result.exit_code == 1
assert "Unknown" in result.stdout
class TestInstallCommand:
@mock.patch("devforge.cli.subprocess.run")
def test_install_specific_tool(self, mock_run):
"""Install a specific tool by name."""
mock_run.return_value = mock.MagicMock(returncode=0, stdout="", stderr="")
result = runner.invoke(app, ["install", "guard"])
assert result.exit_code == 0
assert "Successfully" in result.stdout
mock_run.assert_called_once()
@mock.patch("devforge.cli.subprocess.run")
def test_install_all_uses_all_extra(self, mock_run):
"""'install all' must use the canonical devforge-tools[all] extra, not a comma-joined list."""
mock_run.return_value = mock.MagicMock(returncode=0, stdout="", stderr="")
result = runner.invoke(app, ["install", "all"])
assert result.exit_code == 0
assert "Successfully" in result.stdout
mock_run.assert_called_once()
call_args = mock_run.call_args[0][0] # positional arg: the command list
# Must contain the git+ URL with [all] extra, not a comma-joined list
pkg_arg = next((a for a in call_args if "devforge-cli.git[" in a), None)
expected = "git+https://github.com/Coding-Dev-Tools/devforge-cli.git[all]"
assert pkg_arg == expected, f"Expected {expected}, got {pkg_arg}"
def test_install_unknown_tool(self):
"""Error on unknown tool name."""
result = runner.invoke(app, ["install", "nonexistent"])
assert result.exit_code == 1
assert "Unknown" in result.stdout
assert "Available:" in result.stdout
@mock.patch("devforge.cli.subprocess.run")
def test_install_failure(self, mock_run):
"""Handle pip install failure gracefully."""
mock_run.return_value = mock.MagicMock(returncode=1, stdout="", stderr="Error message")
result = runner.invoke(app, ["install", "guard"])
assert result.exit_code == 1
assert "failed" in result.stdout.lower()
class TestVersionsCommand:
def test_versions_runs(self):
"""List all tool versions without error."""
result = runner.invoke(app, ["versions"])
assert result.exit_code == 0
def test_versions_unknown_tool_fails(self):
"""Error on unknown tool name."""
result = runner.invoke(app, ["versions", "nonexistent"])
assert result.exit_code == 1
assert "Unknown" in result.stdout
@mock.patch("devforge.cli.subprocess.run")
def test_versions_specific_tool_not_installed(self, mock_run):
"""Show 'not installed' for a tool that isn't installed."""
mock_run.return_value = mock.MagicMock(returncode=1, stdout="", stderr="")
result = runner.invoke(app, ["versions", "guard"])
assert result.exit_code == 0
assert "guard" in result.stdout
assert "not installed" in result.stdout
class TestIsToolInstalled:
def test_builtin_module_is_installed(self):
"""stdlib module should always be found."""
assert _is_tool_installed("sys") is True
def test_missing_module_is_not_installed(self):
"""Nonexistent module should return False."""
assert _is_tool_installed("_devforge_no_such_pkg_xyz") is False
class TestDispatchCommands:
def test_invalid_tool_subcommand(self):
"""Reject dispatch to an unknown tool subcommand."""
result = runner.invoke(app, ["nonexistent"])
assert result.exit_code != 0
assert "No such command" in result.stdout or "No such command" in result.stderr
@mock.patch("devforge.cli._is_tool_installed", return_value=False)
def test_dispatch_not_installed_shows_install_hint(self, _mock):
"""When a tool is not installed, dispatch shows a clear install hint (not a silent exit)."""
result = runner.invoke(app, ["guard"])
assert result.exit_code == 1
assert "not installed" in result.stdout
assert 'pip install "git+https://github.com/Coding-Dev-Tools/devforge-cli.git[guard]"' in result.stdout
@mock.patch("devforge.cli._is_tool_installed", return_value=True)
@mock.patch("devforge.cli.subprocess.run")
def test_dispatch_installed_tool_runs(self, mock_run, _mock_installed):
"""When a tool is installed, dispatch calls the subprocess."""
mock_run.return_value = mock.MagicMock(returncode=0)
with mock.patch("devforge.cli.sys.exit"):
runner.invoke(app, ["guard"])
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
assert "api_contract_guardian" in cmd
@mock.patch("devforge.cli._is_tool_installed", return_value=True)
@mock.patch("devforge.cli.subprocess.run")
def test_dispatch_forwards_tool_flags(self, mock_run, _mock_installed):
"""Tool flags (e.g. `--config file.yaml`) must reach the underlying CLI.
Regression guard for the silent-failure trap where typer rejected any
argument beginning with `-` as 'No such option' before the tool ran.
With ignore_unknown_options/allow_extra_args, such flags are forwarded
via ctx.args.
"""
mock_run.return_value = mock.MagicMock(returncode=0)
with mock.patch("devforge.cli.sys.exit"):
runner.invoke(app, ["guard", "--config", "x.yaml", "--verbose"])
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
# Underlying module is launched...
assert "api_contract_guardian" in cmd
# ...and the tool flags are forwarded, not swallowed by typer.
assert "--config" in cmd
assert "x.yaml" in cmd
assert "--verbose" in cmd
@mock.patch("devforge.cli._is_tool_installed", return_value=False)
def test_dispatch_install_hint_escapes_extra_brackets(self, _mock):
"""The '[tool]' extra in the install hint must survive rich markup parsing.
A regression guard: an unescaped '[guard]' was previously swallowed by
rich's markup parser, rendering 'pip install devforge' with no extra.
"""
result = runner.invoke(app, ["guard"])
assert result.exit_code == 1
assert 'pip install "git+https://github.com/Coding-Dev-Tools/devforge-cli.git[guard]"' in result.stdout
class TestHelp:
def test_help(self):
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "tools" in result.stdout
assert "versions" in result.stdout
assert "guard" in result.stdout