diff --git a/AUTHORS b/AUTHORS index bd1e27cb37d..be237baa389 100644 --- a/AUTHORS +++ b/AUTHORS @@ -540,6 +540,7 @@ Yannick Péroux Yao Xiao Yoav Caspi Yuliang Shao +Yury V. Zaytsev Yusuke Kadowaki Yutian Li Yuval Shimon diff --git a/changelog/15174.bugfix.rst b/changelog/15174.bugfix.rst new file mode 100644 index 00000000000..84009a0c42c --- /dev/null +++ b/changelog/15174.bugfix.rst @@ -0,0 +1 @@ +Fixed a race as concurrent pytest sessions of the same user would fail at completion with ``FileNotFoundError`` while removing a dead ``pytest-current`` symlink. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8eb593d194d..4f42d231d1d 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -398,7 +398,7 @@ def cleanup_dead_symlinks(root: Path) -> None: for left_dir in root.iterdir(): if left_dir.is_symlink(): if not left_dir.resolve().exists(): - left_dir.unlink() + left_dir.unlink(missing_ok=True) def cleanup_numbered_dir( diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 0b33a74b926..61aacd077ab 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -516,6 +516,22 @@ def test_cleanup_ignores_symlink(self, tmp_path): attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) self._do_cleanup(tmp_path) + def test_cleanup_dead_symlink_removed_concurrently( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + the_symlink = tmp_path / (self.PREFIX + "current") + attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) + resolve = Path.resolve + + def resolve_after_concurrent_unlink(path: Path, strict: bool = False) -> Path: + assert path == the_symlink + path.unlink() # another session removes the dead symlink first + return resolve(path, strict) + + monkeypatch.setattr(Path, "resolve", resolve_after_concurrent_unlink) + pathlib.cleanup_dead_symlinks(tmp_path) + assert not the_symlink.is_symlink() + def test_removal_accepts_lock(self, tmp_path): dir = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) create_cleanup_lock(dir)