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
« 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.
3Tests the full pipeline: tool plugin → enrichment → formatted output,
4verifying that doc_url flows correctly through all layers.
5"""
7from __future__ import annotations
9import json
10from pathlib import Path
12import pytest
13from assertpy import assert_that
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
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
29@pytest.fixture
30def enriched_ruff_result(monkeypatch: pytest.MonkeyPatch) -> ToolResult:
31 """Create a ToolResult with RuffIssues and enriched doc_urls.
33 Args:
34 monkeypatch: Pytest monkeypatch for environment isolation.
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 )
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)
72 return result
75# =============================================================================
76# Grid output
77# =============================================================================
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.
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")
91 assert_that(output).contains("Docs")
92 assert_that(output).contains("line-too-long")
93 assert_that(output).contains("unused-import")
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")
108 assert_that(output).does_not_contain("Docs")
111# =============================================================================
112# JSON output
113# =============================================================================
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.
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"
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 )
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")
144# =============================================================================
145# Markdown output
146# =============================================================================
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.
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"
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 )
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 )
177# =============================================================================
178# CSV output
179# =============================================================================
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.
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"
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 )
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 )
210# =============================================================================
211# DocUrlTemplate enum
212# =============================================================================
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/")
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 )
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 )
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 )
245# =============================================================================
246# SARIF helpUri
247# =============================================================================
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
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 )
264 doc_urls = {"E501": "https://docs.astral.sh/ruff/rules/line-too-long/"}
265 sarif = to_sarif([suggestion], doc_urls=doc_urls)
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 )
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
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 )
288 sarif = to_sarif([suggestion])
290 rules = sarif["runs"][0]["tool"]["driver"]["rules"]
291 assert_that(rules[0]).does_not_contain_key("helpUri")