From a67a83fb4279ebed1cad6b63e4a5afd9a825bb9a Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 4 Oct 2026 15:58:09 +0000 Subject: [PATCH] Run the tests under the race detector with make test-race (closes #18) script/test-race runs go test -race in a digest-pinned Debian golang image that has gcc, since the detector needs cgo and the build keeps it off. The checkout is mounted read-only and the container is removed afterwards. The tests run as the calling user, or as nobody when that is root, so the tests that make a file unreadable still see the read fail. It is not part of make check. The detector found no races. Model: opus-5-5 --- Makefile | 5 ++++- README.md | 11 +++++++++++ TODO.md | 9 +++++++-- script/test-race | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100755 script/test-race diff --git a/Makefile b/Makefile index 67c7260..5adfcec 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ BINARY := sfdupes VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) LDFLAGS := -X main.Version=$(VERSION) -.PHONY: sfdupes build bootstrap setup test lint fmt fmt-check check docker hooks clean +.PHONY: sfdupes build bootstrap setup test test-race lint fmt fmt-check check docker hooks clean # Standard targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern. @@ -27,6 +27,9 @@ setup: test: @script/test +test-race: + @script/test-race + lint: @script/lint diff --git a/README.md b/README.md index 8e6c030..854b82c 100644 --- a/README.md +++ b/README.md @@ -750,6 +750,15 @@ entrypoints are: need the name call it, so they stay identical across repositories. - `script/test` — run the test suite with a 30-second timeout and coverage enabled, rerunning verbosely on failure so the logs show which test failed. +- `script/test-race` — run the test suite under the race detector with a + 60-second timeout. The detector needs cgo and a C compiler, which the build + never uses, so the tests run in a digest-pinned Debian `golang` image that has + `gcc`, with the checkout mounted read-only; the container is removed when it + exits. They run as the calling user, or as `nobody` when that is root, because + several tests make a file unreadable and root reads it anyway; only then must + the checkout be readable by other users. Not part of `script/check`. Every run + starts with empty caches, so it needs the network and takes minutes, and the + mount needs a local docker daemon. - `script/lint` — run the linter. It builds `Dockerfile.lint`, which copies the repository into the digest-pinned `golangci/golangci-lint` image and runs `golangci-lint config verify` and `golangci-lint run` as build steps, so a @@ -837,6 +846,8 @@ compile recipe: - `make setup` — prepare a fresh clone: `bootstrap` plus the pre-commit hook. - `make test` — run the test suite (30-second timeout; reruns with `-v` on failure). +- `make test-race` — run the test suite under the race detector, in Docker (see + `script/test-race`); requires `docker`. Not part of `make check`. - `make lint` — run `golangci-lint` with the repo config, in Docker (see `script/lint`); requires `docker`. - `make fmt` / `make fmt-check` — format the Go sources and the Markdown / diff --git a/TODO.md b/TODO.md index 5e65e74..792964b 100644 --- a/TODO.md +++ b/TODO.md @@ -28,6 +28,10 @@ # Completed Steps +- `make test-race` runs the test suite under the race detector in a cgo-enabled + container, outside `make check` (2026-10-04, + https://git.eeqj.de/sneak/sfdupes/issues/18) + - `make fmt` and `make fmt-check` run prettier over all Markdown, in Docker, and CI checks it; all Markdown reformatted (2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/19) @@ -492,5 +496,6 @@ Accepted divergences (no action): - flat single-package layout with `.go` files in the repo root — fine for a small single-binary tool per the Go styleguide; the tracker audit agrees -- `go test` runs without `-race` — the repo mandates `CGO_ENABLED=0` (pure-Go - builds) and the race detector requires cgo +- `make test` runs without `-race` — the repo mandates `CGO_ENABLED=0` (pure-Go + builds) and the race detector requires cgo, so the detector runs in a separate + cgo-enabled container, `make test-race`, which is not part of `make check` diff --git a/script/test-race b/script/test-race new file mode 100755 index 0000000..8e85862 --- /dev/null +++ b/script/test-race @@ -0,0 +1,40 @@ +#!/bin/sh +# script/test-race: run the test suite under the race detector. Not part +# of script/check. +# +# The race detector needs cgo and a C compiler, which the host build +# never uses, so the tests run in a golang image that has gcc. The +# checkout is mounted read-only, so the docker daemon must be local. The +# container starts with empty caches every time: each run downloads the +# dependencies and compiles them with the detector, which needs the +# network and takes minutes. +# +# The tests run as the calling user, never as root: several of them make +# a file unreadable and expect reading it to fail, and root reads it +# anyway. When the caller is root they run as nobody, and then the +# checkout must be readable by other users. Neither user has a home +# directory in the image, so HOME is /tmp, where Go puts its build cache. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +# golang:1.25-trixie, 2026-10-04. Debian rather than the Alpine image the +# Dockerfile builds with, because this one includes gcc. +IMAGE="golang@sha256:2c4c60ef415fbfa5e90300722293bef36c5e63fae17570ce18f580af933dbd73" + +main() { + user="$(id -u):$(id -g)" + if [ "$(id -u)" -eq 0 ]; then + user=65534:65534 + fi + docker run --rm \ + --user "$user" \ + --env HOME=/tmp \ + --env CGO_ENABLED=1 \ + --volume "$ROOT:/src:ro" \ + --workdir /src \ + "$IMAGE" \ + go test -race -timeout 60s ./... +} + +main "$@"