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**

Discussion:
BoboTiG: Thanks @rrzaripov!
Do you are working on adding tests?
BoboTiG: Also, we should check this is fixed for all patforms.
pablomartinezm: There's a potential caveat using 'in' and 'startswith'. Figure out a folder system like this: 

My folder:
a/
a/b
a/bb

Action
1. Move /a/b to /a/c

My virtual folder after action:
a/
a/c
a/cb

You should at least append the system separator at the end of the move_src_path:
```python
if _path.startswith(move_src_path + os.path.sep):
```

rrzaripov: @pablomartinezm good catch! I start rework this ASAP.!
rrzaripov: @BoboTiG I'm reworked PR, your suggestions applied, thank you! I'm sorry, your suggestions from code review not included as additional commit, my mistake, just first time work with code review on github.
BoboTiG: Thanks @rrzaripov, I will merge it later in the day :)
BoboTiG: :thinking: The test passes without the patch applied. Something is missing perhaps?
rrzaripov: @BoboTiG I have stuck with this. When similar test scenario running from console (as bash script), I get expected fail. When test running using pytest result is unexpectedly success. I'm still trying understand what going on, but this make me crazy.

Bash script
```bash
(venv) root@debian-9:/home/vagrant/watchdog# cat test.sh
python3 ./test.py > /tmp/log.txt &
p=$!
sleep 3

mkdir A
mkdir A/B
mv A A2
touch A2/B/example.txt

sleep 3
kill -KILL $p

cat /tmp/log.txt
rm -rf A2
```

Output
```
2019-06-11 16:00:33 - Created directory: ./A
2019-06-11 16:00:33 - Modified directory: .
2019-06-11 16:00:33 - Created directory: ./A/B
2019-06-11 16:00:33 - Modified directory: ./A
2019-06-11 16:00:34 - Moved directory: from ./A to ./A2
2019-06-11 16:00:34 - Modified directory: .
2019-06-11 16:00:34 - Moved directory: from ./A/B to ./A2/B
2019-06-11 16:00:34 - Moved file: from ./A/B/example.txt to ./A2/B/example.txt
2019-06-11 16:00:34 - Created file: ./A/B/example.txt
2019-06-11 16:00:34 - Modified directory: ./A/B
2019-06-11 16:00:34 - Modified file: ./A/B/example.txt
```
BoboTiG: With my new suggestions, the test is failing. do you think it is more correct like that?
rrzaripov: @BoboTiG I'm reworked test and added separate test for Windows, because events sets very different in comparison with Linux. While testing two interesting things was discovered. 
1. In Linux sometimes DirModifiedEvent event is catched. Need to investigate when this occuring and, possible increase timeout to make this test more simple and robust.
2. In Windows DirCreatedEvent event emitted twice while created subdirectory B. I think this is a bug, and I will to investigate.
rrzaripov: @BoboTiG tests passed, PR ready to merge:-)

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 @@ def _recursive_simulate(src_path):
                         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 @@
     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')]