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 98d7024..9ff7707 100644 --- a/README.md +++ b/README.md @@ -827,6 +827,15 @@ and may be invoked directly. The provided entrypoints are: - `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 + `nobody`, because several tests make a file unreadable and root + reads it anyway. 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 @@ -914,6 +923,9 @@ carries the compile recipe: 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 Go sources / verify diff --git a/TODO.md b/TODO.md index 2e1fa6b..cde824b 100644 --- a/TODO.md +++ b/TODO.md @@ -29,6 +29,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) + - tests cover a missing database, `scan` keeping stdout empty, its skip warning, the `report` and `trees` summary lines, and every subcommand going through `runE` (2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/16) @@ -538,5 +542,7 @@ 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..0af3573 --- /dev/null +++ b/script/test-race @@ -0,0 +1,35 @@ +#!/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 nobody, not root: several of them make a file +# unreadable and expect reading it to fail, and root reads it anyway. +# nobody has no home directory, 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() { + docker run --rm \ + --user 65534:65534 \ + --env HOME=/tmp \ + --env CGO_ENABLED=1 \ + --volume "$ROOT:/src:ro" \ + --workdir /src \ + "$IMAGE" \ + go test -race -timeout 60s ./... +} + +main "$@"