Skip to content

Commit cf27d2c

Browse files
implement cleanup for unlocked folders
1 parent 98d90c5 commit cf27d2c

File tree

2 files changed

+141
-29
lines changed

2 files changed

+141
-29
lines changed

src/_pytest/tmpdir.py

+84-29
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
import re
55
import os
6+
import errno
67
import atexit
7-
8+
import operator
89
import six
910
from functools import reduce
10-
11+
import uuid
1112
from six.moves import map
1213
import pytest
1314
import py
@@ -16,6 +17,10 @@
1617
import attr
1718
import shutil
1819
import tempfile
20+
import itertools
21+
22+
23+
get_lock_path = operator.methodcaller("joinpath", ".lock")
1924

2025

2126
def find_prefixed(root, prefix):
@@ -25,22 +30,32 @@ def find_prefixed(root, prefix):
2530
yield x
2631

2732

33+
def extract_suffixees(iter, prefix):
34+
p_len = len(prefix)
35+
for p in iter:
36+
yield p.name[p_len:]
37+
38+
39+
def find_suffixes(root, prefix):
40+
return extract_suffixees(find_prefixed(root, prefix), prefix)
41+
42+
43+
def parse_num(maybe_num):
44+
try:
45+
return int(maybe_num)
46+
except ValueError:
47+
return -1
48+
49+
2850
def _max(iterable, default):
2951
# needed due to python2.7 lacking the default argument for max
3052
return reduce(max, iterable, default)
3153

3254

3355
def make_numbered_dir(root, prefix):
34-
def parse_num(p, cut=len(prefix)):
35-
maybe_num = p.name[cut:]
36-
try:
37-
return int(maybe_num)
38-
except ValueError:
39-
return -1
40-
4156
for i in range(10):
4257
# try up to 10 times to create the folder
43-
max_existing = _max(map(parse_num, find_prefixed(root, prefix)), -1)
58+
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1)
4459
new_number = max_existing + 1
4560
new_path = root.joinpath("{}{}".format(prefix, new_number))
4661
try:
@@ -58,20 +73,29 @@ def parse_num(p, cut=len(prefix)):
5873

5974

6075
def create_cleanup_lock(p):
61-
lock_path = p.joinpath(".lock")
62-
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
63-
pid = os.getpid()
64-
spid = str(pid)
65-
if not isinstance(spid, six.binary_type):
66-
spid = spid.encode("ascii")
67-
os.write(fd, spid)
68-
os.close(fd)
69-
if not lock_path.is_file():
70-
raise EnvironmentError("lock path got renamed after sucessfull creation")
71-
return lock_path
72-
73-
74-
def register_cleanup_lock_removal(lock_path):
76+
lock_path = get_lock_path(p)
77+
try:
78+
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
79+
except OSError as e:
80+
if e.errno == errno.EEXIST:
81+
six.raise_from(
82+
EnvironmentError("cannot create lockfile in {path}".format(path=p)), e
83+
)
84+
else:
85+
raise
86+
else:
87+
pid = os.getpid()
88+
spid = str(pid)
89+
if not isinstance(spid, six.binary_type):
90+
spid = spid.encode("ascii")
91+
os.write(fd, spid)
92+
os.close(fd)
93+
if not lock_path.is_file():
94+
raise EnvironmentError("lock path got renamed after sucessfull creation")
95+
return lock_path
96+
97+
98+
def register_cleanup_lock_removal(lock_path, register=atexit.register):
7599
pid = os.getpid()
76100

77101
def cleanup_on_exit(lock_path=lock_path, original_pid=pid):
@@ -84,12 +108,33 @@ def cleanup_on_exit(lock_path=lock_path, original_pid=pid):
84108
except (OSError, IOError):
85109
pass
86110

87-
return atexit.register(cleanup_on_exit)
111+
return register(cleanup_on_exit)
112+
113+
114+
def delete_a_numbered_dir(path):
115+
create_cleanup_lock(path)
116+
parent = path.parent
88117

118+
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
119+
path.rename(garbage)
120+
shutil.rmtree(str(garbage))
89121

90-
def cleanup_numbered_dir(root, prefix, keep):
91-
# todo
92-
pass
122+
123+
def is_deletable(path, consider_lock_dead_after):
124+
lock = get_lock_path(path)
125+
if not lock.exists():
126+
return True
127+
128+
129+
def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_after):
130+
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1)
131+
max_delete = max_existing - keep
132+
paths = find_prefixed(root, prefix)
133+
paths, paths2 = itertools.tee(paths)
134+
numbers = map(parse_num, extract_suffixees(paths2, prefix))
135+
for path, number in zip(paths, numbers):
136+
if number <= max_delete and is_deletable(path, consider_lock_dead_after):
137+
delete_a_numbered_dir(path)
93138

94139

95140
def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after):
@@ -101,7 +146,12 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after)
101146
except Exception:
102147
raise
103148
else:
104-
cleanup_numbered_dir(root=root, prefix=prefix, keep=keep)
149+
cleanup_numbered_dir(
150+
root=root,
151+
prefix=prefix,
152+
keep=keep,
153+
consider_lock_dead_after=consider_lock_dead_after,
154+
)
105155
return p
106156

107157

@@ -244,3 +294,8 @@ def tmpdir(request, tmpdir_factory):
244294
name = name[:MAXVAL]
245295
x = tmpdir_factory.mktemp(name, numbered=True)
246296
return x
297+
298+
299+
@pytest.fixture
300+
def tmp_path(tmpdir):
301+
return Path(tmpdir)

testing/test_tmpdir.py

+57
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,60 @@ def test_get_user(monkeypatch):
184184
monkeypatch.delenv("USER", raising=False)
185185
monkeypatch.delenv("USERNAME", raising=False)
186186
assert get_user() is None
187+
188+
189+
class TestNumberedDir(object):
190+
PREFIX = "fun-"
191+
192+
def test_make(self, tmp_path):
193+
from _pytest.tmpdir import make_numbered_dir
194+
195+
for i in range(10):
196+
d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
197+
assert d.name.startswith(self.PREFIX)
198+
assert d.name.endswith(str(i))
199+
200+
def test_cleanup_lock_create(self, tmp_path):
201+
d = tmp_path.joinpath("test")
202+
d.mkdir()
203+
from _pytest.tmpdir import create_cleanup_lock
204+
205+
lockfile = create_cleanup_lock(d)
206+
with pytest.raises(EnvironmentError, match="cannot create lockfile in .*"):
207+
create_cleanup_lock(d)
208+
209+
lockfile.unlink()
210+
211+
def test_lock_register_cleanup_removal(self, tmp_path):
212+
from _pytest.tmpdir import create_cleanup_lock, register_cleanup_lock_removal
213+
214+
lock = create_cleanup_lock(tmp_path)
215+
216+
registry = []
217+
register_cleanup_lock_removal(lock, register=registry.append)
218+
219+
cleanup_func, = registry
220+
221+
assert lock.is_file()
222+
223+
cleanup_func(original_pid="intentionally_different")
224+
225+
assert lock.is_file()
226+
227+
cleanup_func()
228+
229+
assert not lock.exists()
230+
231+
cleanup_func()
232+
233+
assert not lock.exists()
234+
235+
def test_cleanup_keep(self, tmp_path):
236+
self.test_make(tmp_path)
237+
from _pytest.tmpdir import cleanup_numbered_dir
238+
239+
cleanup_numbered_dir(
240+
root=tmp_path, prefix=self.PREFIX, keep=2, consider_lock_dead_after=0
241+
)
242+
a, b = tmp_path.iterdir()
243+
print(a, b)

0 commit comments

Comments
 (0)