aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/run-coverage-file.el40
-rw-r--r--tests/test-coverage-core--backend-registry.el28
-rw-r--r--tests/test-coverage-core--parse-lcov.el149
-rw-r--r--tests/test-coverage-core--parse-simplecov.el153
-rw-r--r--tests/test-coverage-elisp--detect.el2
5 files changed, 208 insertions, 164 deletions
diff --git a/tests/run-coverage-file.el b/tests/run-coverage-file.el
new file mode 100644
index 00000000..eeae5de9
--- /dev/null
+++ b/tests/run-coverage-file.el
@@ -0,0 +1,40 @@
+;;; run-coverage-file.el --- Undercover setup for per-file coverage runs -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Loaded via `-l tests/run-coverage-file.el' by the Makefile's coverage
+;; target before each test file runs. Ensures undercover is active and
+;; configured to merge into the shared LCOV output so coverage data
+;; accumulates across all test-file invocations.
+;;
+;; Per-file isolation matches the project's `make test-unit' pattern:
+;; each test file runs in its own Emacs process, so tests that work
+;; under `make test' will also work under `make coverage'. See
+;; docs/design/coverage.org for the rationale.
+
+;;; Code:
+
+(require 'package)
+(setq package-user-dir (expand-file-name "elpa" user-emacs-directory))
+(package-initialize)
+
+(unless (require 'undercover nil t)
+ (message "")
+ (message "ERROR: undercover not installed.")
+ (message "Start Emacs interactively to install it via use-package,")
+ (message "or run: emacs --batch --eval \"(progn (package-refresh-contents) (package-install 'undercover))\"")
+ (message "")
+ (kill-emacs 1))
+
+;; Force coverage collection even when not in CI. Must happen AFTER
+;; `(require 'undercover)' because undercover.el's top-level
+;; `(setq undercover-force-coverage (getenv "UNDERCOVER_FORCE"))'
+;; would otherwise overwrite our value.
+(setq undercover-force-coverage t)
+
+(undercover "modules/*.el"
+ (:report-format 'simplecov)
+ (:report-file ".coverage/simplecov.json")
+ (:merge-report t)
+ (:send-report nil))
+
+;;; run-coverage-file.el ends here
diff --git a/tests/test-coverage-core--backend-registry.el b/tests/test-coverage-core--backend-registry.el
index ac1f6f7d..2369806f 100644
--- a/tests/test-coverage-core--backend-registry.el
+++ b/tests/test-coverage-core--backend-registry.el
@@ -3,7 +3,7 @@
;;; Commentary:
;; Unit tests for the backend registry.
;;
-;; A backend is a plist with at least :name, :detect, :run, and :lcov-path
+;; A backend is a plist with at least :name, :detect, :run, and :report-path
;; keys. `cj/coverage-register-backend' adds or replaces an entry.
;; `cj/--coverage-backend-for-project' resolves which backend applies to
;; a project root, honoring an optional override (buffer-local
@@ -28,7 +28,7 @@
"Normal: registering a backend makes it retrievable by name."
(test-coverage-registry-with-empty
(cj/coverage-register-backend
- '(:name elisp :detect (lambda (_) t) :run ignore :lcov-path ignore))
+ '(:name elisp :detect (lambda (_) t) :run ignore :report-path ignore))
(should (= 1 (length cj/coverage-backends)))
(should (eq 'elisp (plist-get (car cj/coverage-backends) :name)))))
@@ -36,11 +36,11 @@
"Normal: re-registering by name replaces the existing entry at the same position."
(test-coverage-registry-with-empty
(cj/coverage-register-backend
- '(:name elisp :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name elisp :detect (lambda (_) nil) :run ignore :report-path ignore))
(cj/coverage-register-backend
- '(:name python :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name python :detect (lambda (_) nil) :run ignore :report-path ignore))
(cj/coverage-register-backend
- '(:name elisp :detect (lambda (_) t) :run ignore :lcov-path ignore))
+ '(:name elisp :detect (lambda (_) t) :run ignore :report-path ignore))
(should (= 2 (length cj/coverage-backends)))
(should (eq 'elisp (plist-get (nth 0 cj/coverage-backends) :name)))
(should (eq 'python (plist-get (nth 1 cj/coverage-backends) :name)))
@@ -50,11 +50,11 @@
"Normal: resolution returns the first backend whose :detect matches."
(test-coverage-registry-with-empty
(cj/coverage-register-backend
- '(:name a :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name a :detect (lambda (_) nil) :run ignore :report-path ignore))
(cj/coverage-register-backend
- '(:name b :detect (lambda (_) t) :run ignore :lcov-path ignore))
+ '(:name b :detect (lambda (_) t) :run ignore :report-path ignore))
(cj/coverage-register-backend
- '(:name c :detect (lambda (_) t) :run ignore :lcov-path ignore))
+ '(:name c :detect (lambda (_) t) :run ignore :report-path ignore))
(let ((backend (cj/--coverage-backend-for-project "/tmp")))
(should (eq 'b (plist-get backend :name))))))
@@ -69,18 +69,18 @@
"Boundary: no backend's :detect matches returns nil."
(test-coverage-registry-with-empty
(cj/coverage-register-backend
- '(:name a :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name a :detect (lambda (_) nil) :run ignore :report-path ignore))
(cj/coverage-register-backend
- '(:name b :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name b :detect (lambda (_) nil) :run ignore :report-path ignore))
(should (null (cj/--coverage-backend-for-project "/tmp")))))
(ert-deftest test-coverage-backend-for-project-override-bypasses-detect ()
"Boundary: OVERRIDE returns the named backend without calling :detect."
(test-coverage-registry-with-empty
(cj/coverage-register-backend
- '(:name a :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name a :detect (lambda (_) nil) :run ignore :report-path ignore))
(cj/coverage-register-backend
- '(:name b :detect (lambda (_) nil) :run ignore :lcov-path ignore))
+ '(:name b :detect (lambda (_) nil) :run ignore :report-path ignore))
(let ((backend (cj/--coverage-backend-for-project "/tmp" 'b)))
(should (eq 'b (plist-get backend :name))))))
@@ -91,7 +91,7 @@
(cj/coverage-register-backend
`(:name a
:detect ,(lambda (root) (setq captured root) t)
- :run ignore :lcov-path ignore))
+ :run ignore :report-path ignore))
(cj/--coverage-backend-for-project "/my/root")
(should (equal "/my/root" captured)))))
@@ -101,7 +101,7 @@
"Error: OVERRIDE that names an unregistered backend signals user-error."
(test-coverage-registry-with-empty
(cj/coverage-register-backend
- '(:name a :detect (lambda (_) t) :run ignore :lcov-path ignore))
+ '(:name a :detect (lambda (_) t) :run ignore :report-path ignore))
(should-error (cj/--coverage-backend-for-project "/tmp" 'bogus)
:type 'user-error)))
diff --git a/tests/test-coverage-core--parse-lcov.el b/tests/test-coverage-core--parse-lcov.el
deleted file mode 100644
index a0a800ef..00000000
--- a/tests/test-coverage-core--parse-lcov.el
+++ /dev/null
@@ -1,149 +0,0 @@
-;;; test-coverage-core--parse-lcov.el --- Tests for cj/--coverage-parse-lcov -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Unit tests for `cj/--coverage-parse-lcov', the pure helper that
-;; reads an LCOV file and returns a hash table of file → set of
-;; covered line numbers.
-;;
-;; LCOV format (the subset we care about):
-;; SF:<source file>
-;; DA:<line>,<hit count>
-;; end_of_record
-;;
-;; A line counts as "covered" when its hit count is greater than zero.
-
-;;; Code:
-
-(require 'ert)
-
-(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'coverage-core)
-
-(defun test-coverage-parse-lcov--write-temp-lcov (content)
- "Write CONTENT to a temp file and return its path.
-Caller is responsible for deleting the file."
- (let ((file (make-temp-file "test-lcov-" nil ".info")))
- (with-temp-file file
- (insert content))
- file))
-
-;;; Normal cases
-
-(ert-deftest test-coverage-parse-lcov-single-file-all-covered ()
- "Normal: one file with every line hit > 0 returns all lines in the set."
- (let* ((content "SF:/path/to/foo.el\nDA:10,1\nDA:11,2\nDA:12,3\nend_of_record\n")
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let* ((result (cj/--coverage-parse-lcov file))
- (lines (gethash "/path/to/foo.el" result)))
- (should (hash-table-p result))
- (should (= 3 (hash-table-count lines)))
- (should (gethash 10 lines))
- (should (gethash 11 lines))
- (should (gethash 12 lines)))
- (delete-file file))))
-
-(ert-deftest test-coverage-parse-lcov-multiple-files ()
- "Normal: multiple file records in one LCOV file are both parsed."
- (let* ((content (concat "SF:/a.el\nDA:1,1\nDA:2,1\nend_of_record\n"
- "SF:/b.el\nDA:5,1\nend_of_record\n"))
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let ((result (cj/--coverage-parse-lcov file)))
- (should (= 2 (hash-table-count result)))
- (should (gethash "/a.el" result))
- (should (gethash "/b.el" result)))
- (delete-file file))))
-
-(ert-deftest test-coverage-parse-lcov-mixed-hits ()
- "Normal: lines with hit count 0 are excluded; positive counts included."
- (let* ((content "SF:/foo.el\nDA:1,0\nDA:2,1\nDA:3,0\nDA:4,5\nend_of_record\n")
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let* ((result (cj/--coverage-parse-lcov file))
- (lines (gethash "/foo.el" result)))
- (should (= 2 (hash-table-count lines)))
- (should-not (gethash 1 lines))
- (should (gethash 2 lines))
- (should-not (gethash 3 lines))
- (should (gethash 4 lines)))
- (delete-file file))))
-
-;;; Boundary cases
-
-(ert-deftest test-coverage-parse-lcov-empty-file ()
- "Boundary: empty LCOV file returns an empty hash table, not nil."
- (let ((file (test-coverage-parse-lcov--write-temp-lcov "")))
- (unwind-protect
- (let ((result (cj/--coverage-parse-lcov file)))
- (should (hash-table-p result))
- (should (= 0 (hash-table-count result))))
- (delete-file file))))
-
-(ert-deftest test-coverage-parse-lcov-file-with-spaces-in-path ()
- "Boundary: filename with spaces is parsed as one key."
- (let* ((content "SF:/my path/with spaces.el\nDA:1,1\nend_of_record\n")
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let ((result (cj/--coverage-parse-lcov file)))
- (should (gethash "/my path/with spaces.el" result)))
- (delete-file file))))
-
-(ert-deftest test-coverage-parse-lcov-extra-lcov-fields-ignored ()
- "Boundary: LF/LH/BRDA/FN fields don't break parsing; only DA matters."
- (let* ((content (concat "SF:/foo.el\n"
- "FN:10,some-function\n"
- "FNDA:1,some-function\n"
- "DA:10,1\n"
- "DA:11,1\n"
- "LF:2\n"
- "LH:2\n"
- "BRDA:10,0,0,1\n"
- "end_of_record\n"))
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let* ((result (cj/--coverage-parse-lcov file))
- (lines (gethash "/foo.el" result)))
- (should (= 2 (hash-table-count lines)))
- (should (gethash 10 lines))
- (should (gethash 11 lines)))
- (delete-file file))))
-
-(ert-deftest test-coverage-parse-lcov-all-zero-hits ()
- "Boundary: file with no covered lines returns an empty set for that file."
- (let* ((content "SF:/foo.el\nDA:1,0\nDA:2,0\nend_of_record\n")
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let* ((result (cj/--coverage-parse-lcov file))
- (lines (gethash "/foo.el" result)))
- (should (hash-table-p lines))
- (should (= 0 (hash-table-count lines))))
- (delete-file file))))
-
-;;; Error cases
-
-(ert-deftest test-coverage-parse-lcov-missing-file-errors ()
- "Error: nonexistent file signals a user-error naming the path."
- (should-error (cj/--coverage-parse-lcov "/nonexistent/path/xyz.info")
- :type 'user-error))
-
-(ert-deftest test-coverage-parse-lcov-malformed-da-line-skipped ()
- "Error: malformed DA lines are skipped; parsing continues on the rest."
- (let* ((content (concat "SF:/foo.el\n"
- "DA:not-a-number,1\n"
- "DA:10,1\n"
- "DA:\n"
- "DA:11,notanumber\n"
- "DA:12,2\n"
- "end_of_record\n"))
- (file (test-coverage-parse-lcov--write-temp-lcov content)))
- (unwind-protect
- (let* ((result (cj/--coverage-parse-lcov file))
- (lines (gethash "/foo.el" result)))
- (should (= 2 (hash-table-count lines)))
- (should (gethash 10 lines))
- (should (gethash 12 lines)))
- (delete-file file))))
-
-(provide 'test-coverage-core--parse-lcov)
-;;; test-coverage-core--parse-lcov.el ends here
diff --git a/tests/test-coverage-core--parse-simplecov.el b/tests/test-coverage-core--parse-simplecov.el
new file mode 100644
index 00000000..09a3051e
--- /dev/null
+++ b/tests/test-coverage-core--parse-simplecov.el
@@ -0,0 +1,153 @@
+;;; test-coverage-core--parse-simplecov.el --- Tests for cj/--coverage-parse-simplecov -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for `cj/--coverage-parse-simplecov', the pure helper
+;; that reads a simplecov JSON report and returns a hash table of
+;; file → set of covered line numbers.
+;;
+;; Simplecov JSON structure:
+;; { <test-name>: { "coverage": { <path>: [null | 0 | int, ...] } } }
+;;
+;; Array index i (0-based) corresponds to line (i+1).
+;; nil — line is not executable (blank, comment)
+;; 0 — line is executable but not hit
+;; positive int — hit count
+;;
+;; When the JSON has multiple top-level test-name keys, coverage is
+;; unioned across them.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'coverage-core)
+
+(defun test-coverage-parse-simplecov--write-temp-json (content)
+ "Write CONTENT (a JSON string) to a temp file; return its path."
+ (let ((file (make-temp-file "test-simplecov-" nil ".json")))
+ (with-temp-file file
+ (insert content))
+ file))
+
+;;; Normal cases
+
+(ert-deftest test-coverage-parse-simplecov-single-file-all-hit ()
+ "Normal: one file with every executable line hit."
+ (let* ((content "{\"run\":{\"timestamp\":1,\"coverage\":{\"/foo.el\":[null,1,2,3]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let* ((result (cj/--coverage-parse-simplecov file))
+ (lines (gethash "/foo.el" result)))
+ (should (hash-table-p result))
+ (should (= 3 (hash-table-count lines)))
+ (should (gethash 2 lines))
+ (should (gethash 3 lines))
+ (should (gethash 4 lines))
+ (should-not (gethash 1 lines)))
+ (delete-file file))))
+
+(ert-deftest test-coverage-parse-simplecov-multiple-files ()
+ "Normal: multiple files under one test-name are both parsed."
+ (let* ((content "{\"run\":{\"timestamp\":1,\"coverage\":{\"/a.el\":[1,1],\"/b.el\":[null,5]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let ((result (cj/--coverage-parse-simplecov file)))
+ (should (= 2 (hash-table-count result)))
+ (should (gethash "/a.el" result))
+ (should (gethash "/b.el" result))
+ (should (= 2 (hash-table-count (gethash "/a.el" result))))
+ (should (= 1 (hash-table-count (gethash "/b.el" result)))))
+ (delete-file file))))
+
+(ert-deftest test-coverage-parse-simplecov-mixed-hits ()
+ "Normal: null (not executable) and 0 (not hit) are excluded."
+ (let* ((content "{\"run\":{\"timestamp\":1,\"coverage\":{\"/foo.el\":[null,1,0,5,null,0,2]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let* ((result (cj/--coverage-parse-simplecov file))
+ (lines (gethash "/foo.el" result)))
+ (should (= 3 (hash-table-count lines)))
+ (should-not (gethash 1 lines))
+ (should (gethash 2 lines))
+ (should-not (gethash 3 lines))
+ (should (gethash 4 lines))
+ (should-not (gethash 5 lines))
+ (should-not (gethash 6 lines))
+ (should (gethash 7 lines)))
+ (delete-file file))))
+
+;;; Boundary cases
+
+(ert-deftest test-coverage-parse-simplecov-empty-json ()
+ "Boundary: a JSON object with no test-name keys returns empty hash."
+ (let* ((content "{}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let ((result (cj/--coverage-parse-simplecov file)))
+ (should (hash-table-p result))
+ (should (= 0 (hash-table-count result))))
+ (delete-file file))))
+
+(ert-deftest test-coverage-parse-simplecov-file-with-spaces-in-path ()
+ "Boundary: filename with spaces is parsed as one key."
+ (let* ((content "{\"run\":{\"timestamp\":1,\"coverage\":{\"/my path/spaces.el\":[1]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let ((result (cj/--coverage-parse-simplecov file)))
+ (should (gethash "/my path/spaces.el" result)))
+ (delete-file file))))
+
+(ert-deftest test-coverage-parse-simplecov-all-zero-hits ()
+ "Boundary: file with every executable line at 0 returns empty set."
+ (let* ((content "{\"run\":{\"timestamp\":1,\"coverage\":{\"/foo.el\":[0,0,null,0]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let* ((result (cj/--coverage-parse-simplecov file))
+ (lines (gethash "/foo.el" result)))
+ (should (hash-table-p lines))
+ (should (= 0 (hash-table-count lines))))
+ (delete-file file))))
+
+(ert-deftest test-coverage-parse-simplecov-all-null-entries ()
+ "Boundary: all-null coverage array (no executable lines) returns empty set."
+ (let* ((content "{\"run\":{\"timestamp\":1,\"coverage\":{\"/foo.el\":[null,null,null]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let* ((result (cj/--coverage-parse-simplecov file))
+ (lines (gethash "/foo.el" result)))
+ (should (hash-table-p lines))
+ (should (= 0 (hash-table-count lines))))
+ (delete-file file))))
+
+(ert-deftest test-coverage-parse-simplecov-multiple-test-names-unioned ()
+ "Boundary: multiple top-level test-name keys are unioned for the same file."
+ (let* ((content "{\"run1\":{\"timestamp\":1,\"coverage\":{\"/foo.el\":[1,1,0,0]}},\"run2\":{\"timestamp\":2,\"coverage\":{\"/foo.el\":[0,0,1,1]}}}")
+ (file (test-coverage-parse-simplecov--write-temp-json content)))
+ (unwind-protect
+ (let* ((result (cj/--coverage-parse-simplecov file))
+ (lines (gethash "/foo.el" result)))
+ (should (= 4 (hash-table-count lines)))
+ (should (gethash 1 lines))
+ (should (gethash 2 lines))
+ (should (gethash 3 lines))
+ (should (gethash 4 lines)))
+ (delete-file file))))
+
+;;; Error cases
+
+(ert-deftest test-coverage-parse-simplecov-missing-file-errors ()
+ "Error: nonexistent file signals user-error."
+ (should-error (cj/--coverage-parse-simplecov "/nonexistent/path/xyz.json")
+ :type 'user-error))
+
+(ert-deftest test-coverage-parse-simplecov-malformed-json-errors ()
+ "Error: malformed JSON input signals user-error naming the file."
+ (let ((file (test-coverage-parse-simplecov--write-temp-json "{not valid json")))
+ (unwind-protect
+ (should-error (cj/--coverage-parse-simplecov file)
+ :type 'user-error)
+ (delete-file file))))
+
+(provide 'test-coverage-core--parse-simplecov)
+;;; test-coverage-core--parse-simplecov.el ends here
diff --git a/tests/test-coverage-elisp--detect.el b/tests/test-coverage-elisp--detect.el
index 1301495a..0a83449b 100644
--- a/tests/test-coverage-elisp--detect.el
+++ b/tests/test-coverage-elisp--detect.el
@@ -93,7 +93,7 @@ directories; others are files. Returns the temp directory path."
(should (eq 'elisp (plist-get backend :name)))
(should (functionp (plist-get backend :detect)))
(should (functionp (plist-get backend :run)))
- (should (functionp (plist-get backend :lcov-path)))))
+ (should (functionp (plist-get backend :report-path)))))
(provide 'test-coverage-elisp--detect)
;;; test-coverage-elisp--detect.el ends here