aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-duet-coverage-summary.el172
1 files changed, 172 insertions, 0 deletions
diff --git a/tests/test-duet-coverage-summary.el b/tests/test-duet-coverage-summary.el
new file mode 100644
index 0000000..c3e3e21
--- /dev/null
+++ b/tests/test-duet-coverage-summary.el
@@ -0,0 +1,172 @@
+;;; test-duet-coverage-summary.el --- Tests for the coverage summary script -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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