Coverage for lintro / cli_utils / commands / test.py: 95%
95 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Test command implementation for running pytest tests."""
3from typing import Any, cast
5import click
6from click.testing import CliRunner
8from lintro.utils.tool_executor import run_lint_tools_simple
10# Constants
11DEFAULT_PATHS: list[str] = ["."]
12DEFAULT_EXIT_CODE: int = 0
13DEFAULT_ACTION: str = "test"
16def _ensure_pytest_prefix(option_fragment: str) -> str:
17 """Normalize tool option fragments to use the pytest prefix.
19 Args:
20 option_fragment: Raw option fragment from --tool-options.
22 Returns:
23 str: Fragment guaranteed to start with ``pytest:``.
24 """
25 fragment = option_fragment.strip()
26 if not fragment:
27 return fragment
29 lowered = fragment.lower()
30 if lowered.startswith("pytest:"):
31 _, rest = fragment.split(":", 1)
32 return f"pytest:{rest}"
33 return f"pytest:{fragment}"
36@click.command("test")
37@click.argument("paths", nargs=-1, type=click.Path(exists=True))
38@click.option(
39 "--exclude",
40 type=str,
41 help="Comma-separated list of patterns to exclude from testing",
42)
43@click.option(
44 "--include-venv",
45 is_flag=True,
46 help="Include virtual environment directories in testing",
47)
48@click.option(
49 "--output",
50 type=click.Path(),
51 help="Output file path for writing results",
52)
53@click.option(
54 "--output-format",
55 type=click.Choice(["plain", "grid", "markdown", "html", "json", "csv", "github"]),
56 default="grid",
57 help="Output format for displaying results",
58)
59@click.option(
60 "--group-by",
61 type=click.Choice(["file", "code", "none", "auto"]),
62 default="file",
63 help="How to group issues in the output",
64)
65@click.option(
66 "--verbose",
67 "-v",
68 is_flag=True,
69 help="Show verbose output",
70)
71@click.option(
72 "--raw-output",
73 is_flag=True,
74 help="Show raw tool output instead of formatted output",
75)
76@click.option(
77 "--tool-options",
78 type=str,
79 help="Tool-specific options in the format option=value,option=value",
80)
81@click.option(
82 "--list-plugins",
83 is_flag=True,
84 default=False,
85 help="List all installed pytest plugins",
86)
87@click.option(
88 "--check-plugins",
89 is_flag=True,
90 default=False,
91 help=(
92 "Check if required plugins are installed "
93 "(use with --tool-options pytest:required_plugins=plugin1,plugin2)"
94 ),
95)
96@click.option(
97 "--collect-only",
98 is_flag=True,
99 default=False,
100 help="List tests without executing them",
101)
102@click.option(
103 "--fixtures",
104 is_flag=True,
105 default=False,
106 help="List all available fixtures",
107)
108@click.option(
109 "--fixture-info",
110 type=str,
111 default=None,
112 help="Show detailed information about a specific fixture",
113)
114@click.option(
115 "--markers",
116 is_flag=True,
117 default=False,
118 help="List all available markers",
119)
120@click.option(
121 "--parametrize-help",
122 is_flag=True,
123 default=False,
124 help="Show help for parametrized tests",
125)
126@click.option(
127 "--coverage",
128 is_flag=True,
129 default=False,
130 help="Generate test coverage report with missing lines shown in terminal",
131)
132@click.option(
133 "--debug",
134 is_flag=True,
135 help="Enable debug output on console",
136)
137@click.option(
138 "--yes",
139 "-y",
140 is_flag=True,
141 help="Skip confirmation prompt and proceed immediately",
142)
143def test_command(
144 paths: tuple[str, ...],
145 exclude: str | None,
146 include_venv: bool,
147 output: str | None,
148 output_format: str,
149 group_by: str,
150 verbose: bool,
151 raw_output: bool,
152 tool_options: str | None,
153 list_plugins: bool,
154 check_plugins: bool,
155 collect_only: bool,
156 fixtures: bool,
157 fixture_info: str | None,
158 markers: bool,
159 parametrize_help: bool,
160 coverage: bool,
161 debug: bool,
162 yes: bool,
163) -> None:
164 """Run tests using pytest.
166 This CLI command wraps pytest with lintro's output formatting.
168 Args:
169 paths: Paths to test files or directories.
170 exclude: Pattern to exclude paths.
171 include_venv: Whether to include virtual environment directories.
172 output: Output file path.
173 output_format: Output format for displaying results.
174 group_by: How to group issues in the output.
175 verbose: Show verbose output.
176 raw_output: Show raw tool output instead of formatted output.
177 tool_options: Tool-specific options in the format option=value.
178 list_plugins: List all installed pytest plugins.
179 check_plugins: Check if required plugins are installed.
180 collect_only: List tests without executing them.
181 fixtures: List all available fixtures.
182 fixture_info: Show detailed information about a specific fixture.
183 markers: List all available markers.
184 parametrize_help: Show help for parametrized tests.
185 coverage: Generate test coverage report with missing lines.
186 debug: Enable debug output on console.
187 yes: Skip confirmation prompt and proceed immediately.
189 Raises:
190 SystemExit: Process exit with the aggregated exit code.
191 """
192 # Add default paths if none provided
193 path_list: list[str] = list(paths) if paths else list(DEFAULT_PATHS)
195 # Build tool options with pytest prefix
196 tool_option_parts: list[str] = []
198 # Add special mode flags
199 boolean_flags: list[tuple[bool, str]] = [
200 (list_plugins, "pytest:list_plugins=True"),
201 (check_plugins, "pytest:check_plugins=True"),
202 (collect_only, "pytest:collect_only=True"),
203 (fixtures, "pytest:list_fixtures=True"),
204 (markers, "pytest:list_markers=True"),
205 (parametrize_help, "pytest:parametrize_help=True"),
206 (coverage, "pytest:coverage_term_missing=True"),
207 ]
209 for flag_value, option_string in boolean_flags:
210 if flag_value:
211 tool_option_parts.append(option_string)
213 # Handle fixture_info as special case (requires non-empty value)
214 if fixture_info:
215 tool_option_parts.append(f"pytest:fixture_info={fixture_info}")
217 if tool_options:
218 # Prefix with "pytest:" for pytest tool
219 # Parse options carefully to handle values containing commas
220 # Format: key=value,key=value where values can contain commas
221 prefixed_options: list[str] = []
222 parts = tool_options.split(",")
223 i = 0
225 while i < len(parts):
226 current_part = parts[i].strip()
227 if not current_part:
228 i += 1
229 continue
231 # Check if this part looks like a complete option (contains =)
232 # or starts with pytest prefix (already namespaced)
233 if "=" in current_part or current_part.lower().startswith("pytest:"):
234 normalized_part = _ensure_pytest_prefix(current_part)
235 prefixed_options.append(normalized_part)
236 i += 1
237 else:
238 # This part doesn't have =, might be a value continuation
239 # Merge with previous part if it exists and had an =
240 if prefixed_options and "=" in prefixed_options[-1]:
241 # Merge with previous option's value
242 prefixed_options[-1] = f"{prefixed_options[-1]},{current_part}"
243 else:
244 # Standalone option without =, prefix it
245 prefixed_options.append(_ensure_pytest_prefix(current_part))
246 i += 1
248 tool_option_parts.append(",".join(prefixed_options))
250 combined_tool_options: str | None = (
251 ",".join(tool_option_parts) if tool_option_parts else None
252 )
254 # Run with pytest tool
255 exit_code: int = run_lint_tools_simple(
256 action=DEFAULT_ACTION,
257 paths=path_list,
258 tools="pytest",
259 tool_options=combined_tool_options,
260 exclude=exclude,
261 include_venv=include_venv,
262 group_by=group_by,
263 output_format=output_format,
264 verbose=verbose,
265 raw_output=raw_output,
266 output_file=output,
267 debug=debug,
268 yes=yes,
269 )
271 # Exit with code only
272 raise SystemExit(exit_code)
275# Exclude from pytest collection - this is a Click command, not a test function
276cast(Any, test_command).__test__ = False
279def test(
280 paths: tuple[str, ...],
281 exclude: str | None,
282 include_venv: bool,
283 output: str | None,
284 output_format: str,
285 group_by: str,
286 verbose: bool,
287 raw_output: bool = False,
288 tool_options: str | None = None,
289 yes: bool = False,
290) -> None:
291 """Programmatic test function for backward compatibility.
293 Args:
294 paths: tuple: List of file/directory paths to test.
295 exclude: str | None: Comma-separated patterns of files/dirs to exclude.
296 include_venv: bool: Whether to include virtual environment directories.
297 output: str | None: Path to output file for results.
298 output_format: str: Format for displaying results.
299 group_by: str: How to group issues in output.
300 verbose: bool: Whether to show verbose output during execution.
301 raw_output: bool: Whether to show raw tool output instead of formatted output.
302 tool_options: str | None: Tool-specific options.
303 yes: bool: Skip confirmation prompt and proceed immediately.
305 Returns:
306 None: This function does not return a value.
307 """
308 # Build arguments for the click command
309 args: list[str] = []
310 if paths:
311 args.extend(list(paths))
312 if exclude:
313 args.extend(["--exclude", exclude])
314 if include_venv:
315 args.append("--include-venv")
316 if output:
317 args.extend(["--output", output])
318 if output_format:
319 args.extend(["--output-format", output_format])
320 if group_by:
321 args.extend(["--group-by", group_by])
322 if verbose:
323 args.append("--verbose")
324 if raw_output:
325 args.append("--raw-output")
326 if tool_options:
327 args.extend(["--tool-options", tool_options])
328 if yes:
329 args.append("--yes")
331 runner = CliRunner()
332 result = runner.invoke(test_command, args)
334 if result.exit_code != DEFAULT_EXIT_CODE:
335 import sys
337 sys.exit(result.exit_code)
338 return None