1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
;;; 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
|