;;; Code:
+(eval-when-compile
+ (require 'cl))
+
(defconst file-notify--library
(cond
((featurep 'inotify) 'inotify)
DESCRIPTOR should be an object returned by `file-notify-add-watch'.
If it is registered in `file-notify-descriptors', a stopped event is sent."
(let* ((desc (if (consp descriptor) (car descriptor) descriptor))
- (file (if (consp descriptor) (cdr descriptor)))
(registered (gethash desc file-notify-descriptors))
+ (file (if (consp descriptor) (cdr descriptor) (caadr registered)))
(dir (car registered)))
(when (consp registered)
;; Send `stopped' event.
- (dolist (entry (cdr registered))
- (funcall (cdr entry)
- `(,descriptor stopped
- ,(or (and (stringp (car entry))
- (expand-file-name (car entry) dir))
- dir))))
+ (funcall
+ (cdr (assoc file (cdr registered)))
+ `(,descriptor stopped ,(if file (expand-file-name file dir) dir)))
;; Modify `file-notify-descriptors'.
(if (not file)
"A pending file notification events for a future `renamed' action.
It is a form ((DESCRIPTOR ACTION FILE [FILE1-OR-COOKIE]) CALLBACK).")
+(defun file-notify--event-watched-file (event)
+ "Return file or directory being watched.
+Could be different from the directory watched by the backend library."
+ (let* ((desc (if (consp (car event)) (caar event) (car event)))
+ (registered (gethash desc file-notify-descriptors))
+ (file (if (consp (car event)) (cdar event) (caadr registered)))
+ (dir (car registered)))
+ (if file (expand-file-name file dir) dir)))
+
(defun file-notify--event-file-name (event)
"Return file name of file notification event, or nil."
(directory-file-name
(funcall (cadr pending-event) (car pending-event))
(setq pending-event nil))
- ;; Check for stopped.
- (setq
- stopped
- (or
- stopped
- (and
- (memq action '(deleted renamed))
- (= (length (cdr registered)) 1)
- ;; Not, when a file is backed up.
- (not (and (stringp file1) (backup-file-name-p file1)))
- (or
- ;; Watched file or directory is concerned.
- (string-equal
- (file-name-nondirectory file)
- (file-name-nondirectory (car registered)))
- ;; File inside a watched directory is concerned.
- (string-equal
- (file-name-nondirectory file)
- (car (cadr registered)))))))
-
;; Apply callback.
(when (and action
(or
,action ,file ,file1))
(funcall
callback
- `(,(file-notify--descriptor desc (car entry)) ,action ,file)))))
-
- ;; Modify `file-notify-descriptors'.
- (when stopped
- (file-notify-rm-watch (file-notify--descriptor desc file))))))
+ `(,(file-notify--descriptor desc (car entry)) ,action ,file))))
+
+ ;; Send `stopped' event.
+ (when (and (memq action '(deleted renamed))
+ ;; Not, when a file is backed up.
+ (not (and (stringp file1) (backup-file-name-p file1)))
+ ;; Watched file or directory is concerned.
+ (string-equal file (file-notify--event-watched-file event)))
+ (file-notify-rm-watch (file-notify--descriptor desc (car entry))))))))
;; `kqueue', `gfilenotify' and `w32notify' return a unique descriptor
;; for every `file-notify-add-watch', while `inotify' returns a unique
(should (equal (car file-notify--test-event) file-notify--test-desc))
;; Check the file name.
(should
- (or (string-equal (file-notify--event-file-name file-notify--test-event)
- file-notify--test-tmpfile)
- (string-equal (file-notify--event-file-name file-notify--test-event)
- file-notify--test-tmpfile1)
- (string-equal (file-notify--event-file-name file-notify--test-event)
- temporary-file-directory)))
+ (string-prefix-p
+ (file-notify--event-watched-file file-notify--test-event)
+ (file-notify--event-file-name file-notify--test-event)))
;; Check the second file name if exists.
(when (eq (nth 1 file-notify--test-event) 'renamed)
(should
- (or (string-equal (file-notify--event-file1-name file-notify--test-event)
- file-notify--test-tmpfile1)
- (string-equal (file-notify--event-file1-name file-notify--test-event)
- temporary-file-directory)))))
+ (string-prefix-p
+ (file-notify--event-watched-file file-notify--test-event)
+ (file-notify--event-file1-name file-notify--test-event)))))
(defun file-notify--test-event-handler (event)
"Run a test over FILE-NOTIFY--TEST-EVENT.
events, which represent different possible results. Don't wait
longer than timeout seconds for the events to be delivered."
(declare (indent 1))
- (let ((outer (make-symbol "outer")))
- `(let* ((,outer file-notify--test-events)
- (events (if (consp (car ,events)) ,events (list ,events)))
- (max-length (apply 'max (mapcar 'length events)))
- create-lockfiles)
- ;; Flush pending events.
- (file-notify--wait-for-events
- (file-notify--test-timeout)
- (input-pending-p))
- (let (file-notify--test-events)
- ,@body
- (file-notify--wait-for-events
- ;; More events need more time. Use some fudge factor.
- (* (ceiling max-length 100) (file-notify--test-timeout))
- (= max-length (length file-notify--test-events)))
- ;; One of the possible results shall match.
- (should (file-notify--test-with-events-check events))
- (setq ,outer (append ,outer file-notify--test-events)))
- (setq file-notify--test-events ,outer))))
+ `(let* ((events (if (consp (car ,events)) ,events (list ,events)))
+ (max-length (apply 'max (mapcar 'length events)))
+ create-lockfiles)
+ ;; Flush pending events.
+ (file-notify--wait-for-events
+ (file-notify--test-timeout)
+ (input-pending-p))
+ (setq file-notify--test-events nil
+ file-notify--test-results nil)
+ ,@body
+ (file-notify--wait-for-events
+ ;; More events need more time. Use some fudge factor.
+ (* (ceiling max-length 100) (file-notify--test-timeout))
+ (= max-length (length file-notify--test-events)))
+ ;; Check the result sequence just to make sure that all events
+ ;; are as expected.
+ (dolist (result file-notify--test-results)
+ (when (ert-test-failed-p result)
+ (ert-fail
+ (cadr (ert-test-result-with-condition-condition result)))))
+ ;; One of the possible event sequences shall match.
+ (should (file-notify--test-with-events-check events))))
(ert-deftest file-notify-test02-events ()
"Check file creation/change/removal notifications."
"another text" nil file-notify--test-tmpfile nil 'no-message)
(read-event nil nil file-notify--test-read-event-timeout)
(delete-file file-notify--test-tmpfile))
- ;; `file-notify-rm-watch' fires the `stopped' event. Suppress it.
- (let (file-notify--test-events)
- (file-notify-rm-watch file-notify--test-desc)))
+ (file-notify-rm-watch file-notify--test-desc))
;; Check file change and deletion.
(setq file-notify--test-tmpfile (file-notify--test-make-temp-name))
"another text" nil file-notify--test-tmpfile nil 'no-message)
(read-event nil nil file-notify--test-read-event-timeout)
(delete-file file-notify--test-tmpfile))
- ;; `file-notify-rm-watch' fires the `stopped' event. Suppress it.
- (let (file-notify--test-events)
- (file-notify-rm-watch file-notify--test-desc))
+ (file-notify-rm-watch file-notify--test-desc)
;; Check file creation, change and deletion when watching a
;; directory. There must be a `stopped' event when deleting
"any text" nil file-notify--test-tmpfile nil 'no-message)
(read-event nil nil file-notify--test-read-event-timeout)
(delete-directory temporary-file-directory 'recursive))
- ;; `file-notify-rm-watch' fires the `stopped' event. Suppress it.
- (let (file-notify--test-events)
- (file-notify-rm-watch file-notify--test-desc)))
+ (file-notify-rm-watch file-notify--test-desc))
;; Check copy of files inside a directory.
(let ((temporary-file-directory
(set-file-times file-notify--test-tmpfile '(0 0))
(read-event nil nil file-notify--test-read-event-timeout)
(delete-directory temporary-file-directory 'recursive))
- ;; `file-notify-rm-watch' fires the `stopped' event. Suppress it.
- (let (file-notify--test-events)
- (file-notify-rm-watch file-notify--test-desc)))
+ (file-notify-rm-watch file-notify--test-desc))
;; Check rename of files inside a directory.
(let ((temporary-file-directory
;; After the rename, we won't get events anymore.
(read-event nil nil file-notify--test-read-event-timeout)
(delete-directory temporary-file-directory 'recursive))
- ;; `file-notify-rm-watch' fires the `stopped' event. Suppress it.
- (let (file-notify--test-events)
- (file-notify-rm-watch file-notify--test-desc)))
+ (file-notify-rm-watch file-notify--test-desc))
;; Check attribute change. Does not work for cygwin.
(unless (eq system-type 'cygwin)
(set-file-times file-notify--test-tmpfile '(0 0))
(read-event nil nil file-notify--test-read-event-timeout)
(delete-file file-notify--test-tmpfile))
- ;; `file-notify-rm-watch' fires the `stopped' event. Suppress it.
- (let (file-notify--test-events)
- (file-notify-rm-watch file-notify--test-desc)))
-
- ;; Check the global sequence just to make sure that all
- ;; results are as expected.
- (should file-notify--test-results)
- (dolist (result file-notify--test-results)
- (when (ert-test-failed-p result)
- (ert-fail
- (cadr (ert-test-result-with-condition-condition result))))))
+ (file-notify-rm-watch file-notify--test-desc)))
;; Cleanup.
(file-notify--test-cleanup)))
(dotimes (i n)
;; It matters which direction we rename, at least for
;; kqueue. This backend parses directories in alphabetic
- ;; order (x%d before y%d). So we rename both directions.
+ ;; order (x%d before y%d). So we rename into both directions.
(if (zerop (mod i 2))
(progn
(push (expand-file-name (format "x%d" i)) source-file-list)
((or (string-equal (file-notify--test-library) "w32notify")
(file-remote-p temporary-file-directory))
'(changed changed))
+ ;; gfilenotify raises one or two `changed' events
+ ;; randomly, no chance to test. So we accept both cases.
+ ((string-equal "gfilenotify" (file-notify--test-library))
+ '((changed)
+ (changed changed)))
(t '(changed)))
;; There shouldn't be any problem, because the file is kept.
(with-temp-buffer
:tags '(:expensive-test)
(skip-unless (file-notify--test-local-enabled))
+ ;; A directory to be watched.
+ (should
+ (setq file-notify--test-tmpfile
+ (make-temp-file "file-notify-test-parent" t)))
+ ;; A file to be watched.
+ (should
+ (setq file-notify--test-tmpfile1
+ (let ((temporary-file-directory file-notify--test-tmpfile))
+ (file-notify--test-make-temp-name))))
+ (write-region "any text" nil file-notify--test-tmpfile1 nil 'no-message)
(unwind-protect
- (progn
- (setq file-notify--test-tmpfile
- (make-temp-file "dir" t))
- (setq file-notify--test-tmpfile1
- (let ((temporary-file-directory file-notify--test-tmpfile))
- (make-temp-file "file")))
- (cl-flet ((dir-callback (event)
- (let ((file-notify--test-desc file-notify--test-desc1)
- (file-notify--test-tmpfile
- (file-notify--event-file-name event)))
- (file-notify--test-event-handler event)))
- (file-callback (event)
- (let ((file-notify--test-desc file-notify--test-desc2))
- (file-notify--test-event-handler event))))
- (should
- (setq file-notify--test-desc1
- (file-notify-add-watch
- file-notify--test-tmpfile
- '(change attribute-change) #'dir-callback)))
- (should
- (setq file-notify--test-desc2
- (file-notify-add-watch
- file-notify--test-tmpfile1
- '(change attribute-change) #'file-callback)))
- (should (file-notify-valid-p file-notify--test-desc1))
- (should (file-notify-valid-p file-notify--test-desc2))
- (dotimes (i 100)
- (read-event nil nil file-notify--test-read-event-timeout)
- (if (< 0 (random))
- (write-region
- "any text" nil file-notify--test-tmpfile1 t 'no-message)
- (let ((temporary-file-directory file-notify--test-tmpfile))
- (make-temp-file "fileX"))))
- (should (file-notify-valid-p file-notify--test-desc1))
- (should (file-notify-valid-p file-notify--test-desc2))
- (delete-file file-notify--test-tmpfile1)
- (delete-directory file-notify--test-tmpfile 'recursive))
+ (cl-flet (;; Directory monitor.
+ (dir-callback (event)
+ (let ((file-notify--test-desc file-notify--test-desc1))
+ (file-notify--test-event-handler event)))
+ ;; File monitor.
+ (file-callback (event)
+ (let ((file-notify--test-desc file-notify--test-desc2))
+ (file-notify--test-event-handler event))))
+ (should
+ (setq file-notify--test-desc1
+ (file-notify-add-watch
+ file-notify--test-tmpfile
+ '(change) #'dir-callback)))
+ (should
+ (setq file-notify--test-desc2
+ (file-notify-add-watch
+ file-notify--test-tmpfile1
+ '(change) #'file-callback)))
+ (should (file-notify-valid-p file-notify--test-desc1))
+ (should (file-notify-valid-p file-notify--test-desc2))
+ (should-not (equal file-notify--test-desc1 file-notify--test-desc2))
+ ;; gfilenotify raises one or two `changed' events randomly in
+ ;; the file monitor, no chance to test.
+ (unless (string-equal "gfilenotify" (file-notify--test-library))
+ (let ((n 100) events)
+ ;; Compute the expected events.
+ (dotimes (_i (/ n 2))
+ (setq events
+ (append
+ (append
+ ;; Directory monitor and file monitor.
+ (cond
+ ;; In the remote case, there are two `changed'
+ ;; events.
+ ((file-remote-p temporary-file-directory)
+ '(changed changed changed changed))
+ ;; The directory monitor in kqueue does not
+ ;; raise any `changed' event. Just the file
+ ;; monitor event is received.
+ ((string-equal (file-notify--test-library) "kqueue")
+ '(changed))
+ ;; Otherwise, both monitors report the
+ ;; `changed' event.
+ (t '(changed changed)))
+ ;; Just the directory monitor.
+ (cond
+ ;; In kqueue, there is an additional `changed'
+ ;; event. Why?
+ ((string-equal (file-notify--test-library) "kqueue")
+ '(changed created changed))
+ (t '(created changed))))
+ events)))
+
+ ;; Run the test.
+ (file-notify--test-with-events events
+ (dotimes (i n)
+ (read-event nil nil file-notify--test-read-event-timeout)
+ (if (zerop (mod i 2))
+ (write-region
+ "any text" nil file-notify--test-tmpfile1 t 'no-message)
+ (let ((temporary-file-directory file-notify--test-tmpfile))
+ (write-region
+ "any text" nil
+ (file-notify--test-make-temp-name) nil 'no-message)))))))
+
+ ;; If we delete the file, the directory monitor shall still be
+ ;; active. We receive the `deleted' event from both the
+ ;; directory and the file monitor. The `stopped' event is
+ ;; from the file monitor. It's undecided in which order the
+ ;; the directory and the file monitor are triggered.
+ (file-notify--test-with-events
+ '((deleted deleted stopped)
+ (deleted stopped deleted))
+ (delete-file file-notify--test-tmpfile1))
+ (should (file-notify-valid-p file-notify--test-desc1))
+ (should-not (file-notify-valid-p file-notify--test-desc2))
- ;; Check the global sequence just to make sure that all
- ;; results are as expected.
- (should file-notify--test-results)
- (dolist (result file-notify--test-results)
- (when (ert-test-failed-p result)
- (ert-fail
- (cadr (ert-test-result-with-condition-condition result))))))
+ ;; Now we delete the directory.
+ (file-notify--test-with-events
+ (cond
+ ;; In kqueue, just one `deleted' event for the directory
+ ;; is received.
+ ((string-equal (file-notify--test-library) "kqueue")
+ '(deleted stopped))
+ (t (append
+ ;; The directory monitor raises a `deleted' event for
+ ;; every file contained in the directory, we must
+ ;; count them.
+ (make-list
+ (length
+ (directory-files
+ file-notify--test-tmpfile nil
+ directory-files-no-dot-files-regexp 'nosort))
+ 'deleted)
+ ;; The events of the directory itself.
+ '(deleted stopped))))
+ (delete-directory file-notify--test-tmpfile 'recursive))
+ (should-not (file-notify-valid-p file-notify--test-desc1))
+ (should-not (file-notify-valid-p file-notify--test-desc2)))
;; Cleanup.
(file-notify--test-cleanup)))