Fix rmtree to remove directories with read-only files (#5588)
Fix rmtree to remove directories with read-only files
This commit is contained in:
commit
4027254a4b
|
@ -0,0 +1,2 @@
|
||||||
|
Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only,
|
||||||
|
which could lead to pytest crashing when executed a second time with the ``--basetemp`` option.
|
|
@ -14,7 +14,7 @@ import py
|
||||||
import pytest
|
import pytest
|
||||||
from .pathlib import Path
|
from .pathlib import Path
|
||||||
from .pathlib import resolve_from_str
|
from .pathlib import resolve_from_str
|
||||||
from .pathlib import rmtree
|
from .pathlib import rm_rf
|
||||||
|
|
||||||
README_CONTENT = """\
|
README_CONTENT = """\
|
||||||
# pytest cache directory #
|
# pytest cache directory #
|
||||||
|
@ -44,7 +44,7 @@ class Cache:
|
||||||
def for_config(cls, config):
|
def for_config(cls, config):
|
||||||
cachedir = cls.cache_dir_from_config(config)
|
cachedir = cls.cache_dir_from_config(config)
|
||||||
if config.getoption("cacheclear") and cachedir.exists():
|
if config.getoption("cacheclear") and cachedir.exists():
|
||||||
rmtree(cachedir, force=True)
|
rm_rf(cachedir)
|
||||||
cachedir.mkdir()
|
cachedir.mkdir()
|
||||||
return cls(cachedir, config)
|
return cls(cachedir, config)
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,14 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from os.path import expandvars
|
from os.path import expandvars
|
||||||
from os.path import isabs
|
from os.path import isabs
|
||||||
from os.path import sep
|
from os.path import sep
|
||||||
from posixpath import sep as posix_sep
|
from posixpath import sep as posix_sep
|
||||||
|
|
||||||
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
if sys.version_info[:2] >= (3, 6):
|
if sys.version_info[:2] >= (3, 6):
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
|
@ -32,17 +34,42 @@ def ensure_reset_dir(path):
|
||||||
ensures the given path is an empty directory
|
ensures the given path is an empty directory
|
||||||
"""
|
"""
|
||||||
if path.exists():
|
if path.exists():
|
||||||
rmtree(path, force=True)
|
rm_rf(path)
|
||||||
path.mkdir()
|
path.mkdir()
|
||||||
|
|
||||||
|
|
||||||
def rmtree(path, force=False):
|
def rm_rf(path):
|
||||||
if force:
|
"""Remove the path contents recursively, even if some elements
|
||||||
# NOTE: ignore_errors might leave dead folders around.
|
are read-only.
|
||||||
# Python needs a rm -rf as a followup.
|
"""
|
||||||
shutil.rmtree(str(path), ignore_errors=True)
|
|
||||||
else:
|
def chmod_w(p):
|
||||||
shutil.rmtree(str(path))
|
import stat
|
||||||
|
|
||||||
|
mode = os.stat(str(p)).st_mode
|
||||||
|
os.chmod(str(p), mode | stat.S_IWRITE)
|
||||||
|
|
||||||
|
def force_writable_and_retry(function, p, excinfo):
|
||||||
|
p = Path(p)
|
||||||
|
|
||||||
|
# for files, we need to recursively go upwards
|
||||||
|
# in the directories to ensure they all are also
|
||||||
|
# writable
|
||||||
|
if p.is_file():
|
||||||
|
for parent in p.parents:
|
||||||
|
chmod_w(parent)
|
||||||
|
# stop when we reach the original path passed to rm_rf
|
||||||
|
if parent == path:
|
||||||
|
break
|
||||||
|
|
||||||
|
chmod_w(p)
|
||||||
|
try:
|
||||||
|
# retry the function that failed
|
||||||
|
function(str(p))
|
||||||
|
except Exception as e:
|
||||||
|
warnings.warn(PytestWarning("(rm_rf) error removing {}: {}".format(p, e)))
|
||||||
|
|
||||||
|
shutil.rmtree(str(path), onerror=force_writable_and_retry)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root, prefix):
|
def find_prefixed(root, prefix):
|
||||||
|
@ -168,7 +195,7 @@ def maybe_delete_a_numbered_dir(path):
|
||||||
|
|
||||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||||
path.rename(garbage)
|
path.rename(garbage)
|
||||||
rmtree(garbage, force=True)
|
rm_rf(garbage)
|
||||||
except (OSError, EnvironmentError):
|
except (OSError, EnvironmentError):
|
||||||
# known races:
|
# known races:
|
||||||
# * other process did a cleanup at the same time
|
# * other process did a cleanup at the same time
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
@ -311,11 +313,11 @@ class TestNumberedDir:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_rmtree(self, tmp_path):
|
def test_rmtree(self, tmp_path):
|
||||||
from _pytest.pathlib import rmtree
|
from _pytest.pathlib import rm_rf
|
||||||
|
|
||||||
adir = tmp_path / "adir"
|
adir = tmp_path / "adir"
|
||||||
adir.mkdir()
|
adir.mkdir()
|
||||||
rmtree(adir)
|
rm_rf(adir)
|
||||||
|
|
||||||
assert not adir.exists()
|
assert not adir.exists()
|
||||||
|
|
||||||
|
@ -323,9 +325,40 @@ class TestNumberedDir:
|
||||||
afile = adir / "afile"
|
afile = adir / "afile"
|
||||||
afile.write_bytes(b"aa")
|
afile.write_bytes(b"aa")
|
||||||
|
|
||||||
rmtree(adir, force=True)
|
rm_rf(adir)
|
||||||
assert not adir.exists()
|
assert not adir.exists()
|
||||||
|
|
||||||
|
def test_rmtree_with_read_only_file(self, tmp_path):
|
||||||
|
"""Ensure rm_rf can remove directories with read-only files in them (#5524)"""
|
||||||
|
from _pytest.pathlib import rm_rf
|
||||||
|
|
||||||
|
fn = tmp_path / "dir/foo.txt"
|
||||||
|
fn.parent.mkdir()
|
||||||
|
|
||||||
|
fn.touch()
|
||||||
|
|
||||||
|
mode = os.stat(str(fn)).st_mode
|
||||||
|
os.chmod(str(fn), mode & ~stat.S_IWRITE)
|
||||||
|
|
||||||
|
rm_rf(fn.parent)
|
||||||
|
|
||||||
|
assert not fn.parent.is_dir()
|
||||||
|
|
||||||
|
def test_rmtree_with_read_only_directory(self, tmp_path):
|
||||||
|
"""Ensure rm_rf can remove read-only directories (#5524)"""
|
||||||
|
from _pytest.pathlib import rm_rf
|
||||||
|
|
||||||
|
adir = tmp_path / "dir"
|
||||||
|
adir.mkdir()
|
||||||
|
|
||||||
|
(adir / "foo.txt").touch()
|
||||||
|
mode = os.stat(str(adir)).st_mode
|
||||||
|
os.chmod(str(adir), mode & ~stat.S_IWRITE)
|
||||||
|
|
||||||
|
rm_rf(adir)
|
||||||
|
|
||||||
|
assert not adir.is_dir()
|
||||||
|
|
||||||
def test_cleanup_ignores_symlink(self, tmp_path):
|
def test_cleanup_ignores_symlink(self, tmp_path):
|
||||||
the_symlink = tmp_path / (self.PREFIX + "current")
|
the_symlink = tmp_path / (self.PREFIX + "current")
|
||||||
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
|
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
|
||||||
|
@ -349,3 +382,24 @@ def attempt_symlink_to(path, to_path):
|
||||||
|
|
||||||
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
|
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
|
||||||
assert Path(tmpdir) == tmp_path
|
assert Path(tmpdir) == tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_basetemp_with_read_only_files(testdir):
|
||||||
|
"""Integration test for #5524"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
def test(tmp_path):
|
||||||
|
fn = tmp_path / 'foo.txt'
|
||||||
|
fn.write_text('hello')
|
||||||
|
mode = os.stat(str(fn)).st_mode
|
||||||
|
os.chmod(str(fn), mode & ~stat.S_IREAD)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("--basetemp=tmp")
|
||||||
|
assert result.ret == 0
|
||||||
|
# running a second time and ensure we don't crash
|
||||||
|
result = testdir.runpytest("--basetemp=tmp")
|
||||||
|
assert result.ret == 0
|
||||||
|
|
Loading…
Reference in New Issue