Skip to content

Commit 2374cf7

Browse files
committed
Initialize cache before writing data
Writing cache data is interruptible; this reduces the probability of a pathological case where interrupting a cache write can cause the cache directory to never be properly initialized with its supporting files. Unify `Cache.mkdir` with `Cache.set` while I'm here so the former also properly initializes the cache directory. Closes #12167.
1 parent 4489528 commit 2374cf7

File tree

2 files changed

+28
-17
lines changed

2 files changed

+28
-17
lines changed

changelog/12167.trivial.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cache: create supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in the cache directory before writing cache data, reducing the probability of those files being lost.

src/_pytest/cacheprovider.py

+27-17
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
from typing import List
1515
from typing import Optional
1616
from typing import Set
17+
from typing import Tuple
1718
from typing import Union
1819

1920
from .pathlib import resolve_from_str
2021
from .pathlib import rm_rf
2122
from .reports import CollectReport
2223
from _pytest import nodes
2324
from _pytest._io import TerminalWriter
25+
from _pytest.compat import assert_never
2426
from _pytest.config import Config
2527
from _pytest.config import ExitCode
2628
from _pytest.config import hookimpl
@@ -123,6 +125,10 @@ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
123125
stacklevel=3,
124126
)
125127

128+
def _mkdir(self, path: Path) -> None:
129+
self._ensure_cache_dir_and_supporting_files()
130+
path.mkdir(exist_ok=True, parents=True)
131+
126132
def mkdir(self, name: str) -> Path:
127133
"""Return a directory path object with the given name.
128134
@@ -141,7 +147,7 @@ def mkdir(self, name: str) -> Path:
141147
if len(path.parts) > 1:
142148
raise ValueError("name is not allowed to contain path separators")
143149
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
144-
res.mkdir(exist_ok=True, parents=True)
150+
self._mkdir(res)
145151
return res
146152

147153
def _getvaluepath(self, key: str) -> Path:
@@ -178,19 +184,13 @@ def set(self, key: str, value: object) -> None:
178184
"""
179185
path = self._getvaluepath(key)
180186
try:
181-
if path.parent.is_dir():
182-
cache_dir_exists_already = True
183-
else:
184-
cache_dir_exists_already = self._cachedir.exists()
185-
path.parent.mkdir(exist_ok=True, parents=True)
187+
self._mkdir(path.parent)
186188
except OSError as exc:
187189
self.warn(
188190
f"could not create cache path {path}: {exc}",
189191
_ispytest=True,
190192
)
191193
return
192-
if not cache_dir_exists_already:
193-
self._ensure_supporting_files()
194194
data = json.dumps(value, ensure_ascii=False, indent=2)
195195
try:
196196
f = path.open("w", encoding="UTF-8")
@@ -203,17 +203,27 @@ def set(self, key: str, value: object) -> None:
203203
with f:
204204
f.write(data)
205205

206-
def _ensure_supporting_files(self) -> None:
207-
"""Create supporting files in the cache dir that are not really part of the cache."""
208-
readme_path = self._cachedir / "README.md"
209-
readme_path.write_text(README_CONTENT, encoding="UTF-8")
206+
def _ensure_cache_dir_and_supporting_files(self) -> None:
207+
"""Create the cache dir and its supporting files."""
208+
if self._cachedir.is_dir():
209+
return
210210

211-
gitignore_path = self._cachedir.joinpath(".gitignore")
212-
msg = "# Created by pytest automatically.\n*\n"
213-
gitignore_path.write_text(msg, encoding="UTF-8")
211+
self._cachedir.mkdir(exist_ok=True, parents=True)
214212

215-
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
216-
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
213+
files: Iterable[Tuple[str, Union[str, bytes]]] = (
214+
("README.md", README_CONTENT),
215+
(".gitignore", "# Created by pytest automatically.\n*\n"),
216+
("CACHEDIR.TAG", CACHEDIR_TAG_CONTENT),
217+
)
218+
for file, content in files:
219+
if isinstance(content, str):
220+
with open(self._cachedir.joinpath(file), "xt", encoding="UTF-8") as f:
221+
f.write(content)
222+
elif isinstance(content, bytes):
223+
with open(self._cachedir.joinpath(file), "xb") as f:
224+
f.write(content)
225+
else:
226+
assert_never(content)
217227

218228

219229
class LFPluginCollWrapper:

0 commit comments

Comments
 (0)