1+ ; ;; merlin-cap.el --- Merlin and completion-at-point integration -*- coding : utf-8 ; lexical-binding : t -*-
2+ ; ; Licensed under the MIT license.
3+
4+ ; ; Author: Simon Castellan <simon.castellan(_)iuwt.fr>
5+ ; ; Frédéric Bour <frederic.bour(_)lakaban.net>
6+ ; ; Thomas Refis <thomas.refis(_)gmail.com>
7+ ; ; Tim McGilchrist <timmcgil@gmail.com>
8+ ; ; Created: 13 Sep 2024
9+ ; ; Version: 0.1
10+ ; ; Keywords: ocaml languages
11+ ; ; URL: http://github.com/ocaml/merlin
12+
13+ ; ;; Commentary:
14+
15+ ; ; Run tests for merlin-completion-at-point code.
16+
17+ ; ;; Code:
18+
19+ (require 'merlin )
20+ (require 'ert )
21+
22+ (ert-deftest test-merlin-cap--bounds ()
23+ (should (equal (merlin-cap--regions " Aaa.bbb.c" " cc.ddd" )
24+ '(" Aaa.bbb." " Aaa.bbb." " ccc." " ddd" )))
25+ (should (equal (merlin-cap--regions " ~fo" " o.bar" )
26+ '(" " " " " ~foo" " " )))
27+ (should (equal (merlin-cap--regions " " " ~foo.bar" )
28+ '(" " " " " ~foo" " " )))
29+ (should (equal (merlin-cap--regions " ~fo" " o~bar" )
30+ '(" " " " " ~foo" " " )))
31+ (should (equal (merlin-cap--regions " ~foo" " ~bar" )
32+ '(" " " " " ~foo" " " )))
33+ (should (equal (merlin-cap--regions " ~fo" " o.b~ar" )
34+ '(" " " " " ~foo" " " )))
35+ ; ; There's no obvious correct thing to return in this case, so this is fine.
36+ (should (equal (merlin-cap--regions " ~foo.bar" " " )
37+ '(" foo." " foo." " bar" " " )))
38+ (should (equal (merlin-cap--regions " " " ~" )
39+ '(" " " " " ~" " " )))
40+ (should (equal (merlin-cap--regions " " " Aaa.bbb.ccc.ddd" )
41+ '(" " " " " Aaa." " bbb.ccc.ddd" )))
42+ (should (equal (merlin-cap--regions " A" " aa.bbb.ccc.ddd" )
43+ '(" " " " " Aaa." " bbb.ccc.ddd" )))
44+ ; ; An "atom" can also just be a dotted path projecting from an expression
45+ (should (equal (merlin-cap--regions " (foo bar)." " " )
46+ '(" ." " ." " " " " )))
47+ (should (equal (merlin-cap--regions " (foo bar).Aa" " a" )
48+ '(" ." " ." " Aaa" " " )))
49+ (should (equal (merlin-cap--regions " (foo bar).Aaa.Bb" " b.ccc" )
50+ '(" ." " .Aaa." " Bbb." " ccc" )))
51+ (should (equal (merlin-cap--regions " (foo bar).Aaa.bb" " b.ccc" )
52+ '(" ." " .Aaa." " bbb." " ccc" )))
53+ (should (equal (merlin-cap--regions " (foo bar).aaa.bb" " b.ccc" )
54+ '(" .aaa." " .aaa." " bbb." " ccc" )))
55+ ; ; We should omit only uppercase components before point, not lowercase ones
56+ (should (equal (merlin-cap--regions " M." " x" )
57+ '(" " " M." " x" " " )))
58+ (should (equal (merlin-cap--regions " M.t." " x" )
59+ '(" M.t." " M.t." " x" " " )))
60+ (should (equal (merlin-cap--regions " M.N." " x" )
61+ '(" " " M.N." " x" " " )))
62+ (should (equal (merlin-cap--regions " M.t.N." " x" )
63+ '(" M.t." " M.t.N." " x" " " )))
64+ (should (equal (merlin-cap--regions " aa.bB.CC.x" " " )
65+ '(" aa.bB." " aa.bB.CC." " x" " " )))
66+ (should (equal (merlin-cap--regions " Aa.bB.CC.x" " " )
67+ '(" Aa.bB." " Aa.bB.CC." " x" " " )))
68+ (should (equal (merlin-cap--regions " aa.Bb.cc.x" " " )
69+ '(" aa.Bb.cc." " aa.Bb.cc." " x" " " )))
70+ (should (equal (merlin-cap--regions " aa.Bb.Cc.x" " " )
71+ '(" aa." " aa.Bb.Cc." " x" " " ))))
72+
73+ (defvar-local messages-buffer-name " *Messages*" )
74+
75+ (defun merlin-cap--current-message ()
76+ " Like `current-message' but work in batch mode and use `messages-buffer-name' ."
77+ (with-current-buffer messages-buffer-name
78+ (save-excursion
79+ (forward-line -1 )
80+ (buffer-substring (point ) (line-end-position )))))
81+
82+ (defmacro merlin-cap--with-test-buffer (&rest body )
83+ " Run BODY with a temp buffer set up for Merlin completion."
84+ `(with-temp-buffer
85+ (merlin-mode)
86+ (setq-local completion-at-point-functions '(merlin-cap))
87+ (insert "
88+ module Mmaa = struct
89+ module Mmbb = struct
90+ type ttaa = { ffaa : int }
91+ type ttbb = { ffbb : ttaa }
92+ let (vvaa : ttbb) = { ffbb = { ffaa = 0 } }
93+ ;;
94+ end
95+ end
96+
97+ let () = " )
98+ ; ; Don't log during the tests
99+ (let ((merlin-client-log-function nil ))
100+ ,@body )))
101+
102+ (defun merlin-cap--test-complete (prefix suffix new-prefix new-suffix message )
103+ " Trigger completion with point between PREFIX and SUFFIX and compare results.
104+
105+ NEW-PREFIX and NEW-SUFFIX are what's before and after point after
106+ completion, and MESSAGE is the message printed."
107+ (let ((start (point )))
108+ (insert prefix)
109+ (save-excursion (insert suffix))
110+ ; ; clear any previous message, to avoid coalescing [no message]
111+ (message " \n " )
112+ (message " [no message] " )
113+ (completion-at-point )
114+ (let ((end (line-end-position ))
115+ ; ; Just so the ERT error renders more nicely
116+ (point (point )))
117+ (should (equal (list (buffer-substring start point)
118+ (buffer-substring point end)
119+ (merlin-cap--current-message))
120+ (list new-prefix new-suffix message))))
121+ (delete-region start (line-end-position ))))
122+
123+ (ert-deftest test-merlin-cap-completion ()
124+ (with-temp-buffer
125+ (let ((messages-buffer-name (buffer-name (current-buffer ))))
126+ (merlin-cap--with-test-buffer
127+ (let ((merlin-cap-dot-after-module nil ))
128+ (merlin-cap--test-complete " Mma" " "
129+ " Mmaa" " "
130+ " Mmaa: <module>" )
131+ (merlin-cap--test-complete " Mmaa.Mmb" " "
132+ " Mmaa.Mmbb" " "
133+ " Mmaa.Mmbb: <module>" )
134+ (merlin-cap--test-complete " Mmaa.Mmbb.vva" " "
135+ " Mmaa.Mmbb.vvaa" " "
136+ " Mmaa.Mmbb.vvaa: Mmaa.Mmbb.ttbb" ))
137+ ; ; Manually clear the cache, since the differences produced by
138+ ; ; `merlin-cap-dot-after-module' are persisted in the cache.
139+ (setq-local merlin-cap--cache nil )
140+ (let ((merlin-cap-dot-after-module t ))
141+ (merlin-cap--test-complete " Mma" " "
142+ " Mmaa." " "
143+ " [no message]" )
144+ (merlin-cap--test-complete " Mmaa.Mmb" " "
145+ " Mmaa.Mmbb." " "
146+ " [no message]" )
147+ (merlin-cap--test-complete " Mmaa.Mmbb.vva" " "
148+ " Mmaa.Mmbb.vvaa" " "
149+ " Mmaa.Mmbb.vvaa: Mmaa.Mmbb.ttbb" )
150+ (should (equal (length merlin-cap--cache) 3 ))
151+ (merlin-cap--test-complete " Mmaa.Mmbb.vvaa.ff" " "
152+ " Mmaa.Mmbb.vvaa.ffbb" " "
153+ " Mmaa.Mmbb.vvaa.ffbb: Mmaa.Mmbb.ttbb -> Mmaa.Mmbb.ttaa" )
154+ ; ; When completing inside a record we have to include the record name in the
155+ ; ; buffer contents sent to Merlin; that invalidates the cache
156+ (should (equal (length merlin-cap--cache) 1 ))
157+ (merlin-cap--test-complete " Mmaa.Mmbb.vvaa.ffbb.ff" " "
158+ " Mmaa.Mmbb.vvaa.ffbb.ffaa" " "
159+ " Mmaa.Mmbb.vvaa.ffbb.ffaa: Mmaa.Mmbb.ttaa -> int" )
160+ ; ; We're completing in a new part of the record, so again the cache is invalidated
161+ (should (equal (length merlin-cap--cache) 1 ))
162+ ; ; completion in the middle of the atom
163+ (merlin-cap--test-complete " Mmaa.Mmb" " .vva"
164+ " Mmaa.Mmbb." " vva"
165+ " [no message]" )
166+ ; ; partial completion (PCM)
167+ (setq-local merlin-cap--cache nil )
168+ (merlin-cap--test-complete " Mma.Mmb.vva" " "
169+ " Mmaa.Mmbb.vvaa" " "
170+ " Mmaa.Mmbb.vvaa: Mmaa.Mmbb.ttbb" )
171+ ; ; The cache entries appear in reverse order of PCM's lookups;
172+ ; ; first it looks up the existing string, removing a component from the end each time it finds no results;
173+ ; ; eventually PCM just has "Mma." and it queries for "" to find completions, and it finds "Mmaa.";
174+ ; ; from there it can query for "Mmaa." and "Mmaa.Mmbb." to find completions and expand each component.
175+ (should (equal (reverse (mapcar #'car merlin-cap--cache))
176+ '(" Mma.Mmb." " Mma." " " " Mmaa." " Mmaa.Mmbb." )))
177+ ; ; partial completion with a glob
178+ (merlin-cap--test-complete " Mma.*.vva" " "
179+ " Mmaa.Mmbb.vvaa" " "
180+ " Mmaa.Mmbb.vvaa: Mmaa.Mmbb.ttbb" )
181+ ; ; When PCM looks up "Mma.*." and gets no results, that's how it knows it is safe to glob instead.
182+ (should (member " Mma.*." (mapcar #'car merlin-cap--cache)))
183+ ; ; completion with no results
184+ (merlin-cap--test-complete " Mmaa.Mmbbxxx." " "
185+ " Mmaa.Mmbbxxx." " "
186+ " No match" )
187+ ; ; The lack of results is cached.
188+ (should (equal (length merlin-cap--cache) 7 ))
189+ ; ; completion in and after a parenthesized expression
190+ (merlin-cap--test-complete " (Mmaa.Mmbb.vv" " "
191+ " (Mmaa.Mmbb.vvaa" " "
192+ " Mmaa.Mmbb.vvaa: Mmaa.Mmbb.ttbb" )
193+ (merlin-cap--test-complete " (Mmaa.Mmbb.vvaa).ffb" " "
194+ " (Mmaa.Mmbb.vvaa).ffbb" " "
195+ " .ffbb: Mmaa.Mmbb.ttbb -> Mmaa.Mmbb.ttaa" )
196+ ; ; We're completing after a different expression, so no caching.
197+ (should (equal (length merlin-cap--cache) 1 ))
198+ (merlin-cap--test-complete " ((fun x -> x) Mmaa.Mmbb.vvaa).ffbb.ffa" " "
199+ " ((fun x -> x) Mmaa.Mmbb.vvaa).ffbb.ffaa" " "
200+ " .ffbb.ffaa: Mmaa.Mmbb.ttaa -> int" ))))))
201+
202+ (ert-deftest test-merlin-cap-interrupts ()
203+ " Test that `merlin-cap' is robust to being interrupted.
204+
205+ At least at some hardcoded interruption points."
206+ (merlin-cap--with-test-buffer
207+ (let (syms)
208+ ; ; Collect the interruption position symbols
209+ (cl-letf (((symbol-function 'merlin-cap--interrupt-in-test )
210+ (lambda (sym ) (push sym syms))))
211+ (merlin-cap--get-completions " " ))
212+ ; ; Make sure we're actually doing something
213+ (should (> (length syms) 3 ))
214+ ; ; For each position, interrupt at that position.
215+ (dolist (sym-to-interrupt syms)
216+ (let ((procs (process-list )))
217+ (let ((merlin-cap--interrupt-symbol sym-to-interrupt))
218+ ; ; Interrupt it a few times, in case there's only an error the
219+ ; ; second or third time.
220+ (should-error (merlin-cap--get-completions " Mmaa." )
221+ :type 'merlin-cap--test-interrupt )
222+ ; ; Also with a different prefix.
223+ (should-error (merlin-cap--get-completions " Non.existent.Thing." )
224+ :type 'merlin-cap--test-interrupt )
225+ (should-error (merlin-cap--get-completions " Mmaa." )
226+ :type 'merlin-cap--test-interrupt ))
227+ (should (equal (merlin-cap--get-completions " Mmaa." ) '(" Mmbb." )))
228+ ; ; Remove the cache entry added by that presumably-successful completion.
229+ (setq merlin-cap--cache nil )
230+ ; ; All the created processes have been deleted
231+ (should (equal (cl-set-difference (process-list ) procs) '())))))))
232+
233+ (ert-deftest test-merlin-cap-closed-pipe ()
234+ " Test the Merlin server is robust to an EPIPE caused by Emacs.
235+
236+ We delete the Merlin client process without sending all input,
237+ which causes the Merlin server to get EPIPE from all IO, which
238+ it's had bugs with before.
239+
240+ Reliably reproducing these errors may require increasing the
241+ count in `dotimes' ."
242+ (merlin-cap--with-test-buffer
243+ (dotimes (_ 10 )
244+ (dotimes (_ 3 )
245+ (let ((merlin-cap--interrupt-symbol 'sent-half-input ))
246+ (should-error (merlin-cap--get-completions " Mmaa.Mmbb." )
247+ :type 'merlin-cap--test-interrupt )))
248+ (should (equal (merlin-cap--get-completions " Mmaa." ) '(" Mmbb." ))))))
249+
250+ (provide 'merlin-cap-test )
251+ ; ;; merlin-cap-test.el ends here
0 commit comments