Repository: gorakhargosh/watchdog

Issue:
Wrong source path after renaming a top level folder
Steps to reproduce in Ubuntu 18.04

Start monitoring a folder in recursive mode p.e. "/"
Create Folder A
**"/A"**
Create Folder B inside A 
**"/A/B"**
Rename A -> A2
**"/A2/B"**
Create a file inside B, named example.txt
**/A2/B/example.txt**

Bug: The src_path of the event fired when creating the file example.txt is
**/A/B/example.txt**
and should be
**/A2/B/example.txt**

Comments:

Patch:
diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py
index c9c1b5c..1eae327 100644
--- a/src/watchdog/observers/inotify_c.py
+++ b/src/watchdog/observers/inotify_c.py
@@ -332,6 +332,13 @@ class Inotify(object):
                         del self._wd_for_path[move_src_path]
                         self._wd_for_path[inotify_event.src_path] = moved_wd
                         self._path_for_wd[moved_wd] = inotify_event.src_path
+                        if self.is_recursive:
+                            for _path, _wd in self._wd_for_path.copy().items():
+                                if _path.startswith(move_src_path + os.path.sep.encode()):
+                                    moved_wd = self._wd_for_path.pop(_path)
+                                    _move_to_path = _path.replace(move_src_path, inotify_event.src_path)
+                                    self._wd_for_path[_move_to_path] = moved_wd
+                                    self._path_for_wd[moved_wd] = _move_to_path
                     src_path = os.path.join(wd_path, name)
                     inotify_event = InotifyEvent(wd, mask, cookie, name, src_path)
 


Test patch:
diff --git a/tests/test_emitter.py b/tests/test_emitter.py
index a594e70..8b66266 100644
--- a/tests/test_emitter.py
+++ b/tests/test_emitter.py
@@ -32,6 +32,7 @@ from watchdog.events import (
     DirDeletedEvent,
     DirModifiedEvent,
     DirCreatedEvent,
+    DirMovedEvent
 )
 from watchdog.observers.api import ObservedWatch
 
@@ -322,3 +323,186 @@ def test_recursive_off():
 
     with pytest.raises(Empty):
         event_queue.get(timeout=5)
+
+
+@pytest.mark.skipif(platform.is_windows(),
+                    reason="Windows create another set of events for this test")
+def test_renaming_top_level_directory():
+    start_watching()
+
+    mkdir(p('a'))
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirCreatedEvent)
+    assert event.src_path == p('a')
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirModifiedEvent)
+    assert event.src_path == p()
+
+    mkdir(p('a', 'b'))
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirCreatedEvent)
+    assert event.src_path == p('a', 'b')
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirModifiedEvent)
+    assert event.src_path == p('a')
+
+    mv(p('a'), p('a2'))
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('a')
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirModifiedEvent)
+    assert event.src_path == p()
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirModifiedEvent)
+    assert event.src_path == p()
+
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirMovedEvent)
+    assert event.src_path == p('a', 'b')
+
+    open(p('a2', 'b', 'c'), 'a').close()
+
+    # DirModifiedEvent may emitted, but sometimes after waiting time is out.
+    events = []
+    while True:
+        events.append(event_queue.get(timeout=5)[0])
+        if event_queue.empty():
+            break
+
+    assert all([isinstance(e, (FileCreatedEvent, FileMovedEvent, DirModifiedEvent)) for e in events])
+
+    for event in events:
+        if isinstance(event, FileCreatedEvent):
+            assert event.src_path == p('a2', 'b', 'c')
+        elif isinstance(event, FileMovedEvent):
+            assert event.dest_path == p('a2', 'b', 'c')
+            assert event.src_path == p('a', 'b', 'c')
+        elif isinstance(event, DirModifiedEvent):
+            assert event.src_path == p('a2', 'b')
+
+
+@pytest.mark.skipif(platform.is_linux(),
+                    reason="Linux create another set of events for this test")
+def test_renaming_top_level_directory_on_windows():
+    start_watching()
+
+    mkdir(p('a'))
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirCreatedEvent)
+    assert event.src_path == p('a')
+
+    mkdir(p('a', 'b'))
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirCreatedEvent)
+    assert event.src_path == p('a', 'b')
+
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirCreatedEvent)
+    assert event.src_path == p('a', 'b')
+
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirModifiedEvent)
+    assert event.src_path == p('a')
+
+    mv(p('a'), p('a2'))
+    event = event_queue.get(timeout=5)[0]
+    assert isinstance(event, DirMovedEvent)
+    assert event.src_path == p('a', 'b')
+
+    open(p('a2', 'b', 'c'), 'a').close()
+
+    events = []
+    while True:
+        events.append(event_queue.get(timeout=5)[0])
+        if event_queue.empty():
+            break
+
+    assert all([isinstance(e, (FileCreatedEvent, FileMovedEvent, DirMovedEvent, DirModifiedEvent)) for e in events])
+
+    for event in events:
+        if isinstance(event, FileCreatedEvent):
+            assert event.src_path == p('a2', 'b', 'c')
+        elif isinstance(event, FileMovedEvent):
+            assert event.dest_path == p('a2', 'b', 'c')
+            assert event.src_path == p('a', 'b', 'c')
+        elif isinstance(event, DirMovedEvent):
+            assert event.dest_path == p('a2')
+            assert event.src_path == p('a')
+        elif isinstance(event, DirModifiedEvent):
+            assert event.src_path == p('a2', 'b')
+
+
+@pytest.mark.skipif(platform.is_windows(),
+                    reason="Windows create another set of events for this test")
+def test_move_nested_subdirectories():
+    mkdir(p('dir1/dir2/dir3'), parents=True)
+    touch(p('dir1/dir2/dir3', 'a'))
+    start_watching(p(''))
+    mv(p('dir1/dir2'), p('dir2'))
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir1', 'dir2')
+    assert isinstance(event, DirMovedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir1')
+    assert isinstance(event, DirModifiedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert p(event.src_path, '') == p('')
+    assert isinstance(event, DirModifiedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir1/dir2/dir3')
+    assert isinstance(event, DirMovedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir1/dir2/dir3', 'a')
+    assert isinstance(event, FileMovedEvent)
+
+    touch(p('dir2/dir3', 'a'))
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir2/dir3', 'a')
+    assert isinstance(event, FileModifiedEvent)
+
+
+@pytest.mark.skipif(platform.is_linux(),
+                    reason="Linux create another set of events for this test")
+def test_move_nested_subdirectories_on_windows():
+    mkdir(p('dir1/dir2/dir3'), parents=True)
+    touch(p('dir1/dir2/dir3', 'a'))
+    start_watching(p(''))
+    mv(p('dir1/dir2'), p('dir2'))
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir1', 'dir2')
+    assert isinstance(event, FileDeletedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir2')
+    assert isinstance(event, DirCreatedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir2', 'dir3')
+    assert isinstance(event, DirCreatedEvent)
+
+    event = event_queue.get(timeout=5)[0]
+    assert event.src_path == p('dir2', 'dir3', 'a')
+    assert isinstance(event, FileCreatedEvent)
+
+    touch(p('dir2/dir3', 'a'))
+
+    events = []
+    while True:
+        events.append(event_queue.get(timeout=5)[0])
+        if event_queue.empty():
+            break
+
+    assert all([isinstance(e, (FileModifiedEvent, DirModifiedEvent)) for e in events])
+
+    for event in events:
+        if isinstance(event, FileModifiedEvent):
+            assert event.src_path == p('dir2', 'dir3', 'a')
+        elif isinstance(event, DirModifiedEvent):
+            assert event.src_path in [p('dir2'), p('dir2', 'dir3')]