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

1"""Pytest execution logic. 

2 

3This module contains the PytestExecutor class that handles test execution 

4and subprocess operations. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass 

10from typing import TYPE_CHECKING 

11 

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 

15 

16if TYPE_CHECKING: 

17 from lintro.tools.definitions.pytest import PytestPlugin 

18 

19 

20@dataclass 

21class PytestExecutor: 

22 """Handles pytest test execution. 

23 

24 This class encapsulates the logic for executing pytest tests 

25 and handling subprocess operations. 

26 

27 Attributes: 

28 config: PytestConfiguration instance with test execution options. 

29 tool: Reference to the parent tool for subprocess execution. 

30 """ 

31 

32 config: PytestConfiguration 

33 tool: PytestPlugin | None # Required: must be set by the parent tool 

34 

35 def prepare_test_execution( 

36 self, 

37 target_files: list[str], 

38 ) -> int: 

39 """Prepare test execution by collecting tests. 

40 

41 Args: 

42 target_files: Files or directories to test. 

43 

44 Raises: 

45 ValueError: If tool reference is not set. 

46 

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") 

52 

53 # Collect tests to get total count 

54 total_available_tests = collect_tests_once( 

55 self.tool, 

56 target_files, 

57 ) 

58 

59 return total_available_tests 

60 

61 def execute_tests( 

62 self, 

63 cmd: list[str], 

64 ) -> tuple[bool, str, int]: 

65 """Execute pytest tests and parse output. 

66 

67 Args: 

68 cmd: Command to execute. 

69 

70 Raises: 

71 ValueError: If tool reference is not set. 

72 

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") 

78 

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) 

84 

85 def display_run_config( 

86 self, 

87 total_tests: int, 

88 target_files: list[str], 

89 ) -> None: 

90 """Display test run configuration summary. 

91 

92 Args: 

93 total_tests: Total number of tests discovered. 

94 target_files: List of target files/directories. 

95 """ 

96 import click 

97 

98 options = self.config.get_options_dict() 

99 

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" 

112 

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" 

123 

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)" 

131 

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"))