;;; test-coverage-summary.el --- Tests for the bundle coverage-summary kernel -*- lexical-binding: t; -*- ;;; Commentary: ;; ERT tests for languages/elisp/claude/scripts/coverage-summary.el. ;; ;; The kernel is the missing-file detection and the unit-weighted project ;; number; the per-file table is incidental. Tests build a throwaway project ;; tree (source files on disk) plus a SimpleCov JSON report, then assert the ;; kernel's numbers against hand-computed values. ;; ;; Normal / Boundary / Error coverage for each pure function. ;;; Code: (require 'ert) (require 'json) ;; Load the script under test relative to this file, so the test runs from any cwd. (load (expand-file-name "../claude/scripts/coverage-summary.el" (file-name-directory (or load-file-name buffer-file-name))) nil t) ;; --- fixtures -------------------------------------------------------------- (defun cs-test--write (path contents) "Write CONTENTS to PATH, creating parent dirs." (make-directory (file-name-directory path) t) (with-temp-file path (insert contents))) (defmacro cs-test--with-project (spec &rest body) "Build a temp project from SPEC and run BODY with `root', `src', `report' bound. SPEC is a plist: :sources alist of (relpath . contents) source files written under src/ :report the SimpleCov JSON string written to .coverage/simplecov.json (the literal string \"%SRC%\" is replaced with the absolute src dir)." (declare (indent 1)) `(let* ((root (file-name-as-directory (make-temp-file "cs-proj-" t))) (src (expand-file-name "src/" root)) (report (expand-file-name ".coverage/simplecov.json" root))) (unwind-protect (progn (dolist (pair (plist-get ,spec :sources)) (cs-test--write (expand-file-name (car pair) src) (cdr pair))) (cs-test--write report (replace-regexp-in-string "%SRC%" (directory-file-name src) (plist-get ,spec :report) t t)) ,@body) (delete-directory root t)))) (defun cs-test--report (files) "Build a SimpleCov JSON string for FILES. FILES is an alist of (relpath . line-vector-literal), where each line vector is a JSON array string like \"[1, 0, null]\"." (concat "{\"undercover.el\": {\"timestamp\": 0, \"coverage\": {" (mapconcat (lambda (pair) (format "\"%%SRC%%/%s\": %s" (car pair) (cdr pair))) files ", ") "}}}")) ;; --- parse: covered / total ------------------------------------------------ (ert-deftest cs-parse-counts-covered-and-executable () "covered = entries > 0; total = numeric entries; null is non-executable." (cs-test--with-project (list :sources '(("a.el" . ";; a")) :report (cs-test--report '(("a.el" . "[null, 1, 0, 3, null, 0]")))) (let* ((table (cj/coverage-summary--parse-file report)) (rec (gethash (expand-file-name "a.el" src) table))) ;; entries: null,1,0,3,null,0 -> numeric {1,0,3,0}=4 total, >0 {1,3}=2 covered (should (equal rec '(2 . 4)))))) (ert-deftest cs-parse-unions-multiple-suites () "Coverage data unions across multiple top-level suite keys." (cs-test--with-project (list :sources '(("a.el" . ";; a")) :report (concat "{\"s1\": {\"coverage\": {\"%SRC%/a.el\": [1, 0, 0]}}," " \"s2\": {\"coverage\": {\"%SRC%/a.el\": [0, 1, 0]}}}")) (let* ((table (cj/coverage-summary--parse-file report)) (rec (gethash (expand-file-name "a.el" src) table))) ;; union of hits at line 1 (s1) and line 2 (s2): 2 covered of 3 total (should (equal rec '(2 . 3)))))) (ert-deftest cs-parse-missing-report-signals () "A nonexistent report file signals user-error." (should-error (cj/coverage-summary--parse-file "/no/such/report.json") :type 'user-error)) (ert-deftest cs-parse-malformed-json-signals () "Malformed JSON signals user-error rather than a raw json error." (cs-test--with-project (list :sources '(("a.el" . ";; a")) :report "{not json") (should-error (cj/coverage-summary--parse-file report) :type 'user-error))) ;; --- file percentage ------------------------------------------------------- (ert-deftest cs-file-pct-normal () (should (= 50.0 (cj/coverage-summary--file-pct 1 2)))) (ert-deftest cs-file-pct-no-executable-lines-is-100 () "A tracked file with zero executable lines has nothing left uncovered." (should (= 100.0 (cj/coverage-summary--file-pct 0 0)))) (ert-deftest cs-file-pct-fully-covered () (should (= 100.0 (cj/coverage-summary--file-pct 4 4)))) ;; --- missing-file detection (the kernel) ----------------------------------- (ert-deftest cs-missing-finds-ondisk-file-absent-from-report () "A source file on disk but not in the report is reported missing." (cs-test--with-project (list :sources '(("tracked.el" . ";; t") ("untested.el" . ";; u")) :report (cs-test--report '(("tracked.el" . "[1, 1]")))) (let* ((table (cj/coverage-summary--under-dir (cj/coverage-summary--parse-file report) src root)) (tracked (let (ks) (maphash (lambda (k _v) (push k ks)) table) ks)) (missing (cj/coverage-summary--missing tracked src root))) (should (equal missing (list (file-relative-name (expand-file-name "src/untested.el" root) root))))))) (ert-deftest cs-missing-empty-when-all-tracked () "No missing files when every source file appears in the report." (cs-test--with-project (list :sources '(("a.el" . ";; a")) :report (cs-test--report '(("a.el" . "[1]")))) (let* ((table (cj/coverage-summary--under-dir (cj/coverage-summary--parse-file report) src root)) (tracked (let (ks) (maphash (lambda (k _v) (push k ks)) table) ks)) (missing (cj/coverage-summary--missing tracked src root))) (should (null missing))))) ;; --- project number (unit-weighted, missing as 0%) ------------------------- (ert-deftest cs-project-pct-unit-weighted-with-missing-as-zero () "Project number averages per-file pct over tracked+missing; missing=0%." (cs-test--with-project (list :sources '(("full.el" . ";; f") ("half.el" . ";; h") ("untested.el" . ";; u")) :report (cs-test--report '(("full.el" . "[1, 1]") ("half.el" . "[1, 0]")))) ;; full=100%, half=50%, untested missing=0% -> (100+50+0)/3 = 50.0 (should (= 50.0 (cj/coverage-summary--project-pct report src root))))) (ert-deftest cs-project-pct-no-missing () "With every file tracked, the number is the plain unit average." (cs-test--with-project (list :sources '(("full.el" . ";; f") ("half.el" . ";; h")) :report (cs-test--report '(("full.el" . "[1, 1]") ("half.el" . "[1, 0]")))) ;; (100 + 50) / 2 = 75.0 (should (= 75.0 (cj/coverage-summary--project-pct report src root))))) ;; --- end-to-end text ------------------------------------------------------- (ert-deftest cs-text-reports-missing-and-project-number () "The rendered summary names the missing file and the project percentage." (cs-test--with-project (list :sources '(("a.el" . ";; a") ("orphan.el" . ";; o")) :report (cs-test--report '(("a.el" . "[1, 0]")))) (let ((text (cj/coverage-summary-text report src root))) (should (string-match-p "orphan\\.el" text)) (should (string-match-p "count as 0%" text)) ;; a.el = 50%, orphan missing = 0% -> 25.0% (should (string-match-p "25\\.0%" text))))) (provide 'test-coverage-summary) ;;; test-coverage-summary.el ends here