aboutsummaryrefslogtreecommitdiff
path: root/Makefile
blob: 9052026400e53e5e93253cec5871772c43e318a1 (plain)
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
# Makefile for duet.el
# Test targets delegate to tests/Makefile.
# setup / deps / compile / coverage / complexity / doctor operate at project root.
# Run 'make help' for available commands.

EASK ?= eask
EMACS_BATCH = $(EASK) emacs --batch
# Coverage / test loops need default-directory = tests/ so test files'
# relative paths (../duet.el, sibling test files) resolve as they do under
# tests/Makefile.
EMACS_BATCH_TESTS = $(EASK) emacs --batch --eval '(cd "tests/")'

TEST_DIR = tests
SOURCE_FILE = duet.el

# Transport CLIs DUET shells out to.  rsync is the stage-1 default; rclone,
# lftp, and unison arrive in later stages but `deps' installs the full set so
# a contributor's environment is ready ahead of the code that uses them.
TRANSPORT_CLIS = rsync rclone lftp unison

# Cyclomatic-complexity gate (scripts/duet-complexity.el).  The budget is soft
# (the design spec allows a written justification past it); override per run
# with `make complexity COMPLEXITY_THRESHOLD=12'.
COMPLEXITY_THRESHOLD ?= 10
COMPLEXITY_FILES = $(SOURCE_FILE)

# Coverage configuration
COVERAGE_DIR = .coverage
COVERAGE_FILE = $(COVERAGE_DIR)/simplecov.json

# Test-file list used by the coverage loop, mirroring tests/Makefile.
# Coverage runs every test file so the report represents the full suite.
ALL_TESTS = $(filter-out $(TEST_DIR)/test-bootstrap.el, \
                         $(wildcard $(TEST_DIR)/test-*.el))

# Include local overrides if present (per-machine knobs, not committed)
-include makefile-local

.PHONY: help test test-all test-unit test-integration test-file test-one test-name \
        count list validate lint check-deps clean clean-compiled clean-tests \
        setup deps deps-elisp deps-system compile coverage coverage-summary coverage-clean \
        complexity doctor test-live

help:
	@$(MAKE) -C $(TEST_DIR) help

# Test target delegations
test:
	@$(MAKE) -C $(TEST_DIR) test

test-all:
	@$(MAKE) -C $(TEST_DIR) test-all

test-unit:
	@$(MAKE) -C $(TEST_DIR) test-unit

test-integration:
	@$(MAKE) -C $(TEST_DIR) test-integration

test-file:
	@$(MAKE) -C $(TEST_DIR) test-file FILE="$(FILE)"

test-one:
	@$(MAKE) -C $(TEST_DIR) test-one TEST="$(TEST)"

test-name:
	@$(MAKE) -C $(TEST_DIR) test-name TEST="$(TEST)"

count:
	@$(MAKE) -C $(TEST_DIR) count

list:
	@$(MAKE) -C $(TEST_DIR) list

validate:
	@$(MAKE) -C $(TEST_DIR) validate

lint:
	@$(MAKE) -C $(TEST_DIR) lint

check-deps:
	@$(MAKE) -C $(TEST_DIR) check-deps

#
# Dependencies
#

# Install everything DUET needs: Emacs Lisp deps (via eask) plus the transport
# CLIs (via the system package manager).
deps: deps-elisp deps-system

# Install runtime + development Emacs Lisp dependencies via eask.
deps-elisp setup:
	@if ! command -v $(EASK) >/dev/null 2>&1; then \
		echo "[✗] eask not found on PATH"; \
		echo "    Install: npm install -g @emacs-eask/cli"; \
		echo "    Or:      https://emacs-eask.github.io/Getting-Started/Install-Eask/"; \
		exit 1; \
	fi
	@echo "[i] Installing Emacs Lisp dependencies via eask..."
	@$(EASK) install-deps --dev
	@echo "[✓] Emacs Lisp dependencies installed in .eask/"

# Install the transport CLIs via the detected system package manager.  pacman
# is checked first (the maintainer's environment); apt/dnf/zypper/brew follow.
# An unknown manager is not a failure — it prints the package list so the user
# can install by hand.
deps-system:
	@echo "[i] Installing transport CLIs: $(TRANSPORT_CLIS)"
	@if command -v pacman >/dev/null 2>&1; then \
		sudo pacman -S --needed $(TRANSPORT_CLIS); \
	elif command -v apt-get >/dev/null 2>&1; then \
		sudo apt-get install -y $(TRANSPORT_CLIS); \
	elif command -v dnf >/dev/null 2>&1; then \
		sudo dnf install -y $(TRANSPORT_CLIS); \
	elif command -v zypper >/dev/null 2>&1; then \
		sudo zypper install -y $(TRANSPORT_CLIS); \
	elif command -v brew >/dev/null 2>&1; then \
		brew install $(TRANSPORT_CLIS); \
	else \
		echo "[!] No supported package manager found."; \
		echo "    Install these yourself: $(TRANSPORT_CLIS)"; \
	fi

