Coverage for tests / integration / test_doc_url_e2e.py: 100%

82 statements  

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

1"""End-to-end integration tests for the doc_url feature. 

2 

3Tests the full pipeline: tool plugin → enrichment → formatted output, 

4verifying that doc_url flows correctly through all layers. 

5""" 

6 

7from __future__ import annotations 

8 

9import json 

10from pathlib import Path 

11 

12import pytest 

13from assertpy import assert_that 

14 

15from lintro.enums.action import Action 

16from lintro.enums.doc_url_template import DocUrlTemplate 

17from lintro.enums.output_format import OutputFormat 

18from lintro.formatters.formatter import format_issues 

19from lintro.models.core.tool_result import ToolResult 

20from lintro.parsers.ruff.ruff_issue import RuffIssue 

21from lintro.tools.definitions.ruff import RuffPlugin 

22from lintro.utils.output.file_writer import write_output_file 

23 

24# Relies on internal enrichment function to simulate the post-execution 

25# doc_url population step without running actual tool subprocesses. 

26from lintro.utils.tool_executor import _enrich_issues_with_doc_urls 

27 

28 

29@pytest.fixture 

30def enriched_ruff_result(monkeypatch: pytest.MonkeyPatch) -> ToolResult: 

31 """Create a ToolResult with RuffIssues and enriched doc_urls. 

32 

33 Args: 

34 monkeypatch: Pytest monkeypatch for environment isolation. 

35 

36 Returns: 

37 ToolResult with doc_url-enriched issues. 

38 """ 

39 monkeypatch.setenv("LINTRO_TEST_MODE", "1") 

40 issues = [ 

41 RuffIssue( 

42 file="src/main.py", 

43 line=10, 

44 column=5, 

45 code="E501", 

46 message="Line too long (120 > 88)", 

47 ), 

48 RuffIssue( 

49 file="src/utils.py", 

50 line=3, 

51 column=1, 

52 code="F401", 

53 message="os imported but unused", 

54 ), 

55 ] 

56 result = ToolResult( 

57 name="ruff", 

58 success=False, 

59 output="Issues found", 

60 issues_count=len(issues), 

61 issues=issues, 

62 ) 

63 

64 # Simulate the enrichment step that tool_executor performs. 

65 # Relies on internal cache structure (_rule_name_cache) to avoid 

66 # subprocess calls — update if RuffPlugin caching is refactored. 

67 plugin = RuffPlugin() 

68 plugin._rule_name_cache["E501"] = "line-too-long" 

69 plugin._rule_name_cache["F401"] = "unused-import" 

70 _enrich_issues_with_doc_urls(plugin, result) 

71 

72 return result 

73 

74 

75# ============================================================================= 

76# Grid output 

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

78 

79 

80def test_grid_output_contains_docs_column_and_urls( 

81 enriched_ruff_result: ToolResult, 

82) -> None: 

83 """Grid format includes Docs column with URLs when doc_url is set. 

84 

85 Args: 

86 enriched_ruff_result: Enriched ToolResult fixture. 

87 """ 

88 assert enriched_ruff_result.issues is not None 

89 output = format_issues(enriched_ruff_result.issues, output_format="grid") 

90 

91 assert_that(output).contains("Docs") 

92 assert_that(output).contains("line-too-long") 

93 assert_that(output).contains("unused-import") 

94 

95 

96def test_grid_output_omits_docs_when_no_urls() -> None: 

97 """Grid format omits Docs column when no issues have doc_url.""" 

98 issues: list[RuffIssue] = [ 

99 RuffIssue( 

100 file="foo.py", 

101 line=1, 

102 code="E501", 

103 message="test", 

104 ), 

105 ] 

106 output = format_issues(issues, output_format="grid") 

107 

108 assert_that(output).does_not_contain("Docs") 

109 

110 

111# ============================================================================= 

112# JSON output 

113# ============================================================================= 

114 

115 

116def test_json_output_contains_doc_url( 

117 tmp_path: Path, 

118 enriched_ruff_result: ToolResult, 

119) -> None: 

120 """JSON output includes doc_url field on enriched issues. 

121 

122 Args: 

123 tmp_path: Temporary directory for test output. 

124 enriched_ruff_result: Enriched ToolResult fixture. 

125 """ 

126 json_path = tmp_path / "report.json" 

127 

128 write_output_file( 

129 output_path=str(json_path), 

130 output_format=OutputFormat.JSON, 

131 all_results=[enriched_ruff_result], 

132 action=Action.CHECK, 

133 total_issues=2, 

134 total_fixed=0, 

135 ) 

136 

137 content = json.loads(json_path.read_text()) 

138 issues = content["results"][0]["issues"] 

139 assert_that(issues).is_length(2) 

140 assert_that(issues[0]["doc_url"]).contains("line-too-long") 

141 assert_that(issues[1]["doc_url"]).contains("unused-import") 

142 

143 

144# ============================================================================= 

145# Markdown output 

146# ============================================================================= 

147 

148 

149def test_markdown_output_contains_clickable_links( 

150 tmp_path: Path, 

151 enriched_ruff_result: ToolResult, 

152) -> None: 

