Coverage for tests / unit / tools / test_edge_cases.py: 95%
131 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"""Edge case tests for tool plugin handling.
3This module tests edge cases that may occur in real-world usage:
4- Symlink handling (regular and broken symlinks)
5- Long file paths (200, 260, 500+ characters)
6- Unicode in output (bullets, arrows, CJK, emoji, accented chars)
7- Concurrent execution thread safety
8"""
10from __future__ import annotations
12from concurrent.futures import ThreadPoolExecutor
13from pathlib import Path
14from typing import TYPE_CHECKING
15from unittest.mock import MagicMock, patch
17import pytest
18from assertpy import assert_that
20from lintro.enums.tool_name import ToolName
21from lintro.models.core.tool_result import ToolResult
22from lintro.tools.definitions.ruff import RuffPlugin
24if TYPE_CHECKING:
25 from collections.abc import Callable
28# =============================================================================
29# Symlink handling tests
30# =============================================================================
33def test_regular_symlink_is_followed(tmp_path: Path) -> None:
34 """Verify regular symlinks are correctly resolved and processed.
36 Args:
37 tmp_path: Temporary directory path for test files.
38 """
39 # Create a real file
40 real_file = tmp_path / "real_file.py"
41 real_file.write_text("x = 1\n")
43 # Create a symlink to the file
44 symlink_path = tmp_path / "symlink_file.py"
45 symlink_path.symlink_to(real_file)
47 assert_that(symlink_path.exists()).is_true()
48 assert_that(symlink_path.is_symlink()).is_true()
49 assert_that(symlink_path.resolve()).is_equal_to(real_file.resolve())
52def test_broken_symlink_handled_gracefully(tmp_path: Path) -> None:
53 """Verify broken symlinks don't cause crashes during path filtering.
55 Args:
56 tmp_path: Temporary directory path for test files.
57 """
58 # Create a symlink to a non-existent target
59 broken_symlink = tmp_path / "broken_link.py"
60 target_path = tmp_path / "nonexistent.py"
61 broken_symlink.symlink_to(target_path)
63 # The symlink exists as a link but its target doesn't
64 assert_that(broken_symlink.is_symlink()).is_true()
65 assert_that(broken_symlink.exists()).is_false()
67 # Verify we can check for broken symlinks
68 try:
69 broken_symlink.resolve(strict=True)
70 resolved = True
71 except FileNotFoundError:
72 resolved = False
74 assert_that(resolved).is_false()
77def test_symlink_directory_traversal(tmp_path: Path) -> None:
78 """Verify symlinks to directories are handled correctly.
80 Args:
81 tmp_path: Temporary directory path for test files.
82 """
83 # Create a subdirectory with a file
84 subdir = tmp_path / "subdir"
85 subdir.mkdir()
86 (subdir / "file.py").write_text("y = 2\n")
88 # Create a symlink to the directory
89 dir_link = tmp_path / "link_to_subdir"
90 dir_link.symlink_to(subdir)
92 # File should be accessible through the symlink
93 linked_file = dir_link / "file.py"
94 assert_that(linked_file.exists()).is_true()
95 assert_that(linked_file.read_text()).is_equal_to("y = 2\n")
98# =============================================================================
99# Long file path tests
100# =============================================================================
103@pytest.mark.parametrize(
104 ("path_length", "description"),
105 [
106 pytest.param(200, "long-path-200", id="200-chars"),
107 pytest.param(250, "near-windows-limit", id="250-chars"),
108 ],
109)
110def test_long_file_paths_handled(
111 tmp_path: Path,
112 path_length: int,
113 description: str,
114) -> None:
115 """Verify long file paths are handled correctly.
117 Args:
118 tmp_path: Temporary directory path for test files.
119 path_length: Target path length to test.
120 description: Test description.
121 """
122 # Calculate how many nested directories we need
123 # Each directory adds ~5 chars (name + separator)
124 base_len = len(str(tmp_path))
125 remaining = path_length - base_len - 10 # Leave room for filename
127 # Create nested directories
128 current = tmp_path
129 segment_len = 8 # Directory name length
130 while len(str(current)) < base_len + remaining:
131 dir_name = "d" * segment_len
132 current = current / dir_name
133 try:
134 current.mkdir(exist_ok=True)
135 except OSError:
136 # Path might be too long for the OS
137 break
139 # Create a file in the deepest directory
140 if current.exists():
141 test_file = current / "test.py"
142 try:
143 test_file.write_text("x = 1\n")
144 assert_that(test_file.exists()).is_true()
145 assert_that(len(str(test_file))).is_greater_than(100)
146 except OSError:
147 # File creation might fail on some systems
148 pass
151def test_path_with_spaces_and_special_chars(tmp_path: Path) -> None:
152 """Verify paths with spaces and special characters work correctly.
154 Args:
155 tmp_path: Temporary directory path for test files.
156 """
157 # Create a directory with spaces and special chars
158 special_dir = tmp_path / "path with spaces"
159 special_dir.mkdir()
161 test_file = special_dir / "file [1].py"
162 test_file.write_text("x = 1\n")
164 assert_that(test_file.exists()).is_true()
165 assert_that(test_file.read_text()).is_equal_to("x = 1\n")
168# =============================================================================
169# Unicode output tests
170# =============================================================================
173UNICODE_TEST_CASES = [
174 pytest.param("• Bullet point", "bullet", id="bullet"),
175 pytest.param("→ Arrow", "arrow", id="arrow"),
176 pytest.param("✓ Check mark", "checkmark", id="checkmark"),
177 pytest.param("✗ Cross mark", "crossmark", id="crossmark"),
178 pytest.param("中文测试", "cjk-chinese", id="chinese"),
179 pytest.param("日本語テスト", "cjk-japanese", id="japanese"),
180 pytest.param("한국어 테스트", "cjk-korean", id="korean"),
181 pytest.param("😀 Emoji test", "emoji", id="emoji"),
182 pytest.param("Ñ ñ á é í ó ú", "accented", id="accented"),
183 pytest.param("αβγδ ΑΒΓΔ", "greek", id="greek"),
184 pytest.param("∀∃∅∈∉∋∌", "math", id="math-symbols"),
185]
188@pytest.mark.parametrize(
189 ("unicode_text", "category"),
190 UNICODE_TEST_CASES,
191)
192def test_unicode_in_tool_output(unicode_text: str, category: str) -> None:
193 """Verify Unicode characters in tool output are handled correctly.
195 Args:
196 unicode_text: Unicode text to test.
197 category: Category of Unicode being tested.
198 """
199 # Create a mock tool result with Unicode in output
200 result = ToolResult(
201 name=ToolName.RUFF,
202 success=True,
203 issues_count=0,
204 output=f"Message: {unicode_text}",
205 issues=None,
206 )
208 assert_that(result.output).contains(unicode_text)
209 assert_that(str(result.output)).is_not_empty()
212@pytest.mark.parametrize(
213 ("unicode_text", "category"),
214 UNICODE_TEST_CASES,
215)
216def test_unicode_in_file_paths(
217 tmp_path: Path,
218 unicode_text: str,
219 category: str,
220) -> None:
221 """Verify Unicode characters in file paths are handled correctly.
223 Args:
224 tmp_path: Temporary directory path for test files.
225 unicode_text: Unicode text to test in file name.
226 category: Category of Unicode being tested.
227 """
228 # Create a file with Unicode in the name
229 # Remove characters that are invalid in file names
230 safe_name = unicode_text.replace("/", "_").replace("\\", "_")
231 safe_name = "".join(c for c in safe_name if c.isalnum() or c in " _-")[:20]
233 if not safe_name.strip():
234 safe_name = "unicode_file"
236 test_file = tmp_path / f"{safe_name}.py"
237 try:
238 test_file.write_text("x = 1\n")
239 assert_that(test_file.exists()).is_true()
240 except (OSError, UnicodeEncodeError):
241 # Some systems may not support all Unicode in filenames
242 pytest.skip(f"System doesn't support {category} in filenames")
245# =============================================================================
246# Concurrent execution tests
247# =============================================================================
250def test_concurrent_tool_result_creation() -> None:
251 """Verify ToolResult creation is thread-safe under concurrent access.
253 Multiple threads creating ToolResult objects simultaneously should
254 not cause race conditions or data corruption.
255 """
256 results: list[ToolResult] = []
258 def create_result(index: int) -> ToolResult:
259 return ToolResult(
260 name=ToolName.RUFF,
261 success=index % 2 == 0,
262 issues_count=index,
263 output=f"Output {index}",
264 issues=None,
265 )
267 with ThreadPoolExecutor(max_workers=4) as executor:
268 futures = [executor.submit(create_result, i) for i in range(20)]
269 results = [f.result() for f in futures]
271 assert_that(results).is_length(20)
272 for i, result in enumerate(results):
273 assert_that(result.issues_count).is_equal_to(i)
274 assert_that(result.output).is_equal_to(f"Output {i}")
277def test_concurrent_plugin_instantiation(
278 mock_execution_context_factory: Callable[..., MagicMock],
279) -> None:
280 """Verify plugin instances can be created concurrently.
282 Args:
283 mock_execution_context_factory: Factory for creating mock execution contexts.
284 """
285 plugins: list[RuffPlugin] = []
287 def create_plugin(_: int) -> RuffPlugin:
288 return RuffPlugin()
290 with ThreadPoolExecutor(max_workers=4) as executor:
291 futures = [executor.submit(create_plugin, i) for i in range(10)]
292 plugins = [f.result() for f in futures]
294 assert_that(plugins).is_length(10)
295 for plugin in plugins:
296 assert_that(plugin.definition.name).is_equal_to(ToolName.RUFF)
299# =============================================================================
300# Empty and edge input tests
301# =============================================================================
304def test_empty_file_list_handling() -> None:
305 """Verify empty file list is handled gracefully."""
306 plugin = RuffPlugin()
308 with patch.object(plugin, "_prepare_execution") as mock_prepare:
309 mock_ctx = MagicMock()
310 mock_ctx.should_skip = True
311 mock_ctx.early_result = ToolResult(
312 name=ToolName.RUFF,
313 success=True,
314 issues_count=0,
315 output="",
316 issues=None,
317 )
318 mock_prepare.return_value = mock_ctx
320 result = plugin.check([], {})
321 assert_that(result.success).is_true()
322 assert_that(result.issues_count).is_equal_to(0)
325def test_single_file_handling(tmp_path: Path) -> None:
326 """Verify single file is processed correctly.
328 Args:
329 tmp_path: Temporary directory path for test files.
330 """
331 test_file = tmp_path / "single.py"
332 test_file.write_text("x = 1\n")
334 plugin = RuffPlugin()
336 with (
337 patch.object(plugin, "_prepare_execution") as mock_prepare,
338 patch.object(plugin, "_run_subprocess", return_value=(True, "")),
339 ):
340 mock_ctx = MagicMock()
341 mock_ctx.should_skip = False
342 mock_ctx.early_result = None
343 mock_ctx.files = [str(test_file)]
344 mock_ctx.rel_files = ["single.py"]
345 mock_ctx.timeout = 30
346 mock_ctx.cwd = str(tmp_path)
347 mock_prepare.return_value = mock_ctx
349 result = plugin.check([str(test_file)], {})
350 assert_that(result.name).is_equal_to(ToolName.RUFF)