Coverage for tests / unit / cli / test_cli.py: 100%

98 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Unit tests for lintro/cli.py - CLI entry point and LintroGroup.""" 

2 

3from __future__ import annotations 

4 

5import contextlib 

6from unittest.mock import patch 

7 

8import pytest 

9from assertpy import assert_that 

10from click.testing import CliRunner 

11 

12from lintro import __version__ 

13from lintro.cli import LintroGroup, cli, main 

14 

15# ============================================================================= 

16# CLI Entry Point Tests 

17# ============================================================================= 

18 

19 

20def test_cli_version_option(cli_runner: CliRunner) -> None: 

21 """Verify --version shows version and exits cleanly. 

22 

23 Args: 

24 cli_runner: The Click CLI test runner. 

25 """ 

26 result = cli_runner.invoke(cli, ["--version"]) 

27 

28 assert_that(result.exit_code).is_equal_to(0) 

29 assert_that(result.output).contains(__version__) 

30 

31 

32def test_cli_help_option(cli_runner: CliRunner) -> None: 

33 """Verify --help shows help and exits cleanly. 

34 

35 Args: 

36 cli_runner: The Click CLI test runner. 

37 """ 

38 result = cli_runner.invoke(cli, ["--help"]) 

39 

40 assert_that(result.exit_code).is_equal_to(0) 

41 # Rich-formatted help contains "Lintro" 

42 assert_that(result.output).contains("Lintro") 

43 

44 

45def test_cli_no_command_shows_help(cli_runner: CliRunner) -> None: 

46 """Verify running cli without command shows help. 

47 

48 Args: 

49 cli_runner: The Click CLI test runner. 

50 """ 

51 result = cli_runner.invoke(cli, []) 

52 

53 assert_that(result.exit_code).is_equal_to(0) 

54 

55 

56def test_cli_invalid_command(cli_runner: CliRunner) -> None: 

57 """Verify invalid command shows error. 

58 

59 Args: 

60 cli_runner: The Click CLI test runner. 

61 """ 

62 result = cli_runner.invoke(cli, ["nonexistent-command-xyz"]) 

63 

64 assert_that(result.exit_code).is_not_equal_to(0) 

65 

66 

67def test_main_entry_point() -> None: 

68 """Verify main() entry point calls cli().""" 

69 with patch("lintro.cli.cli") as mock_cli: 

70 mock_cli.return_value = None 

71 # main() calls cli() which is a Click command 

72 with contextlib.suppress(SystemExit): 

73 main() 

74 mock_cli.assert_called_once() 

75 

76 

77# ============================================================================= 

78# LintroGroup Tests 

79# ============================================================================= 

80 

81 

82def test_lintro_group_format_help_includes_commands() -> None: 

83 """Verify LintroGroup.format_help includes registered commands.""" 

84 runner = CliRunner() 

85 result = runner.invoke(cli, ["--help"]) 

86 

87 # Should contain command names 

88 assert_that(result.output).contains("check") 

89 assert_that(result.output).contains("format") 

90 

91 

92def test_lintro_group_format_help_includes_aliases() -> None: 

93 """Verify LintroGroup.format_help shows command aliases.""" 

94 runner = CliRunner() 

95 result = runner.invoke(cli, ["--help"]) 

96 

97 # Should contain aliases 

98 assert_that(result.output).contains("chk") 

99 assert_that(result.output).contains("fmt") 

100 

101 

102def test_lintro_group_format_commands_empty() -> None: 

103 """Verify format_commands method exists for compatibility.""" 

104 import click 

105 

106 group = LintroGroup() 

107 ctx = click.Context(cli) 

108 formatter = click.HelpFormatter() 

109 

110 # Should not raise 

111 group.format_commands(ctx, formatter) 

112 

113 

114# ============================================================================= 

115# Command Registration Tests 

116# ============================================================================= 

117 

118 

119def test_cli_has_check_command(cli_runner: CliRunner) -> None: 

120 """Verify check command is registered. 

121 

122 Args: 

123 cli_runner: The Click CLI test runner. 

124 """ 

125 result = cli_runner.invoke(cli, ["check", "--help"]) 

126 

127 assert_that(result.exit_code).is_equal_to(0) 

128 assert_that(result.output).contains("Check files") 

129 

130 

131def test_cli_has_format_command(cli_runner: CliRunner) -> None: 

132 """Verify format command is registered. 

133 

134 Args: 

135 cli_runner: The Click CLI test runner. 

136 """ 

137 result = cli_runner.invoke(cli, ["format", "--help"]) 

138 

139 assert_that(result.exit_code).is_equal_to(0) 

140 assert_that(result.output).contains("Format") 

141 

142 

143def test_cli_has_test_command(cli_runner: CliRunner) -> None: 

144 """Verify test command is registered. 

145 

146 Args: 

147 cli_runner: The Click CLI test runner. 

148 """ 

149 result = cli_runner.invoke(cli, ["test", "--help"]) 

150 

151 assert_that(result.exit_code).is_equal_to(0) 

152 

153 

154def test_cli_has_config_command(cli_runner: CliRunner) -> None: 

155 """Verify config command is registered. 

