PageRenderTime 47ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/packs/clojure/lib/clojure-mode/clojure-test-mode.el

https://github.com/designerdada/dotemacs
Emacs Lisp | 554 lines | 377 code | 80 blank | 97 comment | 11 complexity | 5afe47b21c986d7dde58df5cf1008e58 MD5 | raw file
  1. ;;; clojure-test-mode.el --- Minor mode for Clojure tests
  2. ;; Copyright © 2009-2011 Phil Hagelberg
  3. ;; Author: Phil Hagelberg <technomancy@gmail.com>
  4. ;; URL: http://emacswiki.org/cgi-bin/wiki/ClojureTestMode
  5. ;; Version: 2.1.0
  6. ;; Keywords: languages, lisp, test
  7. ;; Package-Requires: ((clojure-mode "1.7") (nrepl "0.1.7"))
  8. ;; This file is not part of GNU Emacs.
  9. ;;; Commentary:
  10. ;; This file provides support for running Clojure tests (using the
  11. ;; clojure.test framework) via nrepl.el and seeing feedback in the test
  12. ;; buffer about which tests failed or errored.
  13. ;;; Usage:
  14. ;; Once you have an nrepl session active, you can run the tests in the
  15. ;; current buffer with C-c C-,. Failing tests and errors will be
  16. ;; highlighted using overlays. To clear the overlays, use C-c k.
  17. ;; You can jump between implementation and test files with <kbd>C-c C-t</kbd> if
  18. ;; your project is laid out in a way that clojure-test-mode expects. Your
  19. ;; project root should have a `src/` directory containing files that correspond
  20. ;; to their namespace. It should also have a `test/` directory containing files
  21. ;; that correspond to their namespace, and the test namespaces should mirror the
  22. ;; implementation namespaces with the addition of "-test" as the suffix to the
  23. ;; last segment of the namespace.
  24. ;; So `my.project.frob` would be found in `src/my/project/frob.clj` and its
  25. ;; tests would be in `test/my/project/frob_test.clj` in the
  26. ;; `my.project.frob-test` namespace.
  27. ;; This behavior can also be overridden by setting `clojure-test-for-fn` and
  28. ;; `clojure-test-implementation-for-fn` with functions of your choosing.
  29. ;; `clojure-test-for-fn` takes an implementation namespace and returns the full
  30. ;; path of the test file. `clojure-test-implementation-for-fn` takes a test
  31. ;; namespace and returns the full path for the implementation file.
  32. ;;; History:
  33. ;; 1.0: 2009-03-12
  34. ;; * Initial Release
  35. ;; 1.1: 2009-04-28
  36. ;; * Fix to work with latest version of test-is. (circa Clojure 1.0)
  37. ;; 1.2: 2009-05-19
  38. ;; * Add clojure-test-jump-to-(test|implementation).
  39. ;; 1.3: 2009-11-10
  40. ;; * Update to use clojure.test instead of clojure.contrib.test-is.
  41. ;; * Fix bug suppressing test report output in repl.
  42. ;; 1.4: 2010-05-13
  43. ;; * Fix jump-to-test
  44. ;; * Update to work with Clojure 1.2.
  45. ;; * Added next/prev problem.
  46. ;; * Depend upon slime, not swank-clojure.
  47. ;; * Don't move the mark when activating.
  48. ;; 1.5: 2010-09-16
  49. ;; * Allow customization of clojure-test-ns-segment-position.
  50. ;; * Fixes for Clojure 1.2.
  51. ;; * Check for active slime connection.
  52. ;; * Fix test toggling with negative segment-position.
  53. ;; 1.5.1: 2010-11-27
  54. ;; * Add marker between each test run.
  55. ;; 1.5.2: 2011-03-11
  56. ;; * Make clojure-test-run-tests force reload. Requires swank-clojure 1.3.0.
  57. ;; 1.5.3 2011-03-14
  58. ;; * Fix clojure-test-run-test to use fixtures.
  59. ;; 1.5.4 2011-03-16
  60. ;; * Fix clojure-test-run-tests to wait until tests are reloaded.
  61. ;; 1.5.5 2011-04-08
  62. ;; * Fix coloring/reporting
  63. ;; * Don't trigger slime-connected-hook.
  64. ;; 1.5.6 2011-06-15
  65. ;; * Remove heinous clojure.test/report monkeypatch.
  66. ;; 1.6.0 2011-11-06
  67. ;; * Compatibility with Clojure 1.3.
  68. ;; * Support narrowing.
  69. ;; * Fix a bug in clojure-test-mode-test-one-in-ns.
  70. ;; 2.0.0 2012-12-29
  71. ;; * Replace slime with nrepl.el
  72. ;;; TODO:
  73. ;; * Prefix arg to jump-to-impl should open in other window
  74. ;; * Put Testing indicator in modeline while tests are running
  75. ;; * Integrate with M-x next-error
  76. ;; * Error messages need line number.
  77. ;; * Currently show-message needs point to be on the line with the
  78. ;; "is" invocation; this could be cleaned up.
  79. ;;; Code:
  80. (require 'cl)
  81. (require 'clojure-mode)
  82. (require 'which-func)
  83. (require 'nrepl)
  84. (declare-function nrepl-repl-buffer "nrepl.el")
  85. (declare-function nrepl-make-response-handler "nrepl.el")
  86. (declare-function nrepl-send-string "nrepl.el")
  87. (declare-function nrepl-current-ns "nrepl.el")
  88. (declare-function nrepl-current-tooling-session "nrepl.el")
  89. (declare-function nrepl-current-connection-buffer "nrepl.el")
  90. ;; Faces
  91. (defface clojure-test-failure-face
  92. '((((class color) (background light))
  93. :background "orange red") ;; TODO: Hard to read strings over this.
  94. (((class color) (background dark))
  95. :background "firebrick"))
  96. "Face for failures in Clojure tests."
  97. :group 'clojure-test-mode)
  98. (defface clojure-test-error-face
  99. '((((class color) (background light))
  100. :background "orange1")
  101. (((class color) (background dark))
  102. :background "orange4"))
  103. "Face for errors in Clojure tests."
  104. :group 'clojure-test-mode)
  105. (defface clojure-test-success-face
  106. '((((class color) (background light))
  107. :foreground "black"
  108. :background "green")
  109. (((class color) (background dark))
  110. :foreground "black"
  111. :background "green"))
  112. "Face for success in Clojure tests."
  113. :group 'clojure-test-mode)
  114. ;; Counts
  115. (defvar clojure-test-count 0)
  116. (defvar clojure-test-failure-count 0)
  117. (defvar clojure-test-error-count 0)
  118. ;; Consts
  119. (defconst clojure-test-ignore-results
  120. '(:end-test-ns :begin-test-var :end-test-var)
  121. "Results from test-is that we don't use")
  122. ;; Support Functions
  123. (defun clojure-test-nrepl-connected-p ()
  124. (nrepl-current-connection-buffer))
  125. (defun clojure-test-make-handler (callback)
  126. (lexical-let ((buffer (current-buffer))
  127. (callback callback))
  128. (nrepl-make-response-handler buffer
  129. (lambda (buffer value)
  130. (funcall callback buffer value))
  131. (lambda (buffer value)
  132. (nrepl-emit-interactive-output value))
  133. (lambda (buffer err)
  134. (nrepl-emit-interactive-output err))
  135. '())))
  136. (defun clojure-test-eval (string &optional handler)
  137. (nrepl-send-string string
  138. (clojure-test-make-handler (or handler #'identity))
  139. (or (nrepl-current-ns) "user")
  140. (nrepl-current-tooling-session)))
  141. (defun clojure-test-load-reporting ()
  142. "Redefine the test-is report function to store results in metadata."
  143. (when (clojure-test-nrepl-connected-p)
  144. (nrepl-send-string-sync
  145. "(ns clojure.test.mode
  146. (:use [clojure.test :only [file-position *testing-vars* *test-out*
  147. join-fixtures *report-counters* do-report
  148. test-var *initial-report-counters*]]
  149. [clojure.pprint :only [pprint]]))
  150. (def #^{:dynamic true} *clojure-test-mode-out* nil)
  151. (def fail-events #{:fail :error})
  152. (defn report [event]
  153. (if-let [current-test (last clojure.test/*testing-vars*)]
  154. (alter-meta! current-test
  155. assoc :status (conj (:status (meta current-test))
  156. [(:type event)
  157. (:message event)
  158. (when (fail-events (:type event))
  159. (str (:expected event)))
  160. (when (fail-events (:type event))
  161. (str (:actual event)))
  162. (case (:type event)
  163. :fail (with-out-str (pprint (:actual event)))
  164. :error (with-out-str
  165. (clojure.stacktrace/print-cause-trace
  166. (:actual event)))
  167. nil)
  168. (if (and (= (:major *clojure-version*) 1)
  169. (< (:minor *clojure-version*) 2))
  170. ((file-position 2) 1)
  171. (if (= (:type event) :error)
  172. ((file-position 3) 1)
  173. (:line event)))])))
  174. (binding [*test-out* (or *clojure-test-mode-out* *out*)]
  175. ((.getRawRoot #'clojure.test/report) event)))
  176. (defn clojure-test-mode-test-one-var [test-ns test-name]
  177. (let [v (ns-resolve test-ns test-name)
  178. once-fixture-fn (join-fixtures (::once-fixtures (meta (find-ns test-ns))))
  179. each-fixture-fn (join-fixtures (::each-fixtures (meta (find-ns test-ns))))]
  180. (once-fixture-fn
  181. (fn []
  182. (when (:test (meta v))
  183. (each-fixture-fn (fn [] (test-var v))))))))
  184. ;; adapted from test-ns
  185. (defn clojure-test-mode-test-one-in-ns [ns test-name]
  186. (binding [*report-counters* (ref *initial-report-counters*)]
  187. (let [ns-obj (the-ns ns)]
  188. (do-report {:type :begin-test-ns, :ns ns-obj})
  189. ;; If the namespace has a test-ns-hook function, call that:
  190. (if-let [v (find-var (symbol (str (ns-name ns-obj)) \"test-ns-hook\"))]
  191. ((var-get v))
  192. ;; Otherwise, just test every var in the namespace.
  193. (clojure-test-mode-test-one-var ns test-name))
  194. (do-report {:type :end-test-ns, :ns ns-obj}))
  195. (do-report (assoc @*report-counters* :type :summary))))"
  196. (or (nrepl-current-ns) "user")
  197. (nrepl-current-tooling-session))))
  198. (defun clojure-test-get-results (buffer result)
  199. (with-current-buffer buffer
  200. (clojure-test-eval
  201. (concat "(map #(cons (str (:name (meta %)))
  202. (:status (meta %))) (vals (ns-interns '"
  203. (clojure-find-ns) ")))")
  204. #'clojure-test-extract-results)))
  205. (defun clojure-test-extract-results (buffer results)
  206. (with-current-buffer buffer
  207. (let ((result-vars (read results)))
  208. (mapc #'clojure-test-extract-result result-vars)
  209. (clojure-test-echo-results))))
  210. (defun clojure-test-extract-result (result)
  211. "Parse the result from a single test. May contain multiple is blocks."
  212. (dolist (is-result (rest result))
  213. (unless (member (aref is-result 0) clojure-test-ignore-results)
  214. (incf clojure-test-count)
  215. (destructuring-bind (event msg expected actual pp-actual line)
  216. (coerce is-result 'list)
  217. (if (equal :fail event)
  218. (progn (incf clojure-test-failure-count)
  219. (clojure-test-highlight-problem
  220. line event (format "Expected %s, got %s" expected actual)
  221. pp-actual))
  222. (when (equal :error event)
  223. (incf clojure-test-error-count)
  224. (clojure-test-highlight-problem
  225. line event actual pp-actual))))))
  226. (clojure-test-echo-results))
  227. (defun clojure-test-echo-results ()
  228. (message
  229. (propertize
  230. (format "Ran %s tests. %s failures, %s errors."
  231. clojure-test-count clojure-test-failure-count
  232. clojure-test-error-count)
  233. 'face
  234. (cond ((not (= clojure-test-error-count 0)) 'clojure-test-error-face)
  235. ((not (= clojure-test-failure-count 0)) 'clojure-test-failure-face)
  236. (t 'clojure-test-success-face)))))
  237. (defun clojure-test-highlight-problem (line event message pp-actual)
  238. (save-excursion
  239. (goto-char (point-min))
  240. (forward-line (1- line))
  241. (let ((beg (point)))
  242. (end-of-line)
  243. (let ((overlay (make-overlay beg (point))))
  244. (overlay-put overlay 'face (if (equal event :fail)
  245. 'clojure-test-failure-face
  246. 'clojure-test-error-face))
  247. (overlay-put overlay 'help-echo message)
  248. (overlay-put overlay 'message message)
  249. (overlay-put overlay 'actual pp-actual)))))
  250. ;; Problem navigation
  251. (defun clojure-test-find-next-problem (here)
  252. "Go to the next position with an overlay message.
  253. Retuns the problem overlay if such a position is found, otherwise nil."
  254. (let ((current-overlays (overlays-at here))
  255. (next-overlays (next-overlay-change here)))
  256. (while (and (not (equal next-overlays (point-max)))
  257. (or
  258. (not (overlays-at next-overlays))
  259. (equal (overlays-at next-overlays)
  260. current-overlays)))
  261. (setq next-overlays (next-overlay-change next-overlays)))
  262. (if (not (equal next-overlays (point-max)))
  263. (overlay-start (car (overlays-at next-overlays))))))
  264. (defun clojure-test-find-previous-problem (here)
  265. "Go to the next position with the `clojure-test-problem' text property.
  266. Retuns the problem overlay if such a position is found, otherwise nil."
  267. (let ((current-overlays (overlays-at here))
  268. (previous-overlays (previous-overlay-change here)))
  269. (while (and (not (equal previous-overlays (point-min)))
  270. (or
  271. (not (overlays-at previous-overlays))
  272. (equal (overlays-at previous-overlays)
  273. current-overlays)))
  274. (setq previous-overlays (previous-overlay-change previous-overlays)))
  275. (if (not (equal previous-overlays (point-min)))
  276. (overlay-start (car (overlays-at previous-overlays))))))
  277. ;; File navigation
  278. (defun clojure-test-implementation-for (namespace)
  279. "Returns the path of the src file for the given test namespace."
  280. (let* ((namespace (clojure-underscores-for-hyphens namespace))
  281. (segments (split-string namespace "\\."))
  282. (namespace-end (split-string (car (last segments)) "_"))
  283. (namespace-end (mapconcat 'identity (butlast namespace-end 1) "_"))
  284. (impl-segments (append (butlast segments 1) (list namespace-end))))
  285. (format "%s/src/%s.clj"
  286. (locate-dominating-file buffer-file-name "src/")
  287. (mapconcat 'identity impl-segments "/"))))
  288. (defvar clojure-test-implementation-for-fn 'clojure-test-implementation-for
  289. "Var pointing to the function that will return the full path of the
  290. Clojure src file for the given test namespace.")
  291. ;; Commands
  292. (defun clojure-test-run-tests ()
  293. "Run all the tests in the current namespace."
  294. (interactive)
  295. (save-some-buffers nil (lambda () (equal major-mode 'clojure-mode)))
  296. (message "Testing...")
  297. (if (not (clojure-in-tests-p))
  298. (nrepl-load-file (buffer-file-name)))
  299. (save-window-excursion
  300. (if (not (clojure-in-tests-p))
  301. (clojure-jump-to-test))
  302. (clojure-test-clear)
  303. (clojure-test-eval (format "(binding [clojure.test/report clojure.test.mode/report]
  304. (clojure.test/run-tests '%s))"
  305. (clojure-find-ns))
  306. #'clojure-test-get-results)))
  307. (defun clojure-test-run-test ()
  308. "Run the test at point."
  309. (interactive)
  310. (save-some-buffers nil (lambda () (equal major-mode 'clojure-mode)))
  311. (imenu--make-index-alist)
  312. (clojure-test-clear)
  313. (let* ((f (which-function))
  314. (test-name (if (listp f) (first f) f)))
  315. (clojure-test-eval (format "(binding [clojure.test/report clojure.test.mode/report]
  316. (load-file \"%s\")
  317. (clojure.test.mode/clojure-test-mode-test-one-in-ns '%s '%s)
  318. (cons (:name (meta (var %s))) (:status (meta (var %s)))))"
  319. (buffer-file-name) (clojure-find-ns)
  320. test-name test-name test-name)
  321. (lambda (buffer result-str)
  322. (with-current-buffer buffer
  323. (let ((result (read result-str)))
  324. (if (cdr result)
  325. (clojure-test-extract-result result)
  326. (message "Not in a test."))))))))
  327. (defun clojure-test-show-result ()
  328. "Show the result of the test under point."
  329. (interactive)
  330. (let ((overlay (find-if (lambda (o) (overlay-get o 'message))
  331. (overlays-at (point)))))
  332. (if overlay
  333. (message (replace-regexp-in-string "%" "%%"
  334. (overlay-get overlay 'message))))))
  335. (defun clojure-test-pprint-result ()
  336. "Show the result of the test under point."
  337. (interactive)
  338. (let ((overlay (find-if (lambda (o) (overlay-get o 'message))
  339. (overlays-at (point)))))
  340. (when overlay
  341. (with-current-buffer (generate-new-buffer " *test-output*")
  342. (buffer-disable-undo)
  343. (insert (overlay-get overlay 'actual))
  344. (switch-to-buffer-other-window (current-buffer))))))
  345. ;;; ediff results
  346. (defvar clojure-test-ediff-buffers nil)
  347. (defun clojure-test-ediff-cleanup ()
  348. "A function for ediff-cleanup-hook, to cleanup the temporary ediff buffers"
  349. (mapc (lambda (b) (when (get-buffer b) (kill-buffer b)))
  350. clojure-test-ediff-buffers))
  351. (defun clojure-test-ediff-result ()
  352. "Show the result of the test under point as an ediff"
  353. (interactive)
  354. (let ((overlay (find-if (lambda (o) (overlay-get o 'message))
  355. (overlays-at (point)))))
  356. (if overlay
  357. (let* ((m (overlay-get overlay 'actual)))
  358. (let ((tmp-buffer (generate-new-buffer " *clojure-test-mode-tmp*"))
  359. (exp-buffer (generate-new-buffer " *expected*"))
  360. (act-buffer (generate-new-buffer " *actual*")))
  361. (with-current-buffer tmp-buffer
  362. (insert m)
  363. (clojure-mode)
  364. (goto-char (point-min))
  365. (forward-char) ; skip a paren
  366. (paredit-splice-sexp) ; splice
  367. (lexical-let ((p (point))) ; delete "not"
  368. (forward-sexp)
  369. (delete-region p (point)))
  370. (lexical-let ((p (point))) ; splice next sexp
  371. (forward-sexp)
  372. (backward-sexp)
  373. (forward-char)
  374. (paredit-splice-sexp))
  375. (lexical-let ((p (point))) ; delete operator
  376. (forward-sexp)
  377. (delete-region p (point)))
  378. (lexical-let ((p (point))) ; copy first expr
  379. (forward-sexp)
  380. (lexical-let ((p2 (point)))
  381. (with-current-buffer exp-buffer
  382. (insert-buffer-substring-as-yank tmp-buffer (+ 1 p) p2))))
  383. (lexical-let ((p (point))) ; copy next expr
  384. (forward-sexp)
  385. (lexical-let ((p2 (point)))
  386. (with-current-buffer act-buffer
  387. (insert-buffer-substring-as-yank tmp-buffer (+ 1 p) p2)))))
  388. (kill-buffer tmp-buffer)
  389. (setq clojure-test-ediff-buffers
  390. (list (buffer-name exp-buffer) (buffer-name act-buffer)))
  391. (ediff-buffers
  392. (buffer-name exp-buffer) (buffer-name act-buffer)))))))
  393. (defun clojure-test-load-current-buffer ()
  394. (let ((command (format "(clojure.core/load-file \"%s\")\n(in-ns '%s)"
  395. (buffer-file-name)
  396. (clojure-find-ns))))
  397. (nrepl-send-string-sync command)))
  398. (defun clojure-test-clear (&optional callback)
  399. "Remove overlays and clear stored results."
  400. (interactive)
  401. (remove-overlays)
  402. (setq clojure-test-count 0
  403. clojure-test-failure-count 0
  404. clojure-test-error-count 0)
  405. (clojure-test-load-current-buffer))
  406. (defun clojure-test-next-problem ()
  407. "Go to and describe the next test problem in the buffer."
  408. (interactive)
  409. (let* ((here (point))
  410. (problem (clojure-test-find-next-problem here)))
  411. (if problem
  412. (goto-char problem)
  413. (goto-char here)
  414. (message "No next problem."))))
  415. (defun clojure-test-previous-problem ()
  416. "Go to and describe the previous compiler problem in the buffer."
  417. (interactive)
  418. (let* ((here (point))
  419. (problem (clojure-test-find-previous-problem here)))
  420. (if problem
  421. (goto-char problem)
  422. (goto-char here)
  423. (message "No previous problem."))))
  424. (defun clojure-test-jump-to-implementation ()
  425. "Jump from test file to implementation."
  426. (interactive)
  427. (find-file (funcall clojure-test-implementation-for-fn
  428. (clojure-find-package))))
  429. (defvar clojure-test-mode-map
  430. (let ((map (make-sparse-keymap)))
  431. (define-key map (kbd "C-c C-,") 'clojure-test-run-tests)
  432. (define-key map (kbd "C-c ,") 'clojure-test-run-tests)
  433. (define-key map (kbd "C-c M-,") 'clojure-test-run-test)
  434. (define-key map (kbd "C-c C-'") 'clojure-test-ediff-result)
  435. (define-key map (kbd "C-c M-'") 'clojure-test-pprint-result)
  436. (define-key map (kbd "C-c '") 'clojure-test-show-result)
  437. (define-key map (kbd "C-c k") 'clojure-test-clear)
  438. (define-key map (kbd "C-c C-t") 'clojure-jump-between-tests-and-code)
  439. (define-key map (kbd "M-p") 'clojure-test-previous-problem)
  440. (define-key map (kbd "M-n") 'clojure-test-next-problem)
  441. map)
  442. "Keymap for Clojure test mode.")
  443. ;;;###autoload
  444. (define-minor-mode clojure-test-mode
  445. "A minor mode for running Clojure tests.
  446. \\{clojure-test-mode-map}"
  447. nil " Test" clojure-test-mode-map
  448. (when (clojure-test-nrepl-connected-p)
  449. (clojure-test-load-reporting)))
  450. (add-hook 'nrepl-connected-hook 'clojure-test-load-reporting)
  451. (defconst clojure-test-regex
  452. (rx "clojure.test"))
  453. ;;;###autoload
  454. (defun clojure-find-clojure-test ()
  455. (let ((regexp clojure-test-regex))
  456. (save-restriction
  457. (save-excursion
  458. (save-match-data
  459. (goto-char (point-min))
  460. (when (re-search-forward regexp nil t)
  461. (match-string-no-properties 0)))))))
  462. ;;;###autoload
  463. (progn
  464. (defun clojure-test-maybe-enable ()
  465. "Enable clojure-test-mode if the current buffer contains a \"clojure.test\" bit in it."
  466. (when (clojure-find-clojure-test)
  467. (save-window-excursion
  468. (clojure-test-mode t))))
  469. (add-hook 'clojure-mode-hook 'clojure-test-maybe-enable))
  470. (provide 'clojure-test-mode)
  471. ;; Local Variables:
  472. ;; byte-compile-warnings: (not cl-functions)
  473. ;; End:
  474. ;;; clojure-test-mode.el ends here