Coverage for tests / unit / utils / test_path_utils.py: 100%
98 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 path_utils module."""
3from __future__ import annotations
5import os
6from pathlib import Path
7from unittest.mock import MagicMock, patch
9import pytest
10from assertpy import assert_that
12from lintro.utils.path_utils import (
13 find_lintro_ignore,
14 load_lintro_ignore,
15 normalize_file_path_for_display,
16)
18# =============================================================================
19# Tests for find_lintro_ignore
20# =============================================================================
23def test_find_lintro_ignore_in_current_dir(tmp_path: Path) -> None:
24 """Find .lintro-ignore in current directory.
26 Args:
27 tmp_path: Temporary directory path for test files.
28 """
29 ignore_file = tmp_path / ".lintro-ignore"
30 ignore_file.write_text("*.pyc\n")
32 with patch("lintro.utils.path_utils.Path") as mock_path:
33 mock_path.cwd.return_value = tmp_path
34 result = find_lintro_ignore()
36 assert_that(result).is_not_none()
37 assert_that(str(result)).contains(".lintro-ignore")
40def test_find_lintro_ignore_pyproject_stops_search(tmp_path: Path) -> None:
41 """Stop search when pyproject.toml found without .lintro-ignore.
43 Args:
44 tmp_path: Temporary directory path for test files.
45 """
46 pyproject = tmp_path / "pyproject.toml"
47 pyproject.write_text("[tool.lintro]\n")
49 with patch("lintro.utils.path_utils.Path") as mock_path:
50 mock_path.cwd.return_value = tmp_path
51 result = find_lintro_ignore()
53 # Should return None since pyproject exists but no .lintro-ignore
54 assert_that(result).is_none()
57def test_find_lintro_ignore_with_pyproject(tmp_path: Path) -> None:
58 """Find .lintro-ignore when both it and pyproject.toml exist.
60 Args:
61 tmp_path: Temporary directory path for test files.
62 """
63 ignore_file = tmp_path / ".lintro-ignore"
64 ignore_file.write_text("*.pyc\n")
65 pyproject = tmp_path / "pyproject.toml"
66 pyproject.write_text("[tool.lintro]\n")
68 with patch("lintro.utils.path_utils.Path") as mock_path:
69 mock_path.cwd.return_value = tmp_path
70 result = find_lintro_ignore()
72 assert_that(result).is_not_none()
75def test_find_lintro_ignore_returns_none_when_nothing_found(tmp_path: Path) -> None:
76 """Return None when no .lintro-ignore or pyproject.toml found.
78 Args:
79 tmp_path: Temporary directory path for test files.
80 """
81 # Create a deep nested directory without any marker files
82 deep_dir = tmp_path / "a" / "b" / "c"
83 deep_dir.mkdir(parents=True)
85 # Mock Path.cwd() to return the deep directory
86 # and also mock parent traversal to eventually reach tmp_path's parent
87 with patch("lintro.utils.path_utils.Path") as mock_path:
88 # Create a path that has no .lintro-ignore or pyproject.toml
89 mock_cwd = MagicMock()
90 mock_path.cwd.return_value = mock_cwd
92 # Mock the traversal to return paths without markers
93 mock_cwd.__truediv__ = MagicMock(
94 return_value=MagicMock(exists=MagicMock(return_value=False)),
95 )
96 mock_cwd.parent = mock_cwd # Simulate reaching root
98 result = find_lintro_ignore()
100 assert_that(result).is_none()
103# =============================================================================
104# Tests for load_lintro_ignore
105# =============================================================================
108def test_load_lintro_ignore_patterns_from_file(tmp_path: Path) -> None:
109 """Load ignore patterns from .lintro-ignore file.
111 Args:
112 tmp_path: Temporary directory path for test files.
113 """
114 ignore_file = tmp_path / ".lintro-ignore"
115 ignore_file.write_text("*.pyc\n__pycache__/\n# comment\n\nnode_modules/\n")
117 with patch("lintro.utils.path_utils.find_lintro_ignore", return_value=ignore_file):
118 result = load_lintro_ignore()
120 assert_that(result).is_equal_to(["*.pyc", "__pycache__/", "node_modules/"])
123def test_load_lintro_ignore_returns_empty_when_no_file() -> None:
124 """Return empty list when no .lintro-ignore found."""
125 with patch("lintro.utils.path_utils.find_lintro_ignore", return_value=None):
126 result = load_lintro_ignore()
128 assert_that(result).is_empty()
131def test_load_lintro_ignore_handles_file_read_error(tmp_path: Path) -> None:
132 """Handle file read errors gracefully.
134 Args:
135 tmp_path: Description of tmp_path (Path).
136 """
137 ignore_file = tmp_path / ".lintro-ignore"
138 ignore_file.write_text("*.pyc\n")
140 with patch("lintro.utils.path_utils.find_lintro_ignore", return_value=ignore_file):
141 with patch("builtins.open", side_effect=PermissionError("Access denied")):
142 result = load_lintro_ignore()
144 assert_that(result).is_empty()
147def test_load_lintro_ignore_skips_comments_and_empty_lines(tmp_path: Path) -> None:
148 """Skip comments and empty lines.
150 Args:
151 tmp_path: Description of tmp_path (Path).
152 """
153 ignore_file = tmp_path / ".lintro-ignore"
154 ignore_file.write_text("# This is a comment\n\n \n*.pyc\n # Another comment\n")
156 with patch("lintro.utils.path_utils.find_lintro_ignore", return_value=ignore_file):
157 result = load_lintro_ignore()
159 assert_that(result).is_equal_to(["*.pyc"])
162# =============================================================================
163# Tests for normalize_file_path_for_display
164# =============================================================================
167def test_normalize_file_path_relative_path() -> None:
168 """Normalize relative path to start with ./."""
169 result = normalize_file_path_for_display("src/main.py")
170 assert_that(result).starts_with("./")
171 assert_that(result).contains("src")
172 assert_that(result).contains("main.py")
175@pytest.mark.parametrize(
176 ("input_path", "expected"),
177 [
178 ("", ""),
179 (" ", " "),
180 ],
181 ids=["empty_string", "whitespace_string"],
182)
183def test_normalize_file_path_edge_cases(input_path: str, expected: str) -> None:
184 """Handle empty and whitespace strings.
186 Args:
187 input_path: Input path to normalize.
188 expected: Expected normalized result.
189 """
190 result = normalize_file_path_for_display(input_path)
191 assert_that(result).is_equal_to(expected)
194def test_normalize_file_path_preserves_parent_path_prefix(
195 tmp_path: Path,
196 monkeypatch: pytest.MonkeyPatch,
197) -> None:
198 """Preserve ../ prefix for parent paths."""
199 project_dir = tmp_path / "project"
200 project_dir.mkdir()
201 other_dir = tmp_path / "other"
202 other_dir.mkdir()
203 (other_dir / "file.py").touch()
205 monkeypatch.chdir(project_dir)
206 result = normalize_file_path_for_display("../other/file.py")
208 assert_that(result).starts_with("../")
211def test_normalize_file_path_handles_absolute_path() -> None:
212 """Convert absolute path to relative."""
213 cwd = os.getcwd()
214 abs_path = os.path.join(cwd, "test_file.py")
215 result = normalize_file_path_for_display(abs_path)
216 assert_that(result).is_equal_to("./test_file.py")
219def test_normalize_file_path_handles_os_error() -> None:
220 """Return original path on OSError during path resolution.
222 The function catches OSError and returns the original path.
223 """
224 from pathlib import Path
226 with patch.object(Path, "resolve", side_effect=OSError("Error")):
227 result = normalize_file_path_for_display("some/path.py")
229 assert_that(result).is_equal_to("some/path.py")
232def test_normalize_file_path_adds_dot_slash_prefix(
233 tmp_path: Path,
234 monkeypatch: pytest.MonkeyPatch,
235) -> None:
236 """Add ./ prefix to paths that don't have it."""
237 (tmp_path / "src").mkdir()
238 (tmp_path / "src" / "file.py").touch()
240 monkeypatch.chdir(tmp_path)
241 result = normalize_file_path_for_display("src/file.py")
243 assert_that(result).is_equal_to("./src/file.py")