config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

gotest.el (19622B)


      1 ;;; gotest.el --- Launch GO unit tests
      2 
      3 ;; Author: Nicolas Lamirault <nicolas.lamirault@gmail.com>
      4 ;; URL: https://github.com/nlamirault/gotest.el
      5 ;; Version: 0.14.0
      6 ;; Keywords: languages, go, tests
      7 
      8 ;; Package-Requires: ((emacs "24.3") (s "1.11.0") (f "0.19.0"))
      9 
     10 ;; Copyright (C) 2014, 2015, 2016, 2017 Nicolas Lamirault <nicolas.lamirault@gmail.com>
     11 
     12 ;; This program is free software; you can redistribute it and/or
     13 ;; modify it under the terms of the GNU General Public License
     14 ;; as published by the Free Software Foundation; either version 2
     15 ;; of the License, or (at your option) any later version.
     16 
     17 ;; This program is distributed in the hope that it will be useful,
     18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     20 ;; GNU General Public License for more details.
     21 
     22 ;; You should have received a copy of the GNU General Public License
     23 ;; along with this program; if not, write to the Free Software
     24 ;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
     25 ;; 02110-1301, USA.
     26 
     27 ;;; Commentary:
     28 
     29 ;;; Code:
     30 
     31 (require 'compile)
     32 
     33 (require 's)
     34 (require 'f)
     35 (require 'cl-lib)
     36 
     37 (defgroup gotest nil
     38   "GoTest utility"
     39   :group 'go)
     40 
     41 (defcustom go-test-verbose nil
     42   "Display debugging information during test execution."
     43   :type 'boolean
     44   :group 'gotest)
     45 
     46 (defvar-local go-test-go-command nil
     47   "The 'go' command for 'go test' that should be used instead of `go'.
     48 
     49 This variable is buffer-local, set using .dir-locals.el for example.")
     50 
     51 (defvar-local go-run-go-command nil
     52   "The 'go' command for 'go run' that should be used instead of `go'.
     53 
     54 This variable is buffer-local, set using .dir-locals.el for example.")
     55 
     56 (defcustom go-test-gb-command "gb"
     57   "The 'gb' command.
     58 A project based build tool for the Go programming language.
     59 See https://getgb.io."
     60   :type 'string
     61   :group 'gotest)
     62 
     63 (defvar-local go-test-args nil
     64   "Arguments to pass to go test.
     65   This variable is buffer-local, set using .dir-locals.el for example.")
     66 
     67 (defvar-local go-run-args nil
     68   "Arguments to pass to go run.
     69   This variable is buffer-local, set using .dir-locals.el for example.")
     70 
     71 (defvar go-test-history nil
     72   "History list for go test command arguments.")
     73 
     74 (defvar go-run-history nil
     75   "History list for go run command arguments.")
     76 
     77 
     78 ;; Faces
     79 ;; -----------
     80 
     81 (defface go-test--ok-face
     82   '((t (:foreground "#00ff00")))
     83   "Ok face"
     84   :group 'go-test)
     85 
     86 (defface go-test--error-face
     87   '((t (:foreground "#FF0000")))
     88   "Error face"
     89   :group 'go-test)
     90 
     91 (defface go-test--warning-face
     92   '((t (:foreground "#eeee00")))
     93   "Warning face"
     94   :group 'go-test)
     95 
     96 (defface go-test--pointer-face
     97   '((t (:foreground "#ff00ff")))
     98   "Pointer face"
     99   :group 'go-test)
    100 
    101 (defface go-test--standard-face
    102   '((t (:foreground "#ffa500")))
    103   "Standard face"
    104   :group 'go-test)
    105 
    106 
    107 
    108 ;; go-test mode
    109 ;; -----------------
    110 
    111 
    112 (defvar go-test-mode-map
    113   (nconc (make-sparse-keymap) compilation-mode-map)
    114   "Keymap for Go test major mode.")
    115 
    116 (defvar go-test-last-command nil
    117   "Command used last for repeating.")
    118 
    119 (defvar go-test-additional-arguments-function nil
    120   "Function that can be used to programatically add arguments.
    121 
    122 The function will receive the suite and test name as
    123 arguments in that order.")
    124 
    125 
    126 (defconst go-test-font-lock-keywords
    127   '(("error\\:" . 'go-test--error-face)
    128     ("testing: warning:.*" . 'go-test--warning-face)
    129     ("^\s*\\^\\~*\s*$" . 'go-test--pointer-face)
    130     ("^\s*Compilation.*" . 'go-test--standard-face)
    131     ("^\s*gb test.*" . 'go-test--standard-face)
    132     ("^\s*go test.*" . 'go-test--standard-face)
    133     ("^\s*Updating.*" . 'go-test--standard-face)
    134     (".*undefined.*" . 'go-test--warning-face)
    135     ("^\s*FATAL.*" . 'go-test--error-face)
    136     ("^\s*FAIL.*" . 'go-test--error-face)
    137     ("^\s*--- FATAL.*" . 'go-test--error-face)
    138     ("^\s*--- FAIL:.*" . 'go-test--error-face)
    139     ("^\s*=== RUN.*" . 'go-test--ok-face)
    140     ("^\s*--- PASS.*" . 'go-test--ok-face)
    141     ("^\s*PASS.*" . 'go-test--ok-face)
    142     ("^\s*ok.*" . 'go-test--ok-face)
    143     )
    144   "Minimal highlighting expressions for go-test mode.")
    145 
    146 (define-derived-mode go-test-mode compilation-mode "Go-Test."
    147   "Major mode for the Go-Test compilation buffer."
    148   (use-local-map go-test-mode-map)
    149   (setq major-mode 'go-test-mode)
    150   (setq mode-name "Go-Test")
    151   (setq-local truncate-lines t)
    152   ;;(run-hooks 'go-test-mode-hook)
    153   (font-lock-add-keywords nil go-test-font-lock-keywords))
    154 
    155 (defun go-test--compilation-name (mode-name)
    156   "Name of the go test.  MODE-NAME is unused."
    157   "*Go Test*")
    158 
    159 (defun go-test--finished-sentinel (process event)
    160   "Execute after PROCESS return and EVENT is 'finished'."
    161   (compilation-sentinel process event)
    162   (when (equal event "finished\n")
    163     (message "Go Test finished.")))
    164 
    165 
    166 (defvar go-test--current-test-cache nil
    167   "Store the suite and test of the last execution.")
    168 
    169 
    170 (defvar go-test-regexp-prefix
    171   "^[[:space:]]*func[[:space:]]\\(([^()]*?)\\)?[[:space:]]*\\("
    172   "The prefix of the go-test regular expression.")
    173 
    174 (defvar go-test-regexp-suffix
    175   "[[:alpha:][:digit:]_]*\\)("
    176   "The suffix of the go-test regular expression.")
    177 
    178 
    179 (defvar go-test-compilation-error-regexp-alist-alist
    180   '((go-test-testing . ("^\t\\([[:alnum:]-_/.]+\\.go\\):\\([0-9]+\\): .*$" 1 2)) ;; stdlib package testing
    181     (go-test-testify . ("^\tLocation:\t\\([[:alnum:]-_/.]+\\.go\\):\\([0-9]+\\)$" 1 2)) ;; testify package assert
    182     (go-test-gopanic . ("^\t\\([[:alnum:]-_/.]+\\.go\\):\\([0-9]+\\) \\+0x\\(?:[0-9a-f]+\\)" 1 2)) ;; panic()
    183     (go-test-compile . ("^\\([[:alnum:]-_/.]+\\.go\\):\\([0-9]+\\):\\(?:\\([0-9]+\\):\\)? .*$" 1 2 3)) ;; go compiler
    184     (go-test-linkage . ("^\\([[:alnum:]-_/.]+\\.go\\):\\([0-9]+\\): undefined: .*$" 1 2))) ;; go linker
    185   "Alist of values for `go-test-compilation-error-regexp-alist'.
    186 See also: `compilation-error-regexp-alist-alist'.")
    187 
    188 (defcustom go-test-compilation-error-regexp-alist
    189   '(go-test-testing
    190     go-test-testify
    191     go-test-gopanic
    192     go-test-compile
    193     go-test-linkage)
    194   "Alist that specifies how to match errors in go test output.
    195 The default set of regexps should only match the output of the
    196 standard `go' tool, which includes compile, link, stacktrace (panic)
    197 and package testing.  There is support for matching error output
    198 from other packages, such as `testify'.
    199 
    200 Only file names ending in `.go' will be matched by default.
    201 
    202 Instead of an alist element, you can use a symbol, which is
    203 looked up in `go-testcompilation-error-regexp-alist-alist'.
    204 
    205 See also: `compilation-error-regexp-alist'."
    206   :type '(repeat (choice (symbol :tag "Predefined symbol")
    207                          (sexp :tag "Error specification")))
    208   :group 'gotest)
    209 
    210 
    211 ;; Commands
    212 ;; -----------
    213 
    214 
    215 (defun go-test--get-program (args &optional env)
    216   "Return the command to launch unit test.
    217 `ARGS' corresponds to go command line arguments.
    218 When `ENV' concatenate before command."
    219   (let ((command-args (s-concat (or go-test-go-command "go") " test " args)))
    220     (if env
    221         (s-concat env " " command-args)
    222       command-args)))
    223 
    224 
    225 (defun go-test--gb-get-program (args)
    226   "Return the command to launch unit test using GB..
    227 `ARGS' corresponds to go command line arguments."
    228   (s-concat go-test-gb-command " test " args))
    229 
    230 
    231 (defun go-test--get-arguments (defaults history)
    232   "Get optional arguments for go test or go run.
    233 DEFAULTS will be used when there is no prefix argument.
    234 When a prefix argument of '- is given, use the most recent HISTORY item.
    235 When single prefix argument is given, prompt for arguments using HISTORY.
    236 When double prefix argument is given, run command in compilation buffer with
    237 `comint-mode' enabled.
    238 When triple prefix argument is given, prompt for arguments using HISTORY and
    239 run command in compilation buffer `comint-mode' enabled.
    240 When a numeric prefix argument is provided, it is used as the -count flag."
    241   (pcase current-prefix-arg
    242     (`nil defaults)
    243     ((pred integerp) (s-concat (format "-count=%d " current-prefix-arg) defaults))
    244     ((or `- `(16)) (car (symbol-value history)))
    245     ((or `(4) `(64)) (let* ((name (nth 1 (s-split "-" (symbol-name history))))
    246                             (prompt (s-concat "go " name " args: ")))
    247                        (read-shell-command prompt defaults history)))))
    248 
    249 
    250 (defun go-test--get-root-directory()
    251   "Return the root directory to run tests."
    252   (let ((filename (buffer-file-name)))
    253     (when filename
    254       (file-truename (or (locate-dominating-file filename "Makefile")
    255                          "./")))))
    256 
    257 
    258 (defun go-test--get-current-buffer ()
    259   "Return the test buffer for the current `buffer-file-name'.
    260 If `buffer-file-name' ends with `_test.go', `current-buffer' is returned.
    261 Otherwise, `ff-get-other-file' is used to find the test buffer.
    262 For example, if the current buffer is `foo.go', the buffer for
    263 `foo_test.go' is returned."
    264   (if (string-match "_test\.go$" buffer-file-name)
    265       (current-buffer)
    266     (let ((ff-always-try-to-create nil)
    267 	  (filename (ff-get-other-file)))
    268       (when filename
    269 	(find-file-noselect filename)))))
    270 
    271 
    272 (defun go-test--get-current-data (prefix)
    273   "Return the current data: test, example or benchmark.
    274 `PREFIX' defines token to place cursor."
    275   (let ((start (point))
    276         name)
    277     (save-excursion
    278       (end-of-line)
    279       (unless (and
    280                (search-backward-regexp
    281                 (s-concat "^[[:space:]]*func[[:space:]]*" prefix) nil t)
    282                (save-excursion (go-end-of-defun) (< start (point))))
    283         (error "Unable to find data"))
    284       (save-excursion
    285         (search-forward prefix)
    286         (setq name (thing-at-point 'symbol t))))
    287     name))
    288 
    289 (defun go-test--get-current-test-info ()
    290   "Return the current test and suite name."
    291   (save-excursion
    292     (end-of-line)
    293     (if (search-backward-regexp
    294          (format "%s\\(Test\\|Example\\)%s" go-test-regexp-prefix go-test-regexp-suffix)
    295          nil t)
    296         (let ((suite-match (match-string-no-properties 1))
    297               (test-match (match-string-no-properties 2)))
    298           (list
    299            (go-test--get-suite-name-from-match-string suite-match) test-match))
    300       (error "Unable to find a test"))))
    301 
    302 (defun go-test--get-suite-name-from-match-string (the-match-string)
    303   (if (> (length the-match-string) 0)
    304       (progn (string-match "([^()]*?\\*\\([^()]*?\\))" the-match-string)
    305              (s-trim (match-string-no-properties 1 the-match-string)))
    306     ""))
    307 
    308 (defun go-test--get-current-test ()
    309   "Return the current test name."
    310   (cadr (go-test--get-current-test-info)))
    311 
    312 (defun go-test--get-current-benchmark ()
    313   "Return the current benchmark name."
    314   (go-test--get-current-data "Benchmark"))
    315 
    316 
    317 (defun go-test--get-current-example ()
    318   "Return the current example name."
    319   (go-test--get-current-data "Example"))
    320 
    321 
    322 (defun go-test--get-current-file-data (prefix)
    323   "Generate regexp to match test, benchmark or example the current buffer.
    324 `PREFIX' defines token to place cursor."
    325   (let ((buffer (go-test--get-current-buffer)))
    326     (when buffer
    327       (with-current-buffer buffer
    328 	(save-excursion
    329 	  (goto-char (point-min))
    330 	  (when (string-match "\.go$" buffer-file-name)
    331             (let ((regex
    332                    (s-concat "^[[:space:]]*func[[:space:]]*\\(" prefix "[^(]+\\)"))
    333                   result)
    334 	      (while
    335                   (re-search-forward regex nil t)
    336 		(let ((data (buffer-substring-no-properties
    337                              (match-beginning 1) (match-end 1))))
    338                   (setq result (append result (list data)))))
    339 	      (mapconcat 'identity result "|"))))))))
    340 
    341 
    342 (defun go-test--get-current-file-tests ()
    343   "Generate regexp to match test in the current buffer."
    344   (go-test--get-current-file-data "Test"))
    345 
    346 
    347 (defun go-test--get-current-file-benchmarks ()
    348   "Generate regexp to match benchmark in the current buffer."
    349   (go-test--get-current-file-data "Benchmark"))
    350 
    351 
    352 (defun go-test--get-current-file-examples ()
    353   "Generate regexp to match example in the current buffer."
    354   (go-test--get-current-file-data "Example"))
    355 
    356 
    357 (defun go-test--get-current-file-testing-data ()
    358   "Regex with unit test and|or examples."
    359   (let ((tests (go-test--get-current-file-tests))
    360         (examples (go-test--get-current-file-examples)))
    361     (cond ((and (> (length tests) 0)
    362                 (> (length examples) 0))
    363            (s-concat tests "|" examples))
    364           ((= (length tests) 0)
    365            examples)
    366           ((= (length examples) 0)
    367            tests))))
    368 
    369 
    370 (defun go-test--arguments (args)
    371   "Make the go test command argurments using `ARGS'."
    372   (let ((opts args))
    373     (when go-test-args
    374       (setq opts (s-concat go-test-args " " opts)))
    375     (when go-test-verbose
    376       (setq opts (s-concat "-v " opts)))
    377     (go-test--get-arguments opts 'go-test-history)))
    378 
    379 
    380 ;; (defun go-test-compilation-hook (p)
    381 ;;   "Add compilation hooks."
    382 ;;   (set (make-local-variable 'compilation-error-regexp-alist-alist)
    383 ;;        go-test-compilation-error-regexp-alist-alist)
    384 ;;   (set (make-local-variable 'compilation-error-regexp-alist)
    385 ;;        go-test-compilation-error-regexp-alist))
    386 
    387 
    388 ;; (defun go-test-run (args)
    389 ;;   (add-hook 'compilation-start-hook 'go-test-compilation-hook)
    390 ;;   (compile (go-test--get-program (go-test--arguments args)))
    391 ;;   (remove-hook 'compilation-start-hook 'go-test-compilation-hook))
    392 
    393 (defun go-test--go-test (args &optional env)
    394   "Start the go test command using `ARGS'."
    395   (let ((buffer "*Go Test*")) ; (concat "*go-test " args "*")))
    396     (go-test--cleanup buffer)
    397     (compilation-start (go-test--get-program (go-test--arguments args) env)
    398                        'go-test-mode
    399                        'go-test--compilation-name)
    400     (with-current-buffer "*Go Test*"
    401       (rename-buffer buffer))
    402     (set-process-sentinel (get-buffer-process buffer) 'go-test--finished-sentinel)))
    403 
    404 (defun go-test--go-run-get-program (args)
    405   "Return the command to launch go run.
    406 `ARGS' corresponds to go command line arguments."
    407   (s-concat (or go-run-go-command "go") " run " args))
    408 
    409 (defun go-test--go-run-arguments ()
    410   "Arguments for go run."
    411   (let ((opts (if go-run-args
    412                   (s-concat (shell-quote-argument (buffer-file-name)) " " go-run-args)
    413                 (shell-quote-argument (buffer-file-name)))))
    414     (go-test--get-arguments opts 'go-run-history)))
    415 
    416 
    417 ;; (defun gb-test-run (args)
    418 ;;   "Test using GB.
    419 ;; `ARGS' corresponds to command line arguments."
    420 ;;   (add-hook 'compilation-start-hook 'go-test-compilation-hook)
    421 ;;   (compile (go-test--gb-get-program args))
    422 ;;   (remove-hook 'compilation-start-hook 'go-test-compilation-hook))
    423 
    424 
    425 (defun go-test--is-gb-project ()
    426   "Check if project use GB or not."
    427   (let* ((go-test-gb-command (executable-find go-test-gb-command))
    428          (default-directory (if go-test-gb-command (go-test--get-root-directory))))
    429     (and go-test-gb-command
    430          default-directory
    431          (f-dir? "src")
    432          (f-exists? "vendor/manifest"))))
    433 
    434 (defun go-test--cleanup (buffer)
    435   "Clean up the old go-test process BUFFER when a similar process is run."
    436   (when (get-buffer buffer)
    437     (when (get-buffer-process (get-buffer buffer))
    438       (delete-process buffer))
    439     (with-current-buffer buffer
    440       (setq buffer-read-only nil)
    441       (erase-buffer))))
    442 
    443 (defun go-test--gb-start (args)
    444   "Start the GB test command using `ARGS'."
    445   (let ((buffer "*Go Test*")) ;(concat "*go-test " args "*")))
    446     (go-test--cleanup buffer)
    447     (compilation-start (go-test--gb-get-program (go-test--arguments args))
    448                        'go-test-mode
    449                        'go-test--compilation-name)
    450     (with-current-buffer "*Go Test*"
    451       (rename-buffer buffer))
    452     (set-process-sentinel (get-buffer-process buffer) 'go-test--finished-sentinel)))
    453 
    454 
    455 (defun go-test--gb-find-package ()
    456   "Find package of current-file."
    457   (let* ((dir (s-concat (go-test--get-root-directory) "src/"))
    458          (filename (buffer-file-name))
    459          (pkg (f-filename filename)))
    460     (s-replace-all (list (cons dir "") (cons pkg "")) filename)))
    461 
    462 ; API
    463 ;; ----
    464 
    465 
    466 ;; Unit tests
    467 ;; ----------------------
    468 
    469 ;;;###autoload
    470 (defun go-test-current-test-cache ()
    471   "Repeat the previous current test execution."
    472   (interactive)
    473   (go-test-current-test 'last))
    474 
    475 ;;;###autoload
    476 (defun go-test-current-test (&optional last)
    477   "Launch go test on the current test."
    478   (interactive)
    479   (unless (string-equal (symbol-name last) "last")
    480     (setq go-test--current-test-cache (go-test--get-current-test-info)))
    481   (when go-test--current-test-cache
    482     (cl-destructuring-bind (test-suite test-name) go-test--current-test-cache
    483       (let ((test-flag (if (> (length test-suite) 0) "-testify.m " "-run "))
    484             (additional-arguments (if go-test-additional-arguments-function
    485                                       (funcall go-test-additional-arguments-function
    486                                                test-suite test-name) "")))
    487         (when test-name
    488           (if (go-test--is-gb-project)
    489               (go-test--gb-start (s-concat "-test.v=true -test.run=" test-name "\\$ ."))
    490             (go-test--go-test (s-concat test-flag test-name "\\$ . " additional-arguments))))))))
    491 
    492 
    493 ;;;###autoload
    494 (defun go-test-current-file ()
    495   "Launch go test on the current buffer file."
    496   (interactive)
    497   (let ((data (go-test--get-current-file-testing-data)))
    498     (if (go-test--is-gb-project)
    499         (go-test--gb-start (s-concat "-test.v=true -test.run='" data "'"))
    500       (go-test--go-test (s-concat "-run='" data "' .")))))
    501 
    502 
    503 ;;;###autoload
    504 (defun go-test-current-project ()
    505   "Launch go test on the current project."
    506   (interactive)
    507   (if (go-test--is-gb-project)
    508       (go-test--gb-start "all -test.v=true")
    509     (let ((packages (cl-remove-if (lambda (s) (s-contains? "/vendor/" s))
    510                                   (s-split "\n"
    511                                            (shell-command-to-string "go list ./...")))))
    512       (go-test--go-test (s-join " " packages)))))
    513 
    514 
    515 
    516 ;; Benchmarks
    517 ;; ----------------------
    518 
    519 
    520 ;;;###autoload
    521 (defun go-test-current-benchmark ()
    522   "Launch go benchmark on current benchmark."
    523   (interactive)
    524   (let ((benchmark-name (go-test--get-current-benchmark)))
    525     (when benchmark-name
    526       (go-test--go-test (s-concat "-run ^NOTHING -bench " benchmark-name "\\$")))))
    527 
    528 
    529 ;;;###autoload
    530 (defun go-test-current-file-benchmarks ()
    531   "Launch go benchmark on current file benchmarks."
    532   (interactive)
    533   (let ((benchmarks (go-test--get-current-file-benchmarks)))
    534     (go-test--go-test (s-concat "-run ^NOTHING -bench '" benchmarks "'"))))
    535 
    536 
    537 ;;;###autoload
    538 (defun go-test-current-project-benchmarks ()
    539   "Launch go benchmark on current project."
    540   (interactive)
    541   (go-test--go-test (s-concat "-run ^NOTHING -bench .")))
    542 
    543 
    544 ;; Coverage
    545 ;; -------------
    546 
    547 
    548 ;;;###autoload
    549 (defun go-test-current-coverage ()
    550   "Launch go test coverage on the current project."
    551   (interactive)
    552   (if (go-test--is-gb-project)
    553       (let* ((package (go-test--gb-find-package))
    554              (root-dir (go-test--get-root-directory))
    555              (gopath (s-concat "env GOPATH=" root-dir ":" root-dir "vendor")))
    556         (go-test--go-test (s-concat "-cover " package) gopath))
    557     (let ((args (s-concat
    558                  "--coverprofile="
    559                  (expand-file-name
    560                   (read-file-name "Coverage file" nil "cover.out")) " ./.")))
    561       (go-test--go-test args))))
    562 
    563 
    564 ;;;###autoload
    565 (defun go-run (&optional args)
    566   "Launch go run on current buffer file."
    567   (interactive)
    568   ;;(add-hook 'compilation-start-hook 'go-test-compilation-hook)
    569   (compile (go-test--go-run-get-program (go-test--go-run-arguments))
    570            (pcase current-prefix-arg
    571              ((or `(16) `(64)) t)))
    572   ;;(remove-hook 'compilation-start-hook 'go-test-compilation-hook))
    573   )
    574 
    575 
    576 
    577 (provide 'gotest)
    578 ;;; gotest.el ends here