-
Notifications
You must be signed in to change notification settings - Fork 3
/
slite.el
389 lines (331 loc) · 12.2 KB
/
slite.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
385
386
387
388
389
;;; slite.el --- Interactively runs your Common Lisp tests -*- lexical-binding: t -*-
;; Copyright (C) 2021-2023 Arnold Noronha
;; Author: Arnold Noronha <arnold@tdrhq.com>
;; Homepage: https://github.com/tdrhq/slite
;; Keywords: lisp tools
;; Package-Requires: ((emacs "25.1"))
;; SPDX-License-Identifier: Apache-2.0
;;; Commentary:
;; Slite stands for SLIme TEst runner. Slite interactively runs
;; your Common Lisp tests (currently only FiveAM and Parachute are
;; supported). It allows you to see the summary of test failures,
;; jump to test definitions, rerun tests with debugger all from
;; inside Emacs.
;; You might want to add some key bindings in various Lisp mode maps:
;;
;; (define-key emacs-lisp-mode-map (kbd "C-c v") #'slite-run)
;; (define-key lisp-mode-map (kbd "C-c v") #'slite-run)
;; (define-key lisp-mode-map (kbd "C-c j")
;; #'slite-compile-defun-and-run-tests)
;; (with-eval-after-load 'slime
;; (define-key slime-mode-map (kbd "C-c v") #'slite-run))
;; (with-eval-after-load 'sly
;; (define-key sly-mode-map (kbd "C-c v") #'slite-run))
;;; Code:
(require 'cl-lib)
(declare-function sly-compile-defun "sly")
(declare-function sly-edit-definition "sly")
(declare-function sly-eval-async "sly")
(declare-function sly-mode "sly")
(declare-function slime-compile-defun "slime")
(declare-function slime-edit-definition "slime")
(declare-function slime-eval-async "slime")
(declare-function slime-mode "slime")
(defvar slite-results-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "RET") #'slite-describe-result)
(define-key map (kbd "<delete>") #'slite-delete-test)
(define-key map (kbd "r") #'slite-rerun-in-debugger)
(define-key map (kbd "M-.") #'slite-jump-to-test)
(define-key map (kbd "g") #'slite-rerun)
(define-key map (kbd "C-c v") #'slite-run)
map)
"Keymap for `slite-results-mode'.")
(define-derived-mode slite-results-mode tabulated-list-mode
"CL Test Results"
"A tabulated mode to show results from your last Slite run."
(setq tabulated-list-format
[("Result" 5 t)
("Name" 30 t)
("Passed" 10 nil)
("Reason" 35 nil)]))
(defvar slite-success-shell-hook nil)
(defvar slite-success-hook 'slite--on-success)
(defvar slite-details-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "r") #'slite-rerun-in-debugger)
(define-key map (kbd "q") #'slite-details-quit)
map))
(define-derived-mode slite-details-mode fundamental-mode
"Test Results Details"
"A mode to show details for a specific test case from a Slite run."
(read-only-mode))
(defvar slite-slime-impl nil
"Either :slime or :sly. Keep as nil to auto-detect.")
(defun slite--slime-impl ()
"Return :sly or :slime depending on which one we're running against."
(cond
(slite-slime-impl
slite-slime-impl)
((functionp 'sly)
:sly)
((functionp 'slime)
:slime)
(t
(error "Neither SLIME or SLY could be autodetected"))))
(defun slite--pass ()
"The constant PASS with font-face."
#("PASS" 0 4 (face (:foreground "green"))))
(defun slite--fail ()
"The constant FAIL with font-face."
#("FAIL" 0 4 (face (:background "red"
:foreground "white"
:weight bold))))
(defun slite--format-pass-fail (msg)
"Parse the MSG to determine how to render it."
(cond
((equal msg "PASS")
(slite--pass))
((equal msg "FAIL")
(slite--fail))
(t msg)))
(defun slite--format-one-line-reason (s)
"Format S as a one line string."
(replace-regexp-in-string
;; This is common enough and takes up too much screen real estate
"^Unexpected Error: " ""
(replace-regexp-in-string "\n" "" s )))
(cl-defun slite--parse-reason (id)
"Parse the reason for the failure from ID."
(or
(let ((results (plist-get id :results)))
(cl-block inner
(dolist (test-result results)
(let ((reason (plist-get test-result :reason)))
(unless (plist-get test-result :success)
(cl-return-from inner(slite--format-one-line-reason reason)))))))
""))
(defun slite--show-test-results (results buffer)
"Show the test results from RESULTS in BUFFER."
(message "Got test results")
(with-current-buffer buffer
(slite-results-mode)
(setq tabulated-list-entries
(cl-loop for x in results
collect
(let ((data (plist-get x :data))
(id (plist-get x :id)))
(list id
(apply 'vector (slite--format-pass-fail (car data))
(append
(cdr data)
(list
(slite--parse-reason id)))) ))))
(tabulated-list-init-header)
(tabulated-list-print)
(display-buffer buffer)
;;;; I don't think this is the right behavior:
;; (unless (slite--all-tests-passed-p results)
;; (switch-to-buffer buffer))
))
(defun slite--all-tests-passed-p (results)
"Check if all the tests in RESULTS are passing."
(cl-every (lambda (x)
(equal "PASS"
(car (plist-get x :data))))
results))
(defvar slite-history nil)
(defun slite--sl*-read-from-minibuffer (&rest args)
"Call either {slime|sly}-from-minibuffer with the ARGS."
(apply
(cl-ecase (slite--slime-impl)
(:slime 'slime-read-from-minibuffer)
(:sly 'sly-read-from-minibuffer))
args))
(defun slite-run (cmd &optional buffer)
"Interactively run CL tests using the expression CMD and output the results into BUFFER."
(interactive
(list (slite--sl*-read-from-minibuffer "Lisp expression for tests: "
(car slite-history)
'slite-history)))
(slite--run-expr cmd buffer))
(defvar slite--last-expression nil)
(defun slite--run-expr (cmd &optional buffer)
"Non-interactive version of slite-run."
(unless (bufferp buffer)
(setq buffer (get-buffer-create "*Test Results*"))
(with-current-buffer buffer
(setq slite--last-expression cmd)))
(message "Waiting for test results...")
(slite--sl*-eval-async
`(slite::process-results (cl::eval (cl::read-from-string ,cmd)))
(lambda (results)
(when (and
slite-success-hook
(slite--all-tests-passed-p results))
(funcall slite-success-hook))
(slite--show-test-results results buffer))))
(defun slite-rerun ()
"Re-run the last expression."
(interactive)
(slite--run-expr slite--last-expression))
(defun slite--on-success ()
"Callback when tests have run successfully."
(when slite-success-shell-hook
(save-some-buffers t compilation-save-buffers-predicate)
(message "running hook: %s" slite-success-shell-hook)
(shell-command slite-success-shell-hook)))
(make-local-variable 'slite--current-id)
(defun slite-describe-result ()
"Describe the results at point into a buffer."
(interactive)
(let ((buffer (generate-new-buffer "*Test Case Details*")))
(let* ((id (tabulated-list-get-id))
(results (plist-get id :results))
(package (plist-get id :package)))
(with-current-buffer buffer
(insert (plist-get
id
:details))
;; now we add each of the test results
(dolist (result results)
(cond
((plist-get result :success)
(insert (slite--pass)))
(t
(insert (slite--fail))))
(insert " ")
(insert (plist-get result :expression))
(insert "\n\n")
(unless (plist-get result :success)
(insert "------------------\n")
(insert (plist-get result :reason))
(insert "\n")
(insert "------------------")
(insert "\n\n")))
(setq slite--current-id id)
(slite-details-mode)
(slite--sl*-mode)
(slite--set-buffer-package package)
(switch-to-buffer-other-window buffer)))))
(defvar-local slime-buffer-package nil)
(defvar-local sly-buffer-package nil)
(defun slite--set-buffer-package (package)
"Wrapper for {sly|slime}-set-buffer-package."
(cl-ecase (slite--slime-impl)
(:slime
(setq slime-buffer-package package))
(:sly
(setq sly-buffer-package package))))
(defun slite--sl*-mode ()
"Wrappe for {sly|slime}-mode."
(cl-ecase (slite--slime-impl)
(:slime
(slime-mode))
(:sly
(sly-mode))))
(defun slite-details-quit ()
"Simply quits the slite window."
(interactive)
(quit-window t))
(defun slite--current-id ()
"Get the ID of the test we're looking at.
This might either be in the test details view, or in the test
tabulated list."
(or (tabulated-list-get-id)
slite--current-id))
(defun slite--sl*-eval-async (expn callback)
"Wrapper for {sly|slime}-eval-async."
(cl-ecase (slite--slime-impl)
(:slime
(slime-eval-async expn callback))
(:sly
(sly-eval-async expn callback))))
(defun slite--sl*-compile-defun ()
"Wrapper for {sly|slime}-compile-defun."
(cl-ecase (slite--slime-impl)
(:slime
(slime-compile-defun))
(:sly
(sly-compile-defun 1))))
(defun slite-rerun-in-debugger ()
"Re-run the test at point in a debugger."
(interactive)
(let* ((id (slite--current-id))
(framework (plist-get id :framework))
(name (plist-get id :test-name))
(package (plist-get id :package)))
(slite--sl*-eval-async
`(slite/api::rerun-in-debugger ,framework ,name ,package)
(lambda (x)
(message "Result of running %s: %s" name x)))))
(defvar-local slite--buffer-expression nil
"The command used to generate the tests in this buffer.
We'll use this for slite-rerun.")
(defvar slite--last-command-p nil)
(defvar slite--last-read-only-mode nil)
(defun slite-compile-defun-and-run-tests ()
"Compile the current expression and run tests if the compilation passes."
(interactive)
(cond
(slite--last-command-p
(message "A compile-defun-and-run-tests is still running (if \
this is incorrect, setq slite--last-command-p to nil"))
(t
(setq slite--last-command-p t)
(setq slite--last-read-only-mode buffer-read-only)
(setq buffer-read-only t)
(slite--sl*-compile-defun))))
;; FIXME Should _these arguments really be disregarded?
(defun slite--compilation-finished (successp _notes _buffer _loadp)
"Callback for a CL compilation."
(let ((last-command-p slite--last-command-p))
(setq buffer-read-only slite--last-read-only-mode)
(setq slite--last-command-p nil)
(when (and successp last-command-p)
(call-interactively 'slite-run))))
(defun slite-delete-test ()
"Delete the test at point."
(interactive)
(let* ((id (slite--current-id))
(framework (plist-get id :framework))
(name (plist-get id :test-name))
(package (plist-get id :package)))
(when (y-or-n-p (format "Delete the test %s in package %s?" name package))
(slite--sl*-eval-async
`(slite/api::rem-test ,framework ,name ,package)
(lambda (_) (message "Test deleted"))))))
(add-hook (cl-case (slite--slime-impl)
(:sly
'sly-compilation-finished-hook)
(:slime
'slime-compilation-finished-hook))
'slite--compilation-finished)
(defun slite-jump-to-test ()
"Jump to the test at point.
Currently only supported on Lispworks, and requires a patched
version of FiveAM. Please see README."
(interactive)
(let* ((id (slite--current-id))
(name (plist-get id :test-name))
(package (plist-get id :package)))
(cl-ecase (slite--slime-impl)
(:slime
(let ((slime-buffer-package package))
(slime-edit-definition name)))
(:sly
(let ((sly-buffer-package package))
(sly-edit-definition name))))))
(defun slite--define-keybindings ()
"Define a set of default keybindings to be used with slite."
(define-key emacs-lisp-mode-map (kbd "C-c v") #'slite-run)
(define-key lisp-mode-map (kbd "C-c v") #'slite-run)
(define-key lisp-mode-map (kbd "C-c j")
#'slite-compile-defun-and-run-tests)
(with-eval-after-load 'slime
(define-key slime-mode-map (kbd "C-c v") #'slite-run))
(with-eval-after-load 'sly
(define-key sly-mode-map (kbd "C-c v") #'slite-run)))
(provide 'slite)
;;; slite.el ends here