diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-15 02:13:10 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-15 02:13:10 -0500 |
| commit | 4d8f979948d5349404a36fe335eb77955d068a8d (patch) | |
| tree | e7fec48e51d7a11257bb1a4cc5bb4b685b1d5782 | |
| parent | d9fa8f4db2b9e6d7f610094950b460cdee146e47 (diff) | |
| download | dotemacs-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.el | 84 | ||||
| -rw-r--r-- | tests/test-coverage-summary.el | 91 | ||||
| -rw-r--r-- | todo.org | 55 |
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 @@ -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: |
