Coverage for tests / unit / tools / core / test_line_length_checker.py: 100%
91 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"""Unit tests for the shared line length checker utility."""
3from __future__ import annotations
5import json
6import subprocess
7from typing import TYPE_CHECKING
8from unittest.mock import MagicMock, patch
10import pytest
11from assertpy import assert_that
13from lintro.tools.core.line_length_checker import (
14 LineLengthViolation,
15 check_line_length_violations,
16)
18if TYPE_CHECKING:
19 from collections.abc import Generator
22# --- LineLengthViolation dataclass tests ---
25def test_line_length_violation_default_code_is_e501() -> None:
26 """Test that the default code is E501."""
27 violation = LineLengthViolation(
28 file="test.py",
29 line=10,
30 column=89,
31 message="Line too long (100 > 88)",
32 )
33 assert_that(violation.code).is_equal_to("E501")
36def test_line_length_violation_all_fields_set() -> None:
37 """Test creating a violation with all fields."""
38 violation = LineLengthViolation(
39 file="/path/to/file.py",
40 line=42,
41 column=100,
42 message="Line too long (120 > 88)",
43 code="E501",
44 )
45 assert_that(violation.file).is_equal_to("/path/to/file.py")
46 assert_that(violation.line).is_equal_to(42)
47 assert_that(violation.column).is_equal_to(100)
48 assert_that(violation.message).is_equal_to("Line too long (120 > 88)")
49 assert_that(violation.code).is_equal_to("E501")
52# --- Fixtures for check_line_length_violations tests ---
55@pytest.fixture
56def mock_ruff_available() -> Generator[MagicMock, None, None]:
57 """Mock shutil.which to return ruff as available.
59 Yields:
60 MagicMock: Configured mock for shutil.which.
61 """
62 with patch("shutil.which", return_value="/usr/bin/ruff") as mock:
63 yield mock
66@pytest.fixture
67def mock_subprocess() -> Generator[MagicMock, None, None]:
68 """Mock subprocess.run for testing.
70 Yields:
71 MagicMock: Configured mock for subprocess.run.
72 """
73 with patch("subprocess.run") as mock:
74 mock.return_value = MagicMock(stdout="[]", returncode=0)
75 yield mock
78# --- check_line_length_violations function tests ---
81def test_check_line_length_empty_files_returns_empty_list() -> None:
82 """Test that empty file list returns empty violations."""
83 result = check_line_length_violations(files=[])
84 assert_that(result).is_empty()
87def test_check_line_length_ruff_not_available_returns_empty() -> None:
88 """Test that missing ruff returns empty violations without error."""
89 with patch("shutil.which", return_value=None) as mock_which:
90 result = check_line_length_violations(files=["test.py"])
91 assert_that(result).is_empty()
92 mock_which.assert_called_once_with("ruff")
95def test_check_line_length_successful_detection(
96 mock_ruff_available: MagicMock,
97 mock_subprocess: MagicMock,
98) -> None:
99 """Test successful detection of E501 violations.
101 Args:
102 mock_ruff_available: Mock fixture ensuring ruff is available.
103 mock_subprocess: Mock fixture for subprocess operations.
104 """
105 ruff_output = json.dumps(
106 [
107 {
108 "filename": "/path/to/file.py",
109 "location": {"row": 10, "column": 89},
110 "message": "Line too long (100 > 88)",
111 "code": "E501",
112 },
113 ],
114 )
115 mock_subprocess.return_value = MagicMock(stdout=ruff_output, returncode=1)
117 result = check_line_length_violations(files=["file.py"], cwd="/path/to")
119 assert_that(result).is_length(1)
120 assert_that(result[0].file).is_equal_to("/path/to/file.py")
121 assert_that(result[0].line).is_equal_to(10)
122 assert_that(result[0].column).is_equal_to(89)
123 assert_that(result[0].message).is_equal_to("Line too long (100 > 88)")
124 assert_that(result[0].code).is_equal_to("E501")
127def test_check_line_length_custom_line_length(
128 mock_ruff_available: MagicMock,
129 mock_subprocess: MagicMock,
130) -> None:
131 """Test that custom line_length is passed to ruff.
133 Args:
134 mock_ruff_available: Mock fixture ensuring ruff is available.
135 mock_subprocess: Mock fixture for subprocess operations.
136 """
137 check_line_length_violations(files=["test.py"], line_length=100)
139 call_args = mock_subprocess.call_args
140 cmd = call_args[0][0]
141 assert_that(cmd).contains("--line-length")
142 assert_that(cmd).contains("100")
145@pytest.mark.parametrize(
146 "exception,description",
147 [
148 (subprocess.TimeoutExpired(cmd=["ruff"], timeout=30), "timeout"),
149 (FileNotFoundError("ruff not found"), "file_not_found"),
150 (RuntimeError("Unexpected error"), "generic_error"),
151 ],
152 ids=["timeout", "file_not_found", "generic_error"],
153)
154def test_check_line_length_exception_returns_empty(
155 mock_ruff_available: MagicMock,
156 exception: Exception,
157 description: str,
158) -> None:
159 """Test that various exceptions return empty list gracefully.
161 Args:
162 mock_ruff_available: Mock fixture ensuring ruff is available.
163 exception: The exception to be raised by subprocess.run.
164 description: Description of the test case.
165 """
166 with patch("subprocess.run", side_effect=exception):
167 result = check_line_length_violations(files=["test.py"], timeout=30)
168 assert_that(result).is_empty()
171@pytest.mark.parametrize(
172 "stdout,description",
173 [
174 ("not valid json", "invalid_json"),
175 ("", "empty_stdout"),
176 ],
177 ids=["invalid_json", "empty_stdout"],
178)
179def test_check_line_length_invalid_output_returns_empty(
180 mock_ruff_available: MagicMock,
181 stdout: str,
182 description: str,
183) -> None:
184 """Test that invalid/empty stdout returns empty list gracefully.
186 Args:
187 mock_ruff_available: Mock fixture ensuring ruff is available.
188 stdout: The stdout output from subprocess.run.
189 description: Description of the test case.
190 """
191 with patch("subprocess.run") as mock_run:
192 mock_run.return_value = MagicMock(stdout=stdout, returncode=1)
193 result = check_line_length_violations(files=["test.py"])
194 assert_that(result).is_empty()
197def test_check_line_length_relative_paths_converted_to_absolute(
198 mock_ruff_available: MagicMock,
199 mock_subprocess: MagicMock,
200) -> None:
201 """Test that relative file paths are converted to absolute.
203 Args:
204 mock_ruff_available: Mock fixture ensuring ruff is available.
205 mock_subprocess: Mock fixture for subprocess operations.
206 """
207 check_line_length_violations(
208 files=["src/module.py", "tests/test_module.py"],
209 cwd="/project",
210 )
212 call_args = mock_subprocess.call_args
213 cmd = call_args[0][0]
214 assert_that(cmd).contains("/project/src/module.py")
215 assert_that(cmd).contains("/project/tests/test_module.py")
218def test_check_line_length_old_ruff_json_format(
219 mock_ruff_available: MagicMock,
220 mock_subprocess: MagicMock,
221) -> None:
222 """Test compatibility with older Ruff JSON format (no location wrapper).
224 Args:
225 mock_ruff_available: Mock fixture ensuring ruff is available.
226 mock_subprocess: Mock fixture for subprocess operations.
227 """
228 ruff_output = json.dumps(
229 [
230 {
231 "filename": "/path/to/file.py",
232 "row": 15,
233 "column": 100,
234 "message": "Line too long (110 > 88)",
235 "code": "E501",
236 },
237 ],
238 )
239 mock_subprocess.return_value = MagicMock(stdout=ruff_output, returncode=1)
241 result = check_line_length_violations(files=["file.py"], cwd="/path/to")
243 assert_that(result).is_length(1)
244 assert_that(result[0].line).is_equal_to(15)
245 assert_that(result[0].column).is_equal_to(100)
248def test_check_line_length_multiple_violations(
249 mock_ruff_available: MagicMock,
250 mock_subprocess: MagicMock,
251) -> None:
252 """Test handling multiple E501 violations.
254 Args:
255 mock_ruff_available: Mock fixture ensuring ruff is available.
256 mock_subprocess: Mock fixture for subprocess operations.
257 """
258 ruff_output = json.dumps(
259 [
260 {
261 "filename": "/path/file1.py",
262 "location": {"row": 10, "column": 89},
263 "message": "Line too long (100 > 88)",
264 "code": "E501",
265 },
266 {
267 "filename": "/path/file2.py",
268 "location": {"row": 25, "column": 89},
269 "message": "Line too long (150 > 88)",
270 "code": "E501",
271 },
272 {
273 "filename": "/path/file1.py",
274 "location": {"row": 50, "column": 89},
275 "message": "Line too long (200 > 88)",
276 "code": "E501",
277 },
278 ],
279 )
280 mock_subprocess.return_value = MagicMock(stdout=ruff_output, returncode=1)
282 result = check_line_length_violations(files=["file1.py", "file2.py"])
284 assert_that(result).is_length(3)
285 assert_that([v.file for v in result]).contains("/path/file1.py", "/path/file2.py")
288def test_check_line_length_command_includes_required_flags(
289 mock_ruff_available: MagicMock,
290 mock_subprocess: MagicMock,
291) -> None:
292 """Test that the ruff command includes required flags.
294 Args:
295 mock_ruff_available: Mock for ruff availability check.
296 mock_subprocess: Mock for subprocess calls.
297 """
298 check_line_length_violations(files=["test.py"])
300 call_args = mock_subprocess.call_args
301 cmd = call_args[0][0]
303 assert_that(cmd).contains("check")
304 assert_that(cmd).contains("--select")
305 assert_that(cmd).contains("E501")
306 assert_that(cmd).contains("--output-format")
307 assert_that(cmd).contains("json")
308 assert_that(cmd).contains("--no-cache")