Coverage for tests / integration / test_pydoclint_integration.py: 96%

80 statements  

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

1"""Integration tests for pydoclint core. 

2 

3Note: The simplified pydoclint plugin reads configuration from [tool.pydoclint] 

4in pyproject.toml. See docs/tool-analysis/pydoclint-analysis.md for settings. 

5""" 

6 

7import shutil 

8import subprocess 

9from pathlib import Path 

10 

11import pytest 

12from assertpy import assert_that 

13from loguru import logger 

14 

15from lintro.plugins import ToolRegistry 

16 

17logger.remove() 

18logger.add(lambda msg: print(msg, end=""), level="INFO") 

19 

20SAMPLE_FILE = "test_samples/tools/python/pydoclint/pydoclint_violations.py" 

21 

22 

23def run_pydoclint_directly(file_path: Path) -> tuple[bool, str, int]: 

24 """Run pydoclint directly on a file and return result tuple. 

25 

26 Args: 

27 file_path: Path to the file to check with pydoclint. 

28 

29 Returns: 

30 tuple[bool, str, int]: A tuple of (success, output, issues_count). 

31 """ 

32 cmd = [ 

33 "pydoclint", 

34 "--quiet", 

35 str(file_path), 

36 ] 

37 result = subprocess.run( 

38 cmd, 

39 capture_output=True, 

40 text=True, 

41 check=False, 

42 ) 

43 output = result.stdout + result.stderr 

44 issues = [line for line in output.splitlines() if "DOC" in line and ":" in line] 

45 issues_count = len(issues) 

46 success = issues_count == 0 and result.returncode == 0 

47 return success, output, issues_count 

48 

49 

50def _ensure_pydoclint_available() -> None: 

51 """Skip test if pydoclint CLI is not runnable. 

52 

53 Attempts to execute `pydoclint --version` to verify that the CLI exists 

54 and is runnable in the current environment. 

55 """ 

56 try: 

57 result = subprocess.run( 

58 ["pydoclint", "--version"], 

59 capture_output=True, 

60 text=True, 

61 check=False, 

62 ) 

63 if result.returncode != 0: 

64 pytest.skip("pydoclint CLI not working; skipping direct CLI test") 

65 except FileNotFoundError: 

66 pytest.skip("pydoclint CLI not installed; skipping direct CLI test") 

67 

68 

69def test_pydoclint_reports_violations_direct(tmp_path: Path) -> None: 

70 """Pydoclint CLI: Should detect and report violations in a sample file. 

71 

72 Args: 

73 tmp_path: Pytest temporary directory fixture. 

74 """ 

75 _ensure_pydoclint_available() 

76 sample_file = tmp_path / "pydoclint_violations.py" 

77 shutil.copy(SAMPLE_FILE, sample_file) 

78 logger.info("[TEST] Running pydoclint directly on sample file...") 

79 success, output, issues = run_pydoclint_directly(sample_file) 

80 logger.info(f"[LOG] Pydoclint found {issues} issues. Output:\n{output}") 

81 assert_that(success).is_false().described_as( 

82 "Pydoclint should fail when violations are present.", 

83 ) 

84 assert_that(issues).is_greater_than(0).described_as( 

85 "Pydoclint should report at least one issue.", 

86 ) 

87 assert_that(output).contains("DOC").described_as( 

88 "Pydoclint output should contain error codes.", 

89 ) 

90 

91 

92def test_pydoclint_reports_violations_through_lintro(tmp_path: Path) -> None: 

93 """Lintro PydoclintTool: Should detect and report violations in a sample file. 

94 

95 Args: 

96 tmp_path: Pytest temporary directory fixture. 

97 """ 

98 _ensure_pydoclint_available() 

99 sample_file = tmp_path / "pydoclint_violations.py" 

100 shutil.copy(SAMPLE_FILE, sample_file) 

101 logger.info(f"SAMPLE_FILE: {sample_file}, exists: {sample_file.exists()}") 

102 logger.info("[TEST] Running PydoclintTool through lintro on sample file...") 

103 tool = ToolRegistry.get("pydoclint") 

104 assert_that(tool).is_not_none() 

