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
commit4d8f979948d5349404a36fe335eb77955d068a8d (patch)
treee7fec48e51d7a11257bb1a4cc5bb4b685b1d5782
parentd9fa8f4db2b9e6d7f610094950b460cdee146e47 (diff)
downloaddotemacs-4d8f979948d5349404a36fe335eb77955d068a8d.tar.gz
dotemacs-4d8f979948d5349404a36fe335eb77955d068a8d.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
-rw-r--r--todo.org55
3 files changed, 211 insertions, 19 deletions
diff --git a/scripts/coverage-summary.el b/scripts/coverage-summary.el
index 4947171f..f2c66f4a 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 01c9efa0..2dc9d517 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
diff --git a/todo.org b/todo.org
index 889ab28f..917cef5d 100644
--- a/todo.org
+++ b/todo.org
@@ -780,13 +780,29 @@ Expected outcome:
- Add a note to the local repository docs so future package failures do not
lead to permanent insecure defaults.
-*** PROJECT [#B] Make coverage reporting account for untracked modules :tests:
+*** DONE [#B] Make coverage reporting account for untracked modules :tests:
+CLOSED: [2026-05-15 Fri]
The current coverage result is useful but easy to overread. =make coverage=
reported =65.43%= for files that undercover saw, but only 49 of 104 module
files appeared in =.coverage/simplecov.json=.
-**** TODO [#B] Teach the coverage report to list modules missing from SimpleCov
+Definition: in this task, "untracked modules" means repository-owned
+=modules/*.el= files that should be part of the Emacs configuration coverage
+universe but have no entry in =.coverage/simplecov.json= after =make coverage=
+runs. These files may be missing because no test required them, because loading
+was skipped due to package/environment guards, or because instrumentation did
+not see them. They are distinct from tracked modules with 0% covered lines,
+which already appear in SimpleCov and can be scored directly.
+
+Completed 2026-05-15:
+- Both child tasks are done.
+- =make coverage-summary= reports missing modules explicitly and also reports a
+ separate project-module score where missing modules count as 0%.
+- Focused summary tests and byte-compilation of the summary helper passed.
+
+**** DONE [#B] Teach the coverage report to list modules missing from SimpleCov
+CLOSED: [2026-05-15 Fri]
Expected outcome:
- Compare =modules/*.el= against paths present in =.coverage/simplecov.json=.
@@ -794,7 +810,19 @@ Expected outcome:
- Do not silently fold those files into the percentage until we decide the
semantics. A visible missing-file count is enough for v1.
-**** TODO [#B] Decide whether unreported modules count as 0% coverage
+Done 2026-05-15:
+- =make coverage-summary= now compares direct =modules/*.el= files on disk
+ against the module paths present in =.coverage/simplecov.json=.
+- The terminal report appends a =Not in SimpleCov report= section with a count
+ and the missing module paths.
+- Missing modules are explicitly excluded from the displayed percentage for
+ now; the policy question below remains open.
+- Added focused tests in =tests/test-coverage-summary.el= for missing-module
+ reporting and for ignoring =.elc= files and nested paths outside direct
+ =modules/*.el= ownership.
+
+**** DONE [#B] Decide whether unreported modules count as 0% coverage
+CLOSED: [2026-05-15 Fri]
This is a policy decision:
- Counting missing modules as 0% gives a more honest project-level number.
@@ -805,8 +833,25 @@ Recommendation: display both:
- Project module coverage: includes unreported module files as 0% or reports
them separately with an explicit caveat.
-Related existing task: [#B] "Coverage audit: untested and lightly-tested
-modules".
+Decision 2026-05-15:
+- Keep the existing SimpleCov percentage as the line-weighted
+ =instrumented coverage= number. It only covers modules that SimpleCov saw and
+ has real executable-line denominators for.
+- Also display a separate module-weighted =project module coverage= score over
+ all direct =modules/*.el= files. Modules present in SimpleCov contribute their
+ per-file coverage percentage; modules absent from SimpleCov count as 0%.
+- Do not pretend missing modules have known executable-line counts. Counting
+ them as 0% at the module level is honest about risk without inventing a line
+ denominator.
+
+Done 2026-05-15:
+- =make coverage-summary= now prints both the existing line-weighted summary
+ and a separate =Project module coverage= line that includes missing modules
+ as 0%.
+- The missing-module section now states that missing modules count as 0% in the
+ project-module score.
+- Updated =tests/test-coverage-summary.el= to assert the policy and the
+ displayed project-module percentage.
*** TODO [#B] Add a lightweight architecture smoke test for startup contracts :tests: