1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
# Makefile for pearl.el test suite
# Usage:
# make test - Run all tests (excluding :slow tagged)
# make test-all - Run every test, including :slow tagged
# make test-file FILE=mapping - Run tests in one file
# make test-one TEST=name - Run one specific test
# make test-unit - Run unit tests only
# make test-integration - Run integration tests only
# make clean - Remove byte-compiled files
# Configuration
EASK ?= eask
# eask treats the CWD as its workspace and reads .eask/ from there. All
# eask invocations must run from project root so the project's .eask/
# is picked up. The (cd "tests/") --eval restores Emacs default-directory
# so test files' relative paths (../pearl.el, test-bootstrap.el)
# resolve correctly.
PROJECT_ROOT := $(abspath ..)
EMACS_BATCH = cd $(PROJECT_ROOT) && $(EASK) emacs --batch --eval '(cd "tests/")'
# Include local overrides if present (per-machine knobs, not committed)
-include makefile-local
# Test files
ALL_TESTS = $(filter-out test-bootstrap.el,$(wildcard test-*.el))
UNIT_TESTS = $(filter-out test-integration-%.el,$(ALL_TESTS))
INTEGRATION_TESTS = $(wildcard test-integration-*.el)
# ERT selector that excludes tests tagged :slow. Applied to default
# test runs so a slow integration suite doesn't dominate the fast
# feedback path. test-all runs everything; test-one and test-name
# honor the user-supplied pattern verbatim.
ERT_FAST_SELECTOR = (ert-run-tests-batch-and-exit '(not (tag :slow)))
# Colors for output (if terminal supports it)
RED = \033[0;31m
GREEN = \033[0;32m
YELLOW = \033[1;33m
NC = \033[0m
.PHONY: all test test-all test-file test-one test-name test-unit test-integration validate lint clean help check-deps count list
all: test
# Verify eask + installed deps are available
check-deps:
@if ! command -v $(EASK) >/dev/null 2>&1; then \
printf "$(RED)Error: eask not found on PATH$(NC)\n"; \
echo "Install: npm install -g @emacs-eask/cli"; \
echo " or: https://emacs-eask.github.io/Getting-Started/Install-Eask/"; \
exit 1; \
fi
@if [ ! -d $(PROJECT_ROOT)/.eask ]; then \
printf "$(YELLOW)Warning: .eask not found — run 'make setup' from project root$(NC)\n"; \
exit 1; \
fi
@$(EMACS_BATCH) -l check-deps.el >$(PROJECT_ROOT)/tests/check-deps-output.log 2>&1 || { \
printf "$(RED)Error: required Emacs Lisp test dependencies are missing$(NC)\n"; \
cat $(PROJECT_ROOT)/tests/check-deps-output.log; \
exit 1; \
}
@printf "$(GREEN)✓ eask available, required Emacs Lisp deps loadable$(NC)\n"
# Run all tests (excluding :slow)
test: check-deps
@printf "$(YELLOW)Running all tests ($(words $(ALL_TESTS)) files, excluding :slow)...$(NC)\n"
@$(MAKE) --no-print-directory test-unit
@$(MAKE) --no-print-directory test-integration
@printf "$(GREEN)[✓] All tests complete$(NC)\n"
# Run every test, including :slow tagged
test-all: check-deps
@printf "$(YELLOW)Running all tests including :slow ($(words $(ALL_TESTS)) files)...$(NC)\n"
@failed=0; \
for testfile in $(ALL_TESTS); do \
echo " Testing $$testfile..."; \
$(EMACS_BATCH) -l ert -l "$$testfile" \
--eval '(ert-run-tests-batch-and-exit t)' || failed=$$((failed + 1)); \
done; \
if [ $$failed -eq 0 ]; then \
printf "$(GREEN)[✓] All tests passed$(NC)\n"; \
else \
printf "$(RED)[✗] $$failed test file(s) failed$(NC)\n"; \
exit 1; \
fi
# Run tests in one file
test-file: check-deps
ifndef FILE
@printf "$(RED)Error: FILE not specified$(NC)\n"
@echo "Usage: make test-file FILE=mapping"
@echo " make test-file FILE=test-pearl-mapping.el"
@exit 1
endif
@TESTFILE=$$(find . -maxdepth 1 -name "*$(FILE)*.el" -type f | head -1 | sed 's|^\./||'); \
if [ -z "$$TESTFILE" ]; then \
printf "$(RED)Error: No test file matching '$(FILE)' found$(NC)\n"; \
exit 1; \
fi; \
printf "$(YELLOW)Running tests in $$TESTFILE...$(NC)\n"; \
$(EMACS_BATCH) -l ert -l "$$TESTFILE" \
--eval "$(ERT_FAST_SELECTOR)" 2>&1 | tee $(PROJECT_ROOT)/tests/test-file-output.log; \
if [ $$? -eq 0 ]; then \
printf "$(GREEN)✓ All tests in $$TESTFILE passed!$(NC)\n"; \
else \
printf "$(RED)✗ Some tests failed.$(NC)\n"; \
exit 1; \
fi
# Run one specific test (fuzzy match by name)
test-one: check-deps
ifndef TEST
@printf "$(RED)Error: TEST not specified$(NC)\n"
@echo "Usage: make test-one TEST=priority"
@echo " make test-one TEST=test-pearl-map-priority-urgent"
@exit 1
endif
@printf "$(YELLOW)Searching for test matching '$(TEST)'...$(NC)\n"
@TESTFILE=$$(grep -l "ert-deftest.*$(TEST)" test-*.el 2>/dev/null | head -1); \
if [ -z "$$TESTFILE" ]; then \
printf "$(RED)Error: No test matching '$(TEST)' found$(NC)\n"; \
exit 1; \
fi; \
TESTNAME=$$(grep "ert-deftest.*$(TEST)" "$$TESTFILE" | sed 's/^(ert-deftest \([^ ]*\).*/\1/' | head -1); \
printf "$(YELLOW)Running test '$$TESTNAME' in $$TESTFILE...$(NC)\n"; \
$(EMACS_BATCH) -l ert -l "$$TESTFILE" \
--eval "(ert-run-tests-batch-and-exit \"$$TESTNAME\")" 2>&1; \
if [ $$? -eq 0 ]; then \
printf "$(GREEN)✓ Test $$TESTNAME passed!$(NC)\n"; \
else \
printf "$(RED)✗ Test $$TESTNAME failed.$(NC)\n"; \
exit 1; \
fi
# Run only unit tests (excluding :slow)
test-unit: check-deps
@printf "$(YELLOW)Running unit tests ($(words $(UNIT_TESTS)) files, excluding :slow)...$(NC)\n"
@failed=0; \
for testfile in $(UNIT_TESTS); do \
echo " Testing $$testfile..."; \
$(EMACS_BATCH) -l ert -l "$$testfile" \
--eval "$(ERT_FAST_SELECTOR)" || failed=$$((failed + 1)); \
done; \
if [ $$failed -eq 0 ]; then \
printf "$(GREEN)[✓] All unit tests passed$(NC)\n"; \
else \
printf "$(RED)[✗] $$failed unit test file(s) failed$(NC)\n"; \
exit 1; \
fi
# Run only integration tests (excluding :slow)
test-integration: check-deps
@printf "$(YELLOW)Running integration tests ($(words $(INTEGRATION_TESTS)) files, excluding :slow)...$(NC)\n"
@if [ -z "$(INTEGRATION_TESTS)" ]; then \
printf "$(YELLOW) (no integration test files yet)$(NC)\n"; \
fi
@failed=0; \
for testfile in $(INTEGRATION_TESTS); do \
echo " Testing $$testfile..."; \
$(EMACS_BATCH) -l ert -l "$$testfile" \
--eval "$(ERT_FAST_SELECTOR)" || failed=$$((failed + 1)); \
done; \
if [ $$failed -eq 0 ]; then \
printf "$(GREEN)[✓] All integration tests passed$(NC)\n"; \
else \
printf "$(RED)[✗] $$failed integration test file(s) failed$(NC)\n"; \
exit 1; \
fi
# Run tests matching a name pattern (ERT selector)
test-name: check-deps
ifndef TEST
@printf "$(RED)Error: TEST not specified$(NC)\n"
@echo "Usage: make test-name TEST=test-pearl-map"
@echo " make test-name TEST='test-pearl-map-*'"
@exit 1
endif
@printf "$(YELLOW)Running tests matching pattern: $(TEST)...$(NC)\n"
@$(EMACS_BATCH) -l ert \
--eval "(dolist (f (directory-files \".\" t \"^test-.*\\\\.el$$\")) (load f))" \
--eval '(ert-run-tests-batch-and-exit "$(TEST)")'
# Count tests
count:
@echo "Test file inventory:"
@for f in $(ALL_TESTS); do \
count=$$(grep -c "^(ert-deftest" "$$f" 2>/dev/null || echo 0); \
printf "%3d tests - %s\n" "$$count" "$$f"; \
done | sort -rn
@total=$$(find . -name "test-*.el" -exec grep -c "^(ert-deftest" {} \; | awk '{sum+=$$1} END {print sum}'); \
printf "$(GREEN)Total: $$total tests across $(words $(ALL_TESTS)) files$(NC)\n"
# List all available tests
list:
@echo "Available tests:"
@grep -h "^(ert-deftest" test-*.el | sed 's/^(ert-deftest \([^ ]*\).*/ \1/' | sort
# Validate Emacs Lisp syntax (parens balance — no deps needed)
validate:
@printf "$(YELLOW)Validating Emacs Lisp syntax...$(NC)\n"
@failed=0; \
total=0; \
for file in ../pearl.el test-*.el; do \
if [ -f "$$file" ] && [ ! -d "$$file" ]; then \
total=$$((total + 1)); \
output=$$(emacs --batch -Q --eval "(progn \
(setq byte-compile-error-on-warn nil) \
(find-file \"$$file\") \
(condition-case err \
(progn \
(check-parens) \
(message \"✓ $$file - parentheses balanced\")) \
(error \
(message \"✗ $$file: %s\" (error-message-string err)) \
(kill-emacs 1))))" 2>&1 | grep -E '(✓|✗)'); \
if [ $$? -eq 0 ]; then \
printf "$(GREEN)$$output$(NC)\n"; \
else \
printf "$(RED)$$output$(NC)\n"; \
failed=$$((failed + 1)); \
fi; \
fi; \
done; \
if [ $$failed -eq 0 ]; then \
printf "$(GREEN)✓ All $$total files validated successfully$(NC)\n"; \
else \
printf "$(RED)✗ $$failed of $$total files failed validation$(NC)\n"; \
exit 1; \
fi
# Comprehensive linting with elisp-lint (via eask-installed dev dep).
# Validators disabled and why:
# - checkdoc: covered by `eask lint checkdoc' as its own MELPA-prep step.
# - package-lint: covered by `eask lint package' as its own step.
# - indent-character: project uses spaces; validator defaults to requiring tabs.
# - fill-column: validator default (70) is stricter than this project wants.
# - indent: false positives on dash threading macros (`->', `->>').
lint: check-deps
@printf "$(YELLOW)Running elisp-lint...$(NC)\n"
@$(EMACS_BATCH) \
-l $(PROJECT_ROOT)/pearl.el \
--eval "(require 'elisp-lint)" \
-f elisp-lint-files-batch \
--no-checkdoc \
--no-package-lint \
--no-indent-character \
--no-fill-column \
--no-indent \
$(PROJECT_ROOT)/pearl.el 2>&1; \
if [ $$? -eq 0 ]; then \
printf "$(GREEN)✓ Linting completed successfully$(NC)\n"; \
else \
printf "$(RED)✗ Linting found issues (see above)$(NC)\n"; \
exit 1; \
fi
# Clean byte-compiled files
clean:
@printf "$(YELLOW)Cleaning byte-compiled files...$(NC)\n"
@rm -f *.elc ../*.elc
@rm -f check-deps-output.log test-output.log test-file-output.log test-unit-output.log test-integration-output.log
@printf "$(GREEN)✓ Cleaned$(NC)\n"
# Show help
help:
@echo "pearl Test Suite Makefile"
@echo ""
@echo "Usage:"
@echo " make test - Run all tests, excluding :slow"
@echo " make test-all - Run all tests including :slow"
@echo " make test-unit - Run unit tests only (excluding :slow)"
@echo " make test-integration - Run integration tests only (excluding :slow)"
@echo " make test-file FILE=mapping - Run tests in one file (fuzzy match)"
@echo " make test-one TEST=priority - Run one specific test (fuzzy match)"
@echo " make test-name TEST=pattern - Run tests matching ERT name pattern"
@echo " make validate - Validate Emacs Lisp syntax (parens balance)"
@echo " make lint - Comprehensive linting with elisp-lint"
@echo " make count - Count tests per file"
@echo " make list - List all test names"
@echo " make clean - Remove byte-compiled files and logs"
@echo " make check-deps - Verify eask + loadable Emacs Lisp deps"
@echo " make help - Show this help message"
@echo ""
@echo "Project-root targets (run from project root):"
@echo " make setup - Install all deps via eask"
@echo " make compile - Byte-compile pearl.el"
@echo " make coverage - Generate simplecov JSON via undercover"
@echo ""
@echo "Tagging tests as :slow:"
@echo " (ert-deftest test-foo () :tags '(:slow) ...) — excluded by default"
@echo " Run with 'make test-all' to include them."
@echo ""
@echo "Environment variables:"
@echo " EASK - eask executable (default: eask)"
|