Make the tests fast under the race detector (closes #120)
check / check (push) Successful in 1m37s
check / check (push) Successful in 1m37s
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. Decryption reads the factor from the encrypted data. TestRemovalAsksWithoutHoldingLock and TestFailedCommandReleasesLock wait at most 10 seconds for the in-memory lock all tests share, so they no longer run in parallel with the tests that hold it. The script/cibuild comment no longer says tests are skipped without its memlock ulimit. Model: opus-5-5
This commit is contained in:
@@ -18,6 +18,17 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 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. `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. 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
|
- 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`,
|
(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
|
`secret import` and the stdin buffer no longer try 2 MB, 10 MB, 99 MB, 100 MB
|
||||||
|
|||||||
@@ -353,9 +353,9 @@ func TestRemovalWithoutTerminalFailsAtOnce(t *testing.T) {
|
|||||||
// for its answer, another command can take the state directory lock and
|
// for its answer, another command can take the state directory lock and
|
||||||
// change the secret, and that the removal then removes nothing, since the
|
// change the secret, and that the removal then removes nothing, since the
|
||||||
// secret is no longer what the question named.
|
// secret is no longer what the question named.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // times commands against the in-memory lock all tests share
|
||||||
func TestRemovalAsksWithoutHoldingLock(t *testing.T) {
|
func TestRemovalAsksWithoutHoldingLock(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
r := newRemoval(t, "rm")
|
r := newRemoval(t, "rm")
|
||||||
|
|
||||||
answers, answerWriter := io.Pipe()
|
answers, answerWriter := io.Pipe()
|
||||||
|
|||||||
@@ -52,8 +52,12 @@ func runSecretWithStdin(stdin string, env map[string]string, args ...string) (st
|
|||||||
return cli.ExecuteCommandInProcess(args, stdin, env)
|
return cli.ExecuteCommandInProcess(args, stdin, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMain runs before all tests and ensures the binary is built
|
// TestMain runs before all tests and ensures the binary is built. It also
|
||||||
|
// makes passphrase encryption in the tests cheap (see
|
||||||
|
// secret.ScryptWorkFactor); the binary keeps age's work factor.
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
secret.ScryptWorkFactor = 1
|
||||||
|
|
||||||
// Get the current working directory
|
// Get the current working directory
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -235,9 +235,9 @@ func TestEncryptPipedIntoAdd(t *testing.T) {
|
|||||||
|
|
||||||
// TestFailedCommandReleasesLock checks that a command failing after it
|
// TestFailedCommandReleasesLock checks that a command failing after it
|
||||||
// took the state directory lock leaves the lock free for the next command.
|
// took the state directory lock leaves the lock free for the next command.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // times commands against the in-memory lock all tests share
|
||||||
func TestFailedCommandReleasesLock(t *testing.T) {
|
func TestFailedCommandReleasesLock(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ var (
|
|||||||
// terminal to read the mnemonic from, reading it failed, or it was empty.
|
// terminal to read the mnemonic from, reading it failed, or it was empty.
|
||||||
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
|
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
|
||||||
|
|
||||||
|
// ScryptWorkFactor is, when not zero, the scrypt work factor that
|
||||||
|
// EncryptWithPassphrase uses instead of age's, 18: log2 of scrypt's cost
|
||||||
|
// parameter N. Deriving a key with age's takes about a second and 256 MiB, on
|
||||||
|
// purpose, since so does every guess at the passphrase. Only tests set it,
|
||||||
|
// lower, before any test runs, so that the passphrase unlockers they create
|
||||||
|
// cost nothing; the program leaves it zero. Decryption takes the work factor
|
||||||
|
// from the encrypted data, so it needs no setting.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // set by the tests of the packages that use this one
|
||||||
|
var ScryptWorkFactor int
|
||||||
|
|
||||||
// EncryptToRecipient encrypts data to a recipient using age
|
// EncryptToRecipient encrypts data to a recipient using age
|
||||||
// The data parameter should be a LockedBuffer for secure memory handling
|
// The data parameter should be a LockedBuffer for secure memory handling
|
||||||
func EncryptToRecipient(
|
func EncryptToRecipient(
|
||||||
@@ -142,6 +153,10 @@ func EncryptWithPassphrase(
|
|||||||
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
|
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ScryptWorkFactor != 0 {
|
||||||
|
recipient.SetWorkFactor(ScryptWorkFactor)
|
||||||
|
}
|
||||||
|
|
||||||
return EncryptToRecipient(data, recipient)
|
return EncryptToRecipient(data, recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ var (
|
|||||||
errNotImplementedInMock = errors.New("not implemented in mock")
|
errNotImplementedInMock = errors.New("not implemented in mock")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestMain makes passphrase encryption in the tests cheap; see
|
||||||
|
// ScryptWorkFactor.
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ScryptWorkFactor = 1
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
// MockVault is a test implementation of the VaultInterface
|
// MockVault is a test implementation of the VaultInterface
|
||||||
type MockVault struct {
|
type MockVault struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package vault_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -28,6 +29,14 @@ const (
|
|||||||
testPassphrase = "test-passphrase"
|
testPassphrase = "test-passphrase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestMain makes passphrase encryption in the tests cheap; see
|
||||||
|
// secret.ScryptWorkFactor.
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
secret.ScryptWorkFactor = 1
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
// testMnemonicBuffer returns testMnemonic in a locked buffer that is
|
// testMnemonicBuffer returns testMnemonic in a locked buffer that is
|
||||||
// destroyed when the test ends.
|
// destroyed when the test ends.
|
||||||
func testMnemonicBuffer(t *testing.T) *memguard.LockedBuffer {
|
func testMnemonicBuffer(t *testing.T) *memguard.LockedBuffer {
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/cibuild: run the CI build. The Dockerfile runs script/check
|
# script/cibuild: run the CI build. The Dockerfile runs script/check
|
||||||
# (via make check), so a successful build implies all checks pass.
|
# (via make check), so a successful build implies all checks pass.
|
||||||
# The Gitea workflow runs this on push. The memlock ulimit lets the tests
|
# The Gitea workflow runs this on push. The memlock ulimit lifts the limit
|
||||||
# that lock large secrets in memory (memguard mlocks them) run; under the
|
# on memory the tests lock (memguard mlocks secrets); they also pass under
|
||||||
# lower limit of a plain `docker build .` they are skipped.
|
# the lower limit of a plain `docker build .`.
|
||||||
# A cached build checks nothing: a new CHECK_EPOCH on every run makes the
|
# 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
|
# Dockerfile's check steps run again on an unchanged tree, while its base
|
||||||
# images and module downloads stay cached.
|
# images and module downloads stay cached.
|
||||||
|
|||||||
Reference in New Issue
Block a user