Compare commits

...
Author SHA1 Message Date
sneak 0a112fad71 Keep make test under 2 GB of memory (closes #344)
check / check (push) Waiting to run
Every test that starts a database hashed the bootstrap admin password
with Argon2id at 64 MB, which under -race holds about 150 MB per hash,
across dozens of parallel tests. The test packages that hash now lower
the memory cost to 1 MB from TestMain. One test still hashes and
verifies at the shipped parameters, which are unchanged.

script/test also runs at most four packages and eight parallel tests
at once: unbounded, a many-core host linked and ran every test binary
together.

Model: opus-5-5
2026-10-01 19:49:01 +00:00
11 changed files with 145 additions and 3 deletions
+6
View File
@@ -79,3 +79,9 @@ func (d *Database) ExportSetBannerOut(w io.Writer) {
func DummyPasswordHashForTest() string { func DummyPasswordHashForTest() string {
return dummyPasswordHash() return dummyPasswordHash()
} }
// HashPasswordWithDefaultsForTest hashes with the shipped Argon2id
// parameters, which TestMain has lowered for HashPassword itself.
func HashPasswordWithDefaultsForTest(password string) (string, error) {
return hashPasswordWith(password, DefaultPasswordConfig())
}
+14
View File
@@ -0,0 +1,14 @@
package database_test
import (
"testing"
"sneak.berlin/go/webhooker/internal/database"
)
// TestMain lowers the password hashing cost before any test runs. See
// database.LowerPasswordHashCostForTest.
func TestMain(m *testing.M) {
database.LowerPasswordHashCostForTest()
m.Run()
}
+15 -1
View File
@@ -63,10 +63,24 @@ func DefaultPasswordConfig() *PasswordConfig {
} }
} }
// hashConfig is what HashPassword hashes with: the defaults above.
// Only a test binary changes it, through LowerPasswordHashCostForTest,
// before any test runs.
//
//nolint:gochecknoglobals // see above
var hashConfig = DefaultPasswordConfig()
// HashPassword generates an Argon2id hash of the password // HashPassword generates an Argon2id hash of the password
func HashPassword(password string) (string, error) { func HashPassword(password string) (string, error) {
config := DefaultPasswordConfig() return hashPasswordWith(password, hashConfig)
}
// hashPasswordWith generates an Argon2id hash of the password with
// the given parameters.
func hashPasswordWith(
password string,
config *PasswordConfig,
) (string, error) {
// Generate a salt // Generate a salt
salt := make([]byte, config.SaltLen) salt := make([]byte, config.SaltLen)
+30
View File
@@ -192,6 +192,36 @@ func TestHashPasswordUniqueness(t *testing.T) {
} }
} }
// TestHashPassword_ShippedParameters hashes and verifies at the
// shipped Argon2id parameters. Every other test hashes at the lowered
// memory cost TestMain sets, so this is the one that keeps production
// hashing covered. One hash and one verification: each costs 64 MB.
func TestHashPassword_ShippedParameters(t *testing.T) {
t.Parallel()
password := "correct horse battery staple"
hash, err := database.HashPasswordWithDefaultsForTest(password)
if err != nil {
t.Fatalf("hashing with the shipped parameters: %v", err)
}
const shipped = "$argon2id$v=19$m=65536,t=1,p=4$"
if !strings.HasPrefix(hash, shipped) {
t.Errorf("hash = %q, want prefix %q", hash, shipped)
}
valid, err := database.VerifyPassword(password, hash)
if err != nil {
t.Fatalf("VerifyPassword() error = %v", err)
}
if !valid {
t.Error("VerifyPassword() returned false for correct password")
}
}
// TestVerifyDummyPassword_DoesRealWork covers the anti-enumeration // TestVerifyDummyPassword_DoesRealWork covers the anti-enumeration
// path. Login charges an unknown username a verification against a // path. Login charges an unknown username a verification against a
// dummy hash so that a nonexistent account is not answered in // dummy hash so that a nonexistent account is not answered in
+17
View File
@@ -45,3 +45,20 @@ func NewTestWebhookDBManagerWithLogger(
log: log, log: log,
} }
} }
// testArgon2Memory is the Argon2id memory cost, in KiB, that test
// binaries hash with: 1 MB instead of the shipped 64 MB.
const testArgon2Memory = 1024
// LowerPasswordHashCostForTest makes HashPassword use a 1 MB Argon2id
// memory cost instead of the shipped 64 MB, for the rest of the
// process. Call it from TestMain, before any test runs.
//
// Every test that starts a database hashes the bootstrap admin
// password, and a package runs dozens of those tests in parallel.
// Under the race detector each 64 MB hash holds about 150 MB resident.
// VerifyPassword reads the cost from the hash it checks, so
// verification follows. Production code never calls this.
func LowerPasswordHashCostForTest() {
hashConfig.Memory = testArgon2Memory
}
+14
View File
@@ -0,0 +1,14 @@
package gormlog_test
import (
"testing"
"sneak.berlin/go/webhooker/internal/database"
)
// TestMain lowers the password hashing cost before any test runs. See
// database.LowerPasswordHashCostForTest.
func TestMain(m *testing.M) {
database.LowerPasswordHashCostForTest()
m.Run()
}
+14
View File
@@ -0,0 +1,14 @@
package handlers_test
import (
"testing"
"sneak.berlin/go/webhooker/internal/database"
)
// TestMain lowers the password hashing cost before any test runs. See
// database.LowerPasswordHashCostForTest.
func TestMain(m *testing.M) {
database.LowerPasswordHashCostForTest()
m.Run()
}
+14
View File
@@ -0,0 +1,14 @@
package resetpw_test
import (
"testing"
"sneak.berlin/go/webhooker/internal/database"
)
// TestMain lowers the password hashing cost before any test runs. See
// database.LowerPasswordHashCostForTest.
func TestMain(m *testing.M) {
database.LowerPasswordHashCostForTest()
m.Run()
}
+1 -1
View File
@@ -140,7 +140,7 @@ func (n *noopEvictor) EvictWebhook(string) {}
// and the database, exactly as internal/handlers builds them. // and the database, exactly as internal/handlers builds them.
// //
// One application per test function, not per case: every start that // One application per test function, not per case: every start that
// finds no account seeds one at 64 MB of Argon2id, and this package's // finds no account seeds one with an Argon2id hash, and this package's
// budget is not the place to spend that repeatedly. // budget is not the place to spend that repeatedly.
func newServerApp( func newServerApp(
t *testing.T, dir string, t *testing.T, dir string,
+14
View File
@@ -0,0 +1,14 @@
package server_test
import (
"testing"
"sneak.berlin/go/webhooker/internal/database"
)
// TestMain lowers the password hashing cost before any test runs. See
// database.LowerPasswordHashCostForTest.
func TestMain(m *testing.M) {
database.LowerPasswordHashCostForTest()
m.Run()
}
+6 -1
View File
@@ -22,13 +22,18 @@
# The one figure above 90s is GOMAXPROCS 1, a synthetic core floor rather than # The one figure above 90s is GOMAXPROCS 1, a synthetic core floor rather than
# a condition CI runs under. If a CPU-limited runner ever puts a real run near # a condition CI runs under. If a CPU-limited runner ever puts a real run near
# 67s, that is the datum to revisit the org figure with. # 67s, that is the datum to revisit the org figure with.
#
# -p 4 -parallel 8 keep the run under 2 GB of memory: at most four test
# binaries build or run at once, each with at most eight parallel tests. Under
# -race every test binary and every link costs a few hundred MB, so the
# defaults (one per core) add up to several GB on a many-core host.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
go test -v -race -timeout 90s ./... go test -v -race -p 4 -parallel 8 -timeout 90s ./...
} }
main "$@" main "$@"