|
|
;;; alchemist-test-mode.el --- Minor mode for Elixir test files.
|
|
|
|
|
|
;; Copyright © 2015 Samuel Tonini
|
|
|
|
|
|
;; Author: Samuel Tonini <tonini.samuel@gmail.com
|
|
|
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
|
|
;; 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
;; Minor mode for Elixir test files.
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
(require 'dash)
|
|
|
(require 'alchemist-project)
|
|
|
|
|
|
(defgroup alchemist-test-mode nil
|
|
|
"Minor mode for Elixir ExUnit files."
|
|
|
:prefix "alchemist-test-mode-"
|
|
|
:group 'alchemist)
|
|
|
|
|
|
;; Variables
|
|
|
|
|
|
(defcustom alchemist-test-mode-highlight-tests t
|
|
|
"Non-nil means that specific functions for testing will
|
|
|
be highlighted with more significant font faces."
|
|
|
:type 'boolean
|
|
|
:group 'alchemist-test-mode)
|
|
|
|
|
|
(defcustom alchemist-test-display-compilation-output nil
|
|
|
"if Non-nil, compilation informations will be displayed
|
|
|
in the test report buffer."
|
|
|
:type 'boolean
|
|
|
:group 'alchemist-test-mode)
|
|
|
|
|
|
(defcustom alchemist-test-status-modeline t
|
|
|
"if Non-nil, the face of local `mode-name' variable will change with test run status.
|
|
|
|
|
|
For example, when `alchemist-mix-test' fails, the `mode-name' will be
|
|
|
formatted with the `alchemist-test--failed-face' face, to symbolize failing tests."
|
|
|
:type 'boolean
|
|
|
:group 'alchemist-test)
|
|
|
|
|
|
(defcustom alchemist-test-ask-about-save t
|
|
|
"Non-nil means 'alchemist-test-excute` asks which buffers to save before running.
|
|
|
Otherwise, it saves all modified buffers without asking."
|
|
|
:type 'boolean
|
|
|
:group 'alchemist-test)
|
|
|
|
|
|
(defvar alchemist-test--last-run-status "")
|
|
|
|
|
|
(defconst alchemist-test-report-buffer-name "*alchemist test report*"
|
|
|
"Name of the test report buffer.")
|
|
|
|
|
|
(defconst alchemist-test-report-process-name "alchemist-test-process"
|
|
|
"Name of the test report process.")
|
|
|
|
|
|
(defconst alchemist-test--failing-files-regex
|
|
|
"\\( [0-9]+).+\n\s+\\)\\([-A-Za-z0-9./_]+:[0-9]+\\)$")
|
|
|
(defconst alchemist-test--stacktrace-files-regex
|
|
|
"\\( \\)\\([-A-Za-z0-9./_]+:[0-9]+\\).*")
|
|
|
|
|
|
;; Faces
|
|
|
|
|
|
(defface alchemist-test--test-file-and-location-face
|
|
|
'((t (:inherit font-lock-variable-name-face :weight bold)))
|
|
|
"Face for the file where the failed test are."
|
|
|
:group 'alchemist-test)
|
|
|
|
|
|
(defface alchemist-test--stacktrace-file-and-location-face
|
|
|
'((t (:inherit font-lock-keyword-face :weight bold)))
|
|
|
"Face for the stacktrace files."
|
|
|
:group 'alchemist-test)
|
|
|
|
|
|
(defface alchemist-test--success-face
|
|
|
'((t (:inherit font-lock-variable-name-face :bold t :background "darkgreen" :foreground "white")))
|
|
|
"Face for successful compilation run."
|
|
|
:group 'alchemist-test)
|
|
|
|
|
|
(defface alchemist-test--failed-face
|
|
|
'((t (:inherit font-lock-variable-name-face :bold t :background "red" :foreground "white")))
|
|
|
"Face for failed compilation run."
|
|
|
:group 'alchemist-test)
|
|
|
|
|
|
(defvar alchemist-test--mode-name-face 'mode-line)
|
|
|
|
|
|
(defvar alchemist-test-at-point #'alchemist-mix-test-at-point)
|
|
|
(defvar alchemist-test-this-buffer #'alchemist-mix-test-this-buffer)
|
|
|
(defvar alchemist-test #'alchemist-mix-test)
|
|
|
(defvar alchemist-test-file #'alchemist-mix-test-file)
|
|
|
(defvar alchemist-test-jump-to-previous-test #'alchemist-test-mode-jump-to-previous-test)
|
|
|
(defvar alchemist-test-jump-to-next-test #'alchemist-test-mode-jump-to-next-test)
|
|
|
(defvar alchemist-test-list-tests #'alchemist-test-mode-list-tests)
|
|
|
|
|
|
(defvar alchemist-test-report-mode-map
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
(define-key map "q" #'quit-window)
|
|
|
(define-key map "t" #'toggle-truncate-lines)
|
|
|
(define-key map "r" #'alchemist-mix-rerun-last-test)
|
|
|
(define-key map (kbd "M-n") #'alchemist-test-next-result)
|
|
|
(define-key map (kbd "M-p") #'alchemist-test-previous-result)
|
|
|
(define-key map (kbd "M-N") #'alchemist-test-next-stacktrace-file)
|
|
|
(define-key map (kbd "M-P") #'alchemist-test-previous-stacktrace-file)
|
|
|
(define-key map (kbd "C-c C-k") #'alchemist-report-interrupt-current-process)
|
|
|
map))
|
|
|
|
|
|
(defvar alchemist-test-mode-map
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
(define-key map (kbd "C-c , s") alchemist-test-at-point)
|
|
|
(define-key map (kbd "C-c , v") alchemist-test-this-buffer)
|
|
|
(define-key map (kbd "C-c , a") alchemist-test)
|
|
|
(define-key map (kbd "C-c , f") alchemist-test-file)
|
|
|
(define-key map (kbd "C-c , p") alchemist-test-jump-to-previous-test)
|
|
|
(define-key map (kbd "C-c , n") alchemist-test-jump-to-next-test)
|
|
|
(define-key map (kbd "C-c , l") alchemist-test-list-tests)
|
|
|
map)
|
|
|
"Keymap for `alchemist-test-mode'.")
|
|
|
|
|
|
(defconst alchemist-test-mode--test-regex
|
|
|
(let ((whitespace-opt "[[:space:]]*")
|
|
|
(whitespace "[[:space:]]+"))
|
|
|
(concat "\\(^" whitespace-opt "test" whitespace "\\(?10:.+\\)" whitespace "do" whitespace-opt "$"
|
|
|
"\\|"
|
|
|
whitespace " [0-9]+) test .+\\)")))
|
|
|
|
|
|
;; Private functions
|
|
|
|
|
|
(defun alchemist-test--set-modeline-color (status)
|
|
|
(setq alchemist-test--mode-name-face
|
|
|
(if (string-prefix-p "finished" status)
|
|
|
'alchemist-test--success-face
|
|
|
'alchemist-test--failed-face)))
|
|
|
|
|
|
(defun alchemist-test--render-report (buffer)
|
|
|
(with-current-buffer buffer
|
|
|
(let ((inhibit-read-only t))
|
|
|
(alchemist-test--render-files))))
|
|
|
|
|
|
(defun alchemist-test--render-files ()
|
|
|
(alchemist-test--render-test-failing-files)
|
|
|
(alchemist-test--render-stacktrace-files))
|
|
|
|
|
|
(defun alchemist-test--render-test-failing-files ()
|
|
|
(alchemist-test--render-file alchemist-test--failing-files-regex
|
|
|
'alchemist-test--test-file-and-location-face))
|
|
|
|
|
|
(defun alchemist-test--render-stacktrace-files ()
|
|
|
(alchemist-test--render-file alchemist-test--stacktrace-files-regex
|
|
|
'alchemist-test--stacktrace-file-and-location-face))
|
|
|
|
|
|
(defun alchemist-test--render-file (regex face)
|
|
|
(save-excursion
|
|
|
(goto-char (point-min))
|
|
|
(while (re-search-forward regex nil t)
|
|
|
(let ((file (buffer-substring-no-properties (match-beginning 2) (match-end 2))))
|
|
|
(goto-char (match-beginning 2))
|
|
|
(replace-match "" nil nil nil 2)
|
|
|
(insert-text-button file
|
|
|
'face face
|
|
|
'file file
|
|
|
'follow-link t
|
|
|
'action #'alchemist-test--open-file
|
|
|
'help-echo "visit the source location")))))
|
|
|
|
|
|
(defun alchemist-test--open-file (button)
|
|
|
(save-match-data
|
|
|
(string-match "\\([-A-Za-z0-9./_]+\\):\\([0-9]+\\)" (button-get button 'file))
|
|
|
(let* ((file-with-line (button-get button 'file))
|
|
|
(file (substring-no-properties file-with-line (match-beginning 1) (match-end 1)))
|
|
|
(line (string-to-number (substring-no-properties file-with-line (match-beginning 2) (match-end 2))))
|
|
|
(file-path (if (file-exists-p file)
|
|
|
file
|
|
|
(expand-file-name (concat (alchemist-project-root) file)))))
|
|
|
(with-current-buffer (find-file-other-window file-path)
|
|
|
(goto-char (point-min))
|
|
|
(forward-line (- line 1))))))
|
|
|
|
|
|
(defun alchemist-test--handle-exit (status buffer)
|
|
|
(when alchemist-test-status-modeline
|
|
|
(alchemist-test--set-modeline-color status))
|
|
|
(with-current-buffer buffer
|
|
|
(let ((inhibit-read-only t))
|
|
|
(alchemist-test--render-files))))
|
|
|
|
|
|
(defun alchemist-test-mode--buffer-contains-tests-p ()
|
|
|
"Return nil if the current buffer contains no tests, non-nil if it does."
|
|
|
(alchemist-utils-occur-in-buffer-p (current-buffer) alchemist-test-mode--test-regex))
|
|
|
|
|
|
(defun alchemist-test-mode--tests-in-buffer ()
|
|
|
"Return an alist of tests in this buffer.
|
|
|
|
|
|
The keys in the list are the test names (e.g., the string passed to the test/2
|
|
|
macro) while the values are the position at which the test matched."
|
|
|
(save-match-data
|
|
|
(save-excursion
|
|
|
(goto-char (point-min))
|
|
|
(let ((tests '()))
|
|
|
(while (re-search-forward alchemist-test-mode--test-regex nil t)
|
|
|
(let* ((position (car (match-data)))
|
|
|
(matched-string (match-string 10)))
|
|
|
(set-text-properties 0 (length matched-string) nil matched-string)
|
|
|
(add-to-list 'tests (cons matched-string position) t)))
|
|
|
tests))))
|
|
|
|
|
|
(defun alchemist-test-mode--highlight-syntax ()
|
|
|
(if alchemist-test-mode-highlight-tests
|
|
|
(font-lock-add-keywords nil
|
|
|
'(("^\s+\\(test\\)\s+" 1
|
|
|
font-lock-variable-name-face t)
|
|
|
("^\s+\\(assert[_a-z]*\\|refute[_a-z]*\\|flunk\\)\s+" 1
|
|
|
font-lock-type-face t)
|
|
|
("^\s+\\(assert[_a-z]*\\|refute[_a-z]*\\|flunk\\)\(" 1
|
|
|
font-lock-type-face t)))))
|
|
|
|
|
|
;; Public functions
|
|
|
|
|
|
(define-derived-mode alchemist-test-report-mode fundamental-mode "Alchemist Test Report"
|
|
|
"Major mode for presenting Elixir test results.
|
|
|
|
|
|
\\{alchemist-test-report-mode-map}"
|
|
|
(setq buffer-read-only t)
|
|
|
(setq-local truncate-lines t)
|
|
|
(setq-local electric-indent-chars nil))
|
|
|
|
|
|
(defun alchemist-test-save-buffers ()
|
|
|
"Save some modified file-visiting buffers."
|
|
|
(save-some-buffers (not alchemist-test-ask-about-save) nil))
|
|
|
|
|
|
(defun alchemist-test-clean-compilation-output (output)
|
|
|
(if (not alchemist-test-display-compilation-output)
|
|
|
(with-temp-buffer
|
|
|
(insert output)
|
|
|
(delete-matching-lines "^Compiled .+" (point-min) (point-max))
|
|
|
(delete-matching-lines "^Generated .+" (point-min) (point-max))
|
|
|
(buffer-substring-no-properties (point-min) (point-max)))
|
|
|
output))
|
|
|
|
|
|
(defun alchemist-test-execute (command-list)
|
|
|
(message "Testing...")
|
|
|
(let* ((command (mapconcat 'concat (-flatten command-list) " ")))
|
|
|
(alchemist-test-save-buffers)
|
|
|
(alchemist-report-run command
|
|
|
alchemist-test-report-process-name
|
|
|
alchemist-test-report-buffer-name
|
|
|
'alchemist-test-report-mode
|
|
|
#'alchemist-test--handle-exit)))
|
|
|
|
|
|
(defun alchemist-test-initialize-modeline ()
|
|
|
"Initialize the mode-line face."
|
|
|
(when alchemist-test-status-modeline
|
|
|
(setq mode-name
|
|
|
'(:eval (propertize "Elixir" 'face alchemist-test--mode-name-face)))))
|
|
|
|
|
|
(defun alchemist-test-reset-modeline ()
|
|
|
"Reset the current mode-line face to default."
|
|
|
(setq mode-name "Elixir"))
|
|
|
|
|
|
(defun alchemist-test-mode-jump-to-next-test ()
|
|
|
"Jump to the next ExUnit test. If there are no tests after the current
|
|
|
position, jump to the first test in the buffer. Do nothing if there are no tests
|
|
|
in this buffer."
|
|
|
(interactive)
|
|
|
(alchemist-utils-jump-to-next-matching-line alchemist-test-mode--test-regex 'back-to-indentation))
|
|
|
|
|
|
(defun alchemist-test-mode-jump-to-previous-test ()
|
|
|
"Jump to the previous ExUnit test. If there are no tests before the current
|
|
|
position, jump to the last test in the buffer. Do nothing if there are no tests
|
|
|
in this buffer."
|
|
|
(interactive)
|
|
|
(alchemist-utils-jump-to-previous-matching-line alchemist-test-mode--test-regex 'back-to-indentation))
|
|
|
|
|
|
(defun alchemist-test-next-result ()
|
|
|
"Jump to the next error in the test report.
|
|
|
|
|
|
If there are no error after the current position,
|
|
|
jump to the first error in the test report.
|
|
|
Do nothing if there are no error in this test report."
|
|
|
(interactive)
|
|
|
(alchemist-utils-jump-to-next-matching-line alchemist-test--failing-files-regex
|
|
|
'back-to-indentation))
|
|
|
|
|
|
(defun alchemist-test-previous-result ()
|
|
|
"Jump to the previous error in the test report.
|
|
|
|
|
|
If there are no error before the current position,
|
|
|
jump to the first error in the test report.
|
|
|
Do nothing if there are no error in this test report."
|
|
|
(interactive)
|
|
|
(alchemist-utils-jump-to-previous-matching-line alchemist-test--failing-files-regex
|
|
|
#'(lambda ()
|
|
|
(forward-line 1)
|
|
|
(back-to-indentation))))
|
|
|
|
|
|
(defun alchemist-test-next-stacktrace-file ()
|
|
|
"Jump to the next stacktrace file in the test report.
|
|
|
|
|
|
If there are no stacktrace file after the current position,
|
|
|
jump to the first stacktrace file in the test report.
|
|
|
Do nothing if there are no stacktrace file in this test report."
|
|
|
(interactive)
|
|
|
(alchemist-utils-jump-to-next-matching-line alchemist-test--stacktrace-files-regex
|
|
|
'back-to-indentation))
|
|
|
|
|
|
(defun alchemist-test-previous-stacktrace-file ()
|
|
|
"Jump to the previous stacktrace file in the test report.
|
|
|
|
|
|
If there are no stacktrace file before the current position,
|
|
|
jump to the first stacktrace file in the test report.
|
|
|
Do nothing if there are no stacktrace file in this test report."
|
|
|
(interactive)
|
|
|
(alchemist-utils-jump-to-previous-matching-line alchemist-test--stacktrace-files-regex
|
|
|
'back-to-indentation))
|
|
|
|
|
|
(defun alchemist-test-mode-list-tests ()
|
|
|
"List ExUnit tests (calls to the test/2 macro) in the current buffer and jump
|
|
|
to the selected one."
|
|
|
(interactive)
|
|
|
(let* ((tests (alchemist-test-mode--tests-in-buffer))
|
|
|
(selected (completing-read "Test: " tests))
|
|
|
(position (cdr (assoc selected tests))))
|
|
|
(goto-char position)
|
|
|
(back-to-indentation)))
|
|
|
|
|
|
(defun alchemist-test-toggle-test-report-display ()
|
|
|
"Toggle between display or hidding `alchemist-test-report-buffer-name' buffer."
|
|
|
(interactive)
|
|
|
(let* ((buffer (get-buffer alchemist-test-report-buffer-name))
|
|
|
(window (get-buffer-window buffer)))
|
|
|
(if buffer
|
|
|
(if window
|
|
|
(quit-window nil window)
|
|
|
(display-buffer buffer))
|
|
|
(message "No Alchemist test report buffer exists."))))
|
|
|
|
|
|
;;;###autoload
|
|
|
(define-minor-mode alchemist-test-mode
|
|
|
"Minor mode for Elixir ExUnit files.
|
|
|
|
|
|
The following commands are available:
|
|
|
|
|
|
\\{alchemist-test-mode-map}"
|
|
|
:lighter ""
|
|
|
:keymap alchemist-test-mode-map
|
|
|
:group 'alchemist
|
|
|
(when alchemist-test-mode
|
|
|
(alchemist-test-mode--highlight-syntax)))
|
|
|
|
|
|
;;;###autoload
|
|
|
(defun alchemist-test-enable-mode ()
|
|
|
(if (alchemist-utils-test-file-p)
|
|
|
(alchemist-test-mode)))
|
|
|
|
|
|
;;;###autoload
|
|
|
(dolist (hook '(alchemist-mode-hook))
|
|
|
(add-hook hook 'alchemist-test-enable-mode))
|
|
|
|
|
|
(provide 'alchemist-test-mode)
|
|
|
|
|
|
;;; alchemist-test-mode.el ends here
|