diff options
| -rw-r--r-- | modules/coverage-core.el | 52 | ||||
| -rw-r--r-- | tests/test-coverage-core--intersect.el | 144 |
2 files changed, 196 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 diff --git a/tests/test-coverage-core--intersect.el b/tests/test-coverage-core--intersect.el new file mode 100644 index 00000000..00542cb2 --- /dev/null +++ b/tests/test-coverage-core--intersect.el @@ -0,0 +1,144 @@ +;;; test-coverage-core--intersect.el --- Tests for cj/--coverage-intersect -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for `cj/--coverage-intersect', the pure helper that +;; combines covered-line data (from LCOV) with changed-line data +;; (from git diff) into per-file records ready for the report buffer. +;; +;; Return shape: a list of plists, one per changed file, sorted by path. +;; (:path "modules/foo.el" +;; :changed-lines (10 11 12) +;; :covered-lines (10 11) ; nil when the file isn't tracked +;; :uncovered-lines (12) ; nil when the file isn't tracked +;; :tracked t) ; nil when the file isn't in the LCOV data + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'coverage-core) + +(defun test-coverage-intersect--hash-of-lines (pairs) + "Build a file → line-set hash table from PAIRS. +Each pair is (FILE . (LINES...)); LINES becomes a hash-table of line → t." + (let ((result (make-hash-table :test 'equal))) + (dolist (pair pairs) + (let ((lines (make-hash-table :test 'eql))) + (dolist (line (cdr pair)) + (puthash line t lines)) + (puthash (car pair) lines result))) + result)) + +;;; Normal cases + +(ert-deftest test-coverage-intersect-single-file-all-covered () + "Normal: one changed file, all lines covered." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("foo.el" 10 11 12)))) + (changed (test-coverage-intersect--hash-of-lines + '(("foo.el" 10 11 12)))) + (result (cj/--coverage-intersect covered changed)) + (record (car result))) + (should (= 1 (length result))) + (should (equal "foo.el" (plist-get record :path))) + (should (equal '(10 11 12) (plist-get record :covered-lines))) + (should (equal nil (plist-get record :uncovered-lines))) + (should (equal '(10 11 12) (plist-get record :changed-lines))) + (should (eq t (plist-get record :tracked))))) + +(ert-deftest test-coverage-intersect-single-file-partial () + "Normal: partial coverage — 10 and 12 covered, 11 not." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("foo.el" 10 12 50)))) + (changed (test-coverage-intersect--hash-of-lines + '(("foo.el" 10 11 12)))) + (result (cj/--coverage-intersect covered changed)) + (record (car result))) + (should (equal '(10 12) (plist-get record :covered-lines))) + (should (equal '(11) (plist-get record :uncovered-lines))) + (should (eq t (plist-get record :tracked))))) + +(ert-deftest test-coverage-intersect-multiple-files-sorted () + "Normal: multiple files sorted by path." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("a.el" 1) ("b.el" 5) ("c.el" 10)))) + (changed (test-coverage-intersect--hash-of-lines + '(("c.el" 10) ("a.el" 1) ("b.el" 5)))) + (result (cj/--coverage-intersect covered changed)) + (paths (mapcar (lambda (r) (plist-get r :path)) result))) + (should (equal '("a.el" "b.el" "c.el") paths)))) + +;;; Boundary cases + +(ert-deftest test-coverage-intersect-file-not-tracked () + "Boundary: file has changed lines but isn't in the covered set at all." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("tracked.el" 1 2 3)))) + (changed (test-coverage-intersect--hash-of-lines + '(("README.md" 5 6 7)))) + (result (cj/--coverage-intersect covered changed)) + (record (car result))) + (should (= 1 (length result))) + (should (equal "README.md" (plist-get record :path))) + (should (equal '(5 6 7) (plist-get record :changed-lines))) + (should (equal nil (plist-get record :covered-lines))) + (should (equal nil (plist-get record :uncovered-lines))) + (should (eq nil (plist-get record :tracked))))) + +(ert-deftest test-coverage-intersect-tracked-file-none-covered () + "Boundary: file is in LCOV but none of the changed lines are covered." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("foo.el" 1 2 3)))) + (changed (test-coverage-intersect--hash-of-lines + '(("foo.el" 10 11 12)))) + (result (cj/--coverage-intersect covered changed)) + (record (car result))) + (should (eq t (plist-get record :tracked))) + (should (equal nil (plist-get record :covered-lines))) + (should (equal '(10 11 12) (plist-get record :uncovered-lines))))) + +(ert-deftest test-coverage-intersect-empty-changed-lines () + "Boundary: file with empty changed-lines (deletion-only) appears with all lists empty." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("foo.el" 1 2)))) + (changed (test-coverage-intersect--hash-of-lines + '(("gone.el")))) + (result (cj/--coverage-intersect covered changed)) + (record (car result))) + (should (= 1 (length result))) + (should (equal "gone.el" (plist-get record :path))) + (should (equal nil (plist-get record :changed-lines))) + (should (equal nil (plist-get record :covered-lines))) + (should (equal nil (plist-get record :uncovered-lines))))) + +(ert-deftest test-coverage-intersect-empty-changed-returns-empty () + "Boundary: empty CHANGED hash table returns an empty list, not nil-as-error." + (let* ((covered (test-coverage-intersect--hash-of-lines + '(("foo.el" 1 2 3)))) + (changed (make-hash-table :test 'equal)) + (result (cj/--coverage-intersect covered changed))) + (should (listp result)) + (should (= 0 (length result))))) + +(ert-deftest test-coverage-intersect-empty-covered-all-not-tracked () + "Boundary: empty COVERED means every changed file is not-tracked." + (let* ((covered (make-hash-table :test 'equal)) + (changed (test-coverage-intersect--hash-of-lines + '(("a.el" 1) ("b.el" 5)))) + (result (cj/--coverage-intersect covered changed))) + (should (= 2 (length result))) + (dolist (record result) + (should (eq nil (plist-get record :tracked)))))) + +;;; Error cases + +(ert-deftest test-coverage-intersect-nil-inputs-return-empty () + "Error: nil CHANGED returns an empty list rather than erroring." + (should (equal nil (cj/--coverage-intersect nil nil))) + (should (equal nil (cj/--coverage-intersect + (test-coverage-intersect--hash-of-lines '(("x" 1))) + nil)))) + +(provide 'test-coverage-core--intersect) +;;; test-coverage-core--intersect.el ends here |
