aboutsummaryrefslogtreecommitdiff
path: root/modules/coverage-elisp.el
blob: 0c640c50616303f3a61c468211e5d55b96044bde (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
;;; coverage-elisp.el --- Elisp coverage backend for coverage-core -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>

;;; Commentary:
;;
;; Layer: 2 (Core UX).
;; Category: C/P.
;; Load shape: eager.
;; Eager reason: registers the elisp coverage backend after coverage-core; cheap.
;; Top-level side effects: backend registration via use-package.
;; Runtime requires: coverage-core.
;; Direct test load: yes.
;;
;; Registers the `elisp' coverage backend with `coverage-core'.
;;
;; Detection: a project root with a Makefile / Eask / Cask plus any
;; .el files (either at root or under modules/).  Loose on purpose —
;; `.dir-locals.el' can pin the backend explicitly when the heuristic
;; guesses wrong.
;;
;; :run invokes `make coverage' in a compilation buffer.  On success,
;; the callback is invoked with the LCOV path; on failure, the buffer
;; stays visible for the user to inspect.
;;
;; :report-path resolves to `<project-root>/.coverage/simplecov.json',
;; which matches the path the Makefile's coverage target writes to.
;; The simplecov JSON format is used because undercover's `:merge-report'
;; support only covers simplecov, not LCOV.

;;; Code:

(require 'coverage-core)

(use-package undercover
  :defer t)

(defconst cj/--coverage-elisp-report-relative-path
  ".coverage/simplecov.json"
  "Project-relative path to the simplecov JSON produced by `make coverage'.")

(defun cj/--coverage-elisp-project-root (&optional root)
  "Return ROOT or fall back to projectile's root or `default-directory'."
  (or root
	  (and (fboundp 'projectile-project-root)
		   (projectile-project-root))
	  default-directory))

(defun cj/--coverage-elisp-detect (root)
  "Return non-nil if ROOT looks like an Elisp project.
The heuristic needs both (a) a Makefile, Eask, or Cask at ROOT and
\(b) any .el files at ROOT or under modules/."
  (and (or (file-exists-p (expand-file-name "Makefile" root))
		   (file-exists-p (expand-file-name "Eask" root))
		   (file-exists-p (expand-file-name "Cask" root)))
	   (or (file-expand-wildcards (expand-file-name "modules/*.el" root))
		   (file-expand-wildcards (expand-file-name "*.el" root)))))

(defun cj/--coverage-elisp-report-path (&optional root)
  "Return the absolute path to the simplecov JSON file for ROOT."
  (expand-file-name cj/--coverage-elisp-report-relative-path
					(cj/--coverage-elisp-project-root root)))

(defun cj/--coverage-elisp-run (callback)
  "Run `make coverage' asynchronously.
CALLBACK is invoked with the report path when the build finishes
successfully.  On failure, no callback is invoked and the compilation
buffer stays visible so the user can read the error."
  (let* ((default-directory (cj/--coverage-elisp-project-root))
		 (buffer (compilation-start "make coverage" nil
									(lambda (_mode) "*coverage-run*"))))
	(with-current-buffer buffer
	  (add-hook 'compilation-finish-functions
				(lambda (_buf status)
				  (when (string-match-p "^finished" status)
					(funcall callback (cj/--coverage-elisp-report-path))))
				nil t))))

(cj/coverage-register-backend
 (list :name 'elisp
	   :detect #'cj/--coverage-elisp-detect
	   :run #'cj/--coverage-elisp-run
	   :report-path #'cj/--coverage-elisp-report-path))

(provide 'coverage-elisp)
;;; coverage-elisp.el ends here