;;; test-duet-coverage-summary.el --- Tests for the coverage summary script -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Unit tests for scripts/coverage-summary.el — the batch helper behind ;; `make coverage-summary'. Drives the SimpleCov parsers, the per-file record ;; builder, basename/path key matching, and the formatted text on synthetic ;; reports written to temp files (no committed fixtures, no prior coverage ;; run). Covers the normal percentage path, boundary cases (fully covered, no ;; executable lines, multi-key union, basename match), and error cases (missing ;; file, malformed JSON, a tracked source absent from the report as 0%). ;;; Code: (require 'ert) (load (expand-file-name "../scripts/coverage-summary.el" (file-name-directory (or load-file-name buffer-file-name))) nil t) (defconst test-duet-cov-root "/tmp/duet-cov-test/" "Synthetic project root used to anchor source-file paths in fixtures.") (defun test-duet-cov--write (json) "Write JSON to a temp file and return its path." (let ((f (make-temp-file "duet-cov" nil ".json"))) (with-temp-file f (insert json)) f)) (defun test-duet-cov--report (hits-array) "Write a SimpleCov report mapping duet.el under the test root to HITS-ARRAY. HITS-ARRAY is a JSON array literal string such as \"[null, 1, 0]\"." (test-duet-cov--write (format "{\"undercover\": {\"coverage\": {\"%sduet.el\": %s}}}" test-duet-cov-root hits-array))) (defmacro test-duet-cov--with (json-or-path varname &rest body) "Bind VARNAME to a report path from JSON-OR-PATH, run BODY, then delete it." (declare (indent 2)) `(let ((,varname ,json-or-path)) (unwind-protect (progn ,@body) (when (file-exists-p ,varname) (delete-file ,varname))))) ;;; --- Parsers --- (ert-deftest test-duet-coverage-parse-counts-only-hit-lines () "`parse-simplecov' records only lines whose hit count is greater than zero." (test-duet-cov--with (test-duet-cov--report "[null, 1, 0, 3, null, 0]") f (let ((tbl (duet-coverage--parse-simplecov f)) (key (expand-file-name "duet.el" test-duet-cov-root))) (should (= 2 (duet-coverage--line-count tbl key)))))) (ert-deftest test-duet-coverage-executable-counts-all-numeric-lines () "`executable-lines' records every numeric entry, hit or not." (test-duet-cov--with (test-duet-cov--report "[null, 1, 0, 3, null, 0]") f (let ((tbl (duet-coverage--executable-lines f)) (key (expand-file-name "duet.el" test-duet-cov-root))) (should (= 4 (duet-coverage--line-count tbl key)))))) (ert-deftest test-duet-coverage-parse-unions-across-test-keys () "Coverage under multiple top-level keys is unioned (undercover :merge-report)." (test-duet-cov--with (test-duet-cov--write (format (concat "{\"a\": {\"coverage\": {\"%sduet.el\": [1, 0, null]}}," " \"b\": {\"coverage\": {\"%sduet.el\": [0, 1, null]}}}") test-duet-cov-root test-duet-cov-root)) f (let ((tbl (duet-coverage--parse-simplecov f)) (key (expand-file-name "duet.el" test-duet-cov-root))) (should (= 2 (duet-coverage--line-count tbl key)))))) ;;; --- Records --- (ert-deftest test-duet-coverage-record-normal-percentage () "A half-covered file reports covered/total and a 50% figure." (test-duet-cov--with (test-duet-cov--report "[null, 1, 0, 3, null, 0]") f (let ((r (car (duet-coverage--records f '("duet.el") test-duet-cov-root)))) (should (plist-get r :present)) (should (= 2 (plist-get r :covered))) (should (= 4 (plist-get r :total))) (should (< 49.9 (plist-get r :percent) 50.1))))) (ert-deftest test-duet-coverage-record-fully-covered-is-100 () "Every executable line hit reports 100%." (test-duet-cov--with (test-duet-cov--report "[1, 2, 3]") f (let ((r (car (duet-coverage--records f '("duet.el") test-duet-cov-root)))) (should (= 3 (plist-get r :covered))) (should (= 3 (plist-get r :total))) (should (< 99.9 (plist-get r :percent) 100.1))))) (ert-deftest test-duet-coverage-record-no-executable-lines-is-100 () "A present file with no executable lines is 100% — nothing left uncovered." (test-duet-cov--with (test-duet-cov--report "[null, null]") f (let ((r (car (duet-coverage--records f '("duet.el") test-duet-cov-root)))) (should (plist-get r :present)) (should (= 0 (plist-get r :total))) (should (< 99.9 (plist-get r :percent) 100.1))))) (ert-deftest test-duet-coverage-record-missing-source-counts-as-zero () "A tracked source absent from the report is flagged missing and counts as 0%." (test-duet-cov--with (test-duet-cov--write (format "{\"u\": {\"coverage\": {\"%sother.el\": [1, 2]}}}" test-duet-cov-root)) f (let ((r (car (duet-coverage--records f '("duet.el") test-duet-cov-root)))) (should-not (plist-get r :present)) (should (= 0 (plist-get r :covered))) (should (= 0.0 (plist-get r :percent)))))) (ert-deftest test-duet-coverage-record-matches-by-basename () "A report whose path is written differently still resolves by basename." (test-duet-cov--with (test-duet-cov--write "{\"u\": {\"coverage\": {\"duet.el\": [1, 0, 1]}}}") f (let ((r (car (duet-coverage--records f '("duet.el") test-duet-cov-root)))) (should (plist-get r :present)) (should (= 2 (plist-get r :covered))) (should (= 3 (plist-get r :total)))))) ;;; --- Error handling --- (ert-deftest test-duet-coverage-missing-report-errors () "Parsing a nonexistent report signals a user-error." (should-error (duet-coverage--parse-simplecov "/nonexistent/duet-cov.json") :type 'user-error)) (ert-deftest test-duet-coverage-malformed-json-errors () "Malformed JSON signals a user-error rather than a raw json error." (test-duet-cov--with (test-duet-cov--write "{ this is not json") f (should-error (duet-coverage--parse-simplecov f) :type 'user-error))) ;;; --- Formatted text --- (ert-deftest test-duet-coverage-summary-text-reports-both-numbers () "The summary names the file, a line-coverage figure, and source coverage." (test-duet-cov--with (test-duet-cov--report "[null, 1, 0, 3, null, 0]") f (let ((txt (duet-coverage-summary-text f '("duet.el") test-duet-cov-root))) (should (string-match-p "duet\\.el" txt)) (should (string-match-p "Line coverage" txt)) (should (string-match-p "Source coverage" txt)) (should (string-match-p "50\\.0%" txt))))) (ert-deftest test-duet-coverage-summary-text-lists-missing-source () "The summary's missing section names a tracked source absent from the report." (test-duet-cov--with (test-duet-cov--write (format "{\"u\": {\"coverage\": {\"%sother.el\": [1, 2]}}}" test-duet-cov-root)) f (let ((txt (duet-coverage-summary-text f '("duet.el") test-duet-cov-root))) (should (string-match-p "Not in coverage report" txt)) (should (string-match-p "duet\\.el" txt))))) (provide 'test-duet-coverage-summary) ;;; test-duet-coverage-summary.el ends here