aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-15 02:13:10 -0500
committerCraig Jennings <c@cjennings.net>2026-05-15 02:13:10 -0500
commitb9af4484a3d9f915f0018b29a55c404e01591b0c (patch)
tree6f4fbf60be44e6f4ee428cb812542444b0c0895a
parent043576dea5a06d100900d690a57297b9a8fa7a84 (diff)
downloaddotemacs-b9af4484a3d9f915f0018b29a55c404e01591b0c.tar.gz
dotemacs-b9af4484a3d9f915f0018b29a55c404e01591b0c.zip
feat(coverage): report modules missing from SimpleCov + project-module score
=make coverage= used to print a line-weighted percentage that only saw files SimpleCov instrumented. 104 modules existed on disk but only 49 appeared in =.coverage/simplecov.json=, so the headline number was flattering: untouched modules counted for nothing. The summary script now adds two things on top of the existing report: - A =Not in SimpleCov report= section listing modules present under =modules/*.el= but absent from the SimpleCov output. Missing-module detection is exactly direct =modules/*.el=; subdirectories and =.elc= files are ignored. - A =Project module coverage= line that is module-weighted across every direct =modules/*.el= file. Tracked modules contribute their per-file coverage percentage; missing modules contribute 0%. The original line-weighted SimpleCov percentage stays as the =instrumented coverage= number. The new module-weighted score is the honest project-level reading: missing modules count as 0% without inventing a fake executable-line denominator for them. Tests assert the missing-module section, the new percentage, and the ignore rules for .elc / nested files.
-rw-r--r--scripts/coverage-summary.el84
-rw-r--r--tests/test-coverage-summary.el91
2 files changed, 161 insertions, 14 deletions
diff --git a/scripts/coverage-summary.el b/scripts/coverage-summary.el
index 4947171f4..f2c66f4ab 100644
--- a/scripts/coverage-summary.el
+++ b/scripts/coverage-summary.el
@@ -35,6 +35,80 @@ Returned keys are relative to PROJECT-ROOT for readable terminal output."
table)
result))
+(defun cj/coverage-summary--module-files (module-dir project-root)
+ "Return repository module files under MODULE-DIR, relative to PROJECT-ROOT.
+
+This intentionally uses only direct =modules/*.el= files. Compiled files,
+subdirectories, and non-Elisp assets are outside this repository's module
+coverage universe."
+ (let ((module-dir (file-name-as-directory (expand-file-name module-dir)))
+ (project-root (file-name-as-directory (expand-file-name project-root))))
+ (sort
+ (mapcar (lambda (path)
+ (file-relative-name path project-root))
+ (directory-files module-dir t "\\.el\\'"))
+ #'string<)))
+
+(defun cj/coverage-summary--missing-module-files (tracked-table module-dir project-root)
+ "Return modules present on disk but absent from TRACKED-TABLE.
+
+TRACKED-TABLE should use readable project-relative paths, such as the table
+returned by `cj/coverage-summary--modules-only'."
+ (let (tracked)
+ (maphash (lambda (path _lines) (push path tracked)) tracked-table)
+ (seq-difference
+ (cj/coverage-summary--module-files module-dir project-root)
+ tracked
+ #'string=)))
+
+(defun cj/coverage-summary--format-missing-modules (missing)
+ "Return a report section for MISSING module files.
+
+MISSING is a list of project-relative module paths that are absent from the
+SimpleCov report."
+ (with-temp-buffer
+ (insert (format "\nNot in SimpleCov report: %d module%s\n"
+ (length missing)
+ (if (= 1 (length missing)) "" "s")))
+ (if missing
+ (progn
+ (insert "These modules had no coverage entry; they count as 0% in project module coverage.\n")
+ (dolist (path missing)
+ (insert (format " %s\n" path))))
+ (insert "Every modules/*.el file appears in the SimpleCov report.\n"))
+ (buffer-string)))
+
+(defun cj/coverage-summary--record-module-percent (record)
+ "Return RECORD's per-module coverage percentage.
+
+RECORD is a plist from `cj/--coverage-intersect'. A tracked module with no
+executable lines contributes 100%; there is nothing executable left uncovered."
+ (let ((total (length (plist-get record :changed-lines)))
+ (covered (length (plist-get record :covered-lines))))
+ (if (> total 0)
+ (/ (* 100.0 covered) total)
+ 100.0)))
+
+(defun cj/coverage-summary--format-project-module-coverage (records missing)
+ "Return the project-module coverage policy section.
+
+The existing SimpleCov total is line-weighted over files present in the report.
+This section is module-weighted over all direct `modules/*.el' files: tracked
+modules contribute their per-file coverage percentage, while MISSING modules
+contribute 0%."
+ (let* ((tracked (seq-filter (lambda (rec) (plist-get rec :tracked)) records))
+ (tracked-count (length tracked))
+ (missing-count (length missing))
+ (total-count (+ tracked-count missing-count))
+ (score (apply #'+ (mapcar #'cj/coverage-summary--record-module-percent
+ tracked)))
+ (pct (if (> total-count 0)
+ (/ score total-count)
+ 0.0)))
+ (format (concat "\nProject module coverage: %.1f%%"
+ " (%d tracked, %d missing, %d total; missing modules count as 0%%)\n")
+ pct tracked-count missing-count total-count)))
+
(defun cj/coverage-summary-text (report-file module-dir project-root)
"Return a whole-project coverage summary for MODULE-DIR from REPORT-FILE."
(let* ((covered (cj/coverage-summary--modules-only
@@ -45,8 +119,14 @@ Returned keys are relative to PROJECT-ROOT for readable terminal output."
(cj/--coverage-simplecov-executable-lines report-file)
module-dir
project-root))
- (records (cj/--coverage-intersect covered executable)))
- (cj/--coverage-format-summary records "modules/")))
+ (records (cj/--coverage-intersect covered executable))
+ (missing (cj/coverage-summary--missing-module-files executable
+ module-dir
+ project-root)))
+ (concat
+ (cj/--coverage-format-summary records "modules/")
+ (cj/coverage-summary--format-project-module-coverage records missing)
+ (cj/coverage-summary--format-missing-modules missing))))
(defun cj/coverage-print-module-summary (report-file module-dir project-root)
"Print a whole-project coverage summary for MODULE-DIR from REPORT-FILE."
diff --git a/tests/test-coverage-summary.el b/tests/test-coverage-summary.el
index 01c9efa08..2dc9d517b 100644
--- a/tests/test-coverage-summary.el
+++ b/tests/test-coverage-summary.el
@@ -18,6 +18,12 @@
(insert content))
file))
+(defun test-coverage-summary--touch (file)
+ "Create FILE and its parent directory."
+ (make-directory (file-name-directory file) t)
+ (with-temp-file file
+ (insert ";;; fixture\n")))
+
(ert-deftest test-coverage-summary-modules-only ()
"Normal: summary includes modules files and ignores non-module files."
(let* ((root (file-name-as-directory (make-temp-file "coverage-root-" t)))
@@ -29,14 +35,20 @@
module-a module-b other))
(file (test-coverage-summary--write-json content)))
(unwind-protect
- (let ((output (cj/coverage-summary-text
- file
- (expand-file-name "modules" root)
- root)))
+ (progn
+ (test-coverage-summary--touch module-a)
+ (test-coverage-summary--touch module-b)
+ (test-coverage-summary--touch other)
+ (let ((output (cj/coverage-summary-text
+ file
+ (expand-file-name "modules" root)
+ root)))
(should (string-match-p "modules/a\\.el" output))
(should (string-match-p "modules/b\\.el" output))
(should-not (string-match-p "tests/test-a\\.el" output))
- (should (string-match-p "2 of 5" output)))
+ (should (string-match-p "2 of 5" output))
+ (should (string-match-p "Project module coverage: 33\\.3%" output))
+ (should (string-match-p "Not in SimpleCov report: 0 modules" output))))
(delete-file file)
(delete-directory root t))))
@@ -50,17 +62,72 @@
low high))
(file (test-coverage-summary--write-json content)))
(unwind-protect
- (let* ((output (cj/coverage-summary-text
- file
- (expand-file-name "modules" root)
- root))
- (pos-low (string-match "modules/low\\.el" output))
- (pos-high (string-match "modules/high\\.el" output)))
+ (progn
+ (test-coverage-summary--touch low)
+ (test-coverage-summary--touch high)
+ (let* ((output (cj/coverage-summary-text
+ file
+ (expand-file-name "modules" root)
+ root))
+ (pos-low (string-match "modules/low\\.el" output))
+ (pos-high (string-match "modules/high\\.el" output)))
(should pos-low)
(should pos-high)
- (should (< pos-low pos-high)))
+ (should (< pos-low pos-high))))
(delete-file file)
(delete-directory root t))))
+(ert-deftest test-coverage-summary-lists-modules-missing-from-simplecov ()
+ "Normal: modules on disk but absent from SimpleCov are listed separately."
+ (let* ((root (file-name-as-directory (make-temp-file "coverage-root-" t)))
+ (tracked (expand-file-name "modules/tracked.el" root))
+ (missing-a (expand-file-name "modules/missing-a.el" root))
+ (missing-b (expand-file-name "modules/missing-b.el" root))
+ (content (format
+ "{\"run\":{\"coverage\":{\"%s\":[1,0,null,1]}}}"
+ tracked))
+ (file (test-coverage-summary--write-json content)))
+ (unwind-protect
+ (progn
+ (test-coverage-summary--touch tracked)
+ (test-coverage-summary--touch missing-a)
+ (test-coverage-summary--touch missing-b)
+ (let ((output (cj/coverage-summary-text
+ file
+ (expand-file-name "modules" root)
+ root)))
+ (should (string-match-p "Total: 2 of 3 lines covered" output))
+ (should (string-match-p
+ "Project module coverage: 22\\.2% (1 tracked, 2 missing, 3 total; missing modules count as 0%)"
+ output))
+ (should (string-match-p "Not in SimpleCov report: 2 modules" output))
+ (should (string-match-p "modules/missing-a\\.el" output))
+ (should (string-match-p "modules/missing-b\\.el" output))
+ (should (string-match-p "count as 0% in project module coverage" output))))
+ (delete-file file)
+ (delete-directory root t))))
+
+(ert-deftest test-coverage-summary-missing-module-helper-ignores-elc-and-subdirs ()
+ "Boundary: untracked module detection is exactly direct modules/*.el files."
+ (let* ((root (file-name-as-directory (make-temp-file "coverage-root-" t)))
+ (module-dir (expand-file-name "modules" root))
+ (tracked-file (expand-file-name "modules/tracked.el" root))
+ (missing-file (expand-file-name "modules/missing.el" root))
+ (compiled-file (expand-file-name "modules/compiled.elc" root))
+ (nested-file (expand-file-name "modules/nested/not-a-module.el" root))
+ (tracked-table (make-hash-table :test 'equal)))
+ (unwind-protect
+ (progn
+ (test-coverage-summary--touch tracked-file)
+ (test-coverage-summary--touch missing-file)
+ (make-directory (file-name-directory compiled-file) t)
+ (with-temp-file compiled-file (insert "compiled"))
+ (test-coverage-summary--touch nested-file)
+ (puthash "modules/tracked.el" t tracked-table)
+ (should (equal (cj/coverage-summary--missing-module-files
+ tracked-table module-dir root)
+ '("modules/missing.el"))))
+ (delete-directory root t))))
+
(provide 'test-coverage-summary)
;;; test-coverage-summary.el ends here