140 lines
9.2 KiB
Makefile
140 lines
9.2 KiB
Makefile
# Backend battery as one command. `make battery` IS the manual battery every session used to retype;
|
|
# it is the entry point CI will call, so what runs locally and what runs there cannot drift.
|
|
#
|
|
# Run from backend/. Toolchain and linter are PINNED here (never "latest"): the linter's findings are a
|
|
# gate, and a gate that changes under you on someone else's machine is not a gate.
|
|
|
|
# THIS_MAKEFILE is captured FIRST, before anything could `include`: $(lastword $(MAKEFILE_LIST)) names this
|
|
# file only until the first include, after which it names the included one. realpath, not abspath, because
|
|
# abspath does not resolve symlinks — a Makefile symlinked from the repo root would have anchored the copy
|
|
# below at the root and swept the whole repository, .env included.
|
|
THIS_MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
|
|
|
GO ?= go
|
|
GO_MIN_VERSION := 1.26.4
|
|
GOLANGCI_LINT ?= golangci-lint
|
|
GOLANGCI_VERSION := 2.12.2
|
|
# backend/bin/ is already gitignored, so the vet tool leaves no untracked file behind.
|
|
TMVET := bin/tmvet
|
|
|
|
.PHONY: build vet fmt lint test mutations battery battery-stand tools-check
|
|
|
|
build: tools-check
|
|
$(GO) build ./...
|
|
|
|
# `-vettool` REPLACES the standard vet suite rather than adding to it (measured: a copylocks finding
|
|
# that plain `go vet` reports disappears under -vettool), so both passes have to run. Both tag sets too:
|
|
# the paid `live` tests compile only under -tags live and would otherwise rot unseen.
|
|
vet:
|
|
$(GO) vet ./...
|
|
$(GO) vet -tags live ./...
|
|
$(GO) build -o $(TMVET) ./cmd/tmvet
|
|
$(GO) vet -vettool=$(TMVET) ./...
|
|
$(GO) vet -tags live -vettool=$(TMVET) ./...
|
|
|
|
# `gofmt -l` exits 0 even when it names files, so the emptiness of its output is the assertion.
|
|
fmt:
|
|
@test -z "$$(gofmt -l .)" || { echo "gofmt: not formatted:"; gofmt -l .; exit 1; }
|
|
|
|
tools-check:
|
|
@$(GO) version | grep -qE 'go1\.(2[6-9]|[3-9][0-9])' || { \
|
|
echo "Go $(GO_MIN_VERSION)+ required (go.mod floor); got: $$($(GO) version)"; exit 1; }
|
|
@$(GOLANGCI_LINT) --version 2>/dev/null | grep -q " $(GOLANGCI_VERSION) " || { \
|
|
echo "golangci-lint $(GOLANGCI_VERSION) required (findings are version-dependent)."; \
|
|
echo "install: https://github.com/golangci/golangci-lint/releases/tag/v$(GOLANGCI_VERSION)"; exit 1; }
|
|
|
|
lint: tools-check
|
|
$(GOLANGCI_LINT) run --timeout=15m ./...
|
|
|
|
# -race needs cgo (a C toolchain). If it is missing this fails loudly — dropping -race would turn a
|
|
# missing toolchain into a green run that proved less than it claims.
|
|
#
|
|
# The PER-PACKAGE timeout is stated rather than left at Go's 10-minute default, because under the load
|
|
# this project treats as normal the default is not far enough away to be one. `internal/pipeline` under
|
|
# -race has been measured from ~95 s on a quiet host to ~560 s with three parallel agent sessions
|
|
# sharing it — a 6x spread on load alone — and past the default the whole battery ends in a per-package
|
|
# timeout that looks like a failure and is not one (it has done so twice). Raising the ceiling weakens
|
|
# no assertion: the same tests run, to completion. A package that genuinely hangs still fails, twenty
|
|
# minutes later.
|
|
test:
|
|
$(GO) test ./... -race -count=1 -timeout=20m
|
|
|
|
# The mutation gate: the catalogue's battery subset, planted and run against a COPY of this tree.
|
|
#
|
|
# WHY IT EXISTS. The catalogue proved that a mutation EXISTS, not that anything catches it (backlog row
|
|
# 313): 211 of its entries were verified only by grepping for their anchor, so a green catalogue read as
|
|
# "the pin holds" while saying "the planting still applies". Running all of it is not the answer:
|
|
# 137 of the 225 entries drive internal/pipeline, which is ~16 s each WITHOUT -race (the tool never
|
|
# passes it), so the full sweep is ~40 min of arithmetic, not the six hours row 313 records — that figure is the same count at
|
|
# the battery's -race cost. Forty minutes is still not something a session runs per edit — so a named SUBSET
|
|
# runs here, and the subset's composition lives in the catalogue's own `battery` flag. Listing ids in this file would be a
|
|
# second carrier of that composition, and the next entry added would fall outside the gate silently.
|
|
#
|
|
# ⚠ THE COPY IS THE CONTRACT, NOT A PRECAUTION. tmmutate EDITS the sources it plants in. Pointed at the
|
|
# working tree, a hard kill leaves a planting behind — that has happened, and an independent reviewer took
|
|
# measurements off the poisoned tree and reported a defect that did not exist. The copy is of the WORKING
|
|
# TREE rather than of HEAD, because a gate that judged HEAD would not see the pins the current change adds.
|
|
# It is removed on the failing path too (trap), or /tmp fills up one battery at a time.
|
|
#
|
|
# ⚠ THE COPY IS MADE WITHOUT .env, NOT CLEANED OF IT AFTERWARDS. `cp -a` then `rm -f` leaves a window in
|
|
# which the keys are on disk outside the repository, and a SIGKILL inside that window is not covered by the
|
|
# trap; tar excludes the file before it is ever written.
|
|
#
|
|
# ⚠ TWO STEPS WITH &&, NOT A PIPE. A shell pipeline reports the LAST command's status, so `tar -cf - | tar
|
|
# -x` walks on after a source that failed halfway: the copy is then a TRUNCATED tree, the package still
|
|
# compiles without one test file, and the gate prints SURVIVED about production code — a false defect, which
|
|
# the header of cmd/tmmutate records as having already cost this project one report. The named trade: a
|
|
# parallel edit during the archive step now fails the gate instead of yielding a half copy. bin/ is excluded
|
|
# because nothing here runs the vet tool and it is 9.7 MB of the 17.
|
|
#
|
|
# ⚠ AND THE SOURCE IS ANCHORED TO THIS FILE'S OWN DIRECTORY, not to $(CURDIR). CURDIR is where make was
|
|
# INVOKED, so `make -f backend/Makefile mutations` from the repo root tarred the whole repository — and
|
|
# the exclude is anchored to the archive root, so it matched nothing and the repository's .env files went in.
|
|
# Measured: from the repo root the CURDIR form archived 61023 members carrying TWO .env — backend/.env AND
|
|
# eval/.env, i.e. the leak crossed into another zone's keys; this form archives 488 and zero. (The rest of the recipe still assumes
|
|
# backend/ is the working directory — `go run ./cmd/tmmutate` fails loudly elsewhere — but a loud failure
|
|
# must not be preceded by writing keys to /tmp.)
|
|
MAKEFILE_DIR := $(patsubst %/,%,$(dir $(THIS_MAKEFILE)))
|
|
|
|
# ⛔ THE CORPUS-GATED PINS MUST TAKE PART IN THIS GATE, and without the two variables below they do not.
|
|
# Those harnesses locate the books tree by walking up from their own source file to the repo root; under
|
|
# this recipe the source lives in a COPY under $$tmp, whose parent has no books tree, so they skipped — and
|
|
# the pins that prove the consistency instrument does not lie (the labelled-corpus precision ladder and the
|
|
# cold-run oracle) sat out the one check that defends them. A later shift could then break the verdict
|
|
# column and watch the gate stay green.
|
|
#
|
|
# They are the harnesses' OWN documented overrides, not a hack around them: both read these variables first
|
|
# and fall back to the walk. Pointing them at the real tree is the mechanism working as designed, and on a
|
|
# clone with no books tree the tests still skip exactly as they did before.
|
|
mutations:
|
|
@tmp=$$(mktemp -d) && trap 'rm -rf "$$tmp"' EXIT && trap 'rm -rf "$$tmp"; exit 130' INT TERM && \
|
|
mkdir "$$tmp/backend" && \
|
|
tar -C "$(MAKEFILE_DIR)" -cf "$$tmp/tree.tar" --exclude='./.env*' --exclude=./bin . && \
|
|
tar -C "$$tmp/backend" -xf "$$tmp/tree.tar" && rm -f "$$tmp/tree.tar" && \
|
|
TM_CHECKER_LABELS_DIR="$(MAKEFILE_DIR)/../books/gu-zhenren/labels" \
|
|
TM_RUN_A_DIR="$(MAKEFILE_DIR)/../books/gu-zhenren/door-to-file" \
|
|
$(GO) run ./cmd/tmmutate -root "$$tmp/backend" -battery
|
|
|
|
# The hermetic battery: everything that is green on a bare clone. It ends by NAMING the tests that did
|
|
# not run, because a skip is invisible in `ok` lines and a silent skip reads as coverage.
|
|
# The harvest log goes to a TEMP file, not to .skips.log in the tree. Both normal paths removed the
|
|
# in-tree file, but Ctrl-C during the harvest pass left it behind, and it is not gitignored: the next
|
|
# `git status` then shows a position that is not work, which is how untracked files get swept into a
|
|
# commit or, worse, make a session miss the ones that ARE work. Recorded as debt on 09.08
|
|
# (13-tech-debt-anchors.md, Б-122) and closed here. mktemp+trap is the same idiom as `mutations` above.
|
|
#
|
|
# The INT/TERM trap EXITS; it does not just clean up. A POSIX shell resumes the interrupted line after its
|
|
# handler returns, so a cleanup-only trap left the chain running with the log already deleted — and this
|
|
# recipe's last step then printed "(none)", i.e. NO SKIPS, off a harvest that never finished. A silent skip
|
|
# reading as coverage is the exact failure this pass exists to catch.
|
|
battery: build vet fmt lint test
|
|
@echo "--- did NOT run (no stand data; see battery-stand) ---"
|
|
@log=$$(mktemp) && trap 'rm -f "$$log"' EXIT && trap 'rm -f "$$log"; exit 130' INT TERM && \
|
|
{ $(GO) test ./... -count=1 -v > "$$log" 2>&1 || { echo "the skip-harvest pass FAILED:"; \
|
|
grep -E '^(---|\s+---) FAIL|^FAIL' "$$log"; exit 1; }; } && \
|
|
{ grep -- '--- SKIP' "$$log" || echo "(none)"; }
|
|
|
|
# The stand battery: adds the corpus-gated tests. With the flags set, MISSING data fails instead of
|
|
# skipping, so "the corpus is here" is asserted rather than assumed.
|
|
battery-stand: build vet fmt lint
|
|
TM_MINER_PARITY=1 TM_CHECKER_LABELS=1 $(GO) test ./... -race -count=1 -timeout=20m
|