105 result = tool.check([str(sample_file)], {}) 

106 logger.info( 

107 f"[LOG] Lintro PydoclintTool found {result.issues_count} issues. " 

108 f"Output:\n{result.output}", 

109 ) 

110 assert_that(result.success).is_false().described_as( 

111 "Lintro PydoclintTool should fail when violations are present.", 

112 ) 

113 assert_that(result.issues_count).is_greater_than(0).described_as( 

114 "Lintro PydoclintTool should report at least one issue.", 

115 ) 

116 

117 

118def test_pydoclint_output_consistency_direct_vs_lintro(tmp_path: Path) -> None: 

119 """Pydoclint CLI vs Lintro: Should produce consistent results for the same file. 

120 

121 Args: 

122 tmp_path: Pytest temporary directory fixture. 

123 """ 

124 _ensure_pydoclint_available() 

125 sample_file = tmp_path / "pydoclint_violations.py" 

126 shutil.copy(SAMPLE_FILE, sample_file) 

127 logger.info("[TEST] Comparing pydoclint CLI and Lintro PydoclintTool outputs...") 

128 tool = ToolRegistry.get("pydoclint") 

129 assert_that(tool).is_not_none() 

130 direct_success, direct_output, direct_issues = run_pydoclint_directly(sample_file) 

131 result = tool.check([str(sample_file)], {}) 

132 logger.info( 

133 f"[LOG] CLI issues: {direct_issues}, Lintro issues: {result.issues_count}", 

134 ) 

135 assert_that(direct_success).is_equal_to(result.success).described_as( 

136 "Success/failure mismatch between CLI and Lintro.", 

137 ) 

138 # Issue count may differ slightly due to parsing differences 

139 # But both should find issues 

140 assert_that(direct_issues).is_greater_than(0) 

141 assert_that(result.issues_count).is_greater_than(0) 

142 

143 

144def test_pydoclint_fix_method_not_implemented(tmp_path: Path) -> None: 

145 """Lintro PydoclintTool: .fix() should raise NotImplementedError. 

146 

147 Args: 

148 tmp_path: Pytest temporary directory fixture. 

149 """ 

150 sample_file = tmp_path / "pydoclint_violations.py" 

151 shutil.copy(SAMPLE_FILE, sample_file) 

152 logger.info( 

153 "[TEST] Verifying that PydoclintTool.fix() raises NotImplementedError...", 

154 ) 

155 tool = ToolRegistry.get("pydoclint") 

156 assert_that(tool).is_not_none() 

157 with pytest.raises(NotImplementedError): 

158 tool.fix([str(sample_file)], {}) 

159 logger.info("[LOG] NotImplementedError correctly raised by PydoclintTool.fix().") 

160 

161 

162def test_pydoclint_clean_file_passes(tmp_path: Path) -> None: 

163 """Lintro PydoclintTool: Should pass on a clean file. 

164 

165 Args: 

166 tmp_path: Pytest temporary directory fixture. 

167 """ 

168 _ensure_pydoclint_available() 

169 clean_file = tmp_path / "clean_module.py" 

170 # Clean file following Google style with types in annotations, not docstrings 

171 clean_file.write_text( 

172 '''"""Clean module with proper docstrings.""" 

173 

174 

175def add_numbers(a: int, b: int) -> int: 

176 """Add two numbers together. 

177 

178 Args: 

179 a: The first number. 

180 b: The second number. 

181 

182 Returns: 

183 The sum of a and b. 

184 """ 

185 return a + b 

186''', 

187 ) 

188 logger.info("[TEST] Running PydoclintTool on a clean file...") 

189 tool = ToolRegistry.get("pydoclint") 

190 assert_that(tool).is_not_none() 

191 result = tool.check([str(clean_file)], {}) 

192 logger.info(f"[LOG] Result: success={result.success}, issues={result.issues_count}") 

193 assert_that(result.success).is_true().described_as( 

194 "Lintro PydoclintTool should pass on a clean file.", 

195 ) 

196 assert_that(result.issues_count).is_equal_to(0).described_as( 

197 "Lintro PydoclintTool should find no issues in a clean file.", 

198 )