From 2a06421e7a946befbc0526b2228bb26dc86c4902 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 1 Jun 2026 09:28:26 -0400 Subject: [PATCH] Improve discover stat race test to actually exercise the try/except MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test deleted a file mid-iteration, but is_file() caught it before stat() ran — so the new try/except never executed. Monkeypatch Path.stat to raise FileNotFoundError on the third call for the victim path (after is_symlink and is_file pass), simulating the exact TOCTOU window between is_file() and stat(). --- tests/ingester/test_fs_source.py | 34 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/ingester/test_fs_source.py b/tests/ingester/test_fs_source.py index c827bf0e..f5e088cf 100644 --- a/tests/ingester/test_fs_source.py +++ b/tests/ingester/test_fs_source.py @@ -132,22 +132,34 @@ async def test_fs_source_discover_respects_extension_filter(fs_root: Path): @pytest.mark.asyncio -async def test_fs_source_discover_skips_file_deleted_during_stat(fs_root: Path): - """A file deleted between os.walk() and stat() should be silently +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 = [] - async for event in src.discover(since=None): - events.append(event) - # Delete a file mid-iteration so the next stat() hits a missing file. - victim = fs_root / "b.txt" - if victim.exists(): - victim.unlink() + events = [e async for e in src.discover(since=None)] uris = {e.uri for e in events} - # a.md and sub/c.md should still appear; b.txt may or may not depending - # on iteration order, but the key assertion is no exception was raised. + # 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