Coverage for lintro / tools / implementations / pytest / pytest_executor.py: 63%
43 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"""Pytest execution logic.
3This module contains the PytestExecutor class that handles test execution
4and subprocess operations.
5"""
7from __future__ import annotations
9from dataclasses import dataclass
10from typing import TYPE_CHECKING
12from lintro.tools.implementations.pytest.collection import get_cpu_count
13from lintro.tools.implementations.pytest.markers import collect_tests_once
14from lintro.tools.implementations.pytest.pytest_config import PytestConfiguration
16if TYPE_CHECKING:
17 from lintro.tools.definitions.pytest import PytestPlugin
20@dataclass
21class PytestExecutor:
22 """Handles pytest test execution.
24 This class encapsulates the logic for executing pytest tests
25 and handling subprocess operations.
27 Attributes:
28 config: PytestConfiguration instance with test execution options.
29 tool: Reference to the parent tool for subprocess execution.
30 """
32 config: PytestConfiguration
33 tool: PytestPlugin | None # Required: must be set by the parent tool
35 def prepare_test_execution(
36 self,
37 target_files: list[str],
38 ) -> int:
39 """Prepare test execution by collecting tests.
41 Args:
42 target_files: Files or directories to test.
44 Raises:
45 ValueError: If tool reference is not set.
47 Returns:
48 int: Total number of available tests.
49 """
50 if self.tool is None:
51 raise ValueError("Tool reference not set on executor")
53 # Collect tests to get total count
54 total_available_tests = collect_tests_once(
55 self.tool,
56 target_files,
57 )
59 return total_available_tests
61 def execute_tests(
62 self,
63 cmd: list[str],
64 ) -> tuple[bool, str, int]:
65 """Execute pytest tests and parse output.
67 Args:
68 cmd: Command to execute.
70 Raises:
71 ValueError: If tool reference is not set.
73 Returns:
74 Tuple[bool, str, int]: Tuple of (success, output, return_code).
75 """
76 if self.tool is None:
77 raise ValueError("Tool reference not set on executor")
79 success, output = self.tool._run_subprocess(cmd)
80 # Parse output with actual success status
81 # (pytest returns non-zero on failures)
82 return_code = 0 if success else 1
83 return (success, output, return_code)
85 def display_run_config(
86 self,
87 total_tests: int,
88 target_files: list[str],
89 ) -> None:
90 """Display test run configuration summary.
92 Args:
93 total_tests: Total number of tests discovered.
94 target_files: List of target files/directories.
95 """
96 import click
98 options = self.config.get_options_dict()
100 # Get worker configuration
101 workers = options.get("workers")
102 parallel_preset = options.get("parallel_preset")
103 if parallel_preset:
104 worker_display = f"{parallel_preset} preset"
105 elif workers == "auto" or workers is None:
106 cpu_count = get_cpu_count()
107 worker_display = f"auto ({cpu_count} CPUs)"
108 elif workers and str(workers) != "0":
109 worker_display = str(workers)
110 else:
111 worker_display = "disabled"
113 # Get coverage configuration
114 coverage_enabled = any(
115 [
116 options.get("coverage_term_missing"),
117 options.get("coverage_html"),
118 options.get("coverage_xml"),
119 options.get("coverage_report"),
120 ],
121 )
122 coverage_display = "enabled" if coverage_enabled else "disabled"
124 # Format paths display
125 if len(target_files) == 1:
126 paths_display = target_files[0]
127 elif len(target_files) <= 3:
128 paths_display = ", ".join(target_files)
129 else:
130 paths_display = f"{target_files[0]} (+{len(target_files) - 1} more)"
132 # Build and display config summary
133 config_line = (
134 f"Tests: {total_tests} | "
135 f"Parallel: {worker_display} | "
136 f"Coverage: {coverage_display} | "
137 f"Path: {paths_display}"
138 )
139 click.echo(click.style(f"[LINTRO] {config_line}", fg="cyan"))