|
12 | 12 | import json |
13 | 13 | import os |
14 | 14 | from pathlib import Path |
| 15 | +import shutil |
15 | 16 | import tempfile |
16 | 17 | from typing import final |
17 | 18 |
|
|
33 | 34 | from _pytest.reports import TestReport |
34 | 35 |
|
35 | 36 |
|
36 | | -README_CONTENT = """\ |
| 37 | +CACHEDIR_FILES: dict[str, bytes] = { |
| 38 | + "README.md": b"""\ |
37 | 39 | # pytest cache directory # |
38 | 40 |
|
39 | 41 | This directory contains data from the pytest's cache plugin, |
|
42 | 44 | **Do not** commit this to version control. |
43 | 45 |
|
44 | 46 | See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. |
45 | | -""" |
46 | | - |
47 | | -CACHEDIR_TAG_CONTENT = b"""\ |
| 47 | +""", |
| 48 | + ".gitignore": b"# Created by pytest automatically.\n*\n", |
| 49 | + "CACHEDIR.TAG": b"""\ |
48 | 50 | Signature: 8a477f597d28d172789f06886806bc55 |
49 | 51 | # This file is a cache directory tag created by pytest. |
50 | 52 | # For information about cache directory tags, see: |
51 | 53 | # https://bford.info/cachedir/spec.html |
52 | | -""" |
| 54 | +""", |
| 55 | +} |
| 56 | + |
| 57 | + |
| 58 | +def _make_cachedir(target: Path) -> None: |
| 59 | + """Create the pytest cache directory atomically with supporting files. |
| 60 | +
|
| 61 | + Creates a temporary directory with README.md, .gitignore, and CACHEDIR.TAG, |
| 62 | + then atomically renames it to the target location. If another process wins |
| 63 | + the race, the temporary directory is cleaned up. |
| 64 | + """ |
| 65 | + target.parent.mkdir(parents=True, exist_ok=True) |
| 66 | + path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent)) |
| 67 | + try: |
| 68 | + # Reset permissions to the default, see #12308. |
| 69 | + # Note: there's no way to get the current umask atomically, eek. |
| 70 | + umask = os.umask(0o022) |
| 71 | + os.umask(umask) |
| 72 | + path.chmod(0o777 - umask) |
| 73 | + |
| 74 | + for name, content in CACHEDIR_FILES.items(): |
| 75 | + path.joinpath(name).write_bytes(content) |
| 76 | + |
| 77 | + path.rename(target) |
| 78 | + except OSError as e: |
| 79 | + # If 2 concurrent pytests both race to the rename, the loser |
| 80 | + # gets "Directory not empty" from the rename. In this case, |
| 81 | + # everything is handled so just continue after cleanup. |
| 82 | + # On Windows, the error is a FileExistsError which translates to EEXIST. |
| 83 | + if e.errno not in (errno.ENOTEMPTY, errno.EEXIST): |
| 84 | + raise |
| 85 | + finally: |
| 86 | + shutil.rmtree(path, ignore_errors=True) |
53 | 87 |
|
54 | 88 |
|
55 | 89 | @final |
@@ -202,48 +236,8 @@ def set(self, key: str, value: object) -> None: |
202 | 236 |
|
203 | 237 | def _ensure_cache_dir_and_supporting_files(self) -> None: |
204 | 238 | """Create the cache dir and its supporting files.""" |
205 | | - if self._cachedir.is_dir(): |
206 | | - return |
207 | | - |
208 | | - self._cachedir.parent.mkdir(parents=True, exist_ok=True) |
209 | | - with tempfile.TemporaryDirectory( |
210 | | - prefix="pytest-cache-files-", |
211 | | - dir=self._cachedir.parent, |
212 | | - ) as newpath: |
213 | | - path = Path(newpath) |
214 | | - |
215 | | - # Reset permissions to the default, see #12308. |
216 | | - # Note: there's no way to get the current umask atomically, eek. |
217 | | - umask = os.umask(0o022) |
218 | | - os.umask(umask) |
219 | | - path.chmod(0o777 - umask) |
220 | | - |
221 | | - with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f: |
222 | | - f.write(README_CONTENT) |
223 | | - with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f: |
224 | | - f.write("# Created by pytest automatically.\n*\n") |
225 | | - with open(path.joinpath("CACHEDIR.TAG"), "xb") as f: |
226 | | - f.write(CACHEDIR_TAG_CONTENT) |
227 | | - |
228 | | - try: |
229 | | - path.rename(self._cachedir) |
230 | | - except OSError as e: |
231 | | - # If 2 concurrent pytests both race to the rename, the loser |
232 | | - # gets "Directory not empty" from the rename. In this case, |
233 | | - # everything is handled so just continue (while letting the |
234 | | - # temporary directory be cleaned up). |
235 | | - # On Windows, the error is a FileExistsError which translates to EEXIST. |
236 | | - if e.errno not in (errno.ENOTEMPTY, errno.EEXIST): |
237 | | - raise |
238 | | - else: |
239 | | - # Create a directory in place of the one we just moved so that |
240 | | - # `TemporaryDirectory`'s cleanup doesn't complain. |
241 | | - # |
242 | | - # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. |
243 | | - # See https://github.com/python/cpython/issues/74168. Note that passing |
244 | | - # delete=False would do the wrong thing in case of errors and isn't supported |
245 | | - # until python 3.12. |
246 | | - path.mkdir() |
| 239 | + if not self._cachedir.is_dir(): |
| 240 | + _make_cachedir(self._cachedir) |
247 | 241 |
|
248 | 242 |
|
249 | 243 | class LFPluginCollWrapper: |
|
0 commit comments