Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion chalice/cli/filewatch/stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ def _seed_mtime_cache(self, root_dir):
for rootdir, _, filenames in self._osutils.walk(root_dir):
for filename in filenames:
path = self._osutils.joinpath(rootdir, filename)
self._mtime_cache[path] = self._osutils.mtime(path)
try:
self._mtime_cache[path] = self._osutils.mtime(path)
except (OSError, IOError):
# This can happen with broken symlinks or files that
# disappear between walk() and mtime().
LOGGER.debug("Unable to stat file: %s, skipping.", path)

def _single_pass_poll(self, root_dir, callback):
# type: (str, Callable[[], None]) -> None
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/cli/filewatch/test_stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,42 @@ def callback(*args, **kwargs):
time.sleep(0.2)
else:
raise AssertionError("Expected callback to be invoked but was not.")


class FakeOSUtilsWithBrokenSymlink(object):
"""Simulates a broken symlink that fails os.stat during initial cache seed.

This can happen with symlinks pointing to non-existent files, such as
Emacs lock files (.#app.py) or other temporary symlinks.
"""
def __init__(self):
self.scan_count = 0

def walk(self, rootdir):
self.scan_count += 1
yield 'rootdir', [], ['broken-symlink', 'good-file']

def joinpath(self, *parts):
return os.path.join(*parts)

def mtime(self, path):
if path.endswith('broken-symlink'):
raise FileNotFoundError("No such file or directory")
return 1


def test_can_handle_broken_symlink_during_initial_scan():
"""Test that broken symlinks during initial cache seeding are handled.

This is a regression test for issue #997 where symlinks to non-existent
files (like Emacs .#app.py files) would crash the file watcher during
the initial _seed_mtime_cache call.
"""
osutils = FakeOSUtilsWithBrokenSymlink()
watcher = stat.StatFileWatcher(osutils)
# This should not raise an exception
watcher._seed_mtime_cache('rootdir')
# The good file should be in the cache, the broken symlink should not
assert len(watcher._mtime_cache) == 1
assert 'rootdir/good-file' in watcher._mtime_cache or \
os.path.join('rootdir', 'good-file') in watcher._mtime_cache