aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/coverage-core.el86
-rw-r--r--modules/coverage-elisp.el24
2 files changed, 58 insertions, 52 deletions
diff --git a/modules/coverage-core.el b/modules/coverage-core.el
index f1e8ae88..2209b2f7 100644
--- a/modules/coverage-core.el
+++ b/modules/coverage-core.el
@@ -16,7 +16,7 @@
(defvar cj/coverage-backends nil
"Registry of coverage backends in priority order.
-Each entry is a plist with at least :name, :detect, :run, and :lcov-path.
+Each entry is a plist with at least :name, :detect, :run, and :report-path.
Use `cj/coverage-register-backend' to add or replace an entry.")
(defvar-local cj/coverage-backend nil
@@ -26,7 +26,7 @@ function in registration order. Typically set buffer-locally via
`.dir-locals.el' to pin a specific backend.")
(defun cj/coverage-register-backend (backend)
- "Register BACKEND, a plist with :name, :detect, :run, :lcov-path.
+ "Register BACKEND, a plist with :name, :detect, :run, :report-path.
Appends to `cj/coverage-backends' at the end, or replaces the existing
entry with the same :name in its current position."
(let ((name (plist-get backend :name)))
@@ -65,46 +65,50 @@ Returns the backend plist, or nil when no backend matches."
(funcall (plist-get backend :detect) root))
cj/coverage-backends))))
-(defun cj/--coverage-parse-lcov (file)
- "Parse FILE as LCOV and return a hash table of covered lines.
+(defun cj/--coverage-parse-simplecov (file)
+ "Parse FILE as a simplecov JSON report and return covered lines per file.
Keys are source-file paths (strings). Values are hash tables whose
-keys are line numbers (integers) that had a hit count greater than
-zero. Only the SF, DA, and end_of_record fields are read; other
-LCOV fields are ignored. Malformed DA lines are skipped silently.
-Signals `user-error' if FILE does not exist."
+keys are line numbers (integers) with a hit count greater than zero.
+Lines marked nil (not executable) or 0 (executable but not hit) are
+excluded.
+
+Simplecov JSON structure is:
+ { <test-name>: { \"coverage\": { <path>: [null | 0 | int, ...] } } }
+
+When the JSON contains multiple top-level test-name keys, coverage
+data is unioned across them; useful for files produced with undercover's
+`:merge-report t' option that accumulate runs under a shared key, and
+also for defensive handling of unexpected multi-key shapes.
+
+Signals `user-error' if FILE does not exist or contains malformed JSON."
(unless (file-exists-p file)
- (user-error "LCOV file not found: %s" file))
- (let ((result (make-hash-table :test 'equal))
- (current-file nil)
- (current-lines nil))
- (with-temp-buffer
- (insert-file-contents file)
- (goto-char (point-min))
- (while (not (eobp))
- (let ((line (buffer-substring-no-properties
- (line-beginning-position) (line-end-position))))
- (cond
- ((string-prefix-p "SF:" line)
- (setq current-file (substring line 3))
- (setq current-lines (make-hash-table :test 'eql)))
- ((string-prefix-p "DA:" line)
- (when current-lines
- (let* ((rest (substring line 3))
- (parts (split-string rest ","))
- (line-str (car parts))
- (hits-str (cadr parts))
- (line-num (and line-str (string-match-p "\\`[0-9]+\\'" line-str)
- (string-to-number line-str)))
- (hits (and hits-str (string-match-p "\\`[0-9]+\\'" hits-str)
- (string-to-number hits-str))))
- (when (and line-num hits (> hits 0))
- (puthash line-num t current-lines)))))
- ((string= line "end_of_record")
- (when current-file
- (puthash current-file current-lines result))
- (setq current-file nil
- current-lines nil))))
- (forward-line 1)))
+ (user-error "Simplecov report not found: %s" file))
+ (require 'json)
+ (let* ((json-object-type 'hash-table)
+ (json-array-type 'list)
+ (json-key-type 'string)
+ (data (condition-case err
+ (json-read-file file)
+ (error (user-error "Malformed simplecov JSON in %s: %s"
+ file (error-message-string err)))))
+ (result (make-hash-table :test 'equal)))
+ (maphash
+ (lambda (_test-name section)
+ (when (hash-table-p section)
+ (let ((coverage (gethash "coverage" section)))
+ (when (hash-table-p coverage)
+ (maphash
+ (lambda (path hits-list)
+ (let ((lines (or (gethash path result)
+ (make-hash-table :test 'eql)))
+ (line-num 1))
+ (dolist (hits hits-list)
+ (when (and (numberp hits) (> hits 0))
+ (puthash line-num t lines))
+ (setq line-num (1+ line-num)))
+ (puthash path lines result)))
+ coverage)))))
+ data)
result))
(defconst cj/--coverage-hunk-header-regexp
@@ -180,7 +184,7 @@ Signals `user-error' for any other SCOPE."
(defun cj/--coverage-intersect (covered changed)
"Combine COVERED (LCOV) with CHANGED (git diff) into per-file records.
COVERED and CHANGED are each hash tables from file path to a hash table
-of line numbers (as built by `cj/--coverage-parse-lcov' and
+of line numbers (as built by `cj/--coverage-parse-simplecov' and
`cj/--coverage-parse-diff-output'). Either may be nil, in which case
the result is an empty list.
diff --git a/modules/coverage-elisp.el b/modules/coverage-elisp.el
index 63d89906..048c81dd 100644
--- a/modules/coverage-elisp.el
+++ b/modules/coverage-elisp.el
@@ -13,8 +13,10 @@
;; the callback is invoked with the LCOV path; on failure, the buffer
;; stays visible for the user to inspect.
;;
-;; :lcov-path resolves to `<project-root>/.coverage/lcov.info', which
-;; matches the path the Makefile's coverage target writes to.
+;; :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:
@@ -23,9 +25,9 @@
(use-package undercover
:defer t)
-(defconst cj/--coverage-elisp-lcov-relative-path
- ".coverage/lcov.info"
- "Project-relative path to the LCOV file produced by `make coverage'.")
+(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'."
@@ -44,14 +46,14 @@ The heuristic needs both (a) a Makefile, Eask, or Cask at ROOT and
(or (file-expand-wildcards (expand-file-name "modules/*.el" root))
(file-expand-wildcards (expand-file-name "*.el" root)))))
-(defun cj/--coverage-elisp-lcov-path (&optional root)
- "Return the absolute path to the LCOV file for ROOT."
- (expand-file-name cj/--coverage-elisp-lcov-relative-path
+(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 LCOV path when the build finishes
+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))
@@ -61,14 +63,14 @@ buffer stays visible so the user can read the error."
(add-hook 'compilation-finish-functions
(lambda (_buf status)
(when (string-match-p "^finished" status)
- (funcall callback (cj/--coverage-elisp-lcov-path))))
+ (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
- :lcov-path #'cj/--coverage-elisp-lcov-path))
+ :report-path #'cj/--coverage-elisp-report-path))
(provide 'coverage-elisp)
;;; coverage-elisp.el ends here