Merge pull request #399 from mcdonc/fix/discover-stat-race
fix: handle file deleted during discover() stat() call
This commit is contained in:
commit
b79bbcaed4
2 changed files with 37 additions and 1 deletions
|
|
@ -139,7 +139,12 @@ class FSSource:
|
||||||
if not self.filter.include_file(str(path)):
|
if not self.filter.include_file(str(path)):
|
||||||
continue
|
continue
|
||||||
uri = path.as_uri()
|
uri = path.as_uri()
|
||||||
revision = str(path.stat().st_mtime_ns)
|
try:
|
||||||
|
revision = str(path.stat().st_mtime_ns)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# File was deleted between the os.walk() and stat() call.
|
||||||
|
# Skip it — the next sweep (or watchfiles) will emit DELETE.
|
||||||
|
continue
|
||||||
seen.add(uri)
|
seen.add(uri)
|
||||||
previous = snapshot.get(uri)
|
previous = snapshot.get(uri)
|
||||||
kind = (
|
kind = (
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,37 @@ async def test_fs_source_discover_respects_extension_filter(fs_root: Path):
|
||||||
assert (fs_root / "b.txt").as_uri() not in uris
|
assert (fs_root / "b.txt").as_uri() not in uris
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fs_source_discover_skips_file_deleted_during_stat(
|
||||||
|
fs_root: Path, monkeypatch
|
||||||
|
):
|
||||||
|
"""A file deleted between is_file() and stat() should be silently
|
||||||
|
skipped instead of crashing the entire discover() sweep."""
|
||||||
|
victim = fs_root / "b.txt"
|
||||||
|
original_stat = Path.stat
|
||||||
|
victim_calls = 0
|
||||||
|
|
||||||
|
def _stat_that_fails_on_second_call(self, *args, **kwargs):
|
||||||
|
nonlocal victim_calls
|
||||||
|
if self == victim:
|
||||||
|
victim_calls += 1
|
||||||
|
# First calls are from is_symlink/is_file; the later call
|
||||||
|
# is the explicit stat().st_mtime_ns we want to fail.
|
||||||
|
if victim_calls > 2:
|
||||||
|
raise FileNotFoundError(f"[Errno 2] No such file: '{self}'")
|
||||||
|
return original_stat(self, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Path, "stat", _stat_that_fails_on_second_call)
|
||||||
|
|
||||||
|
src = FSSource(root=fs_root, supported_extensions=[".md", ".txt"])
|
||||||
|
events = [e async for e in src.discover(since=None)]
|
||||||
|
uris = {e.uri for e in events}
|
||||||
|
# b.txt was skipped due to the simulated race; a.md and sub/c.md are fine.
|
||||||
|
assert (fs_root / "a.md").as_uri() in uris
|
||||||
|
assert (fs_root / "sub" / "c.md").as_uri() in uris
|
||||||
|
assert victim.as_uri() not in uris
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_fs_source_discover_respects_ignore_patterns(fs_root: Path):
|
async def test_fs_source_discover_respects_ignore_patterns(fs_root: Path):
|
||||||
src = FSSource(
|
src = FSSource(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue