Compare commits

Author SHA1 Message Date
sneak 666e2438b0 Make the tests fast under the race detector (closes #120)
check / check (push) Successful in 1m48s
Deriving keys from passphrases with scrypt, slow on purpose, took most
of the test time under -race. secret.ScryptWorkFactor, when not zero,
replaces age's work factor when a passphrase encrypts; the tests of
internal/secret, internal/vault and internal/cli set it to 1 in
TestMain, and the program never sets it.

TestRemovalAsksWithoutHoldingLock and TestFailedCommandReleasesLock
time the in-memory lock all tests share, so they no longer run in
parallel. TestConcurrentAddsKeepEveryVersion and
TestGetCommandOutputsToStdout time nothing and now do.

The script/cibuild comment no longer says tests are skipped without
its memlock ulimit.

Model: opus-5-5
2026-10-06 01:17:43 +00:00
9 changed files with 22 additions and 98 deletions
+2 -8
View File
@@ -50,12 +50,7 @@ ARG CHECK_EPOCH
COPY . .
# This cache mount keeps Go's build cache between builds for make test and
# make build; -count=1 in script/test keeps test results out of it. Go's cache
# does not notice C header changes, so the mount has its own id: change the id
# when the C packages installed above change.
RUN --mount=type=cache,id=sneak/secret/go-build,target=/root/.cache/go-build \
make test
RUN make test
# The version stamped into the binary: the VERSION build argument when one
# is given, otherwise `git describe --tags --always` of the .git the build
@@ -63,8 +58,7 @@ RUN --mount=type=cache,id=sneak/secret/go-build,target=/root/.cache/go-build \
# one, the short commit when no tag is reachable. A context that carries .git
# and still yields no version fails the build.
ARG VERSION
RUN --mount=type=cache,id=sneak/secret/go-build,target=/root/.cache/go-build \
version="${VERSION:-$(git describe --tags --always)}"; \
RUN version="${VERSION:-$(git describe --tags --always)}"; \
if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \
[ "$version" = unknown ]; }; then \
echo "no version could be derived although the build context carries .git" >&2; \
+2 -4
View File
@@ -604,8 +604,7 @@ provide:
- `script/build` — build the `secret` binary into the repo root, stamping the
version (`VERSION` from the environment, else `git describe`) and the git
commit
- `script/test` — run `go vet` and the test suite (verbose rerun on failure),
every test on every run, never a result from Go's test cache
- `script/test` — run `go vet` and the test suite (verbose rerun on failure)
- `script/lint` — run `golangci-lint` in docker only: builds `Dockerfile.lint`,
where the linter is a build step that runs on every call, also on an unchanged
tree
@@ -625,8 +624,7 @@ provide:
- `script/cibuild` — CI entrypoint: `docker build --ulimit memlock=-1:-1 .`
(memguard needs mlock; the Dockerfile runs the checks), with a new
`CHECK_EPOCH` build argument on every run so the checks run again on an
unchanged tree; the `Dockerfile` keeps Go's build cache between builds, so
`make test` and `make build` compile only what changed
unchanged tree
- `script/precommit` — pre-commit checks: `go mod tidy` verification, then
`script/check`
- `script/install-precommit` — install the git pre-commit hook that runs
+8 -38
View File
@@ -18,50 +18,20 @@ https://git.eeqj.de/sneak/secret/milestone/12
# Completed Steps
- 2026-10-06: `script/test` runs the tests with the race detector, a 30-second
timeout per package and coverage, as `REPO_POLICIES.md` requires
(https://git.eeqj.de/sneak/secret/issues/32). When they fail, it reruns them
verbosely for the details and then fails anyway, so a test that fails once and
passes on the retry no longer gives a green build. `go vet` still runs first,
and every `go test` keeps `-count=1`.
- 2026-10-06: `make test` in `script/cibuild` no longer compiles the standard
library and every dependency from nothing on every build
(https://git.eeqj.de/sneak/secret/issues/124). The `Dockerfile` runs it and
`make build` with Go's build cache in a BuildKit cache mount, which docker
keeps between builds, so each compiles only what changed since the last build.
The mount has an id of its own, so other repositories' builds do not share it.
`script/test` passes `-count=1`, so every test runs on every build and no
result comes from Go's test cache. A build with an empty cache, such as the
first after docker's build cache is cleared, compiles everything in
`make test` as before.
- 2026-10-06: `TestRemoveIgnoresTerminalOnStdout` and
`TestRemoveAsksAtTerminalOnStdin` no longer wait until Go's test timeout
(https://git.eeqj.de/sneak/secret/issues/126). On Linux, `pty.Open` of
`github.com/creack/pty` v1.1.24 passed the address of a local variable to the
`ioctl` system call as a plain number, through a function call; when Go moved
the goroutine's stack in between, the kernel wrote the terminal's number to
the old place, and `pty.Open` opened `/dev/pts/0` instead of the terminal it
had created. `secret rm` then wrote to that other terminal, and the test read
a terminal no program had open, which never ends. `go.mod` now requires the
commit on that library's main branch that passes a pointer instead; no release
has it yet. Both tests stop reading the terminal when their one-minute context
ends and fail saying so.
- 2026-10-06: The tests run quickly with the race detector on
(https://git.eeqj.de/sneak/secret/issues/120). Most of their time went to
deriving keys from passphrases with scrypt, which is slow on purpose. The new
`secret.ScryptWorkFactor`, when not zero, replaces age's scrypt work factor
when a passphrase encrypts; the tests of `internal/secret`, `internal/vault`
and `internal/cli` set it to 1 before any test runs, and the program never
sets it; `TestGetCommandOutputsToStdout` checks that the passphrase unlocker
the built binary's `secret init` writes names age's work factor, 18.
`TestRemovalAsksWithoutHoldingLock` and `TestFailedCommandReleasesLock` no
longer run in parallel with other tests: each waits at most 10 seconds for the
in-memory lock that every test in the package shares, and other tests'
commands held it longer. `TestConcurrentAddsKeepEveryVersion`, which times
nothing, and `TestGetCommandOutputsToStdout`, which no longer sets an
environment variable its commands do not read, now run in parallel. The
`script/cibuild` comment no longer says that tests are skipped without its
memlock ulimit.
sets it. `TestRemovalAsksWithoutHoldingLock` and
`TestFailedCommandReleasesLock` no longer run in parallel with other tests:
each waits at most 10 seconds for the in-memory lock that every test in the
package shares, and other tests' commands held it longer.
`TestConcurrentAddsKeepEveryVersion`, which times nothing, and
`TestGetCommandOutputsToStdout`, which no longer sets an environment variable
its commands do not read, now run in parallel. The `script/cibuild` comment no
longer says that tests are skipped without its memlock ulimit.
- 2026-10-05: No test stores a secret larger than 1 MiB
(https://git.eeqj.de/sneak/secret/issues/52). The size tests for `secret add`,
`secret import` and the stdin buffer no longer try 2 MB, 10 MB, 99 MB, 100 MB
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.1.3
github.com/btcsuite/btcd/btcutil v1.1.6
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
github.com/creack/pty v1.1.25-0.20260601142114-9246436fffe8 // v1.1.24's Open can return another pty's terminal
github.com/creack/pty v1.1.24
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.18.0
github.com/keybase/go-keychain v0.0.0-20230307172405-3e4884637dd1
+2 -2
View File
@@ -35,8 +35,8 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.25-0.20260601142114-9246436fffe8 h1:CY3gjC7naqYGLMiywvj3suPfa1i0p/QEr7o8ujxL/2M=
github.com/creack/pty v1.1.25-0.20260601142114-9246436fffe8/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+4 -22
View File
@@ -2601,12 +2601,7 @@ func TestRemoveWithoutTerminalFailsAtOnce(t *testing.T) {
// and stderr, not both: whether it asks must depend on stdin alone, where
// the answer is read from. pty.Open returns the two ends of a new terminal:
// tty is the end a program uses as its terminal, and ptmx the end the test
// reads what the terminal shows from and types into. Reading ptmx stops at
// the context's deadline, when secret rm is killed too, so a terminal that
// stays open fails the test then instead of hanging it. The deadline works
// only while ptmx stays non-blocking, as pty.Open of the github.com/creack/pty
// commit in go.mod leaves it: calling ptmx.Fd() or going back to v1.1.24
// makes the read ignore the deadline, without any error.
// reads what the terminal shows from and types into.
// TestRemoveIgnoresTerminalOnStdout runs `echo y | secret rm x` at a
// terminal. stdin is a pipe, so nobody can answer there, and the command
@@ -2624,9 +2619,6 @@ func TestRemoveIgnoresTerminalOnStdout(t *testing.T) {
defer func() { _ = ptmx.Close() }()
deadline, _ := ctx.Deadline()
require.NoError(t, ptmx.SetReadDeadline(deadline))
cmd.Stdin = strings.NewReader("y\n")
cmd.Stdout = tty
cmd.Stderr = tty
@@ -2636,15 +2628,9 @@ func TestRemoveIgnoresTerminalOnStdout(t *testing.T) {
_ = tty.Close()
// The read ends once secret rm has exited and so closed the terminal.
shown, err := io.ReadAll(ptmx)
require.NotErrorIs(t, err, os.ErrDeadlineExceeded,
"the terminal was still open a minute after secret rm started: %s",
shown)
shown, _ := io.ReadAll(ptmx)
err = cmd.Wait()
require.NoError(t, ctx.Err(), "secret rm did not exit within a minute")
require.Error(t, err)
require.Error(t, cmd.Wait())
assert.Contains(t, string(shown), "pass --force")
assert.DirExists(t, secretDir)
}
@@ -2664,9 +2650,6 @@ func TestRemoveAsksAtTerminalOnStdin(t *testing.T) {
defer func() { _ = ptmx.Close() }()
deadline, _ := ctx.Deadline()
require.NoError(t, ptmx.SetReadDeadline(deadline))
cmd.Stdin = tty
// Not a file, so exec.Cmd connects stdout through a pipe.
cmd.Stdout = io.Discard
@@ -2684,8 +2667,7 @@ func TestRemoveAsksAtTerminalOnStdin(t *testing.T) {
terminal := bufio.NewReader(ptmx)
for !bytes.HasSuffix(shown, []byte("[y/N] ")) {
char, err = terminal.ReadByte()
require.NoError(t, err, "secret rm did not ask on the terminal: %s",
shown)
require.NoError(t, err, "secret rm ended without asking: %s", shown)
shown = append(shown, char)
}
-12
View File
@@ -41,18 +41,6 @@ func TestGetCommandOutputsToStdout(t *testing.T) {
output, err := cmd.CombinedOutput()
require.NoError(t, err, "init should succeed: %s", string(output))
// The binary, unlike these tests, encrypts the passphrase unlocker's key
// at age's scrypt work factor, 18. age writes the work factor last on the
// second line of priv.age: "-> scrypt <salt> <work factor>".
vaultDir := filepath.Join(tempDir, "vaults.d", "default")
unlockerName := readFile(t, filepath.Join(vaultDir, "current-unlocker"))
unlockerDir := filepath.Join(vaultDir, "unlockers.d", string(unlockerName))
privAge := readFile(t, filepath.Join(unlockerDir, "priv.age"))
header := strings.SplitN(string(privAge), "\n", 3)
require.Len(t, header, 3, "priv.age should start with an age header")
assert.Regexp(t, `^-> scrypt \S+ 18$`, header[1],
"the passphrase unlocker should be encrypted at scrypt work factor 18")
// Add a secret
//nolint:gosec // G204: test executes the freshly built secret binary
cmd = exec.CommandContext(t.Context(), secretPath, "add", "test/secret")
+1 -2
View File
@@ -6,8 +6,7 @@
# the lower limit of a plain `docker build .`.
# A cached build checks nothing: a new CHECK_EPOCH on every run makes the
# Dockerfile's check steps run again on an unchanged tree, while its base
# images, module downloads and the Go build cache that make test and make
# build use stay cached.
# images and module downloads stay cached.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
+2 -9
View File
@@ -1,6 +1,5 @@
#!/bin/sh
# script/test: run the test suite (vet first, then the tests with the race
# detector; a verbose rerun on failure).
# script/test: run the test suite (vet first, verbose rerun on failure).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -10,13 +9,7 @@ main() {
# CGO is required (Makefile exports this too)
export CGO_ENABLED=1
go vet ./...
# -count=1: run every test, never take a result from Go's test cache,
# which the Dockerfile keeps between builds. The rerun only prints
# details: `exit 1` keeps the script failing even if a flaky test
# passes on the second attempt.
go test -count=1 -timeout 30s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -count=1 -timeout 30s -race -v ./...; exit 1; }
go test ./... || go test -v ./...
}
main "$@"