banger/Makefile
Thales Maciel 5791466498
make: coverage-combined — merge unit-test and smoke covdata
Unit tests and the smoke suite cover different halves of the
codebase: unit for pure-Go branching (error paths, parsers, handler
wiring); smoke for the sudo / firecracker / dm-snap / real-KVM paths
unit tests physically can't reach. Separate reports each tell half
the story.

`make coverage-combined` runs the unit suite with
`-test.gocoverdir` pointed at a fresh binary-format dir, then
merges it with the existing smoke covdata via `go tool covdata
merge`. Modes must match; smoke uses the default 'set', so the
unit run aligns by NOT passing -covermode=atomic.

Output matches the existing `make coverage` layout (per-package
list + total) so the two targets read the same in CI.

`make coverage-combined-html` also emits an HTML report at
build/combined.cover.html for clicking through the uncovered
lines that neither suite touches.

Combined total right now: 72.7% (vs 37.7% unit-only / 49% daemon
via smoke).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:17:17 -03:00

213 lines
10 KiB
Makefile

SHELL := /usr/bin/env bash
GO ?= go
GOFMT ?= gofmt
INSTALL ?= install
PREFIX ?= $(HOME)/.local
BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/lib
DESTDIR ?=
BUILD_DIR ?= build
BUILD_BIN_DIR ?= $(BUILD_DIR)/bin
BUILD_MANUAL_DIR ?= $(BUILD_DIR)/manual
BANGER_BIN ?= $(BUILD_BIN_DIR)/banger
BANGERD_BIN ?= $(BUILD_BIN_DIR)/bangerd
VSOCK_AGENT_BIN ?= $(BUILD_BIN_DIR)/banger-vsock-agent
BINARIES := $(BANGER_BIN) $(BANGERD_BIN) $(VSOCK_AGENT_BIN)
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
# BUILD_INPUTS is everything that can change a binary's bytes: Go sources
# plus embedded assets (catalog.json, future static files). Listing
# everything is cheaper than missing a rebuild — go's own cache absorbs
# any redundant invocations.
BUILD_INPUTS := $(shell find cmd internal -type f | sort)
SHELL_SOURCES := $(shell find scripts -type f -name '*.sh' | sort)
SMOKE_DIR := $(BUILD_DIR)/smoke
SMOKE_BIN_DIR := $(SMOKE_DIR)/bin
SMOKE_COVER_DIR := $(SMOKE_DIR)/covdata
SMOKE_XDG_DIR := $(SMOKE_DIR)/xdg
SMOKE_SCRIPT := scripts/smoke.sh
VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
COMMIT ?= $(shell git rev-parse --verify HEAD 2>/dev/null || echo unknown)
BUILT_AT ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal/buildinfo.Commit=$(COMMIT) -X banger/internal/buildinfo.BuiltAt=$(BUILT_AT)
.DEFAULT_GOAL := help
.PHONY: help build banger bangerd test fmt tidy clean install uninstall lint lint-go lint-shell coverage coverage-html coverage-total coverage-combined coverage-combined-html smoke smoke-build smoke-coverage-html smoke-clean smoke-fresh
help:
@printf '%s\n' \
'Targets:' \
' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \
' make install Build and install banger, bangerd, and the companion vsock helper' \
' make uninstall Stop the daemon and remove installed binaries (leaves user state by default)' \
' make test Run go test ./...' \
' make coverage Run tests with coverage; print per-package + total' \
' make coverage-html Open a browsable per-line HTML report (writes coverage.html)' \
' make coverage-total Print just the total statement coverage (for scripts/CI)' \
' make coverage-combined Merge unit-test + smoke covdata; print per-package + total' \
' make coverage-combined-html HTML report of the merged unit+smoke coverage' \
' make lint Run gofmt + go vet + shellcheck (errors)' \
' make fmt Format Go sources under cmd/ and internal/' \
' make tidy Run go mod tidy' \
' make clean Remove built Go binaries and coverage artefacts' \
' make smoke Build instrumented binaries, run scripts/smoke.sh, report coverage (needs KVM + sudo)' \
' make smoke-fresh smoke-clean + smoke — forces first-install paths (migrations, image pull) into the coverage stamp' \
' make smoke-coverage-html HTML coverage report from the last smoke run' \
' make smoke-clean Remove the smoke build tree'
build: $(BINARIES)
$(BANGER_BIN): $(BUILD_INPUTS) go.mod go.sum
mkdir -p "$(BUILD_BIN_DIR)"
$(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(BANGER_BIN)" ./cmd/banger
$(BANGERD_BIN): $(BUILD_INPUTS) go.mod go.sum
mkdir -p "$(BUILD_BIN_DIR)"
$(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(BANGERD_BIN)" ./cmd/bangerd
$(VSOCK_AGENT_BIN): $(BUILD_INPUTS) go.mod go.sum
mkdir -p "$(BUILD_BIN_DIR)"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(VSOCK_AGENT_BIN)" ./cmd/banger-vsock-agent
test:
$(GO) test ./...
# Coverage targets use -coverpkg=./... so packages without their own
# tests still get counted when another package exercises them (common
# for daemon/* subpackages). coverage.out is gitignored.
coverage:
$(GO) test -coverpkg=./... -coverprofile=coverage.out ./...
@echo ''
@echo 'Per-package:'
@$(GO) tool cover -func=coverage.out | awk -F'\t+' '/^total:/ {total=$$NF; next} {pkg=$$1; sub("banger/", "", pkg); sub("/[^/]+:[0-9]+:$$", "", pkg); pkgs[pkg]+=1; covered[pkg]+=$$NF+0} END {for (p in pkgs) printf " %-40s %.1f%% (avg of %d funcs)\n", p, covered[p]/pkgs[p], pkgs[p] | "sort"; print ""; print "Total statement coverage:", total}'
coverage-html: coverage
$(GO) tool cover -html=coverage.out -o coverage.html
@echo 'wrote coverage.html'
coverage-total:
@$(GO) test -coverpkg=./... -coverprofile=coverage.out ./... >/dev/null 2>&1 && $(GO) tool cover -func=coverage.out | awk '/^total:/ {print $$NF}'
# coverage-combined unions unit-test coverage and smoke coverage into
# one report. Unit tests cover pure-Go logic (error branches, parsing,
# handler wiring); smoke covers the real sudo / firecracker / dm-snap
# paths that unit tests physically can't reach. Separately each tells
# half the story. Merged, this is the single "what's not being
# exercised at all" view.
#
# Requires an up-to-date smoke run (the target depends on smoke-build
# to rebuild instrumented binaries; re-run `make smoke` yourself if
# scenarios changed). Modes must match; smoke uses the default 'set',
# so the unit run below drops the default 'atomic' for alignment.
COMBINED_COVER_DIR := $(BUILD_DIR)/combined
UNIT_COVER_DIR := $(BUILD_DIR)/unit/covdata
coverage-combined:
@test -d "$(SMOKE_COVER_DIR)" && test "$$(ls -A $(SMOKE_COVER_DIR) 2>/dev/null)" || { \
echo 'no smoke covdata at $(SMOKE_COVER_DIR); run `make smoke` first' >&2; exit 1; \
}
rm -rf "$(UNIT_COVER_DIR)" "$(COMBINED_COVER_DIR)"
mkdir -p "$(UNIT_COVER_DIR)" "$(COMBINED_COVER_DIR)"
$(GO) test -cover -coverpkg=./... ./... -args -test.gocoverdir="$(abspath $(UNIT_COVER_DIR))" >/dev/null
$(GO) tool covdata merge -i="$(UNIT_COVER_DIR),$(SMOKE_COVER_DIR)" -o="$(COMBINED_COVER_DIR)"
$(GO) tool covdata textfmt -i="$(COMBINED_COVER_DIR)" -o="$(BUILD_DIR)/combined.cover.out"
@echo ''
@echo 'Per-package (merged unit + smoke):'
@$(GO) tool cover -func="$(BUILD_DIR)/combined.cover.out" | awk -F'\t+' '/^total:/ {total=$$NF; next} {pkg=$$1; sub("banger/", "", pkg); sub("/[^/]+:[0-9]+:$$", "", pkg); pkgs[pkg]+=1; covered[pkg]+=$$NF+0} END {for (p in pkgs) printf " %-40s %.1f%% (avg of %d funcs)\n", p, covered[p]/pkgs[p], pkgs[p] | "sort"; print ""; print "Total statement coverage:", total}'
coverage-combined-html: coverage-combined
$(GO) tool cover -html="$(BUILD_DIR)/combined.cover.out" -o "$(BUILD_DIR)/combined.cover.html"
@echo 'wrote $(BUILD_DIR)/combined.cover.html'
lint: lint-go lint-shell
lint-go:
@unformatted="$$($(GOFMT) -l $(GO_SOURCES))"; \
if [ -n "$$unformatted" ]; then \
printf 'gofmt: the following files are not formatted:\n%s\n' "$$unformatted" >&2; \
exit 1; \
fi
$(GO) vet ./...
lint-shell:
@command -v shellcheck >/dev/null 2>&1 || { echo 'shellcheck is required for make lint-shell' >&2; exit 1; }
shellcheck --severity=error $(SHELL_SOURCES)
fmt:
$(GOFMT) -w $(GO_SOURCES)
tidy:
$(GO) mod tidy
clean:
rm -rf "$(BUILD_BIN_DIR)" coverage.out coverage.html
# Smoke test suite. Builds the three banger binaries with -cover
# instrumentation under $(SMOKE_BIN_DIR), runs scripts/smoke.sh
# with GOCOVERDIR pointed at $(SMOKE_COVER_DIR), and prints the
# resulting coverage. The smoke script fully isolates state via
# XDG_* env vars pointing at a mktemp'd root, so the invoking
# user's real banger install stays untouched.
#
# Requires a KVM-capable Linux host with sudo; fails fast via
# `banger doctor` when either is missing. This is a pre-release
# gate, not CI — the Go test suite is what runs everywhere.
smoke-build: $(SMOKE_BIN_DIR)/.built
$(SMOKE_BIN_DIR)/.built: $(BUILD_INPUTS) go.mod go.sum
mkdir -p "$(SMOKE_BIN_DIR)"
$(GO) build -cover -ldflags '$(GO_LDFLAGS)' -o "$(SMOKE_BIN_DIR)/banger" ./cmd/banger
$(GO) build -cover -ldflags '$(GO_LDFLAGS)' -o "$(SMOKE_BIN_DIR)/bangerd" ./cmd/bangerd
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(SMOKE_BIN_DIR)/banger-vsock-agent" ./cmd/banger-vsock-agent
touch "$@"
smoke: smoke-build
rm -rf "$(SMOKE_COVER_DIR)"
mkdir -p "$(SMOKE_COVER_DIR)" "$(SMOKE_XDG_DIR)"
BANGER_SMOKE_BIN_DIR="$(abspath $(SMOKE_BIN_DIR))" \
BANGER_SMOKE_COVER_DIR="$(abspath $(SMOKE_COVER_DIR))" \
BANGER_SMOKE_XDG_DIR="$(abspath $(SMOKE_XDG_DIR))" \
bash "$(SMOKE_SCRIPT)"
@echo ''
@echo 'Smoke coverage:'
@$(GO) tool covdata percent -i="$(SMOKE_COVER_DIR)"
smoke-coverage-html: smoke
$(GO) tool covdata textfmt -i="$(SMOKE_COVER_DIR)" -o="$(SMOKE_DIR)/cover.out"
$(GO) tool cover -html="$(SMOKE_DIR)/cover.out" -o "$(SMOKE_DIR)/cover.html"
@echo 'wrote $(SMOKE_DIR)/cover.html'
smoke-clean:
rm -rf "$(SMOKE_DIR)"
# smoke-fresh wipes everything under $(SMOKE_DIR) (instrumented
# binaries, coverage pods, persisted XDG state) and runs a full
# smoke from scratch. Useful before a release tag: the regular
# `make smoke` reuses the XDG state across runs to skip the ~290MB
# image pull, which is fast but leaves migrations and image-upsert
# paths cold on every run after the first. smoke-fresh pays the
# time cost to stamp those paths into the coverage report too.
smoke-fresh: smoke-clean smoke
install: build
mkdir -p "$(DESTDIR)$(BINDIR)"
mkdir -p "$(DESTDIR)$(LIBDIR)/banger"
$(INSTALL) -m 0755 "$(BANGER_BIN)" "$(DESTDIR)$(BINDIR)/banger"
$(INSTALL) -m 0755 "$(BANGERD_BIN)" "$(DESTDIR)$(BINDIR)/bangerd"
$(INSTALL) -m 0755 "$(VSOCK_AGENT_BIN)" "$(DESTDIR)$(LIBDIR)/banger/banger-vsock-agent"
# uninstall stops a running daemon (if any) and removes the installed
# binaries. It does NOT touch user data (config, SSH keys, VM state,
# image/kernel caches) — rm -rf those paths manually if wanted; they
# are printed for convenience.
uninstall:
@if [ -x "$(DESTDIR)$(BINDIR)/banger" ]; then \
"$(DESTDIR)$(BINDIR)/banger" daemon stop >/dev/null 2>&1 || true; \
fi
rm -f "$(DESTDIR)$(BINDIR)/banger" "$(DESTDIR)$(BINDIR)/bangerd"
rm -rf "$(DESTDIR)$(LIBDIR)/banger"
@printf '\nRemoved binaries. User data is preserved at:\n'
@printf ' ~/.config/banger/ (config, ssh keys)\n'
@printf ' ~/.local/state/banger/ (VMs, images, kernels, db, logs)\n'
@printf ' ~/.cache/banger/ (OCI layer cache)\n'
@printf '\nDelete those paths manually if you want a full purge.\n'