aboutsummaryrefslogtreecommitdiff
path: root/modules/coverage-core.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-22 17:09:44 -0500
committerCraig Jennings <c@cjennings.net>2026-04-22 17:09:44 -0500
commit3d905270b48ab36f880d686a9c85f157c0460898 (patch)
treecf7063c79939d3db487cbb7d3055c920afc12774 /modules/coverage-core.el
parent759676fcd0a9833dffe4d42b90b36c228ec34861 (diff)
downloaddotemacs-3d905270b48ab36f880d686a9c85f157c0460898.tar.gz
dotemacs-3d905270b48ab36f880d686a9c85f157c0460898.zip
feat(coverage): add intersect helper to combine LCOV with diff
Third and final pure helper for the coverage-report command. Takes the hash tables produced by parse-lcov and parse-diff-output and returns per-file records ready for the report buffer. Output is a list of plists sorted by file path. Each record has :path, :changed-lines, :covered-lines, :uncovered-lines, and :tracked. A file that appears in the diff but not in the LCOV data is :tracked nil with both line lists empty. That way the reporter can distinguish "coverage isn't looking at this file" (README edits, test files, config) from "tests didn't exercise this code." Tests cover Normal (all covered, partial, multiple files sorted), Boundary (file not tracked, tracked file with no covered lines, empty changed-lines from deletion-only hunks, empty inputs), and Error (nil inputs return an empty list instead of erroring). With this helper in place, the core data pipeline is complete: LCOV file + git diff scope go in, per-file records come out. Next up is the backend registry and the elisp backend, then the cj/coverage-report command ties it all together.
Diffstat (limited to 'modules/coverage-core.el')
-rw-r--r--modules/coverage-core.el52
1 files changed, 52 insertions, 0 deletions
diff --git a/modules/coverage-core.el b/modules/coverage-core.el
index a2b03817..e7540434 100644
--- a/modules/coverage-core.el
+++ b/modules/coverage-core.el
@@ -118,5 +118,57 @@ Signals `user-error' for any other SCOPE."
(user-error "Unknown coverage scope: %s" scope)))))
(cj/--coverage-parse-diff-output (shell-command-to-string cmd))))
+(defun cj/--coverage-hash-keys-sorted (table)
+ "Return a sorted list of TABLE's integer keys."
+ (let (keys)
+ (maphash (lambda (k _v) (push k keys)) table)
+ (sort keys #'<)))
+
+(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
+`cj/--coverage-parse-diff-output'). Either may be nil, in which case
+the result is an empty list.
+
+Return value is a list of plists, one per entry in CHANGED, sorted by
+path:
+ (:path PATH
+ :changed-lines LIST-OF-INTS
+ :covered-lines LIST-OF-INTS ; nil when the file isn't tracked
+ :uncovered-lines LIST-OF-INTS ; nil when the file isn't tracked
+ :tracked BOOL)
+
+A file that appears in CHANGED but not in COVERED is marked as
+`:tracked nil'; coverage data is unavailable for it, so no lines
+can be classified as covered or uncovered."
+ (unless (and covered changed)
+ (setq covered (or covered (make-hash-table :test 'equal)))
+ (setq changed (or changed (make-hash-table :test 'equal))))
+ (let (paths records)
+ (maphash (lambda (path _) (push path paths)) changed)
+ (setq paths (sort paths #'string<))
+ (dolist (path paths)
+ (let* ((changed-set (gethash path changed))
+ (changed-lines (cj/--coverage-hash-keys-sorted changed-set))
+ (covered-set (gethash path covered))
+ (tracked (and covered-set t))
+ covered-lines
+ uncovered-lines)
+ (when tracked
+ (dolist (line changed-lines)
+ (if (gethash line covered-set)
+ (push line covered-lines)
+ (push line uncovered-lines)))
+ (setq covered-lines (nreverse covered-lines)
+ uncovered-lines (nreverse uncovered-lines)))
+ (push (list :path path
+ :changed-lines changed-lines
+ :covered-lines covered-lines
+ :uncovered-lines uncovered-lines
+ :tracked tracked)
+ records)))
+ (nreverse records)))
+
(provide 'coverage-core)
;;; coverage-core.el ends here