Repository navigation
Expand file tree
/
Copy pathtest_paths.py
More file actions
419 lines (351 loc) · 16.9 KB
/
Copy pathtest_paths.py
File metadata and controls
419 lines (351 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Tests for paths module."""
import os
import pathlib
import site
import sys
import sysconfig
import tempfile
from unittest.mock import patch
import pytest
from vscode_common_python_lsp.paths import (
PythonFileKind,
as_list,
classify_python_file,
get_relative_path,
is_current_interpreter,
is_match,
is_same_path,
normalize_path,
sanitize_path_for_name_max,
)
class TestAsList:
@pytest.mark.parametrize(
"input_val, expected",
[
([1, 2, 3], [1, 2, 3]),
((1, 2), [1, 2]),
(42, [42]),
("hello", ["hello"]),
(None, [None]),
],
)
def test_as_list(self, input_val, expected):
assert as_list(input_val) == expected
class TestNormalizePath:
def test_returns_absolute(self):
result = normalize_path(".")
assert os.path.isabs(result)
def test_without_symlink_resolve(self):
result = normalize_path("some/path", resolve_symlinks=False)
assert isinstance(result, str)
class TestIsSamePath:
def test_same_path(self, tmp_path):
p = str(tmp_path / "file.txt")
assert is_same_path(p, p)
def test_different_paths(self, tmp_path):
assert not is_same_path(str(tmp_path / "a"), str(tmp_path / "b"))
@pytest.mark.skipif(
not hasattr(os, "symlink"),
reason="os.symlink not available on this platform",
)
def test_symlink_without_resolve(self, tmp_path):
"""Without resolve_symlinks, symlinked paths compare as different."""
real_dir = tmp_path / "real"
real_dir.mkdir()
real_file = real_dir / "test.py"
real_file.write_text("# test", encoding="utf-8")
link_dir = tmp_path / "link"
try:
link_dir.symlink_to(real_dir)
except OSError:
pytest.skip("Unable to create symlink (requires elevated privileges)")
real_path = str(real_dir / "test.py")
link_path = str(link_dir / "test.py")
assert not is_same_path(real_path, link_path)
@pytest.mark.skipif(
not hasattr(os, "symlink"),
reason="os.symlink not available on this platform",
)
def test_symlink_with_resolve(self, tmp_path):
"""With resolve_symlinks=True, symlinked paths compare as equal."""
real_dir = tmp_path / "real"
real_dir.mkdir()
real_file = real_dir / "test.py"
real_file.write_text("# test", encoding="utf-8")
link_dir = tmp_path / "link"
try:
link_dir.symlink_to(real_dir)
except OSError:
pytest.skip("Unable to create symlink (requires elevated privileges)")
real_path = str(real_dir / "test.py")
link_path = str(link_dir / "test.py")
assert is_same_path(real_path, link_path, resolve_symlinks=True)
assert is_same_path(link_path, real_path, resolve_symlinks=True)
def test_resolve_falls_back_on_oserror(self, tmp_path):
"""resolve_symlinks=True falls back to lexical when paths don't exist."""
a = str(tmp_path / "nonexistent_a")
b = str(tmp_path / "nonexistent_b")
assert not is_same_path(a, b, resolve_symlinks=True)
assert is_same_path(a, a, resolve_symlinks=True)
class TestIsCurrentInterpreter:
@pytest.mark.parametrize(
"path, expected",
[
(sys.executable, True),
("/nonexistent/python", False),
],
)
def test_is_current_interpreter(self, path, expected):
assert is_current_interpreter(path) == expected
# ---------------------------------------------------------------------------
# Classifier tests (mirrors flake8 test_stdlib_detection.py)
# ---------------------------------------------------------------------------
class TestClassifyPythonFile:
"""Tests for classify_python_file() — the sole classification API."""
def test_stdlib_module_os(self):
"""os module should be classified as STDLIB."""
assert classify_python_file(os.__file__) == PythonFileKind.STDLIB
def test_stdlib_module_json(self):
"""json module should be classified as STDLIB."""
import json
assert classify_python_file(json.__file__) == PythonFileKind.STDLIB
def test_stdlib_module_sys(self):
"""sys module file (if it has one) should be STDLIB."""
if hasattr(sys, "__file__") and sys.__file__:
assert classify_python_file(sys.__file__) == PythonFileKind.STDLIB
def test_non_python_file(self, tmp_path):
"""A user/workspace file should return None."""
assert classify_python_file(str(tmp_path / "test.py")) is None
def test_system_site_packages(self):
"""Files in system site-packages should be SYSTEM_SITE."""
for site_pkg_dir in site.getsitepackages():
test_file = os.path.join(site_pkg_dir, "pytest", "__init__.py")
kind = classify_python_file(test_file)
assert kind == PythonFileKind.SYSTEM_SITE, (
f"File in site-packages {test_file} should be SYSTEM_SITE, "
f"got {kind}"
)
def test_user_site_packages(self):
"""Files in user site-packages should be USER_SITE."""
user_site = site.getusersitepackages()
if not user_site:
pytest.skip("User site-packages not available")
test_file = os.path.join(user_site, "some_package", "__init__.py")
assert classify_python_file(test_file) == PythonFileKind.USER_SITE
def test_user_site_not_system(self):
"""User site-packages should NOT classify as SYSTEM_SITE."""
user_site = site.getusersitepackages()
if not user_site:
pytest.skip("User site-packages not available")
test_file = os.path.join(user_site, "some_package", "__init__.py")
assert classify_python_file(test_file) != PythonFileKind.SYSTEM_SITE
def test_system_site_not_user(self):
"""System site-packages should NOT classify as USER_SITE."""
pkgs = site.getsitepackages()
if not pkgs:
pytest.skip("No system site-packages")
test_file = os.path.join(pkgs[0], "pytest", "__init__.py")
assert classify_python_file(test_file) != PythonFileKind.USER_SITE
def test_site_packages_not_stdlib(self):
"""Files in site-packages should NOT be STDLIB."""
for site_pkg_dir in site.getsitepackages():
test_file = os.path.join(site_pkg_dir, "pytest", "__init__.py")
assert classify_python_file(test_file) != PythonFileKind.STDLIB
def test_user_site_packages_not_stdlib(self):
"""Files in user site-packages should NOT be STDLIB."""
user_site = site.getusersitepackages()
if user_site:
test_file = os.path.join(user_site, "some_package", "__init__.py")
assert classify_python_file(test_file) != PythonFileKind.STDLIB
def test_random_temp_file_returns_none(self, tmp_path):
"""A temporary file should not classify as any Python kind."""
with tempfile.NamedTemporaryFile(
suffix=".py", dir=str(tmp_path), delete=False
) as tmp:
tmp_file = tmp.name
try:
assert classify_python_file(tmp_file) is None
finally:
os.unlink(tmp_file)
def test_site_packages_under_stdlib_excluded(self):
"""site-packages inside a stdlib root should NOT be STDLIB."""
stdlib_path = sysconfig.get_path("stdlib")
if not stdlib_path:
pytest.skip("stdlib path not available")
test_file = os.path.join(stdlib_path, "site-packages", "mod.py")
assert classify_python_file(test_file) != PythonFileKind.STDLIB
def test_site_packages_backup_not_excluded(self):
"""'site-packages-backup' is NOT 'site-packages' — should be STDLIB."""
stdlib_path = sysconfig.get_path("stdlib")
if not stdlib_path:
pytest.skip("stdlib path not available")
test_file = os.path.join(stdlib_path, "site-packages-backup", "mod.py")
assert classify_python_file(test_file) == PythonFileKind.STDLIB
def test_stdlib_is_not_none(self):
"""stdlib files should return a non-None result (library file)."""
assert classify_python_file(os.__file__) is not None
def test_site_packages_is_not_none(self):
"""site-packages files should return a non-None result (library file)."""
for site_pkg_dir in site.getsitepackages():
test_file = os.path.join(site_pkg_dir, "pytest", "__init__.py")
assert classify_python_file(test_file) is not None
def test_user_site_none_handling(self):
"""When user site-packages is None, should not crash."""
with patch(
"vscode_common_python_lsp.paths._get_user_site_root",
return_value=None,
):
assert classify_python_file("/some/random/file.py") is None
class TestGetRelativePath:
def test_relative_to_workspace(self, tmp_path):
fp = str(tmp_path / "src" / "test.py")
result = get_relative_path(fp, str(tmp_path))
assert result == "src/test.py"
def test_empty_workspace_root(self):
result = get_relative_path("/some/path/file.py", "")
assert isinstance(result, str)
class TestIsMatch:
@pytest.mark.parametrize(
"patterns, file_path, expected",
[
([], "test.py", False),
(["*.py"], "test.py", True),
(["*.js"], "test.py", False),
(["test.py"], "/some/path/test.py", True),
],
)
def test_pattern_matching(self, patterns, file_path, expected):
assert is_match(patterns, file_path) == expected
def test_with_workspace_root(self, tmp_path):
fp = str(tmp_path / "src" / "test.py")
assert is_match(["src/*.py"], fp, str(tmp_path))
# ---------------------------------------------------------------------------
# sanitize_path_for_name_max tests
# ---------------------------------------------------------------------------
# A realistic dev-container/tunnel netloc component (>255 chars).
_LONG_NETLOC = (
"dev-container+7b22686f737450617468223a222f686f6d652f646f6e6e79"
"2f50726f6a656374732f776f72647365617263682d637370222c226c6f63"
"616c446f636b6572223a66616c73652c22636f6e66696746696c65223a7b"
"22246d6964223a312c2270617468223a222f686f6d652f646f6e6e792f50"
"726f6a656374732f776f72647365617263682d6373702f2e646576636f6e"
"7461696e65722f646576636f6e7461696e65722e6a736f6e222c22736368"
"656d65223a227673636f64652d66696c65486f7374227d7d"
)
class TestSanitizePathForNameMax:
"""Tests for sanitize_path_for_name_max() — path sanitisation."""
def test_short_path_unchanged(self):
"""Normal paths should pass through untouched."""
p = os.path.join(os.sep, "home", "user", "project", "src", "main.py")
assert sanitize_path_for_name_max(p) == p
def test_overlong_component_detected(self):
"""The fixture netloc must actually exceed NAME_MAX."""
assert len(_LONG_NETLOC.encode()) > 255
def test_overlong_without_workspace(self):
"""Overlong component is replaced with '_', basename preserved."""
p = os.path.join(os.sep, _LONG_NETLOC, "workspace", "src", "main.py")
result = sanitize_path_for_name_max(p)
for part in pathlib.PurePath(result).parts:
assert len(part.encode()) <= 255
assert result.endswith("main.py")
def test_overlong_with_workspace(self):
"""With a workspace, result preserves sub-path below overlong component."""
p = os.path.join(os.sep, _LONG_NETLOC, "workspace", "src", "main.py")
workspace = os.path.join(os.sep, "workspace")
result = sanitize_path_for_name_max(p, workspace=workspace)
assert result == os.path.join(workspace, "workspace", "src", "main.py")
def test_overlong_with_workspace_preserves_subpath(self):
"""Sub-path below overlong component is preserved when re-rooting."""
p = os.path.join(os.sep, _LONG_NETLOC, "deep", "nested", "path", "app.py")
workspace = os.path.join(os.sep, "home", "user", "project")
result = sanitize_path_for_name_max(p, workspace=workspace)
assert result == os.path.join(workspace, "deep", "nested", "path", "app.py")
def test_multiple_overlong_components(self):
"""Multiple overlong components are all sanitised."""
long1 = "a" * 300
long2 = "b" * 400
p = os.path.join(os.sep, long1, long2, "file.py")
result = sanitize_path_for_name_max(p)
for part in pathlib.PurePath(result).parts:
assert len(part.encode()) <= 255
def test_empty_workspace_falls_back(self):
"""Empty workspace string triggers component-replacement fallback."""
p = os.path.join(os.sep, _LONG_NETLOC, "src", "file.py")
result = sanitize_path_for_name_max(p, workspace="")
assert result.endswith("file.py")
for part in pathlib.PurePath(result).parts:
assert len(part.encode()) <= 255
def test_unicode_path_component(self):
"""Multi-byte UTF-8 components exceeding 255 bytes are sanitised."""
# Each emoji is 4 bytes in UTF-8; 64 emojis = 256 bytes > 255
long_unicode = "\U0001f600" * 64
p = os.path.join(os.sep, long_unicode, "file.py")
result = sanitize_path_for_name_max(p)
for part in pathlib.PurePath(result).parts:
assert len(part.encode("utf-8")) <= 255
def test_overlong_basename_without_workspace(self):
"""Overlong basename without workspace is sanitized, preserving suffix."""
long_name = "x" * 300 + ".py"
p = os.path.join(os.sep, "dev", long_name)
result = sanitize_path_for_name_max(p)
assert pathlib.PurePath(result).name == "_.py"
for part in pathlib.PurePath(result).parts:
assert len(part.encode()) <= 255
def test_overlong_basename_with_workspace(self):
"""Overlong basename with workspace is sanitized, preserving suffix."""
long_name = "a" * 300 + ".py"
p = os.path.join(os.sep, _LONG_NETLOC, "src", long_name)
workspace = os.path.join(os.sep, "workspace")
result = sanitize_path_for_name_max(p, workspace=workspace)
assert pathlib.PurePath(result).name == "_.py"
for part in pathlib.PurePath(result).parts:
assert len(part.encode()) <= 255
def test_overlong_basename_replaced_preserving_suffix(self):
"""If basename itself is too long, it gets replaced preserving suffix."""
long_name = "x" * 300 + ".py"
p = os.path.join(os.sep, "workspace", "src", long_name)
workspace = os.path.join(os.sep, "workspace")
result = sanitize_path_for_name_max(p, workspace=workspace)
# Should replace with "_.py" not reroute under workspace
assert pathlib.PurePath(result).name == "_.py"
def test_workspace_preserves_subpath(self):
"""Intermediate directories below overlong component are preserved."""
p = os.path.join(os.sep, _LONG_NETLOC, "src", "pkg", "main.py")
workspace = os.path.join(os.sep, "ws")
result = sanitize_path_for_name_max(p, workspace=workspace)
assert result == os.path.join(os.sep, "ws", "src", "pkg", "main.py")
def test_workspace_with_overlong_basename_in_tail(self):
"""Workspace branch sanitizes an overlong basename in the tail."""
long_name = "a" * 300 + ".py"
p = os.path.join(os.sep, _LONG_NETLOC, "workspace", long_name)
workspace = os.path.join(os.sep, "ws")
result = sanitize_path_for_name_max(p, workspace=workspace)
assert pathlib.PurePath(result).name == "_.py"
assert result.startswith(workspace)
def test_windows_limit_kind(self):
"""Windows mode uses character count, not byte count."""
# 200 emoji chars = 200 characters but 800 UTF-8 bytes
# Should NOT exceed on windows (200 < 255) but WOULD exceed on posix (800 > 255)
component = "\U0001f600" * 200
p = os.path.join(os.sep, component, "file.py")
result_win = sanitize_path_for_name_max(p, limit_kind="windows")
result_posix = sanitize_path_for_name_max(p, limit_kind="posix")
# Windows: unchanged (200 chars < 255)
assert result_win == p
# POSIX: sanitized (800 bytes > 255)
assert result_posix != p
assert result_posix.endswith("file.py")
def test_root_components_ignored(self):
"""Root/anchor parts like '/' are not flagged as overlong."""
p = os.path.join(os.sep, "normal", "path.py")
assert sanitize_path_for_name_max(p) == p
def test_unicode_posix_ascii_fast_path(self):
"""ASCII-only strings that are short skip the byte-counting loop."""
p = os.path.join(os.sep, "a" * 255, "file.py")
# Exactly 255 is fine
assert sanitize_path_for_name_max(p) == p
# 256 exceeds
p2 = os.path.join(os.sep, "a" * 256, "file.py")
assert sanitize_path_for_name_max(p2) != p2