fedora-csb-system-manager
  1;;; gotest-ui.el --- Major mode for running go test -json
  2
  3;; Copyright 2018 Andreas Fuchs
  4;; Authors: Andreas Fuchs <asf@boinkor.net>
  5
  6;; URL: https://github.com/antifuchs/gotest-ui-mode
  7;; Created: Feb 18, 2018
  8;; Keywords: languages go
  9;; Version: 0.1.0
 10;; Package-Requires: ((emacs "25") (s "1.12.0") (gotest "0.14.0"))
 11
 12;; This file is not a part of GNU Emacs.
 13
 14;; This program is free software; you can redistribute it and/or
 15;; modify it under the terms of the GNU General Public License as
 16;; published by the Free Software Foundation; either version 3.0, or
 17;; (at your option) any later version.
 18
 19;; This program is distributed in the hope that it will be useful,
 20;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 21;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 22;; GNU General Public License for more details.
 23
 24;; You should have received a copy of the GNU General Public License
 25;; along with this program; if not, write to the Free Software
 26;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 27
 28;;; Commentary:
 29
 30;;  Provides support for running go tests with a nice user interface
 31;;  that allows folding away output, highlighting failing tests.
 32
 33;;; Code:
 34
 35(eval-when-compile
 36  (require 'cl))
 37
 38(require 'subr-x)
 39(require 'ewoc)
 40(require 'json)
 41(require 'compile)
 42
 43(defgroup gotest-ui nil
 44  "The go test runner."
 45  :group 'tools)
 46
 47(defface gotest-ui-pass-face '((t :foreground "green"))
 48  "Face for displaying the status of a passing test."
 49  :group 'gotest-ui)
 50
 51(defface gotest-ui-skip-face '((t :foreground "grey"))
 52  "Face for displaying the status of a skipped test."
 53  :group 'gotest-ui)
 54
 55(defface gotest-ui-fail-face '((t :foreground "pink" :weight bold))
 56  "Face for displaying the status of a failed test."
 57  :group 'gotest-ui)
 58
 59(defface gotest-ui-link-face '((t :foreground "white" :weight bold))
 60  "Face for displaying links to go source files."
 61  :group 'gotest-ui)
 62
 63(defcustom gotest-ui-expand-test-statuses '(fail)
 64  "Statuses to expand test cases for.
 65Whenever a test enters this state, it is automatically expanded."
 66  :group 'gotest-ui)
 67
 68(defcustom gotest-ui-test-binary '("go")
 69  "Command list used to invoke the `go' binary."
 70  :group 'gotest-ui)
 71
 72(defcustom gotest-ui-test-args '("test" "-json")
 73  "Argument list used to run tests with JSON output."
 74  :group 'gotest-ui)
 75
 76(defcustom gotest-ui-additional-test-args '()
 77  "Additional args to pass to `go test'."
 78  :group 'gotest-ui)
 79
 80;;;; Data model:
 81
 82(defstruct (gotest-ui-section :named
 83                              (:constructor gotest-ui-section-create)
 84                              (:type vector)
 85                              (:predicate gotest-ui-section-p))
 86  title tests node)
 87
 88;;; `gotest-ui-thing' is a thing that can be under test: a
 89;;; package, or a single test.
 90
 91(defstruct gotest-ui-thing
 92  (name)
 93  (node)
 94  (expanded-p)
 95  (status)
 96  (buffer)    ; the buffer containing this test's output
 97  (elapsed)   ; a floating-point amount of seconds
 98  )
 99
100;;; `gotest-ui-test' is a single test. It contains a status and
101;;; output.
102(defstruct (gotest-ui-test (:include gotest-ui-thing)
103                           (:constructor gotest-ui--make-test-1))
104  (package)
105  (reason))
106
107(defun gotest-ui-test->= (test1 test2)
108  "Returns true if TEST1's name sorts greater than TEST2's."
109  (let ((pkg1 (gotest-ui-test-package test1))
110        (pkg2 (gotest-ui-test-package test2))
111        (name1 (or (gotest-ui-thing-name test1) ""))
112        (name2 (or (gotest-ui-thing-name test2) "")))
113    (if (string= pkg1 pkg2)
114        (string> name1 name2)
115      (string> pkg1 pkg2))))
116
117(defstruct (gotest-ui-status (:constructor gotest-ui--make-status-1))
118  (state)
119  (cmdline)
120  (dir)
121  (output)
122  (node))
123
124(cl-defun gotest-ui--make-status (ewoc cmdline dir)
125  (let ((status (gotest-ui--make-status-1 :state 'run :cmdline (s-join " " cmdline) :dir dir)))
126    (let ((node (ewoc-enter-first ewoc status)))
127      (setf (gotest-ui-status-node status) node))
128    status))
129
130(cl-defun gotest-ui--make-test (ewoc &rest args &key status package name &allow-other-keys)
131  (apply #'gotest-ui--make-test-1 :status (or status "run") args))
132
133;;; Data manipulation routines:
134
135(cl-defun gotest-ui-ensure-test (ewoc package-name base-name &key (status 'run))
136  (let* ((test-name (format "%s.%s" package-name base-name))
137         (test (gethash test-name gotest-ui--tests)))
138    (if test
139        test
140      (setf (gethash test-name gotest-ui--tests)
141            (gotest-ui--make-test ewoc :name base-name :package package-name :status status)))))
142
143(defun gotest-ui-update-status (new-state)
144  (setf (gotest-ui-status-state gotest-ui--status) new-state)
145  (ewoc-invalidate gotest-ui--ewoc (gotest-ui-status-node gotest-ui--status)))
146
147(defun gotest-ui-update-status-output (new-output)
148  (setf (gotest-ui-status-output gotest-ui--status) new-output)
149  (ewoc-invalidate gotest-ui--ewoc (gotest-ui-status-node gotest-ui--status)))
150
151(defun gotest-ui-ensure-output-buffer (thing)
152  (unless (gotest-ui-thing-buffer thing)
153    (with-current-buffer
154        (setf (gotest-ui-thing-buffer thing)
155              (generate-new-buffer (format " *%s" (gotest-ui-thing-name thing))))
156      (setq-local gotest-ui-parse-marker (point-min-marker))
157      (setq-local gotest-ui-insertion-marker (point-min-marker))
158      (set-marker-insertion-type gotest-ui-insertion-marker t)))
159  (gotest-ui-thing-buffer thing))
160
161(defun gotest-ui-mouse-open-file (event)
162  "In gotest-ui mode, open the file/line reference in another window."
163  (interactive "e")
164  (let ((window (posn-window (event-end event)))
165        (pos (posn-point (event-end event)))
166        file line)
167    (if (not (windowp window))
168        (error "No file chosen"))
169    (with-current-buffer (window-buffer window)
170      (goto-char pos)
171      (gotest-ui-open-file-at-point))))
172
173(defun gotest-ui-open-file-at-point ()
174  (interactive)
175  (let ((file (gotest-ui-get-file-for-visit))
176        (line (gotest-ui-get-line-for-visit)))
177    (unless (file-exists-p file)
178      (error "Could not open %s:%d" file line))
179    (with-current-buffer (find-file-other-window file)
180      (goto-char (point-min))
181      (when line
182        (forward-line (1- line))))))
183
184(defun gotest-ui-get-file-for-visit ()
185  (get-text-property (point) 'gotest-ui-file))
186
187(defun gotest-ui-get-line-for-visit ()
188  (string-to-number (get-text-property (point) 'gotest-ui-line)))
189
190(defun gotest-ui-file-from-gopath (package file-basename)
191  (if (or (file-name-absolute-p file-basename)
192          (string-match-p "/" file-basename))
193      file-basename
194    (let ((gopath (or (getenv "GOPATH")
195                      (expand-file-name "~/go"))))
196      (expand-file-name (concat gopath "/src/" package "/" file-basename)))))
197
198(defvar gotest-ui-click-map
199  (let ((map (make-sparse-keymap)))
200    (define-key map [mouse-2] 'gotest-ui-mouse-open-file)
201    map))
202
203(defun gotest-ui-ensure-parsed (thing)
204  (save-excursion
205    (goto-char gotest-ui-parse-marker)
206    (while (re-search-forward "\\([^ \t]+\\.go\\):\\([0-9]+\\)" gotest-ui-insertion-marker t)
207      (let* ((file-basename (match-string 1))
208             (file (gotest-ui-file-from-gopath (gotest-ui-test-package thing) file-basename)))
209        (set-text-properties (match-beginning 0) (match-end 0)
210                             `(face gotest-ui-link-face
211                                    gotest-ui-file ,file
212                                    gotest-ui-line ,(match-string 2)
213                                    keymap ,gotest-ui-click-map
214                                    follow-link t
215                                    ))))
216    (set-marker gotest-ui-parse-marker gotest-ui-insertion-marker)))
217
218(defun gotest-ui-update-thing-output (thing output)
219  (with-current-buffer (gotest-ui-ensure-output-buffer thing)
220    (goto-char gotest-ui-insertion-marker)
221    (let ((overwrites (split-string output "\r")))
222      (insert (car overwrites))
223      (dolist (segment (cdr overwrites))
224        (let ((delete-to (point)))
225          (forward-line 0)
226          (delete-region (point) delete-to))
227        (insert segment)))
228    (set-marker gotest-ui-insertion-marker (point))
229    (gotest-ui-ensure-parsed thing)))
230
231;; TODO: clean up buffers on kill
232
233;;;; Mode definition
234
235(defvar gotest-ui-mode-map
236  (let ((m (make-sparse-keymap)))
237    (suppress-keymap m)
238    ;; key bindings go here
239    (define-key m (kbd "TAB") 'gotest-ui-toggle-expanded)
240    (define-key m (kbd "g") 'gotest-ui-rerun)
241    (define-key m (kbd "RET") 'gotest-ui-open-file-at-point)
242    m))
243
244(define-derived-mode gotest-ui-mode special-mode "go test UI"
245  "Major mode for running go test with JSON output."
246  (setq truncate-lines t)
247  (setq buffer-read-only t)
248  (setq-local line-move-visual t)
249  (setq show-trailing-whitespace nil)
250  (setq list-buffers-directory default-directory)
251  (make-local-variable 'text-property-default-nonsticky)
252  (push (cons 'keymap t) text-property-default-nonsticky))
253
254
255(defun gotest-ui--clear-buffer (buffer)
256  (let ((dir default-directory))
257    (with-current-buffer buffer
258      (when (buffer-live-p gotest-ui--process-buffer)
259        (kill-buffer gotest-ui--process-buffer))
260      (kill-all-local-variables)
261      (let  ((buffer-read-only nil))
262        (erase-buffer))
263      (buffer-disable-undo)
264      (setq-local default-directory dir))))
265
266(defun gotest-ui--setup-buffer (buffer name cmdline dir)
267  (setq-local default-directory dir)
268  (setq gotest-ui--cmdline cmdline
269        gotest-ui--dir dir)
270  (let ((ewoc (ewoc-create 'gotest-ui--pp-test nil nil t))
271        (tests (make-hash-table :test #'equal)))
272    (setq gotest-ui--tests tests)
273    (setq gotest-ui--ewoc ewoc)
274    ;; Drop in the first few ewoc nodes:
275    (setq gotest-ui--status (gotest-ui--make-status ewoc cmdline dir))
276    (gotest-ui-add-section gotest-ui--ewoc 'fail "Failed Tests:")
277    (gotest-ui-add-section gotest-ui--ewoc 'run "Currently Running:")
278    (gotest-ui-add-section gotest-ui--ewoc 'skip "Skipped:")
279    (gotest-ui-add-section gotest-ui--ewoc 'pass "Passed Tests:"))
280  ;; Set up the other buffers:
281  (setq gotest-ui--stderr-process-buffer (generate-new-buffer (format " *%s (stderr)" name)))
282  (with-current-buffer gotest-ui--stderr-process-buffer
283    (setq gotest-ui--ui-buffer buffer))
284  (setq gotest-ui--process-buffer (generate-new-buffer (format " *%s" name)))
285  (with-current-buffer gotest-ui--process-buffer
286    (setq gotest-ui--ui-buffer buffer)))
287
288(defun gotest-ui-add-section (ewoc state name)
289  (let ((section (gotest-ui-section-create :title name :tests (list nil))))
290    (setf (gotest-ui-section-node section)
291          (ewoc-enter-last ewoc section))
292    (push (cons state section) gotest-ui--section-alist)))
293
294(defun gotest-ui-sort-test-into-section (test previous-state)
295  (let (invalidate-nodes)
296    (when-let ((previous-section* (and previous-state
297                                       (assoc previous-state gotest-ui--section-alist))))
298      (let ((previous-section (cdr previous-section*)))
299        (setf (gotest-ui-section-tests previous-section)
300              (delete test (gotest-ui-section-tests previous-section)))
301        (when (null (cdr (gotest-ui-section-tests previous-section)))
302          (push (gotest-ui-section-node previous-section) invalidate-nodes))))
303    ;; Drop the node from the buffer:
304    (when-let (node (gotest-ui-thing-node test))
305      (let ((buffer-read-only nil))
306        (ewoc-delete gotest-ui--ewoc node))
307      (setf (gotest-ui-thing-node test) nil))
308
309    ;; Put it in the next secion:
310    (when-let ((section* (assoc (gotest-ui-thing-status test)
311                                gotest-ui--section-alist)))
312      (let* ((section (cdr section*))
313             (insertion-cons (gotest-ui-section-tests section)))
314        (while (and (cdr insertion-cons)
315                    (gotest-ui-test->= test (cadr insertion-cons)))
316          (setq insertion-cons (cdr insertion-cons)))
317        (rplacd insertion-cons (cons test (cdr insertion-cons)))
318        (let ((insertion-node (if (car insertion-cons)
319                                  (gotest-ui-thing-node (car insertion-cons))
320                                (gotest-ui-section-node section))))
321         (setf (gotest-ui-thing-node test)
322               (ewoc-enter-after gotest-ui--ewoc insertion-node test)))
323        (when (null (cddr (gotest-ui-section-tests section)))
324          (push (gotest-ui-section-node section) invalidate-nodes))))
325    (unless (null invalidate-nodes)
326      (apply 'ewoc-invalidate gotest-ui--ewoc invalidate-nodes))
327    (gotest-ui-thing-node test)))
328
329;;;; Commands:
330
331(defun gotest-ui-toggle-expanded ()
332  "Toggle expandedness of a test/package node"
333  (interactive)
334  (let* ((node (ewoc-locate gotest-ui--ewoc (point)))
335         (data (ewoc-data node)))
336    (when (and data (gotest-ui-thing-p data))
337      (setf (gotest-ui-thing-expanded-p data)
338            (not (gotest-ui-thing-expanded-p data)))
339      (ewoc-invalidate gotest-ui--ewoc node))))
340
341(defun gotest-ui-rerun ()
342  (interactive)
343  (gotest-ui gotest-ui--cmdline :dir gotest-ui--dir))
344
345;;;; Displaying the data:
346
347(defvar-local gotest-ui--tests nil)
348(defvar-local gotest-ui--section-alist nil)
349(defvar-local gotest-ui--ewoc nil)
350(defvar-local gotest-ui--status nil)
351(defvar-local gotest-ui--process-buffer nil)
352(defvar-local gotest-ui--stderr-process-buffer nil)
353(defvar-local gotest-ui--ui-buffer nil)
354(defvar-local gotest-ui--process nil)
355(defvar-local gotest-ui--stderr-process nil)
356(defvar-local gotest-ui--cmdline nil)
357(defvar-local gotest-ui--dir nil)
358
359(cl-defun gotest-ui (cmdline &key dir)
360  (let* ((dir (or dir default-directory))
361         (name (format "*go test: %s in %s" (s-join " " cmdline) dir))
362         (buffer (get-buffer-create name)))
363    (unless (eql buffer (current-buffer))
364      (display-buffer buffer))
365    (with-current-buffer buffer
366      (let ((default-directory dir))
367        (gotest-ui--clear-buffer buffer)
368        (gotest-ui-mode)
369        (gotest-ui--setup-buffer buffer name cmdline dir))
370      (setq gotest-ui--stderr-process
371            (make-pipe-process :name (s-concat name "(stderr)")
372                               :buffer gotest-ui--stderr-process-buffer
373                               :sentinel #'gotest-ui--stderr-process-sentinel
374                               :filter #'gotest-ui-read-stderr))
375      (setq gotest-ui--process
376            (make-process :name name
377                          :buffer gotest-ui--process-buffer
378                          :sentinel #'gotest-ui--process-sentinel
379                          :filter #'gotest-ui-read-stdout
380                          :stderr gotest-ui--stderr-process
381                          :command cmdline)))))
382
383(defun gotest-ui-pp-status (status)
384  (propertize (format "%s" status)
385              'face
386              (case status
387                (fail 'gotest-ui-fail-face)
388                (skip 'gotest-ui-skip-face)
389                (pass 'gotest-ui-pass-face)
390                (otherwise 'default))))
391
392(defun gotest-ui--pp-test-output (test)
393  (with-current-buffer (gotest-ui-ensure-output-buffer test)
394    (propertize (buffer-substring (point-min) (point-max))
395                'line-prefix "\t")))
396
397(defun gotest-ui--pp-test (test)
398  (cond
399   ((gotest-ui-section-p test)
400    (unless (null (cdr (gotest-ui-section-tests test)))
401      (insert "\n" (gotest-ui-section-title test) "\n")))
402   ((gotest-ui-status-p test)
403    (insert (gotest-ui-pp-status (gotest-ui-status-state test)))
404    (insert (format " %s in %s\n\n"
405                    (gotest-ui-status-cmdline test)
406                    (gotest-ui-status-dir test)))
407    (unless (zerop (length (gotest-ui-status-output test)))
408      (insert (format "\n\n%s" (gotest-ui-status-output test)))))
409   ((gotest-ui-test-p test)
410    (let ((status (gotest-ui-thing-status test))
411          (package (gotest-ui-test-package test))
412          (name (gotest-ui-thing-name test)))
413      (insert (gotest-ui-pp-status status))
414      (insert " ")
415      (insert (if name
416                  (format "%s.%s" package name)
417                package))
418      (when-let ((elapsed (gotest-ui-thing-elapsed test)))
419        (insert (format " (%.3fs)" elapsed)))
420      (when-let ((reason (gotest-ui-test-reason test)))
421        (insert (format " [%s]" reason))))
422    (when (and (gotest-ui-thing-expanded-p test)
423               (> (length (gotest-ui--pp-test-output test)) 0))
424      (insert "\n")
425      (insert (gotest-ui--pp-test-output test)))
426    (insert "\n"))))
427
428;;;; Handling input:
429
430(defun gotest-ui--process-sentinel (proc event)
431  (let* ((process-buffer (process-buffer proc))
432         (ui-buffer (with-current-buffer process-buffer gotest-ui--ui-buffer))
433         (inhibit-quit t))
434    (with-local-quit
435      (with-current-buffer ui-buffer
436        (cond
437         ((string= event "finished\n")
438          (gotest-ui-update-status 'pass))
439         ((s-prefix-p "exited abnormally" event)
440          (gotest-ui-update-status 'fail))
441         (t
442          (gotest-ui-update-status event)))))))
443
444(defun gotest-ui--stderr-process-sentinel (proc event)
445  ;; ignore all events
446  nil)
447
448(defun gotest-ui-read-stderr (proc input)
449  (let* ((process-buffer (process-buffer proc))
450         (ui-buffer (with-current-buffer process-buffer gotest-ui--ui-buffer))
451         (inhibit-quit t))
452    (with-local-quit
453      (when (buffer-live-p process-buffer)
454        (with-current-buffer process-buffer
455          (gotest-ui-read-compiler-spew proc process-buffer ui-buffer input))))))
456
457(defun gotest-ui-read-stdout (proc input)
458  (let* ((process-buffer (process-buffer proc))
459         (ui-buffer (with-current-buffer process-buffer gotest-ui--ui-buffer))
460         (inhibit-quit t))
461    (with-local-quit
462      (when (buffer-live-p process-buffer)
463        (gotest-ui-read-json process-buffer (process-mark proc) input)))))
464
465(defun gotest-ui-read-json (process-buffer marker input)
466  (with-current-buffer process-buffer
467    (gotest-ui-read-json-1 process-buffer marker gotest-ui--ui-buffer input)))
468
469(defvar-local gotest-ui--current-failing-test nil)
470
471(defun gotest-ui-read-failing-package (ui-buffer)
472  (when (looking-at "^# \\(.*\\)$")
473    (let* ((package (match-string 1))
474           test)
475      (with-current-buffer ui-buffer
476        (setq test (gotest-ui-ensure-test gotest-ui--ewoc package nil :status 'fail))
477        (gotest-ui-maybe-expand test)
478        (gotest-ui-sort-test-into-section test nil))
479      (forward-line 1)
480      test)))
481
482(defun gotest-ui-read-compiler-spew (proc process-buffer ui-buffer input)
483  (with-current-buffer process-buffer
484    (save-excursion
485      (goto-char (point-max))
486      (insert input)
487      (goto-char (process-mark proc))
488      (while (and (/= (point-max) (line-end-position)) ; incomplete line
489                  (/= (point-max) (point)))
490        (cond
491         (gotest-ui--current-failing-test
492          (cond
493           ((looking-at "^# \\(.*\\)$")
494            (gotest-ui-read-failing-package ui-buffer))
495           (t
496            (let* ((line (buffer-substring (point) (line-end-position)))
497                   (test gotest-ui--current-failing-test))
498              (forward-line 1)
499              (set-marker (process-mark proc) (point))
500              (with-current-buffer ui-buffer
501                (gotest-ui-update-thing-output test (concat line "\n"))
502                (ewoc-invalidate gotest-ui--ewoc (gotest-ui-thing-node test)))))))
503         (t
504          (let ((test (gotest-ui-read-failing-package ui-buffer)))
505            (setq gotest-ui--current-failing-test test)
506            (set-marker (process-mark proc) (point))
507            (with-current-buffer ui-buffer
508              (ewoc-invalidate gotest-ui--ewoc (gotest-ui-thing-node test))))))))))
509
510(defun gotest-ui-read-json-1 (process-buffer marker ui-buffer input)
511  (with-current-buffer process-buffer
512    (save-excursion
513      ;; insert the chunk of output at the end
514      (goto-char (point-max))
515      (insert input)
516
517      ;; try to read the next object (which is hopefully complete now):
518      (let ((nodes
519             (cl-loop
520              for (node . continue) = (gotest-ui-read-test-event process-buffer marker ui-buffer)
521              when node collect node into nodes
522              while continue
523              finally (return nodes))))
524        (when nodes
525          (with-current-buffer ui-buffer
526            (apply #'ewoc-invalidate gotest-ui--ewoc
527                   (cl-remove-if-not (lambda (node) (marker-buffer (ewoc-location node))) (cl-remove-duplicates nodes)))))))))
528
529(defun gotest-ui-read-test-event (process-buffer marker ui-buffer)
530  (goto-char marker)
531  (when (= (point) (line-end-position))
532    (forward-line 1))
533  (case (char-after (point))
534    (?\{
535     ;; It's JSON:
536     (condition-case err
537         (let ((obj (json-read)))
538           (set-marker marker (point))
539           (with-current-buffer ui-buffer
540             (cons (gotest-ui-update-test-status obj) t)))
541       (json-error (cons nil nil))
542       (wrong-type-argument
543        (if (and (eql (cadr err) 'characterp)
544                 (eql (caddr err) :json-eof))
545            ;; This is peaceful & we can ignore it:
546            (cons nil nil)
547          (signal 'wrong-type-argument err)))))
548    (?\F
549     ;; It's a compiler error:
550     (when (looking-at "^FAIL\t\\(.*\\)\s+\\[\\([^]]+\\)\\]\n")
551       (let* ((package-name (match-string 1))
552              (reason (match-string 2))
553              test node)
554         (with-current-buffer ui-buffer
555           (setq test (gotest-ui-ensure-test gotest-ui--ewoc package-name nil :status 'fail)
556                 node (gotest-ui-thing-node test))
557           (setf (gotest-ui-test-reason test) reason)
558           (gotest-ui-sort-test-into-section test nil)
559           (gotest-ui-maybe-expand test))
560         (forward-line 1)
561         (set-marker marker (point))
562         (cons node t))))
563    (otherwise
564     ;; We're done:
565     (cons nil nil))))
566
567(defun gotest-ui-maybe-expand (test)
568  (when (memq (gotest-ui-test-status test) gotest-ui-expand-test-statuses)
569    (setf (gotest-ui-test-expanded-p test) t)))
570
571(defun gotest-ui-update-test-status (json)
572  (let-alist json
573    (let* ((action (intern .Action))
574           (test (gotest-ui-ensure-test gotest-ui--ewoc .Package .Test))
575           (previous-status (gotest-ui-thing-status test)))
576      (case action
577        (run
578         (gotest-ui-sort-test-into-section test nil))
579        (output (gotest-ui-update-thing-output test .Output))
580        (pass
581         (setf (gotest-ui-thing-status test) 'pass
582               (gotest-ui-thing-elapsed test) .Elapsed)
583         (gotest-ui-sort-test-into-section test previous-status)
584         (gotest-ui-maybe-expand test))
585        (fail
586         (setf (gotest-ui-thing-status test) 'fail
587               (gotest-ui-thing-elapsed test) .Elapsed)
588         (gotest-ui-sort-test-into-section test previous-status)
589         (gotest-ui-maybe-expand test))
590        (skip
591         (setf (gotest-ui-thing-status test) 'skip
592               (gotest-ui-thing-elapsed test) .Elapsed)
593         (gotest-ui-sort-test-into-section test previous-status)
594         (gotest-ui-maybe-expand test))
595        (otherwise
596         (setq test nil)))
597      (when test (gotest-ui-thing-node test)))))
598
599;;;; Commands for go-mode:
600
601(defun gotest-ui--command-line (&rest cmdline)
602  (append gotest-ui-test-binary gotest-ui-test-args gotest-ui-additional-test-args
603          cmdline))
604
605;;;###autoload
606(defun gotest-ui-current-test ()
607  "Launch go test with the test that (point) is in."
608  (interactive)
609  (cl-destructuring-bind (test-suite test-name) (go-test--get-current-test-info)
610    (let ((test-flag (if (> (length test-suite) 0) "-m" "-run")))
611      (when test-name
612        (gotest-ui (gotest-ui--command-line test-flag (s-concat test-name "$") "."))))))
613
614;;;###autoload
615(defun gotest-ui-current-file ()
616  "Launch go test on the current buffer file."
617  (interactive)
618  (let* ((data (go-test--get-current-file-testing-data))
619         (run-flag (s-concat "-run=" data "$")))
620    (gotest-ui (gotest-ui--command-line run-flag "."))))
621
622;;;###autoload
623(defun gotest-ui-current-project ()
624  "Launch go test on the current buffer's project."
625  (interactive)
626  (let ((default-directory (projectile-project-root)))
627    (gotest-ui (gotest-ui--command-line "./..."))))
628
629(provide 'gotest-ui)
630
631;;; gotest-ui.el ends here