153 """Markdown output renders doc_url as clickable [docs](url) links. 

154 

155 Args: 

156 tmp_path: Temporary directory for test output. 

157 enriched_ruff_result: Enriched ToolResult fixture. 

158 """ 

159 md_path = tmp_path / "report.md" 

160 

161 write_output_file( 

162 output_path=str(md_path), 

163 output_format=OutputFormat.MARKDOWN, 

164 all_results=[enriched_ruff_result], 

165 action=Action.CHECK, 

166 total_issues=2, 

167 total_fixed=0, 

168 ) 

169 

170 content = md_path.read_text() 

171 assert_that(content).contains("| Docs |") 

172 assert_that(content).contains( 

173 "[docs](https://docs.astral.sh/ruff/rules/line-too-long/)", 

174 ) 

175 

176 

177# ============================================================================= 

178# CSV output 

179# ============================================================================= 

180 

181 

182def test_csv_output_contains_doc_url_column( 

183 tmp_path: Path, 

184 enriched_ruff_result: ToolResult, 

185) -> None: 

186 """CSV output includes doc_url column with URLs. 

187 

188 Args: 

189 tmp_path: Temporary directory for test output. 

190 enriched_ruff_result: Enriched ToolResult fixture. 

191 """ 

192 csv_path = tmp_path / "report.csv" 

193 

194 write_output_file( 

195 output_path=str(csv_path), 

196 output_format=OutputFormat.CSV, 

197 all_results=[enriched_ruff_result], 

198 action=Action.CHECK, 

199 total_issues=2, 

200 total_fixed=0, 

201 ) 

202 

203 content = csv_path.read_text() 

204 assert_that(content).contains("doc_url") 

205 assert_that(content).contains( 

206 "https://docs.astral.sh/ruff/rules/line-too-long/", 

207 ) 

208 

209 

210# ============================================================================= 

211# DocUrlTemplate enum 

212# ============================================================================= 

213 

214 

215def test_template_format_with_code() -> None: 

216 """Templates with {code} produce correct URLs when formatted.""" 

217 url = DocUrlTemplate.RUFF.format(code="line-too-long") 

218 assert_that(url).is_equal_to("https://docs.astral.sh/ruff/rules/line-too-long/") 

219 

220 

221def test_static_template_unchanged() -> None: 

222 """Templates without {code} are usable as plain strings.""" 

223 url = str(DocUrlTemplate.ACTIONLINT) 

224 assert_that(url).is_equal_to( 

225 "https://github.com/rhysd/actionlint/blob/main/docs/checks.md", 

226 ) 

227 

228 

229def test_osv_advisory_url() -> None: 

230 """OSV template produces per-vulnerability URLs.""" 

231 url = DocUrlTemplate.OSV.format(code="GHSA-c3g4-w6cv-6v7h") 

232 assert_that(url).is_equal_to( 

233 "https://osv.dev/vulnerability/GHSA-c3g4-w6cv-6v7h", 

234 ) 

235 

236 

237def test_cargo_audit_advisory_url() -> None: 

238 """Cargo-audit template produces per-advisory URLs.""" 

239 url = DocUrlTemplate.CARGO_AUDIT.format(code="RUSTSEC-2021-0124") 

240 assert_that(url).is_equal_to( 

241 "https://rustsec.org/advisories/RUSTSEC-2021-0124", 

242 ) 

243 

244 

245# ============================================================================= 

246# SARIF helpUri 

247# ============================================================================= 

248 

249 

250def test_sarif_includes_help_uri() -> None: 

251 """SARIF rule descriptors include helpUri from doc_urls map.""" 

252 from lintro.ai.models import AIFixSuggestion 

253 from lintro.ai.output.sarif import to_sarif 

254 

255 suggestion = AIFixSuggestion( 

256 file="src/main.py", 

257 line=10, 

258 code="E501", 

259 tool_name="ruff", 

260 original_code="x = 1", 

261 suggested_code="x = 1", 

262 ) 

263 

264 doc_urls = {"E501": "https://docs.astral.sh/ruff/rules/line-too-long/"} 

265 sarif = to_sarif([suggestion], doc_urls=doc_urls) 

266 

267 rules = sarif["runs"][0]["tool"]["driver"]["rules"] 

268 assert_that(rules).is_length(1) 

269 assert_that(rules[0]["helpUri"]).is_equal_to( 

270 "https://docs.astral.sh/ruff/rules/line-too-long/", 

271 ) 

272 

273 

274def test_sarif_omits_help_uri_when_no_doc_urls() -> None: 

275 """SARIF rule descriptors omit helpUri when no doc_urls provided.""" 

276 from lintro.ai.models import AIFixSuggestion 

277 from lintro.ai.output.sarif import to_sarif 

278 

279 suggestion = AIFixSuggestion( 

280 file="src/main.py", 

281 line=10, 

282 code="E501", 

283 tool_name="ruff", 

284 original_code="x = 1", 

285 suggested_code="x = 1", 

286 ) 

287 

288 sarif = to_sarif([suggestion]) 

289 

290 rules = sarif["runs"][0]["tool"]["driver"]["rules"] 

291 assert_that(rules[0]).does_not_contain_key("helpUri")