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