Author SHA1 Message Date
sneak 6f1b3f9fb7 Stop the terminal tests reading a pty nobody opened (closes #126)
check / check (push) Waiting to run
On Linux, pty.Open of github.com/creack/pty v1.1.24 passes the address
of a local variable to ioctl as a plain number, through a function
call. When Go moved the goroutine's stack in between, the kernel wrote
the pty's number to the old place, and pty.Open opened /dev/pts/0,
another terminal. secret rm wrote there, and the test read a terminal
no program had open, which never ends. Require the commit on the
library's main branch that passes a pointer; no release has it yet.

Both terminal tests now stop reading when their one-minute context
ends, and fail saying what was still waiting.

Model: opus-5-5
2026-10-06 19:42:36 +00:00
8 changed files with 40 additions and 34 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
+12 -10
View File
@@ -18,16 +18,18 @@ https://git.eeqj.de/sneak/secret/milestone/12
# Completed Steps
- 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
+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.24
github.com/creack/pty v1.1.25-0.20260601142114-9246436fffe8 // v1.1.24's Open can return another pty's terminal
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.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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/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=
+19 -4
View File
@@ -2601,7 +2601,9 @@ 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.
// 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.
// TestRemoveIgnoresTerminalOnStdout runs `echo y | secret rm x` at a
// terminal. stdin is a pipe, so nobody can answer there, and the command
@@ -2619,6 +2621,9 @@ 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
@@ -2628,9 +2633,15 @@ func TestRemoveIgnoresTerminalOnStdout(t *testing.T) {
_ = tty.Close()
// The read ends once secret rm has exited and so closed the terminal.
shown, _ := io.ReadAll(ptmx)
shown, err := io.ReadAll(ptmx)
require.NotErrorIs(t, err, os.ErrDeadlineExceeded,
"the terminal was still open a minute after secret rm started: %s",
shown)
require.Error(t, cmd.Wait())
err = cmd.Wait()
require.NoError(t, ctx.Err(), "secret rm did not exit within a minute")
require.Error(t, err)
assert.Contains(t, string(shown), "pass --force")
assert.DirExists(t, secretDir)
}
@@ -2650,6 +2661,9 @@ 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
@@ -2667,7 +2681,8 @@ 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 ended without asking: %s", shown)
require.NoError(t, err, "secret rm did not ask on the terminal: %s",
shown)
shown = append(shown, char)
}
+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)"
+1 -3
View File
@@ -9,9 +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
go test -count=1 ./... || go test -count=1 -v ./...
go test ./... || go test -v ./...
}
main "$@"