Skip to content

Commit

Permalink
Fix removal of very long paths on Windows (#6755)
Browse files Browse the repository at this point in the history
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
  • Loading branch information
torcolvin and nicoddemus committed Jun 2, 2020
1 parent 589176e commit 56e6482
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -267,6 +267,7 @@ Tom Dalton
Tom Viner
Tomáš Gavenčiak
Tomer Keren
Tor Colvin
Trevor Bekolay
Tyler Goodlet
Tzu-ping Chung
Expand Down
1 change: 1 addition & 0 deletions changelog/6755.bugfix.rst
@@ -0,0 +1 @@
Support deleting paths longer than 260 characters on windows created inside tmpdir.
32 changes: 32 additions & 0 deletions src/_pytest/pathlib.py
Expand Up @@ -100,10 +100,41 @@ def chmod_rw(p: str) -> None:
return True


def ensure_extended_length_path(path: Path) -> Path:
"""Get the extended-length version of a path (Windows).
On Windows, by default, the maximum length of a path (MAX_PATH) is 260
characters, and operations on paths longer than that fail. But it is possible
to overcome this by converting the path to "extended-length" form before
performing the operation:
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
On Windows, this function returns the extended-length absolute version of path.
On other platforms it returns path unchanged.
"""
if sys.platform.startswith("win32"):
path = path.resolve()
path = Path(get_extended_length_path_str(str(path)))
return path


def get_extended_length_path_str(path: str) -> str:
"""Converts to extended length path as a str"""
long_path_prefix = "\\\\?\\"
unc_long_path_prefix = "\\\\?\\UNC\\"
if path.startswith((long_path_prefix, unc_long_path_prefix)):
return path
# UNC
if path.startswith("\\\\"):
return unc_long_path_prefix + path[2:]
return long_path_prefix + path


def rm_rf(path: Path) -> None:
"""Remove the path contents recursively, even if some elements
are read-only.
"""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror)

Expand Down Expand Up @@ -220,6 +251,7 @@ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> Non

def maybe_delete_a_numbered_dir(path: Path) -> None:
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
path = ensure_extended_length_path(path)
lock_path = None
try:
lock_path = create_cleanup_lock(path)
Expand Down
24 changes: 24 additions & 0 deletions testing/test_pathlib.py
Expand Up @@ -5,6 +5,7 @@

import pytest
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import get_extended_length_path_str
from _pytest.pathlib import get_lock_path
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import Path
Expand Down Expand Up @@ -89,3 +90,26 @@ def renamed_failed(*args):
lock_path = get_lock_path(path)
maybe_delete_a_numbered_dir(path)
assert not lock_path.is_file()


def test_long_path_during_cleanup(tmp_path):
"""Ensure that deleting long path works (particularly on Windows (#6775))."""
path = (tmp_path / ("a" * 250)).resolve()
if sys.platform == "win32":
# make sure that the full path is > 260 characters without any
# component being over 260 characters
assert len(str(path)) > 260
extended_path = "\\\\?\\" + str(path)
else:
extended_path = str(path)
os.mkdir(extended_path)
assert os.path.isdir(extended_path)
maybe_delete_a_numbered_dir(path)
assert not os.path.isdir(extended_path)


def test_get_extended_length_path_str():
assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo"
assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo"
assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo"
assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo"

0 comments on commit 56e6482

Please sign in to comment.