-
Notifications
You must be signed in to change notification settings - Fork 24
/
lsp-dart-test-support.el
384 lines (325 loc) · 15.1 KB
/
lsp-dart-test-support.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
;;; lsp-dart-test-support.el --- Test support for LSP Dart -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2022 Eric Dallo
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; UI guide lines on flutter widgets
;;; Code:
(require 'cl-lib)
(require 'lsp-mode)
(require 'lsp-dart-protocol)
(require 'lsp-dart-utils)
(require 'lsp-dart-dap)
;;; Internal
(defconst lsp-dart-test--process-buffer-name "*LSP Dart - tests process*")
(defvar-local lsp-dart-test--tests nil)
(defvar lsp-dart-test--running-tests nil)
(cl-defstruct lsp-dart-running-test
(id nil)
(name nil)
(start-time nil))
(defun lsp-dart-test--test-kind-p (kind)
"Return non-nil if KIND is a test type."
(or (string= kind "UNIT_TEST_TEST")
(string= kind "UNIT_TEST_GROUP")))
(defun lsp-dart-test--last-index-of (regex str &optional ignore-case)
"Find the last index of a REGEX in a string STR.
IGNORE-CASE is a optional arg to ignore the case sensitive on regex search."
(let ((start 0)
(case-fold-search ignore-case)
idx)
(while (string-match regex str start)
(setq idx (match-beginning 0))
(setq start (match-end 0)))
idx))
(defun lsp-dart-test--build-command ()
"Build the dart or flutter build command checking project type."
(if (lsp-dart-flutter-project-p)
(lsp-dart-flutter-command)
(lsp-dart-pub-command)))
(defcustom lsp-dart-test-extra-args '()
"Extra arguments to be passed to test command (e.g: --no-sound-null-safety)."
:type '(repeat string)
:group 'lsp-dart)
(defun lsp-dart-test--build-command-extra-args ()
"Build the dart or flutter extra args."
(if (lsp-dart-flutter-project-p)
(append '("test" "--machine") lsp-dart-test-extra-args)
'("run" "test" "-r" "json")))
(defun lsp-dart-test--build-test-name (names)
"Build the test name from a group of test NAMES."
(when (and names
(not (seq-empty-p names)))
(->> names
(--map (substring it
(+ (cl-search "(" it) 2)
(- (lsp-dart-test--last-index-of ")" it) 1)))
(--reduce (format "%s %s" acc it)))))
(defun lsp-dart-test--escape-test-name (name)
"Return the dart safe escaped test NAME."
(let ((escaped-str (regexp-quote name)))
(seq-doseq (char '("(" ")" "{" "}"))
(setq escaped-str (replace-regexp-in-string char
(concat "\\" char)
escaped-str nil t)))
escaped-str))
(defun lsp-dart-test--running-p ()
"Return non-nil if some test is already running."
(comint-check-proc lsp-dart-test--process-buffer-name))
(defun lsp-dart-test--set-running-test (id test)
"Add TEST with key ID."
(setf (alist-get id lsp-dart-test--running-tests) test))
(defun lsp-dart-test--get-running-test (id)
"Return the test from ID if exists."
(alist-get id lsp-dart-test--running-tests))
(cl-defgeneric lsp-dart-test--handle-notification (type notification)
"Extension point for handling custom events.
TYPE is the event to handle.
NOTIFICATION is the event notification.")
(cl-defmethod lsp-dart-test--handle-notification (type _notification)
"Default handler for TYPE."
(message "No event handler for '%s'" type))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql start)) notification)
"Handle start NOTIFICATION."
(setq lsp-dart-test--running-tests nil)
(run-hook-with-args 'lsp-dart-test-all-start-notification-hook notification))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql testStart)) notification)
"Handle testStart NOTIFICATION."
(-let (((&TestStartNotification :time :test (&Test :id :name?)) notification))
(lsp-dart-test--set-running-test id (make-lsp-dart-running-test :id id
:name name?
:start-time time))
(run-hook-with-args 'lsp-dart-test-start-notification-hook notification)))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql allSuites)) _notification)
"Handle allSuites NOTIFICATION.")
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql suite)) notification)
"Handle suite NOTIFICATION."
(run-hook-with-args 'lsp-dart-test-suite-notification-hook notification))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql group)) notification)
"Handle group NOTIFICATION."
(run-hook-with-args 'lsp-dart-test-group-notification-hook notification))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql testDone)) notification)
"Handle test done NOTIFICATION."
(-let (((&TestDoneNotification :test-id) notification))
(when-let (test (lsp-dart-test--get-running-test test-id))
(run-hook-with-args 'lsp-dart-test-done-notification-hook
notification
(lsp-dart-running-test-name test)
(lsp-dart-running-test-start-time test)))))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql done)) notification)
"Handle done NOTIFICATION."
(run-hook-with-args 'lsp-dart-test-all-done-notification-hook notification))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql print)) notification)
"Handle print NOTIFICATION."
(run-hook-with-args 'lsp-dart-test-print-notification-hook notification))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql error)) notification)
"Handle error NOTIFICATION."
(run-hook-with-args 'lsp-dart-test-error-notification-hook notification))
(cl-defmethod lsp-dart-test--handle-notification ((_event (eql debug)) _notification)
"Ignore this event")
(defun lsp-dart-test--clean-process-buffer ()
"Clean test process buffer."
(when-let (process-buffer (get-buffer lsp-dart-test--process-buffer-name))
(when (get-buffer-process process-buffer)
(delete-process (get-buffer-process process-buffer)))
(with-current-buffer process-buffer
(let ((inhibit-read-only t))
(erase-buffer)))))
(defun lsp-dart-test--raw->response (raw-response)
"Parse RAW-RESPONSE into a list of responses."
(when (string-prefix-p "{" (string-trim raw-response))
(--> raw-response
string-trim
(replace-regexp-in-string (regexp-quote "}\n{") "}|||{" it nil 'literal)
(split-string it "|||")
(-map (lambda (el) (lsp--read-json el)) it))))
(defun lsp-dart-test--run-process (command &optional args)
"Spawn COMMAND with ARGS on a separated buffer."
(lsp-dart-test--clean-process-buffer)
(let ((process-buffer (get-buffer-create lsp-dart-test--process-buffer-name)))
(with-current-buffer process-buffer
(setq-local default-directory (or (lsp-dart-get-project-root) default-directory))
(unless (derived-mode-p 'lsp-dart-test-process-mode)
(lsp-dart-test-process-mode))
(apply #'make-comint-in-buffer lsp-dart-test--process-buffer-name process-buffer (car command) nil (append (cdr command) args)))))
(defun lsp-dart-test--run (&optional test)
"Run Dart/Flutter test command in a compilation buffer.
If TEST is nil, it will run all tests from project.
If TEST is non nil, it will check if contains any test specific name
to run otherwise run all tests from file-name in TEST."
(if test
(let* ((names (plist-get test :names))
(kind (plist-get test :kind))
(test-file (file-relative-name (plist-get test :file-name)
(lsp-dart-get-project-root)))
(test-name (lsp-dart-test--build-test-name names))
(group-kind? (string= kind "UNIT_TEST_GROUP"))
(test-arg (when test-name
(concat "^"
(lsp-dart-test--escape-test-name test-name)
(unless group-kind? "$")))))
(when names
(lsp-workspace-set-metadata "last-ran-test" test))
(lsp-dart-test--run-process (lsp-dart-test--build-command)
(-> (lsp-dart-test--build-command-extra-args)
(lsp-dart-assoc-if test-arg "--name")
(lsp-dart-assoc-if test-arg test-arg)
(append (list test-file)))))
(lsp-dart-test--run-process (lsp-dart-test--build-command) (lsp-dart-test--build-command-extra-args)))
(run-hooks 'lsp-dart-test-run-started-hook))
(defun lsp-dart-test--debug (test)
"Debug Dart/Flutter TEST."
(let* ((file-name (plist-get test :file-name))
(names (plist-get test :names))
(kind (plist-get test :kind))
(test-name (lsp-dart-test--build-test-name names))
(group-kind? (string= kind "UNIT_TEST_GROUP"))
(regex (concat "^"
(lsp-dart-test--escape-test-name test-name)
(unless group-kind? "$")))
(test-arg `("--name" ,regex)))
(lsp-workspace-set-metadata "last-ran-test" test)
(if (lsp-dart-flutter-project-p)
(lsp-dart-dap-debug-flutter-test file-name test-arg)
(lsp-dart-dap-debug-dart-test file-name test-arg))))
(defun lsp-dart-test--test-at-point ()
"Return the test at point.
Return the test which has the smallest range of all tests ranges in the
current buffer."
(-some--> lsp-dart-test--tests
(--filter (-let* (((beg . end) (plist-get it :code-range)))
(and (>= (point) beg)
(<= (point) end))) it)
(--min-by (-let* (((beg1 . end1) (plist-get it :code-range))
((beg2 . end2) (plist-get other :code-range)))
(and (< beg1 beg2)
(> end1 end2))) it)))
(defun lsp-dart-test--handle-process-response (raw-response)
"Handle test process RAW-RESPONSE."
(-map (lambda (notification)
(lsp-dart-test--handle-notification (intern (lsp-get notification :type)) notification))
(lsp-dart-test--raw->response raw-response)))
(defun lsp-dart-test--add-test (items &optional names)
"Add to test listfor ITEMS.
NAMES arg is optional and are the group of tests representing a test name."
(seq-doseq (item items)
(-let* (((&Outline :children :code-range test-range :element
(&Element :kind :name :range)) item)
(test-kind? (lsp-dart-test--test-kind-p kind))
(concatened-names (if test-kind?
(append names (list name))
names))
(new-test (list :file-name (file-truename (buffer-file-name))
:names (append names (list name))
:kind kind
:code-range (lsp--range-to-region test-range)
:element-range (lsp--range-to-region range))))
(when test-kind?
(add-to-list 'lsp-dart-test--tests new-test))
(unless (seq-empty-p children)
(lsp-dart-test--add-test children concatened-names)))))
(lsp-defun lsp-dart-test--check-tests ((&OutlineNotification :uri :outline (&Outline :children)))
"Check URI and outline for test adding them."
(when (lsp-dart-test-file-p uri)
(when-let (buffer (find-buffer-visiting (lsp--uri-to-path uri)))
(with-current-buffer buffer
(setq lsp-dart-test--tests nil)
(lsp-dart-test--add-test children)
(run-hook-with-args 'lsp-dart-tests-added-hook lsp-dart-test--tests)))))
;;; Public
(defun lsp-dart-test-file-p (file-name)
"Return non-nil if FILE-NAME is a dart test files."
(string-match "_test.dart" file-name))
;;; Public interface
;;;###autoload
(defun lsp-dart-run-test-at-point ()
"Run test at point."
(interactive)
(if-let (test (lsp-dart-test--test-at-point))
(lsp-dart-test--run test)
(lsp-dart-log "No test found at point.")))
;;;###autoload
(defun lsp-dart-debug-test-at-point ()
"Debug test at point."
(interactive)
(if-let (test (lsp-dart-test--test-at-point))
(lsp-dart-test--debug test)
(lsp-dart-log "No test found at point.")))
;;;###autoload
(defun lsp-dart-run-test-file ()
"Run Dart/Flutter test command only for current buffer."
(interactive)
(if (lsp-dart-test-file-p (buffer-file-name))
(lsp-dart-test--run (->> (current-buffer)
buffer-name
file-truename
(list :file-name)))
(lsp-dart-log "Current buffer is not a Dart/Flutter test file.")))
;;;###autoload
(defun lsp-dart-run-all-tests ()
"Run each test from project."
(interactive)
(lsp-dart-test--run))
;;;###autoload
(defun lsp-dart-visit-last-test ()
"Visit the last ran test going to test definition."
(interactive)
(-if-let* ((test (lsp-workspace-get-metadata "last-ran-test"))
(file-name (plist-get test :file-name))
(buffer (or (get-file-buffer file-name)
(find-file file-name)))
(beg-position (car (plist-get test :element-range))))
(if-let ((window (get-buffer-window buffer 'visible)))
(progn
(select-window window)
(goto-char beg-position))
(with-current-buffer buffer
(switch-to-buffer buffer nil t)
(goto-char beg-position)))
(lsp-dart-log "No last test found.")))
;;;###autoload
(defun lsp-dart-run-last-test ()
"Run the last ran test."
(interactive)
(if-let ((test (lsp-workspace-get-metadata "last-ran-test")))
(lsp-dart-test--run test)
(lsp-dart-log "No last test found.")))
;;;###autoload
(defun lsp-dart-debug-last-test ()
"Debug the last ran test."
(interactive)
(if-let ((test (lsp-workspace-get-metadata "last-ran-test")))
(lsp-dart-test--debug test)
(lsp-dart-log "No last test found.")))
;;;###autoload
(define-derived-mode lsp-dart-test-process-mode comint-mode lsp-dart-test--process-buffer-name
"Major mode for dart tests process."
(setq comint-prompt-read-only nil)
(setq comint-process-echoes nil)
(setq process-connection-type nil)
(if (lsp-dart-flutter-project-p)
(setenv "PATH" (concat (car (lsp-dart-flutter-command)) ":" (getenv "PATH")))
(setenv "PATH" (concat (car (lsp-dart-pub-command)) ":" (getenv "PATH"))))
(setq-local comint-output-filter-functions #'lsp-dart-test--handle-process-response))
(define-minor-mode lsp-dart-test-mode
"Mode for saving tests info for runs."
:global nil
:init-value nil
:lighter nil
(cond
(lsp-dart-test-mode
(add-hook 'lsp-dart-outline-arrived-hook #'lsp-dart-test--check-tests nil t))
(t
(remove-hook 'lsp-dart-outline-arrived-hook #'lsp-dart-test--check-tests t))))
(provide 'lsp-dart-test-support)
;;; lsp-dart-test-support.el ends here