# sr-ht-ecore — build scaffolding (bench.sr.ht / cov.sr.ht family style). # # This repository is a LIBRARY: no cmd/, no daemon, no stylesheet, no apk. There # is nothing to install and nothing to package, so the targets a service's # Makefile carries for those — css, keygen, install, check-version — have no # meaning here and are absent rather than stubbed. # # What is left is what the seven services pinning this module actually need from # it: that it compiles, that it is formatted and vets clean, that its tests pass, # and that its coverage profile and its benchmarks come out in the shape # .build.yml uploads to cov.srht.bigb.es and bench.srht.bigb.es. That is what the # targets below are, and .build.yml calls them by name — the Go command lines # live here, once, instead of being copied into the manifest. GO?=go GOFMT?=gofmt # PKG is what to run and COUNT is `go test -count`, both variables so that # `make test-race COUNT=10 PKG=./bearer/` needs no retyping of the flags that # get forgotten when a command is retyped by hand. PKG?=./... COUNT?=1 # TEST_TIMEOUT is the per-package ceiling handed to `go test`, and -timeout is # per test binary, so what it has to cover is the slowest package and not the # suite. `make test` does not need it: 11 s wall for the whole tree on an M4 # Pro, measured with go1.25 at -count=1, no package over 10 s. `make bench` # is where it earns its keep — measured on that same machine at BENCH_COUNT=10, # 6 min 06 s wall over 29 benchmark cases, of which grants took 96.4 s, bearer # 78.5 s and csrf 73.0 s. A sixth of the toolchain's ten-minute default is # comfortable on a laptop and not obviously comfortable on a builds.sr.ht VM, # which is a slower machine running the same fixed number of iterations. # Twenty minutes is the number the sibling services carry, for the same reason: # what it prevents — `panic: test timed out` over three hundred lines of # goroutine dump — reads like a hang in the code rather than like a budget. TEST_TIMEOUT?=20m # BENCH_COUNT is `go test -count` for the `bench` target. Ten is bench.sr.ht's # own number (its SPEC ch. 5): a confidence interval becomes finite at six # repetitions and a comparison becomes significant at four, so anything under # six uploads points the service can only mark "low n". It is a variable so a # laptop can say `make bench BENCH_COUNT=1` when it only wants to know that the # benchmarks still run. BENCH_COUNT?=10 # Where `make cover` writes the profile. .build.yml overrides it to $HOME — # that is where builds.sr.ht's artifacts: looks — and the default is the # checkout, which is what a developer means by `make cover`. It lands untracked # there; `make clean` is what removes it, and .gitignore does not list it. COVERPROFILE?=cover.out # The sibling services default to `build`, and each of them says in the same # breath that it was `help` while there was nothing to build. Here there is # nothing to build — `build` is a compile check, not a product — so the default # stays where theirs started, and a bare `make` spends no time proving something # the author did not ask about. .DEFAULT_GOAL := help help: @echo "targets:" @echo " help this list (the default target)" @echo " build compile every package: go build \$$(PKG)" @echo " test go test \$$(PKG) with an explicit -timeout" @echo " test-race the same under -race and repeatable:" @echo " make test-race COUNT=10 PKG=./bearer/" @echo " cover test with -covermode=atomic into \$$(COVERPROFILE) and" @echo " print the total; the profile .build.yml POSTs to cov.sr.ht" @echo " (COVERPROFILE=$(COVERPROFILE))" @echo " bench go test -bench=. \$$(PKG) as benchfmt on stdout, the format" @echo " .build.yml POSTs to bench.sr.ht (BENCH_COUNT=$(BENCH_COUNT));" @echo " everything that is not benchfmt is filtered out — see" @echo " BENCH_FILTER, and 'make bench BENCH_FILTER=.' for the raw run" @echo " vet go vet \$$(PKG)" @echo " fmt rewrite this module's Go files with gofmt" @echo " check-fmt the same as a refusal: fail if gofmt would rewrite anything" @echo " (this is the one CI runs; fmt writes, gates must not)" @echo " clean remove \$$(COVERPROFILE)" # There is no binary to link, so this is a compile check and nothing else. It is # still worth a target: `go vet` compiles too, but a build failure reported by # vet reads like a vet finding. build: $(GO) build $(PKG) test: $(GO) test -timeout $(TEST_TIMEOUT) $(PKG) # The same suite under the race detector, repeatable. Half of what this library # holds is middleware sitting on every request of every service — chimw's # logger, middleware's chain, mcphttp's cache — so the run that matters for it # is this one, not `test`. test-race: $(GO) test -race -count=$(COUNT) -timeout $(TEST_TIMEOUT) $(PKG) # -covermode=atomic and not the default `set`: `set` records whether a statement # ran, atomic records how often, and cov.sr.ht reads hit counts. The profile is # the artifact; the printed total is a convenience. # # The `test -s` in the middle is not decoration. A profile that came out empty # is still a valid file, and uploading it would report success over a report # that covers nothing — the same failure the .build.yml bench task greps # against. cover: $(GO) test -covermode=atomic -coverprofile=$(COVERPROFILE) -timeout $(TEST_TIMEOUT) $(PKG) @test -s $(COVERPROFILE) || { echo "$(COVERPROFILE) is empty" >&2; exit 1; } $(GO) tool cover -func=$(COVERPROFILE) | tail -1 # The benchmarks in the form bench.sr.ht ingests: benchfmt on stdout, which is # what `go test -bench` writes. -run='^$$' keeps the tests out of it — their # output is noise the parser skips and their runtime is time added to a run # whose point is the benchmarks. -benchmem because B/op and allocs/op are half # of what a middleware benchmark means and they cost nothing to collect. # # BENCH_FILTER is the part that is not boilerplate, and it is here because a # benchmark's own stdout is not only the benchmark. `go test` merges the test # binary's stderr into its stdout, so anything the code under test logs during a # measured loop lands in the middle of the benchfmt document. Measured on this # tree at BENCH_COUNT=10: bearer's invalid-token case makes sr-ht-core's # auth.DecodeBearerToken call log.Printf once per iteration, and an unfiltered # run was 901 MB and 9.5 million lines deep — around the 290 result lines a full # run has — when it was stopped, a third of the way through. Filtered, the same # run is 340 lines and 31 KB. benchfmt ignores lines it cannot parse, so the # unfiltered file would upload "successfully": a POST of a gigabyte of noise, # whatever bench.sr.ht's byte budget makes of it. # # So the target emits the benchfmt grammar (its configuration lines and its # result lines) plus the words that say a run failed, and nothing else. The # filter is a variable: `make bench BENCH_FILTER=.` is the same run with every # line of it. It is a defence and not a cure — the cure is for a benchmark that # provokes logging to silence the logger first, the way middleware's discardLog # and chimw's io.Discard handler already do. # # The exit status travels in a file because the grep is a pipe: taking the # pipeline's status would report grep's opinion of the output instead of whether # the benchmarks ran, which is the failure mode that makes a red suite read # green. BENCH_FILTER?=^(goos|goarch|pkg|cpu): |^Benchmark|^(PASS|FAIL|ok|--- |panic: ) bench: @echo "+ $(GO) test -run='^$$' -bench=. -benchmem -count=$(BENCH_COUNT) -timeout $(TEST_TIMEOUT) $(PKG)" >&2 @st=$$(mktemp); \ { $(GO) test -run='^$$' -bench=. -benchmem -count=$(BENCH_COUNT) \ -timeout $(TEST_TIMEOUT) $(PKG) 2>&1; echo $$? > "$$st"; } \ | grep -E '$(BENCH_FILTER)' || true; \ rc=$$(cat "$$st"); rm -f "$$st"; \ test -n "$$rc" || rc=1; \ exit "$$rc" vet: $(GO) vet $(PKG) # GOFMT_DIRS is `go list`'s answer and not a literal `.`, and that is the whole # point of the two targets below. The repository convention here is to create # git worktrees under .worktrees/, so a plain `gofmt -l .` at the root # would walk into another checkout of this same module and report — or, for # `fmt`, rewrite — files that are not in this working tree at all. A worktree # carries its own go.mod, so it is a different module to `go list`, and ./... # never leaves this one. GOFMT_DIRS=$$($(GO) list -f '{{.Dir}}' $(PKG)) fmt: $(GOFMT) -l -w $(GOFMT_DIRS) # `gofmt -l` prints the offending files and STILL EXITS 0, so it passes any && # chain and any CI task that only looks at the status. This target is that check # written so a failure stops the build, and it is the one .build.yml calls; fmt # above rewrites and must never be what a gate runs. check-fmt: @out=$$($(GOFMT) -l $(GOFMT_DIRS)); \ if [ -n "$$out" ]; then \ echo "gofmt would rewrite:" >&2; \ echo "$$out" >&2; \ exit 1; \ fi; \ echo "gofmt: clean" # Only the profile: `bench` writes to stdout and creates no file of its own, so # there is nothing else here to remove. clean: rm -f $(COVERPROFILE) .PHONY: help build test test-race cover bench vet fmt check-fmt clean