diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-06 10:31:30 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-06 10:31:30 -0500 |
| commit | 95dbb5abdbb746cf5da9f7926740d17205ac8d55 (patch) | |
| tree | 0e807d43d8f8ce32b3790efc716c433d35ceca3c /Makefile | |
| parent | 6ecd1e9bf1e3d0cdd3861077318541e193ca4532 (diff) | |
| download | duet-95dbb5abdbb746cf5da9f7926740d17205ac8d55.tar.gz duet-95dbb5abdbb746cf5da9f7926740d17205ac8d55.zip | |
build: add Eask, test harness, and dev tooling
I brought the skeleton up to a working package baseline (Phase 0 in the design spec). Eask defines the package and its dev deps. A root Makefile delegates test targets to tests/Makefile and adds compile, coverage, lint, doctor, and clean, matching the layout the other packages use.
deps installs both halves DUET needs: the Emacs Lisp deps via eask, and the transport CLIs (rsync, rclone, lftp, unison) via the system package manager, so a contributor's environment is ready before the code that shells out to them.
make complexity runs a small homegrown McCabe branch counter (scripts/duet-complexity.el). No off-the-shelf tool measures Emacs Lisp: lizard doesn't support it and codemetrics is an interactive overlay, so DUET owns one. The counting is pure and covered by Normal/Boundary/Error tests. The budget is soft and the target is advisory.
The ERT harness (bootstrap, check-deps, per-file undercover coverage) and a smoke test prove the loop works end to end.
Diffstat (limited to 'Makefile')
| -rw-r--r-- | Makefile | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9052026 --- /dev/null +++ b/Makefile @@ -0,0 +1,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 |
