script/test silently passes on flaky failures, and runs without -race or -timeout #32
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found auditing the repo against
REPO_POLICIES.md. Present onmainand on the pendinggolangci-v2.12.2branch alike. This is the highest-severity non-code finding in the audit, because it degrades the gate that every other item on this milestone is verified by.The defect
script/testin full, the operative lines:Three separate policy violations in one line, one of which is a live correctness hole.
1. A flaky test yields a green build. The rerun is the last command in the script, so its exit status becomes the script's exit status. A test that fails on the first run and passes on the retry produces exit 0. Policy calls this exact failure mode out by name:
> The
exit 1ensures the target always fails after a rerun — the first run already proved the tests are broken, so the build must not pass even if a flaky test happens to succeed on the second attempt. The rerun exists solely for diagnostic output.The blast radius is everything: the
Dockerfilerunsmake test,.gitea/workflows/check.ymlrunsscript/cibuildwhich runsdocker build, so a flaky failure anywhere in the suite is invisible in CI. For a tool whose tests cover key derivation, encryption round-trips, and unlocker behavior, "the test failed once and we shipped it" is not an acceptable outcome.2. No
-race. Policy's Go pattern specifies-race. Its absence matters concretely here: issue #34 concerns missing file locking and non-atomic writes ininternal/vault/secrets.go. Race detection is precisely the instrument that would catch regressions in that area, and it is switched off.CGO_ENABLED=1is already exported by this script, so-racewill work with no further change.3. No
-timeout 30sand no-cover. Policy:>
make testmust complete in under 20 seconds. Add a 30-second timeout in the Makefile.Without it, a test that deadlocks on a lock or blocks on a terminal read hangs the CI runner until the job-level timeout kills it, with no indication which test hung.
Definition of done
script/testrunsgo test -timeout 30s -race -cover ./...first.On failure it reruns verbosely and then exits non-zero, exactly per the policy pattern:
go vet ./...continues to run first, unchanged.The script stays POSIX sh, keeps
set -eu, keeps the existingCGO_ENABLED=1export and the repo-rootcdidiom.make checkis green with the new script, and the full suite still finishes in under 20 seconds with-raceenabled — race instrumentation is a real slowdown, so this must be measured, not assumed.If enabling
-raceor-timeout 30smakes any existing test fail or time out, that is a genuine bug this change has uncovered. Fix the underlying code or the test — do not weaken the flags, do not add skips, and report what was found on this issue.TODO.mdupdated in the same commit.Implementation requirements
--ulimit memlock=-1:-1(seescript/cibuild) for the memguard test. Verify locally under the same conditions CI uses rather than only on the host.-raceon, in a comment here. If it exceeds 20 seconds, do not silently raise the timeout — say so and stop, because that is a policy conflict that needs a decision rather than a workaround.Implementation plan
Branch
fix-script-test-raceofforigin/main(6e5e0db), in a scratch worktree.1.
script/test— the only file changed besidesTODO.md.Keep the shebang,
set -eu, theROOT/cdidiom, and theCGO_ENABLED=1export exactly as they are. Keepgo vet ./...as the first command inmain(). Replace only thego test ./... || go test -v ./...line with the policy pattern verbatim:The trailing
exit 1is the whole point: the rerun is diagnostic only and can never make the script succeed. No other script is touched,.golangci.ymlis not touched.2. Verification, in this order.
make fmt(no Go changes expected, but run it so the tree is canonical).make checkon the host first, to get fast feedback and a first look at whether-racefires.script/cibuild—docker build --ulimit memlock=-1:-1 ., which is what CI actually runs. TheDockerfilerunsmake check, so this is the CI-equivalent gate, and the memlock ulimit is what keeps the memguard test honest. A host-only pass does not count as verified.-raceon and report the number here. Policy budget is 20 seconds. If it comes in over, I will report the measurement and stop rather than raise the timeout — that is a decision, not a workaround.3. If the race detector fires.
I will not remove
-race, addt.Skip, weaken a test, or stretch the timeout. Per #34 this repo has no file locking and no atomic writes in non-test code, so a detector hit is a genuine finding. I will report the racing goroutines, the shared state, and the file paths here in detail. A small, obviously in-scope fix I will make; anything substantial stays with #34 and I will stop rather than half-fix it inside this commit.4. Bookkeeping.
TODO.mdgets one additive entry in the same commit, nothing else in the file rewritten — PR #29 is merge-ready and also editsTODO.md, so a broad edit here would only manufacture a conflict.Commit subject ends with
(closes #32). PR opened againstmain; labels and assignees left alone.Result: implemented, measured, and blocked on a policy decision
PR #53 (branch
fix-script-test-race, commit3d615ef).script/testand one additiveTODO.mdentry, nothing else. The script keepsset -eu, theCGO_ENABLED=1export, the repo-rootcd, andgo vet ./...first; only the test line changed, to the policy pattern verbatim with the trailingexit 1.Confirmed end to end that the exit-status hole is closed: the run prints
--- Rerunning with -v for details ---and then exits non-zero, so the rerun can no longer launder a first-run failure into a green build.Measured wall-clock, as requested
Taken with
script/cibuild(docker build --ulimit memlock=-1:-1 .), not on the host — the host'smemlockhard limit is 8192 KB, so the memguard 10MB case panics withcould not acquire lock ... limit reachedbefore the suite gets far enough to time. The container is the only honest measurement here.-race,maintoday: 18.7s for themake testlayer, about 9s of which isgo vetplus compilation. Slowest packageinternal/cliat 9.6s, everything else under 2s. Inside the 20s budget, but with almost no headroom.-race: the suite fails.internal/cliis killed at the 30s per-package timeout. Extrapolating from the completed subtests it needs roughly 55-60s, about 6x its non-race time.So the answer to the question this issue asked me to measure rather than assume: no, the suite does not fit in 20 seconds with
-raceon — it misses by roughly 3x. Per the instruction in the issue body, I am reporting that and stopping rather than raising the timeout.The race detector did not fire
Worth being precise, because it is the more interesting half of the result.
internal/secret,internal/vault,pkg/agehdandpkg/bip85all ran to completion under-raceand reported zero data races.internal/cliwas killed at 30s, so its coverage under the detector is partial.The
internal/clifailure is not a race, not a deadlock, and not a hang. Nothing is blocked on a lock or a terminal read. The goroutine at the timeout is insidememguard/core.Wipe, reached fromcli.(*Instance).ImportSecretatinternal/cli/secrets.go:579— ordinary forward progress, just slow. The cost is concentrated in the giant-size cases:24.19s of the 30s budget gone before
TestImportSecretVariousSizeseven starts, and that test is the same size ladder over the import path. Race instrumentation touches every memory access, and these cases push ~300MB through age encryption and memguard wipes, so the ~10x hit on the large cases is expected rather than anomalous.This also means the clean result says nothing about #34. The suite contains no test that exercises the vault from two goroutines at once, so the detector had nothing to catch. #34's missing file locking and non-atomic writes remain entirely unaddressed and unmeasured by this change.
Why I did not "just fix the test"
The definition of done says to fix the underlying code or the test rather than weaken the flags. I could not find a fix that is both in scope and not a weakening:
-timeoutpast 30s papers over it, and the 20s wall-clock budget would still be missed by 3x.t.Skiportesting.Short()on the large cases is a skip, explicitly forbidden.internal/cli/secrets.go:245and:515(const maxSize = 100 * 1024 * 1024) from both sides — 99MB and 100MB-minus-1 must pass, 101MB must fail.No small in-scope change closes a 6x gap, so forcing one into this commit would have been worse than reporting it.
Filed for the decision: #52
#52 carries the full numbers and four options — build-tag split for the large cases with a separate slower CI job, trimming to a single boundary pair, amending the policy budget for this repo, or optimizing the path. My read is the build-tag split, since it is the only one that gives up nothing, but it is a policy call and should be decided rather than picked by me.
PR #53 is therefore red on purpose and should land together with whatever #52 concludes.
make lintandmake fmt-checkare both clean on the branch;make testfails only for the reason above.