Skip to content

Commit 96a8988

Browse files
committed
Fix rmtree to remove directories with read-only files
Fix #5524
1 parent 2c402f4 commit 96a8988

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

changelog/5524.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only,
2+
which could lead to pytest crashing when executed a second time with the ``--basetemp`` option.

src/_pytest/pathlib.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ def ensure_reset_dir(path):
3737

3838

3939
def rmtree(path, force=False):
40+
def force_writable_and_retry(function, path, excinfo):
41+
import stat
42+
43+
mode = os.stat(path).st_mode
44+
os.chmod(path, mode | stat.S_IWRITE)
45+
function(path)
46+
4047
if force:
41-
# NOTE: ignore_errors might leave dead folders around.
42-
# Python needs a rm -rf as a followup.
43-
shutil.rmtree(str(path), ignore_errors=True)
48+
shutil.rmtree(str(path), onerror=force_writable_and_retry)
4449
else:
4550
shutil.rmtree(str(path))
4651

testing/test_tmpdir.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
import stat
13
import sys
24

35
import attr
@@ -326,6 +328,35 @@ def test_rmtree(self, tmp_path):
326328
rmtree(adir, force=True)
327329
assert not adir.exists()
328330

331+
def test_rmtree_with_read_only_file(self, tmp_path):
332+
"""Ensure rmtree can remove directories with read-only files in them (#5524)"""
333+
from _pytest.pathlib import rmtree
334+
335+
fn = tmp_path / "dir/foo.txt"
336+
fn.parent.mkdir()
337+
338+
fn.touch()
339+
340+
mode = os.stat(str(fn)).st_mode
341+
os.chmod(str(fn), mode & ~stat.S_IREAD)
342+
rmtree(fn.parent, force=True)
343+
344+
assert not fn.parent.is_dir()
345+
346+
def test_rmtree_with_read_only_directory(self, tmp_path):
347+
"""Ensure rmtree can remove read-only directories (#5524)"""
348+
from _pytest.pathlib import rmtree
349+
350+
adir = tmp_path / "dir"
351+
adir.mkdir()
352+
353+
(adir / "foo.txt").touch()
354+
mode = os.stat(str(adir)).st_mode
355+
os.chmod(str(adir), mode & ~stat.S_IREAD)
356+
rmtree(adir, force=True)
357+
358+
assert not adir.is_dir()
359+
329360
def test_cleanup_ignores_symlink(self, tmp_path):
330361
the_symlink = tmp_path / (self.PREFIX + "current")
331362
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
@@ -349,3 +380,24 @@ def attempt_symlink_to(path, to_path):
349380

350381
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
351382
assert Path(tmpdir) == tmp_path
383+
384+
385+
def test_basetemp_with_read_only_files(testdir):
386+
"""Integration test for #5524"""
387+
testdir.makepyfile(
388+
"""
389+
import os
390+
import stat
391+
392+
def test(tmp_path):
393+
fn = tmp_path / 'foo.txt'
394+
fn.write_text('hello')
395+
mode = os.stat(str(fn)).st_mode
396+
os.chmod(str(fn), mode & ~stat.S_IREAD)
397+
"""
398+
)
399+
result = testdir.runpytest("--basetemp=tmp")
400+
assert result.ret == 0
401+
# running a second time and ensure we don't crash
402+
result = testdir.runpytest("--basetemp=tmp")
403+
assert result.ret == 0

0 commit comments

Comments
 (0)