branch: externals/eglot commit 49fb02fc1c838da6c4764d498540115c85acbdff Author: João Távora <joaotav...@gmail.com> Commit: João Távora <joaotav...@gmail.com>
Use RLS in Travis CI and add actual tests Also run a hook when connected * eglot-tests.el (eglot--with-dirs-and-files) (eglot--make-file-or-dirs, eglot--call-with-dirs-and-files) (eglot--find-file-noselect): New helpers. (auto-detect-running-server, auto-reconnect): New actual tests. * eglot.el (eglot-connect): Run hook when connected (eglot-connect-hook): New variable * .travis.yml: Use rust stable and install rls * README.md: Update mention of automated tests --- .travis.yml | 9 +++- README.md | 3 +- eglot-tests.el | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ eglot.el | 3 ++ 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a89327..2f0db4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ -language: generic +language: rust sudo: false +rust: + - stable env: global: @@ -13,7 +15,10 @@ install: # Configure $PATH: Emacs installed to /tmp/emacs - export PATH=/tmp/emacs/bin:${PATH} - emacs --version - + # Install RLS + - rustup update + - rustup component add rls-preview rust-analysis rust-src + script: - make check diff --git a/README.md b/README.md index 4683951..e5a2394 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,8 @@ Under the hood: - Project support doesn't need `projectile.el`, uses Emacs's `project.el` - Requires the upcoming Emacs 26 - Contained in one file -- Its missing tests! This is *not good* +- Has automated tests that check against actual LSP servers + [lsp]: https://microsoft.github.io/language-server-protocol/ [rls]: https://github.com/rust-lang-nursery/rls diff --git a/eglot-tests.el b/eglot-tests.el index 0f29519..119d873 100644 --- a/eglot-tests.el +++ b/eglot-tests.el @@ -24,9 +24,137 @@ ;;; Code: (require 'eglot) +(require 'cl-lib) (require 'ert) +;; Helpers + +(defmacro eglot--with-dirs-and-files (dirs &rest body) + (declare (indent defun) (debug t)) + `(eglot--call-with-dirs-and-files + ,dirs #'(lambda () ,@body))) + +(defun eglot--make-file-or-dirs (ass) + (let ((file-or-dir-name (car ass)) + (content (cdr ass))) + (cond ((listp content) + (make-directory file-or-dir-name 'parents) + (let ((default-directory (concat default-directory "/" file-or-dir-name))) + (mapc #'eglot--make-file-or-dirs content))) + ((stringp content) + (with-temp-buffer + (insert content) + (write-region nil nil file-or-dir-name nil 'nomessage))) + (t + (message "[yas] oops don't know this content"))))) + +(defun eglot--call-with-dirs-and-files (dirs fn) + (let* ((default-directory (make-temp-file "eglot--fixture" t)) + new-buffers new-processes) + (with-temp-message "" + (unwind-protect + (let ((find-file-hook + (cons (lambda () (push (current-buffer) new-buffers)) + find-file-hook)) + (eglot-connect-hook + (lambda (proc) (push proc new-processes)))) + (mapc #'eglot--make-file-or-dirs dirs) + (funcall fn)) + (eglot--message "Killing buffers %s, deleting %s, killing %s" + (mapconcat #'buffer-name new-buffers ", ") + default-directory + new-processes) + (delete-directory default-directory 'recursive) + (let ((eglot-autoreconnect nil)) + (mapc #'eglot-shutdown + (cl-remove-if-not #'process-live-p new-processes))) + (mapc #'kill-buffer new-buffers))))) + +(cl-defmacro eglot--with-test-timeout (timeout &body body) + (declare (indent 1) (debug t)) + `(eglot--call-with-test-timeout ,timeout (lambda () ,@body))) + +(defun eglot--call-with-test-timeout (timeout fn) + (let* ((tag (make-symbol "tag")) + (timed-out (make-symbol "timeout")) + (timer ) + (eglot-request-timeout 1) + (retval)) + (unwind-protect + (setq retval + (catch tag + (setq timer + (run-with-timer timeout nil + (lambda () (throw tag timed-out)))) + (funcall fn))) + (cancel-timer timer) + (when (eq retval timed-out) + (error "Test timeout!"))))) + +(defun eglot--find-file-noselect (file &optional noerror) + (unless (or noerror + (file-readable-p file)) (error "%s does not exist" file)) + (find-file-noselect file)) + + +;; `rust-mode' is not a part of emacs. So define these two shims which +;; should be more than enough for testing +(unless (functionp 'rust-mode) + (define-derived-mode rust-mode prog-mode "Rust")) +(add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-mode)) + + (ert-deftest dummy () "A dummy test" (should t)) +(ert-deftest auto-detect-running-server () + "Visit a file and M-x eglot, then visit a neighbour. " + (let (proc) + (eglot--with-test-timeout 2 + (eglot--with-dirs-and-files + '(("project" . (("coiso.rs" . "bla") + ("merdix.rs" . "bla"))) + ("anotherproject" . (("cena.rs" . "bla")))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.rs") + (setq proc + (eglot 'rust-mode `(transient . ,default-directory) + '("rls"))) + (should (eglot--current-process))) + (with-current-buffer + (eglot--find-file-noselect "project/merdix.rs") + (should (eglot--current-process)) + (should (eq (eglot--current-process) proc))) + (with-current-buffer + (eglot--find-file-noselect "anotherproject/cena.rs") + (should-error (eglot--current-process-or-lose))))))) + +(ert-deftest auto-reconnect () + "Start a server. Kill it. Watch it reconnect." + (let (proc + (eglot-autoreconnect 1)) + (eglot--with-test-timeout 3 + (eglot--with-dirs-and-files + '(("project" . (("coiso.rs" . "bla") + ("merdix.rs" . "bla")))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.rs") + (setq proc + (eglot 'rust-mode `(transient . ,default-directory) + '("rls"))) + ;; In 1.2 seconds > `eglot-autoreconnect' kill servers. We + ;; should have a automatic reconnection. + (run-with-timer 1.2 nil (lambda () (delete-process proc))) + (while (process-live-p proc) (accept-process-output nil 0.5)) + (should (eglot--current-process)) + ;; Now try again too quickly + (setq proc (eglot--current-process)) + (run-with-timer 0.5 nil (lambda () (delete-process proc))) + (while (process-live-p proc) (accept-process-output nil 0.5)) + (should (not (eglot--current-process)))))))) + (provide 'eglot-tests) ;;; eglot-tests.el ends here + +;; Local Variables: +;; checkdoc-force-docstrings-flag: nil +;; End: diff --git a/eglot.el b/eglot.el index 3d5d492..4477242 100644 --- a/eglot.el +++ b/eglot.el @@ -217,6 +217,8 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) +(defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") + (defun eglot--connect (project managed-major-mode short-name contact interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." @@ -238,6 +240,7 @@ INTERACTIVE is t if inside interactive call." (null eglot-autoreconnect))))))) (setf (eglot--short-name proc) short-name) (push proc (gethash project eglot--processes-by-project)) + (run-hook-with-args 'eglot-connect-hook proc) (erase-buffer) (read-only-mode t) (cl-destructuring-bind (&key capabilities)