aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-05 03:10:50 -0500
committerCraig Jennings <c@cjennings.net>2026-05-05 03:10:50 -0500
commit77fe06b0c6f01d1d76bdf503c1b12286ac29aa0a (patch)
treedf8c6ddbd9f48c2bcc18b4e9ef18bb15ea0280f0
parent720d6597bf6751ee6d75bb224dd468aa2a78d7ee (diff)
downloadorg-drill-77fe06b0c6f01d1d76bdf503c1b12286ac29aa0a.tar.gz
org-drill-77fe06b0c6f01d1d76bdf503c1b12286ac29aa0a.zip
build: add make coverage target via undercover
I want to track test coverage as I work through the upstream issue backlog, so I added an undercover-based flow that mirrors how `make test-unit` already runs each file in its own Cask Emacs process. The Makefile gets `make coverage` and `make coverage-clean`. A new helper at `tests/run-coverage-file.el` instruments `org-drill.el` before the source is loaded. Undercover merges per-file results into a single simplecov JSON at `.coverage/simplecov.json`. I added `undercover` as a Cask development dep and `.coverage/` to `.gitignore` so the report stays local. I also renamed `make install` to `make setup`. The old name read like "deploy the package onto my system," but the target only installs Cask deps into the local `.cask/` directory. `setup` is closer to what it actually does, and all the internal `: install` prerequisites move with it. Baseline at this commit is 10.8% (208/1928 lines on org-drill.el).
-rw-r--r--.gitignore2
-rw-r--r--Cask3
-rw-r--r--Makefile66
-rw-r--r--tests/run-coverage-file.el33
4 files changed, 95 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore
index 7f89e14..f7fabe8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ org-drill.html
/docs
todo.org
+/.ai/
+/.coverage/
diff --git a/Cask b/Cask
index 5eaf201..c9ebcb2 100644
--- a/Cask
+++ b/Cask
@@ -6,6 +6,7 @@
(package-file "org-drill.el")
(development
- (depends-on "assess"))
+ (depends-on "assess")
+ (depends-on "undercover"))
(depends-on "org" "9.2")
diff --git a/Makefile b/Makefile
index b4ece5d..9f506b4 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@
# make robot - Run basic robot tests
# make robot-all - Run all robot tests
# make docker-test - Run tests in Docker (multiple Emacs versions)
-# make install - Install dependencies via Cask
+# make setup - Install dependencies via Cask
# make clean - Remove generated files
# Emacs binary to use (override with: make EMACS=emacs29 test)
@@ -38,9 +38,14 @@ EMACS_BATCH = $(EMACS) --batch --no-site-file --no-site-lisp
# Docker configuration
DOCKER_TAG=26
-.PHONY: help test test-all test-unit test-integration test-file test-name install build clean clean-elc
+# Coverage configuration
+COVERAGE_DIR = .coverage
+COVERAGE_FILE = $(COVERAGE_DIR)/simplecov.json
+
+.PHONY: help test test-all test-unit test-integration test-file test-name setup build clean clean-elc
.PHONY: robot robot-all robot-basic robot-leitner robot-all-card robot-spanish robot-explainer
.PHONY: docker-test test-cp test-git
+.PHONY: coverage coverage-clean
# Default target
help:
@@ -58,8 +63,12 @@ help:
@echo " make test-file FILE=<filename> - Run specific test file"
@echo " make test-name TEST=<pattern> - Run tests matching pattern"
@echo ""
+ @echo "Coverage:"
+ @echo " make coverage - Generate simplecov JSON at $(COVERAGE_FILE)"
+ @echo " make coverage-clean - Delete the coverage report file"
+ @echo ""
@echo "Advanced Targets:"
- @echo " make install - Install dependencies via Cask"
+ @echo " make setup - Install dependencies via Cask"
@echo " make build - Build package via Cask"
@echo " make docker-test - Run tests in Docker (multiple Emacs versions)"
@echo " make clean - Remove generated files"
@@ -82,7 +91,7 @@ help:
all: robot test-unit
# Install dependencies
-install:
+setup:
@if ! command -v $(CASK) >/dev/null 2>&1; then \
echo "[✗] Cask not found. Please install Cask first:"; \
echo " https://github.com/cask/cask"; \
@@ -104,7 +113,7 @@ test: robot test-unit
test-all: robot-all test-unit test-integration
# Run unit tests only
-test-unit: install
+test-unit: setup
@echo "[i] Running unit tests ($(words $(UNIT_TESTS)) files)..."
@failed=0; \
for test in $(UNIT_TESTS); do \
@@ -124,7 +133,7 @@ test-unit: install
fi
# Run integration tests only
-test-integration: install
+test-integration: setup
@echo "[i] Running integration tests ($(words $(INTEGRATION_TESTS)) files)..."
@if [ $(words $(INTEGRATION_TESTS)) -eq 0 ]; then \
echo "[i] No integration tests found"; \
@@ -149,7 +158,7 @@ test-integration: install
# Run specific test file
# Usage: make test-file FILE=org-drill-test.el
-test-file: install
+test-file: setup
ifndef FILE
@echo "[✗] Error: FILE parameter required"
@echo "Usage: make test-file FILE=org-drill-test.el"
@@ -167,7 +176,7 @@ endif
# Run specific test by name/pattern
# Usage: make test-name TEST=load-test
# make test-name TEST="find-*"
-test-name: install
+test-name: setup
ifndef TEST
@echo "[✗] Error: TEST parameter required"
@echo "Usage: make test-name TEST=load-test"
@@ -184,6 +193,47 @@ endif
@echo "[✓] Tests matching '$(TEST)' complete"
#
+# Coverage (undercover + simplecov JSON)
+#
+# Each unit-test file runs in its own Emacs process (matching `make
+# test-unit'); run-coverage-file.el instruments org-drill.el before the
+# source is loaded, and undercover merges per-file results into a single
+# simplecov JSON.
+
+coverage: coverage-clean setup $(COVERAGE_DIR)
+ @echo "[i] Cleaning .elc files so undercover can instrument source..."
+ @find . -name "*.elc" -delete
+ @echo "[i] Running coverage across $(words $(UNIT_TESTS)) unit-test file(s)..."
+ @echo " (slower than 'make test-unit' — each file runs in its own Emacs)"
+ @failed=0; \
+ for test in $(UNIT_TESTS); do \
+ echo " Coverage: $$test..."; \
+ $(EMACS_ENV) $(CASK) emacs --batch -q \
+ -l ert \
+ -l assess \
+ -l $(TEST_DIR)/run-coverage-file.el \
+ -l org-drill.el \
+ -l $$test \
+ -f ert-run-tests-batch-and-exit || failed=$$((failed + 1)); \
+ done; \
+ if [ $$failed -gt 0 ]; then \
+ echo "[!] $$failed test file(s) failed during coverage run"; \
+ exit 1; \
+ fi
+ @if [ -f $(COVERAGE_FILE) ]; then \
+ echo "[✓] Coverage report: $(COVERAGE_FILE) ($$(du -h $(COVERAGE_FILE) | cut -f1))"; \
+ else \
+ echo "[!] No coverage file produced; check that undercover is installed"; \
+ exit 1; \
+ fi
+
+coverage-clean:
+ @rm -f $(COVERAGE_FILE)
+
+$(COVERAGE_DIR):
+ @mkdir -p $(COVERAGE_DIR)
+
+#
# Robot Tests (Automated UI Tests)
#
diff --git a/tests/run-coverage-file.el b/tests/run-coverage-file.el
new file mode 100644
index 0000000..e328a3f
--- /dev/null
+++ b/tests/run-coverage-file.el
@@ -0,0 +1,33 @@
+;;; run-coverage-file.el --- Undercover setup for per-file coverage runs -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Loaded by `make coverage' before each test file runs, BEFORE
+;; org-drill.el is loaded. Instrumenting must happen first so the
+;; subsequent `(require 'org-drill)' or `-l org-drill.el' picks up the
+;; instrumented source.
+;;
+;; Coverage data is merged across per-file invocations into a single
+;; simplecov JSON at .coverage/simplecov.json.
+
+;;; Code:
+
+(unless (require 'undercover nil t)
+ (message "")
+ (message "ERROR: undercover not installed.")
+ (message "Add `undercover' to Cask as a development dep, then run `make setup'.")
+ (message "")
+ (kill-emacs 1))
+
+;; Force coverage collection in non-CI environments. Must be set after
+;; loading undercover because the library's top-level form
+;; `(setq undercover-force-coverage (getenv "UNDERCOVER_FORCE"))' would
+;; otherwise overwrite the value.
+(setq undercover-force-coverage t)
+
+(undercover "org-drill.el"
+ (:report-format 'simplecov)
+ (:report-file ".coverage/simplecov.json")
+ (:merge-report t)
+ (:send-report nil))
+
+;;; run-coverage-file.el ends here