test: cover is_dir() OSError guard in _full_scandir_discovery (#846)#852
test: cover is_dir() OSError guard in _full_scandir_discovery (#846)#852
is_dir() OSError guard in _full_scandir_discovery (#846)#852Conversation
Add test_full_scandir_is_dir_oserror_skips_entry to verify that a root-level DirEntry whose is_dir() raises OSError (e.g. broken symlink or EACCES on lstat) is silently skipped rather than crashing discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds targeted unit-test coverage for a previously untested OSError guard in session discovery (_full_scandir_discovery), ensuring discovery skips a root-level entry whose DirEntry.is_dir() raises instead of crashing.
Changes:
- Add
test_full_scandir_is_dir_oserror_skips_entryto cover thetry/except OSErroraroundsession_entry.is_dir(follow_symlinks=False). - Extend test imports to include
MagicMockto simulate a failingDirEntry.is_dir()call.
- Remove eager original_scandir(path) call that leaked a ScandirIterator when the root branch returns _WrappedCtx instead - Use 'with original_scandir(path) as it:' in __enter__ to deterministically close the iterator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Keep ScandirIterator open for the lifetime of the _WrappedCtx context manager so DirEntry.is_dir() calls don't fail on DT_UNKNOWN filesystems (where closing the iterator invalidates the dir fd needed for lstat). The iterator is now yielded lazily and closed in __exit__, matching the real os.scandir lifecycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Change _patched_scandir return type from Iterator to AbstractContextManager[Iterator[os.DirEntry[str]]] so both branches type-check without # type: ignore[return-value] comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
| def __init__(self) -> None: | ||
| self._it: Iterator[os.DirEntry[str]] | None = None | ||
|
|
||
| def _iter_wrapped_entries(self) -> Iterator[os.DirEntry[str]]: | ||
| if self._it is None: | ||
| return | ||
| for e in self._it: | ||
| if e.name == "sess-bad": | ||
| m = MagicMock(spec=os.DirEntry) | ||
| m.name = e.name | ||
| m.path = e.path | ||
| m.is_dir.side_effect = OSError("lstat failed") | ||
| yield cast(os.DirEntry[str], m) | ||
| else: | ||
| yield e | ||
|
|
||
| def __enter__(self) -> Iterator[os.DirEntry[str]]: | ||
| self._it = original_scandir(path) | ||
| return self._iter_wrapped_entries() | ||
|
|
||
| def __exit__(self, *a: object) -> None: | ||
| if self._it is not None: | ||
| self._it.close() # type: ignore[union-attr] | ||
| self._it = None |
There was a problem hiding this comment.
self._it is typed as Iterator[...] | None, but it’s always the concrete iterator returned by os.scandir(...), which provides .close(). Typing it as the concrete scandir iterator type (or a small Protocol exposing __iter__ + close()) would let you call .close() without # type: ignore[union-attr], keeping the test strictly type-safe.
|
❌ Pipeline orchestrator: review-response loop reached 3 rounds. Marking as stuck for human review. |
Closes #846
What
Adds
test_full_scandir_is_dir_oserror_skips_entrytoTestDiscoverWithIdentityNoAbsentPlanStatintests/copilot_usage/test_parser.py, covering the previously untestedtry/except OSErrorguard aroundsession_entry.is_dir(follow_symlinks=False)in_full_scandir_discovery.How
The test creates two session directories (
sess-good,sess-bad), then patchesos.scandirto wrap the root-level entries so thatsess-bad'sDirEntry.is_dir()raisesOSError("lstat failed"). It asserts that_discover_with_identityreturns exactly one session (the good one), confirming the faulting entry is silently skipped.Why
Without this test, a refactor that removes the inner
try/except OSErroraroundis_dir()would introduce a crash on broken symlinks with no test catching the regression.