# Byte-compile duet.el — surfaces free-variable / unused-let / suspicious-call
# warnings that checkdoc and elisp-lint don't catch.  byte-compile-error-on-warn
# is t so any warning fails the build.
compile:
	@echo "[i] Byte-compiling $(SOURCE_FILE)..."
	@$(EMACS_BATCH) \
		--eval "(progn \
		  (setq byte-compile-error-on-warn t) \
		  (batch-byte-compile))" $(SOURCE_FILE)
	@echo "[✓] Compilation complete"

#
# Cyclomatic complexity (scripts/duet-complexity.el)
#

# Report per-function complexity and exit non-zero if any function exceeds the
# soft budget.  Advisory: run on demand, not wired into `make test'.
complexity:
	@echo "[i] Scanning complexity (budget = $(COMPLEXITY_THRESHOLD))..."
	@emacs -Q --batch -L scripts -l duet-complexity \
		--eval "(duet-complexity-batch '($(foreach f,$(COMPLEXITY_FILES),\"$(f)\")) $(COMPLEXITY_THRESHOLD))"

#
# Doctor — verify the runtime environment
#

# Report which transport CLIs are present and confirm duet loads.  Missing CLIs
# are warnings (rclone/lftp/unison land in later stages); a load failure is the
# only hard error.
doctor:
	@echo "[i] DUET doctor"
	@echo "Transport executables:"
	@for c in $(TRANSPORT_CLIS); do \
		if command -v $$c >/dev/null 2>&1; then \
			printf "  [✓] %-8s %s\n" "$$c" "$$(command -v $$c)"; \
		else \
			printf "  [!] %-8s missing (install via 'make deps-system')\n" "$$c"; \
		fi; \
	done
	@echo "Package load:"
	@if $(EMACS_BATCH) -l $(SOURCE_FILE) --eval "(require 'duet)" >/dev/null 2>&1; then \
		echo "  [✓] (require 'duet) succeeds"; \
	else \
		echo "  [✗] duet failed to load"; \
		exit 1; \
	fi

# Run env-gated live remote tests.  Skipped unless DUET_LIVE_TESTS is set, since
# they need real remote hosts and credentials.
test-live:
	@if [ -z "$$DUET_LIVE_TESTS" ]; then \
		echo "[i] DUET_LIVE_TESTS is unset — skipping live remote tests."; \
		echo "    Set DUET_LIVE_TESTS=1 (and the host/path vars the tests document) to run them."; \
	else \
		$(MAKE) -C $(TEST_DIR) test-name TEST='live'; \
	fi

#
# Coverage (undercover + simplecov JSON)
#
# Each test file runs in its own Emacs process (matching test-unit);
# tests/run-coverage-file.el instruments duet.el before the source is loaded,
# and undercover merges per-file results into a single simplecov JSON.

coverage: coverage-clean $(COVERAGE_DIR)
	@echo "[i] Cleaning .elc files so undercover can instrument source..."
	@find . -name "*.elc" -delete
	@echo "[i] Running coverage across $(words $(ALL_TESTS)) test file(s)..."
	@echo "    (slower than 'make test' — each file runs in its own Emacs)"
	@failed=0; \
	for test in $(ALL_TESTS); do \
		echo "  Coverage: $$test..."; \
		testfile=$$(basename $$test); \
		$(EMACS_BATCH_TESTS) \
			-l ert \
			-l run-coverage-file.el \
			-l ../$(SOURCE_FILE) \
			-l $$testfile \
			--eval "(ert-run-tests-batch-and-exit t)" || failed=$$((failed + 1)); \
	done; \
	if [ $$failed -gt 0 ]; then \
		echo "[!] $$failed test file(s) failed during coverage run"; \
		exit 1; \
	fi
	@coverage_file="$${COVERAGE_FILE_ACTUAL:-$(COVERAGE_FILE)}"; \
	[ -n "$$CI" ] && coverage_file="$(COVERAGE_DIR)/coveralls.json"; \
	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
	@# Print the human-readable summary after a local run.  CI emits
	@# coveralls.json (not simplecov.json) and the upload action reports
	@# instead, so skip the terminal summary there.
	@if [ -z "$$CI" ] && [ -f $(COVERAGE_FILE) ]; then \
		$(MAKE) --no-print-directory coverage-summary; \
	fi

# Print a human-readable summary of the SimpleCov report.
coverage-summary:
	@if [ ! -f $(COVERAGE_FILE) ]; then \
		echo "[!] No coverage report at $(COVERAGE_FILE). Run 'make coverage' first."; \
		exit 1; \
	fi
	@$(EMACS_BATCH) -L scripts -l coverage-summary \
		--eval '(duet-coverage-print-summary "$(COVERAGE_FILE)" (list "$(SOURCE_FILE)") "$(CURDIR)")'

coverage-clean:
	@rm -f $(COVERAGE_FILE)

$(COVERAGE_DIR):
	@mkdir -p $(COVERAGE_DIR)

#
# Cleaning
#

clean: clean-compiled clean-tests
	@rm -rf $(COVERAGE_DIR)

clean-compiled:
	@rm -f *.elc scripts/*.elc

clean-tests:
	@$(MAKE) -C $(TEST_DIR) clean