Skip to content

Commit 77bea05

Browse files
Refactor Integration tests
- Use single F# LSP session for all tests: The seems to be a race condition when restarting the F# LSP session for each test. - "dotnet restore" before running integration tests - Use own util functions for eglot instead of downloading eglot-tests - Fix flymake integration test: Need to notify `textDocument/didChange` to trigger diagnostics. - Improve Debugging of integration tests: Output jsonrpc events buffer on timeout.
1 parent fc92b95 commit 77bea05

File tree

3 files changed

+222
-65
lines changed

3 files changed

+222
-65
lines changed

Eldev

-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
; -*- mode: emacs-lisp; lexical-binding: t -*-
2-
;; FIXME: Use if-modified-since header?
3-
(unless (and (file-exists-p "eglot-tests.el") (< (time-to-seconds (time-subtract (current-time)
4-
(file-attribute-modification-time (file-attributes "eglot-tests.el"))))
5-
86400))
6-
(url-copy-file "https://raw.githubusercontent.com/joaotavora/eglot/master/eglot-tests.el" "eglot-tests.el" t))
7-
82

93
(setq package-lint-main-file "eglot-fsharp.el")
104
(setq eldev-project-main-file "eglot-fsharp.el")

test/eglot-fsharp-integration-util.el

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
;;; eglot-fsharp-integration-util.el --- Helper for eglot integration tests -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2022 Jürgen Hötzel
4+
5+
;; Author: Jürgen Hötzel <[email protected]>
6+
;; Keywords: processes
7+
8+
;; This program is free software; you can redistribute it and/or modify
9+
;; it under the terms of the GNU General Public License as published by
10+
;; the Free Software Foundation, either version 3 of the License, or
11+
;; (at your option) any later version.
12+
13+
;; This program is distributed in the hope that it will be useful,
14+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
;; GNU General Public License for more details.
17+
18+
;; You should have received a copy of the GNU General Public License
19+
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
21+
;;; Commentary:
22+
23+
;;
24+
25+
;;; Code:
26+
(require 'edebug)
27+
28+
(cl-defmacro eglot-fsharp--with-timeout (timeout &body body)
29+
(declare (indent 1) (debug t))
30+
`(eglot-fsharp--call-with-timeout ,timeout (lambda () ,@body)))
31+
32+
(defun eglot-fsharp--call-with-timeout (timeout fn)
33+
(let* ((tag (gensym "eglot-test-timeout"))
34+
(timed-out (make-symbol "timeout"))
35+
(timeout-and-message
36+
(if (listp timeout) timeout
37+
(list timeout "waiting for test to finish")))
38+
(timeout (car timeout-and-message))
39+
(message (cadr timeout-and-message))
40+
(timer)
41+
(retval))
42+
(unwind-protect
43+
(setq retval
44+
(catch tag
45+
(setq timer
46+
(run-with-timer timeout nil
47+
(lambda ()
48+
(unless edebug-active
49+
(throw tag timed-out)))))
50+
(funcall fn)))
51+
(cancel-timer timer)
52+
(when (eq retval timed-out)
53+
(warn "Received Events for %s : %s"
54+
(file-name-nondirectory (buffer-file-name))
55+
(with-current-buffer (jsonrpc-events-buffer (eglot-current-server)) (buffer-string)))
56+
(error "%s" (concat "Timed out " message))))))
57+
58+
59+
(defun eglot-fsharp--find-file-noselect (file &optional noerror)
60+
(unless (or noerror
61+
(file-readable-p file)) (error "%s does not exist" file))
62+
(find-file-noselect file))
63+
64+
(defun eglot-fsharp--tests-connect (&optional timeout)
65+
(let* ((timeout (or timeout 10))
66+
(eglot-sync-connect t)
67+
(eglot-connect-timeout timeout))
68+
(apply #'eglot--connect (eglot--guess-contact))))
69+
70+
(cl-defmacro eglot-fsharp--wait-for ((events-sym &optional (timeout 1) message) args &body body)
71+
"Spin until FN match in EVENTS-SYM, flush events after it.
72+
Pass TIMEOUT to `eglot--with-timeout'."
73+
(declare (indent 2) (debug (sexp sexp sexp &rest form)))
74+
`(eglot-fsharp--with-timeout '(,timeout ,(or message
75+
(format "waiting for:\n%s" (pp-to-string body))))
76+
(let ((event
77+
(cl-loop thereis (cl-loop for json in ,events-sym
78+
for method = (plist-get json :method)
79+
when (keywordp method)
80+
do (plist-put json :method
81+
(substring
82+
(symbol-name method)
83+
1))
84+
when (funcall
85+
(jsonrpc-lambda ,args ,@body) json)
86+
return (cons json before)
87+
collect json into before)
88+
for i from 0
89+
when (zerop (mod i 5))
90+
;; do (eglot--message "still struggling to find in %s"
91+
;; ,events-sym)
92+
do
93+
;; `read-event' is essential to have the file
94+
;; watchers come through.
95+
(read-event "[eglot] Waiting a bit..." nil 0.1)
96+
(accept-process-output nil 0.1))))
97+
(setq ,events-sym (cdr event))
98+
(eglot--message "Event detected:\n%s"
99+
(pp-to-string (car event))))))
100+
101+
102+
(cl-defmacro eglot-fsharp--sniffing ((&key server-requests
103+
server-notifications
104+
server-replies
105+
client-requests
106+
client-notifications
107+
client-replies)
108+
&rest body)
109+
"Run BODY saving LSP JSON messages in variables, most recent first."
110+
(declare (indent 1) (debug (sexp &rest form)))
111+
(let ((log-event-ad-sym (make-symbol "eglot-fsharp--event-sniff")))
112+
`(unwind-protect
113+
(let ,(delq nil (list server-requests
114+
server-notifications
115+
server-replies
116+
client-requests
117+
client-notifications
118+
client-replies))
119+
(advice-add
120+
#'jsonrpc--log-event :before
121+
(lambda (_proc message &optional type)
122+
(cl-destructuring-bind (&key method id _error &allow-other-keys)
123+
message
124+
(let ((req-p (and method id))
125+
(notif-p method)
126+
(reply-p id))
127+
(cond
128+
((eq type 'server)
129+
(cond (req-p ,(when server-requests
130+
`(push message ,server-requests)))
131+
(notif-p ,(when server-notifications
132+
`(push message ,server-notifications)))
133+
(reply-p ,(when server-replies
134+
`(push message ,server-replies)))))
135+
((eq type 'client)
136+
(cond (req-p ,(when client-requests
137+
`(push message ,client-requests)))
138+
(notif-p ,(when client-notifications
139+
`(push message ,client-notifications)))
140+
(reply-p ,(when client-replies
141+
`(push message ,client-replies)))))))))
142+
'((name . ,log-event-ad-sym)))
143+
,@body)
144+
(advice-remove #'jsonrpc--log-event ',log-event-ad-sym))))
145+
146+
147+
148+
(defun eglot-fsharp--sniff-diagnostics (file-name-suffix)
149+
(eglot-fsharp--sniffing (:server-notifications s-notifs)
150+
(eglot-fsharp--wait-for (s-notifs 20)
151+
(&key _id method params &allow-other-keys)
152+
(and
153+
(string= method "textDocument/publishDiagnostics")
154+
(string-suffix-p file-name-suffix (plist-get params :uri))))))
155+
156+
(defun eglot-fsharp--sniff-method (method-name)
157+
(eglot-fsharp--sniffing (:server-notifications s-notifs)
158+
(eglot-fsharp--wait-for (s-notifs 20)
159+
(&key _id method params &allow-other-keys)
160+
(and
161+
(string= method method-name)))))
162+
163+
(provide 'eglot-fsharp-integration-util)
164+
;;; integration-util.el ends here

test/integration-tests.el

+58-59
Original file line numberDiff line numberDiff line change
@@ -26,67 +26,66 @@
2626

2727
(require 'buttercup)
2828
(require 'eglot-fsharp)
29-
(require 'eglot-tests)
29+
(load "test/eglot-fsharp-integration-util.el")
3030

31-
(defun eglot-fsharp--sniff-diagnostics (file-name-suffix)
32-
(eglot--sniffing (:server-notifications s-notifs)
33-
(eglot--wait-for (s-notifs 20)
34-
(&key _id method params &allow-other-keys)
35-
(and
36-
(string= method "textDocument/publishDiagnostics")
37-
(string-suffix-p file-name-suffix (plist-get params :uri))))))
31+
(describe "F# LSP Installation"
32+
:before-all (setq latest-version (eglot-fsharp--latest-version))
33+
(it "succeeds using version 0.52.0"
34+
(eglot-fsharp--maybe-install "0.52.0")
35+
(expect (eglot-fsharp--installed-version) :to-equal "0.52.0"))
36+
(it (format "succeeds using latest version: %s)" latest-version)
37+
(eglot-fsharp--maybe-install)
38+
(expect (eglot-fsharp--installed-version) :to-equal latest-version)))
39+
40+
(describe "F# LSP Client"
41+
:before-all (progn (setq latest-version (eglot-fsharp--latest-version))
42+
(with-temp-buffer (unless (zerop (process-file "dotnet" nil (current-buffer) nil "restore" "test/Test1"))
43+
(signal 'file-error (buffer-string))))
44+
(eglot-fsharp--maybe-install)
45+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/FileTwo.fs")
46+
(eglot-fsharp--tests-connect 10)
47+
(eglot-fsharp--sniff-method "fsharp/notifyWorkspace")))
48+
49+
(it "Can be invoked"
50+
;; FIXME: Should use dotnet tool run
51+
(expect (process-file (eglot-fsharp--path-to-server) nil nil nil "--version")
52+
:to-equal 0))
53+
(it "is enabled on F# Files"
54+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/FileTwo.fs")
55+
(expect (type-of (eglot--current-server-or-lose)) :to-be 'eglot-fsautocomplete)))
56+
(it "shows flymake errors"
57+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Error.fs")
58+
(flymake-mode t)
59+
(flymake-start)
60+
(goto-char (point-min))
61+
(search-forward "nonexisting")
62+
(insert "x")
63+
(eglot--signal-textDocument/didChange)
64+
(eglot-fsharp--sniff-diagnostics "test/Test1/Error.fs")
65+
(flymake-goto-next-error 1 '() t)
66+
(expect (face-at-point) :to-be 'flymake-error )))
67+
(it "provides completion"
68+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/FileTwo.fs")
69+
(expect (plist-get (eglot--capabilities (eglot--current-server-or-lose)) :completionProvider) :not :to-be nil)))
70+
(it "completes function in other modules"
71+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Program.fs")
72+
(search-forward "X.func")
73+
(delete-char -3)
74+
(completion-at-point)
75+
(expect (looking-back "X\\.func") :to-be t)))
76+
(it "finds definition in pervasives"
77+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Program.fs")
78+
(search-forward "printfn")
79+
(expect (current-word) :to-equal "printfn") ;sanity check
80+
(call-interactively #'xref-find-definitions)
81+
(expect (file-name-nondirectory (buffer-file-name)) :to-equal "fslib-extra-pervasives.fs")))
82+
(it "finds definitions in other files of Project"
83+
(with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Program.fs")
84+
(goto-char 150)
85+
(expect (current-word) :to-equal "NewObjectType") ;sanity check
86+
(call-interactively #'xref-find-definitions)
87+
(expect (file-name-nondirectory (buffer-file-name)) :to-equal "FileTwo.fs"))))
3888

39-
(describe "F# LSP server"
40-
:var (latest-version)
41-
:before-all (setq latest-version (eglot-fsharp--latest-version))
42-
(it "can be installed @version 0.52.0"
43-
(eglot-fsharp--maybe-install "0.52.0")
44-
(expect (eglot-fsharp--installed-version) :to-equal "0.52.0"))
45-
(it (format "Can be installed (using latest version: %s)" latest-version)
46-
(eglot-fsharp--maybe-install)
47-
(expect (eglot-fsharp--installed-version) :to-equal latest-version))
48-
(it "Can be invoked"
49-
;; FIXME: Should use dotnet tool run
50-
(expect (car (process-lines (eglot-fsharp--path-to-server) "--version"))
51-
:to-match (rx line-start (? "FsAutoComplete" (1+ space)) (eval (eglot-fsharp--installed-version)))))
52-
(it "shows flymake errors"
53-
(with-current-buffer (eglot--find-file-noselect "test/Test1/Error.fs")
54-
(eglot--tests-connect 10)
55-
(search-forward "nonexisting")
56-
(flymake-mode t)
57-
(flymake-start)
58-
(goto-char (point-min))
59-
(eglot-fsharp--sniff-diagnostics "test/Test1/Error.fs")
60-
(flymake-goto-next-error 1 '() t)
61-
(expect (face-at-point) :to-be 'flymake-error )))
62-
(it "is enabled on F# Files"
63-
(with-current-buffer (eglot--find-file-noselect "test/Test1/FileTwo.fs")
64-
(expect (type-of (eglot--current-server-or-lose)) :to-be 'eglot-fsautocomplete)))
65-
(it "provides completion"
66-
(with-current-buffer (eglot--find-file-noselect "test/Test1/FileTwo.fs")
67-
(eglot-fsharp--sniff-diagnostics "test/Test1/FileTwo.fs")
68-
(expect (plist-get (eglot--capabilities (eglot--current-server-or-lose)) :completionProvider) :not :to-be nil)))
69-
(it "completes function in other modules"
70-
(with-current-buffer (eglot--find-file-noselect "test/Test1/Program.fs")
71-
(search-forward "X.func")
72-
(delete-char -3)
73-
(eglot-fsharp--sniff-diagnostics "test/Test1/Program.fs")
74-
(completion-at-point)
75-
(expect (looking-back "X\\.func") :to-be t)))
76-
(it "finds definition in pervasives"
77-
(with-current-buffer (eglot--find-file-noselect "test/Test1/Program.fs")
78-
(eglot--tests-connect 10)
79-
(search-forward "printfn")
80-
(eglot-fsharp--sniff-diagnostics "test/Test1/Program.fs")
81-
(expect (current-word) :to-equal "printfn") ;sanity check
82-
(call-interactively #'xref-find-definitions)
83-
(expect (file-name-nondirectory (buffer-file-name)) :to-equal "fslib-extra-pervasives.fs")))
84-
(it "finds definitions in other files of Project"
85-
(with-current-buffer (eglot--find-file-noselect "test/Test1/Program.fs")
86-
(goto-char 150)
87-
(expect (current-word) :to-equal "NewObjectType") ;sanity check
88-
(call-interactively #'xref-find-definitions)
89-
(expect (file-name-nondirectory (buffer-file-name)) :to-equal "FileTwo.fs"))))
9089

9190
(provide 'integration-tests)
9291
;;; integration-tests.el ends here

0 commit comments

Comments
 (0)