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

1"""Unit tests for path_utils module.""" 

2 

3from __future__ import annotations 

4 

5import os 

6from pathlib import Path 

7from unittest.mock import MagicMock, patch 

8 

9import pytest 

10from assertpy import assert_that 

11 

12from lintro.utils.path_utils import ( 

13 find_lintro_ignore, 

14 load_lintro_ignore, 

15 normalize_file_path_for_display, 

16) 

17 

18# ============================================================================= 

19# Tests for find_lintro_ignore 

20# ============================================================================= 

21 

22 

23def test_find_lintro_ignore_in_current_dir(tmp_path: Path) -> None: 

24 """Find .lintro-ignore in current directory. 

25 

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") 

31 

32 with patch("lintro.utils.path_utils.Path") as mock_path: 

33 mock_path.cwd.return_value = tmp_path 

34 result = find_lintro_ignore() 

35 

36 assert_that(result).is_not_none() 

37 assert_that(str(result)).contains(".lintro-ignore") 

38 

39 

40def test_find_lintro_ignore_pyproject_stops_search(tmp_path: Path) -> None: 

41 """Stop search when pyproject.toml found without .lintro-ignore. 

42 

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") 

48 

49 with patch("lintro.utils.path_utils.Path") as mock_path: 

50 mock_path.cwd.return_value = tmp_path 

51 result = find_lintro_ignore() 

52 

53 # Should return None since pyproject exists but no .lintro-ignore 

54 assert_that(result).is_none() 

55 

56 

57def test_find_lintro_ignore_with_pyproject(tmp_path: Path) -> None: 

58 """Find .lintro-ignore when both it and pyproject.toml exist. 

59 

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") 

67 

68 with patch("lintro.utils.path_utils.Path") as mock_path: 

69 mock_path.cwd.return_value = tmp_path 

70 result = find_lintro_ignore() 

71 

72 assert_that(result).is_not_none() 

73 

74 

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. 

77 

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) 

84 

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 

91 

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 

97 

98 result = find_lintro_ignore() 

99 

100 assert_that(result).is_none() 

101 

102 

103# ============================================================================= 

104# Tests for load_lintro_ignore 

105# ============================================================================= 

106 

107 

108def test_load_lintro_ignore_patterns_from_file(tmp_path: Path) -> None: 

109 """Load ignore patterns from .lintro-ignore file. 

110 

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") 

116 

117 with patch("lintro.utils.path_utils.find_lintro_ignore", return_value=ignore_file): 

118 result = load_lintro_ignore() 

119 

120 assert_that(result).is_equal_to(["*.pyc", "__pycache__/", "node_modules/"]) 

121 

122 

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() 

127 

128 assert_that(result).is_empty() 

129 

130 

131def test_load_lintro_ignore_handles_file_read_error(tmp_path: Path) -> None: 

132 """Handle file read errors gracefully. 

133 

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") 

139 

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() 

143 

144 assert_that(result).is_empty() 

145 

146 

147def test_load_lintro_ignore_skips_comments_and_empty_lines(tmp_path: Path) -> None: 

148 """Skip comments and empty lines. 

149 

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") 

155 

156 with patch("lintro.utils.path_utils.find_lintro_ignore", return_value=ignore_file): 

157 result = load_lintro_ignore() 

158 

159 assert_that(result).is_equal_to(["*.pyc"]) 

160 

161 

162# ============================================================================= 

163# Tests for normalize_file_path_for_display 

164# ============================================================================= 

165 

166 

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") 

173 

174 

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. 

185 

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) 

192 

193 

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() 

204 

205 monkeypatch.chdir(project_dir) 

206 result = normalize_file_path_for_display("../other/file.py") 

207 

208 assert_that(result).starts_with("../") 

209 

210 

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") 

217 

218 

219def test_normalize_file_path_handles_os_error() -> None: 

220 """Return original path on OSError during path resolution. 

221 

222 The function catches OSError and returns the original path. 

223 """ 

224 from pathlib import Path 

225 

226 with patch.object(Path, "resolve", side_effect=OSError("Error")): 

227 result = normalize_file_path_for_display("some/path.py") 

228 

229 assert_that(result).is_equal_to("some/path.py") 

230 

231 

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() 

239 

240 monkeypatch.chdir(tmp_path) 

241 result = normalize_file_path_for_display("src/file.py") 

242 

243 assert_that(result).is_equal_to("./src/file.py")