diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/run-coverage-file.el | 40 | ||||
| -rw-r--r-- | tests/test-coverage-core--backend-registry.el | 28 | ||||
| -rw-r--r-- | tests/test-coverage-core--parse-lcov.el | 149 | ||||
| -rw-r--r-- | tests/test-coverage-core--parse-simplecov.el | 153 | ||||
| -rw-r--r-- | tests/test-coverage-elisp--detect.el | 2 |
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 |
