diff options
Diffstat (limited to 'test/lisp/progmodes/eglot-tests.el')
-rw-r--r-- | test/lisp/progmodes/eglot-tests.el | 175 |
1 files changed, 99 insertions, 76 deletions
diff --git a/test/lisp/progmodes/eglot-tests.el b/test/lisp/progmodes/eglot-tests.el index 4b6528351b2..7ac26732737 100644 --- a/test/lisp/progmodes/eglot-tests.el +++ b/test/lisp/progmodes/eglot-tests.el @@ -31,23 +31,20 @@ ;; Some of these tests rely on the GNU ELPA package company.el and ;; yasnippet.el being available. -;; Some of the tests require access to a remote host files. Since -;; this could be problematic, a mock-up connection method "mock" is -;; used. Emulating a remote connection, it simply calls "sh -i". -;; Tramp's file name handlers still run, so this test is sufficient -;; except for connection establishing. - -;; If you want to test a real Tramp connection, set -;; $REMOTE_TEMPORARY_FILE_DIRECTORY to a suitable value in order to -;; overwrite the default value. If you want to skip tests accessing a -;; remote host, set this environment variable to "/dev/null" or -;; whatever is appropriate on your system. +;; Some of the tests require access to a remote host files, which is +;; mocked in the simplest case. If you want to test a real Tramp +;; connection, override $REMOTE_TEMPORARY_FILE_DIRECTORY to a suitable +;; value (FIXME: like what?) in order to overwrite the default value. +;; +;; IMPORTANT: Since Eglot is a :core ELPA package, these tests are + ;;supposed to run on Emacsen down to 26.3. Do not use bleeding-edge + ;;functionality not compatible with that Emacs version. ;;; Code: (require 'eglot) (require 'cl-lib) (require 'ert) -(require 'tramp) ; must be prior ert-x +(require 'tramp) (require 'ert-x) ; ert-simulate-command (require 'edebug) (require 'python) ; some tests use pylsp @@ -59,6 +56,11 @@ ;;; Helpers +(defun eglot--test-message (format &rest args) + "Message out with FORMAT with ARGS." + (message "[eglot-tests] %s" + (apply #'format format args))) + (defmacro eglot--with-fixture (fixture &rest body) "Setup FIXTURE, call BODY, teardown FIXTURE. FIXTURE is a list. Its elements are of the form (FILE . CONTENT) @@ -102,23 +104,23 @@ then restored." (push (cons (car spec) (symbol-value (car spec))) syms-to-restore) (set (car spec) (cadr spec))) ((stringp (car spec)) (push spec file-specs)))) + (eglot--test-message "[%s]: test start" (ert-test-name (ert-running-test))) (unwind-protect - (let* ((home (getenv "HOME")) - (process-environment + (let* ((process-environment (append `(;; Set XDF_CONFIG_HOME to /dev/null to prevent ;; user-configuration to have an influence on ;; language servers. (See github#441) "XDG_CONFIG_HOME=/dev/null" ;; ... on the flip-side, a similar technique by - ;; Emacs's test makefiles means that HOME is set to - ;; /nonexistent. This breaks some common - ;; installations for LSP servers like pylsp, making - ;; these tests mostly useless, so we hack around it - ;; here with a great big hack. + ;; Emacs's test makefiles means that HOME is + ;; spoofed to /nonexistent, or sometimes /tmp. + ;; This breaks some common installations for LSP + ;; servers like pylsp, rust-analyzer making these + ;; tests mostly useless, so we hack around it here + ;; with a great big hack. ,(format "HOME=%s" - (if (file-exists-p home) home - (format "/home/%s" (getenv "USER"))))) + (expand-file-name (format "~%s" (user-login-name))))) process-environment)) ;; Prevent "Can't guess python-indent-offset ..." messages. (python-indent-guess-indent-offset-verbose . nil) @@ -127,8 +129,8 @@ then restored." (setq created-files (mapcan #'eglot--make-file-or-dir file-specs)) (prog1 (funcall fn) (setq test-body-successful-p t))) - (eglot--message - "Test body was %s" (if test-body-successful-p "OK" "A FAILURE")) + (eglot--test-message "[%s]: %s" (ert-test-name (ert-running-test)) + (if test-body-successful-p "OK" "FAILED")) (unwind-protect (let ((eglot-autoreconnect nil)) (dolist (server new-servers) @@ -137,8 +139,7 @@ then restored." (eglot-shutdown server nil 3 (not test-body-successful-p)) (error - (eglot--message "Non-critical shutdown error after test: %S" - oops)))) + (eglot--test-message "Non-critical cleanup error: %S" oops)))) (when (not test-body-successful-p) ;; We want to do this after the sockets have ;; shut down such that any pending data has been @@ -151,21 +152,21 @@ then restored." (jsonrpc-events-buffer server))))) (cond (noninteractive (dolist (buffer buffers) - (eglot--message "%s:" (buffer-name buffer)) + (eglot--test-message "contents of `%s':" (buffer-name buffer)) (princ (with-current-buffer buffer (buffer-string)) 'external-debugging-output))) (t - (eglot--message "Preserved for inspection: %s" - (mapconcat #'buffer-name buffers ", ")))))))) + (eglot--test-message "Preserved for inspection: %s" + (mapconcat #'buffer-name buffers ", ")))))))) (eglot--cleanup-after-test fixture-directory created-files syms-to-restore))))) (defun eglot--cleanup-after-test (fixture-directory created-files syms-to-restore) (let ((buffers-to-delete (delete nil (mapcar #'find-buffer-visiting created-files)))) - (eglot--message "Killing %s, wiping %s, restoring %s" - buffers-to-delete - fixture-directory - (mapcar #'car syms-to-restore)) + (eglot--test-message "Killing %s, wiping %s, restoring %s" + buffers-to-delete + fixture-directory + (mapcar #'car syms-to-restore)) (cl-loop for (sym . val) in syms-to-restore do (set sym val)) (dolist (buf buffers-to-delete) ;; have to save otherwise will get prompted @@ -253,12 +254,12 @@ then restored." (advice-remove #'jsonrpc--log-event ',log-event-ad-sym)))) (cl-defmacro eglot--wait-for ((events-sym &optional (timeout 1) message) args &body body) - "Spin until FN match in EVENTS-SYM, flush events after it. -Pass TIMEOUT to `eglot--with-timeout'." (declare (indent 2) (debug (sexp sexp sexp &rest form))) `(eglot--with-timeout '(,timeout ,(or message (format "waiting for:\n%s" (pp-to-string body)))) - (let ((event + (eglot--test-message "waiting for `%s'" (with-output-to-string + (mapc #'princ ',body))) + (let ((events (cl-loop thereis (cl-loop for json in ,events-sym for method = (plist-get json :method) when (keywordp method) @@ -272,16 +273,21 @@ Pass TIMEOUT to `eglot--with-timeout'." collect json into before) for i from 0 when (zerop (mod i 5)) - ;; do (eglot--message "still struggling to find in %s" - ;; ,events-sym) + ;; do (eglot--test-message "still struggling to find in %s" + ;; ,events-sym) do ;; `read-event' is essential to have the file ;; watchers come through. - (read-event "[eglot] Waiting a bit..." nil 0.1) + (cond ((fboundp 'flush-standard-output) + (read-event nil nil 0.1) (princ ".") + (flush-standard-output)) + (t + (read-event "." nil 0.1))) (accept-process-output nil 0.1)))) - (setq ,events-sym (cdr event)) - (eglot--message "Event detected:\n%s" - (pp-to-string (car event)))))) + (setq ,events-sym (cdr events)) + (cl-destructuring-bind (&key method id &allow-other-keys) (car events) + (eglot--test-message "detected: %s" + (or method (and id (format "id=%s" id)))))))) ;; `rust-mode' is not a part of Emacs, so we define these two shims ;; which should be more than enough for testing. @@ -408,7 +414,7 @@ Pass TIMEOUT to `eglot--with-timeout'." ) (should (eglot--tests-connect)) (let (register-id) - (eglot--wait-for (s-requests 1) + (eglot--wait-for (s-requests 3) (&key id method &allow-other-keys) (setq register-id id) (string= method "client/registerCapability")) @@ -435,7 +441,7 @@ Pass TIMEOUT to `eglot--with-timeout'." (eglot--find-file-noselect "diag-project/main.c") (eglot--sniffing (:server-notifications s-notifs) (eglot--tests-connect) - (eglot--wait-for (s-notifs 2) + (eglot--wait-for (s-notifs 10) (&key _id method &allow-other-keys) (string= method "textDocument/publishDiagnostics")) (flymake-start) @@ -445,16 +451,19 @@ Pass TIMEOUT to `eglot--with-timeout'." (ert-deftest eglot-test-diagnostic-tags-unnecessary-code () "Test rendering of diagnostics tagged \"unnecessary\"." - (skip-unless (executable-find "rust-analyzer")) - (skip-unless (executable-find "cargo")) + (skip-unless (executable-find "clangd")) (eglot--with-fixture - '(("diagnostic-tag-project" . - (("main.rs" . - "fn main() -> () { let test=3; }")))) + `(("diag-project" . + (("main.cpp" . "int main(){float a = 42.2; return 0;}")))) (with-current-buffer - (eglot--find-file-noselect "diagnostic-tag-project/main.rs") - (let ((eglot-server-programs '((rust-mode . ("rust-analyzer"))))) - (should (zerop (shell-command "cargo init"))) + (eglot--find-file-noselect "diag-project/main.cpp") + (eglot--make-file-or-dir '(".git")) + (eglot--make-file-or-dir + `("compile_commands.json" . + ,(jsonrpc--json-encode + `[(:directory ,default-directory :command "/usr/bin/c++ -Wall -c main.cpp" + :file ,(expand-file-name "main.cpp"))]))) + (let ((eglot-server-programs '((c++-mode . ("clangd"))))) (eglot--sniffing (:server-notifications s-notifs) (eglot--tests-connect) (eglot--wait-for (s-notifs 10) @@ -732,7 +741,7 @@ pylsp prefers autopep over yafp, despite its README stating the contrary." (should (zerop (shell-command "cargo init"))) (eglot--sniffing (:server-notifications s-notifs) (should (eglot--tests-connect)) - (eglot--wait-for (s-notifs 10) (&key method &allow-other-keys) + (eglot--wait-for (s-notifs 20) (&key method &allow-other-keys) (string= method "textDocument/publishDiagnostics"))) (goto-char (point-max)) (eglot--simulate-key-event ?.) @@ -798,33 +807,35 @@ pylsp prefers autopep over yafp, despite its README stating the contrary." (should (= 4 (length (flymake--project-diagnostics)))))))))) (ert-deftest eglot-test-project-wide-diagnostics-rust-analyzer () - "Test diagnostics through multiple files in a TypeScript LSP." + "Test diagnostics through multiple files in rust-analyzer." (skip-unless (executable-find "rust-analyzer")) (skip-unless (executable-find "cargo")) + (skip-unless (executable-find "git")) (eglot--with-fixture '(("project" . (("main.rs" . - "fn main() -> () { let test=3; }") + "fn main() -> i32 { return 42.2;}") ("other-file.rs" . "fn foo() -> () { let hi=3; }")))) - (eglot--make-file-or-dir '(".git")) (let ((eglot-server-programs '((rust-mode . ("rust-analyzer"))))) - ;; Open other-file, and see diagnostics arrive for main.rs + ;; Open other-file.rs, and see diagnostics arrive for main.rs, + ;; which we didn't open. (with-current-buffer (eglot--find-file-noselect "project/other-file.rs") + (should (zerop (shell-command "git init"))) (should (zerop (shell-command "cargo init"))) (eglot--sniffing (:server-notifications s-notifs) (eglot--tests-connect) (flymake-start) - (eglot--wait-for (s-notifs 10) - (&key _id method &allow-other-keys) - (string= method "textDocument/publishDiagnostics")) - (let ((diags (flymake--project-diagnostics))) - (should (= 2 (length diags))) - ;; Check that we really get a diagnostic from main.rs, and - ;; not from other-file.rs - (should (string-suffix-p - "main.rs" - (flymake-diagnostic-buffer (car diags)))))))))) + (eglot--wait-for (s-notifs 20) + (&key _id method params &allow-other-keys) + (and (string= method "textDocument/publishDiagnostics") + (string-suffix-p "main.rs" (plist-get params :uri)))) + (let* ((diags (flymake--project-diagnostics))) + (should (cl-some (lambda (diag) + (let ((locus (flymake-diagnostic-buffer diag))) + (and (stringp (flymake-diagnostic-buffer diag)) + (string-suffix-p "main.rs" locus)))) + diags)))))))) (ert-deftest eglot-test-json-basic () "Test basic autocompletion in vscode-json-languageserver." @@ -856,8 +867,8 @@ pylsp prefers autopep over yafp, despite its README stating the contrary." '((c-mode . ("clangd"))))) (with-current-buffer (eglot--find-file-noselect "project/foo.c") - (setq-local eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column) - (setq-local eglot-current-column-function #'eglot-lsp-abiding-column) + (setq-local eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos) + (setq-local eglot-current-linepos-function #'eglot-utf-16-linepos) (eglot--sniffing (:client-notifications c-notifs) (eglot--tests-connect) (end-of-line) @@ -866,12 +877,12 @@ pylsp prefers autopep over yafp, despite its README stating the contrary." (eglot--wait-for (c-notifs 2) (&key params &allow-other-keys) (should (equal 71 (cadddr (cadadr (aref (cadddr params) 0)))))) (beginning-of-line) - (should (eq eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column)) - (funcall eglot-move-to-column-function 71) + (should (eq eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos)) + (funcall eglot-move-to-linepos-function 71) (should (looking-at "p"))))))) (ert-deftest eglot-test-lsp-abiding-column () - "Test basic `eglot-lsp-abiding-column' and `eglot-move-to-lsp-abiding-column'." + "Test basic LSP character counting logic." (skip-unless (executable-find "clangd")) (eglot-tests--lsp-abiding-column-1)) @@ -1258,16 +1269,28 @@ macro will assume it exists." (defvar tramp-histfile-override) (defun eglot--call-with-tramp-test (fn) + (unless (>= emacs-major-version 27) + (ert-skip "Eglot Tramp support only on Emacs >= 27")) ;; Set up a Tramp method that’s just a shell so the remote host is ;; really just the local host. - (let* ((tramp-remote-path (cons 'tramp-own-remote-path tramp-remote-path)) + (let* ((tramp-remote-path (cons 'tramp-own-remote-path + tramp-remote-path)) (tramp-histfile-override t) (tramp-verbose 1) - (temporary-file-directory ert-remote-temporary-file-directory) + (temporary-file-directory + (or (bound-and-true-p ert-remote-temporary-file-directory) + (prog1 (format "/mock::%s" temporary-file-directory) + (add-to-list + 'tramp-methods + '("mock" + (tramp-login-program "sh") (tramp-login-args (("-i"))) + (tramp-direct-async ("-c")) (tramp-remote-shell "/bin/sh") + (tramp-remote-shell-args ("-c")) (tramp-connection-timeout 10))) + (add-to-list 'tramp-default-host-alist + `("\\`mock\\'" nil ,(system-name))) + (when (and noninteractive (not (file-directory-p "~/"))) + (setenv "HOME" temporary-file-directory))))) (default-directory temporary-file-directory)) - ;; We must check the remote LSP server. So far, just "clangd" is used. - (unless (executable-find "clangd" 'remote) - (ert-skip "Remote clangd not found")) (funcall fn))) (ert-deftest eglot-test-tramp-test () |