156 

157 Args: 

158 cli_runner: The Click CLI test runner. 

159 """ 

160 result = cli_runner.invoke(cli, ["config", "--help"]) 

161 

162 assert_that(result.exit_code).is_equal_to(0) 

163 

164 

165def test_cli_has_versions_command(cli_runner: CliRunner) -> None: 

166 """Verify versions command is registered. 

167 

168 Args: 

169 cli_runner: The Click CLI test runner. 

170 """ 

171 result = cli_runner.invoke(cli, ["versions", "--help"]) 

172 

173 assert_that(result.exit_code).is_equal_to(0) 

174 

175 

176def test_cli_has_list_tools_command(cli_runner: CliRunner) -> None: 

177 """Verify list-tools command is registered. 

178 

179 Args: 

180 cli_runner: The Click CLI test runner. 

181 """ 

182 result = cli_runner.invoke(cli, ["list-tools", "--help"]) 

183 

184 assert_that(result.exit_code).is_equal_to(0) 

185 

186 

187def test_cli_has_init_command(cli_runner: CliRunner) -> None: 

188 """Verify init command is registered. 

189 

190 Args: 

191 cli_runner: The Click CLI test runner. 

192 """ 

193 result = cli_runner.invoke(cli, ["init", "--help"]) 

194 

195 assert_that(result.exit_code).is_equal_to(0) 

196 

197 

198# ============================================================================= 

199# Command Alias Tests 

200# ============================================================================= 

201 

202 

203@pytest.mark.parametrize( 

204 ("alias", "canonical"), 

205 [ 

206 ("chk", "check"), 

207 ("fmt", "format"), 

208 ("tst", "test"), 

209 ("cfg", "config"), 

210 ("ver", "versions"), 

211 ("ls", "list-tools"), 

212 ], 

213 ids=[ 

214 "chk->check", 

215 "fmt->format", 

216 "tst->test", 

217 "cfg->config", 

218 "ver->versions", 

219 "ls->list-tools", 

220 ], 

221) 

222def test_cli_alias_resolves_to_command( 

223 cli_runner: CliRunner, 

224 alias: str, 

225 canonical: str, 

226) -> None: 

227 """Verify command aliases resolve to canonical commands. 

228 

229 Args: 

230 cli_runner: The Click CLI test runner. 

231 alias: The alias command name. 

232 canonical: The canonical command name. 

233 """ 

234 result = cli_runner.invoke(cli, [alias, "--help"]) 

235 

236 assert_that(result.exit_code).is_equal_to(0) 

237 

238 

239# ============================================================================= 

240# Command Chaining Tests 

241# ============================================================================= 

242 

243 

244def test_lintro_group_invoke_normalizes_comma_separated_commands() -> None: 

245 """Verify comma-separated commands are normalized.""" 

246 runner = CliRunner() 

247 # This tests the parsing logic - actual execution would require mocking 

248 with ( 

249 patch("lintro.cli_utils.commands.check.run_lint_tools_simple") as mock_check, 

250 patch( 

251 "lintro.cli_utils.commands.format.run_lint_tools_simple", 

252 ) as mock_fmt, 

253 ): 

254 mock_check.return_value = 0 

255 mock_fmt.return_value = 0 

256 # Test comma-separated command detection 

257 runner.invoke(cli, ["fmt", ",", "chk"]) 

258 # Both commands should have been invoked for chained execution 

259 assert_that(mock_fmt.called).is_true() 

260 assert_that(mock_check.called).is_true() 

261 

262 

263def test_lintro_group_invoke_single_command() -> None: 

264 """Verify single command execution works normally.""" 

265 runner = CliRunner() 

266 with patch("lintro.cli_utils.commands.check.run_lint_tools_simple") as mock: 

267 mock.return_value = 0 

268 runner.invoke(cli, ["check", "."]) 

269 mock.assert_called_once() 

270 

271 

272def test_lintro_group_invoke_handles_keyboard_interrupt() -> None: 

273 """Verify KeyboardInterrupt is re-raised during command chaining.""" 

274 runner = CliRunner() 

275 with patch("lintro.cli_utils.commands.check.run_lint_tools_simple") as mock: 

276 mock.side_effect = KeyboardInterrupt() 

277 result = runner.invoke(cli, ["check", "."]) 

278 # CliRunner catches KeyboardInterrupt and sets exit code 

279 assert_that(result.exit_code).is_not_equal_to(0) 

280 

281 

282def test_lintro_group_invoke_aggregates_exit_codes() -> None: 

283 """Verify chained commands aggregate exit codes (max).""" 

284 runner = CliRunner() 

285 with ( 

286 patch("lintro.cli_utils.commands.format.run_lint_tools_simple") as mock_fmt, 

287 patch("lintro.cli_utils.commands.check.run_lint_tools_simple") as mock_chk, 

288 ): 

289 # First command succeeds, second fails 

290 mock_fmt.return_value = 0 

291 mock_chk.return_value = 1 

292 result = runner.invoke(cli, ["fmt", ",", "chk"]) 

293 # Result should be max of exit codes 

294 assert_that(result.exit_code).is_equal_to(1)