From 07411649eca9cc2b01e679c6c41807fce5e7c335 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:07:50 +1000 Subject: [PATCH] test: RSA key-size + QMP socket-wait seams to speed up test suites (#497) --- spinifex/admin/admin.go | 9 +++++++-- spinifex/admin/ipsec.go | 2 +- spinifex/admin/main_test.go | 14 ++++++++++++++ spinifex/utils/utils_test.go | 2 +- spinifex/vm/lifecycle.go | 7 ++++++- spinifex/vm/lifecycle_test.go | 6 ++++++ 6 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 spinifex/admin/main_test.go diff --git a/spinifex/admin/admin.go b/spinifex/admin/admin.go index 31dbedfb3..f01d1d887 100644 --- a/spinifex/admin/admin.go +++ b/spinifex/admin/admin.go @@ -500,9 +500,14 @@ func GenerateNATSToken() (string, error) { return "nats_" + base64.URLEncoding.EncodeToString(bytes)[:32], nil } +// certKeyBits is the RSA key size used when generating certificate keys. +// It is a seam so tests can lower it for faster key generation; production +// keeps the 4096-bit default. +var certKeyBits = 4096 + // GenerateCACert generates a Certificate Authority certificate and key. func GenerateCACert(caCertPath, caKeyPath string) error { - caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + caPrivateKey, err := rsa.GenerateKey(rand.Reader, certKeyBits) if err != nil { return fmt.Errorf("failed to generate CA private key: %w", err) } @@ -653,7 +658,7 @@ func GenerateSignedCert(certPath, keyPath, caCertPath, caKeyPath string, extraIP return fmt.Errorf("CA key is not RSA") } - serverPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + serverPrivateKey, err := rsa.GenerateKey(rand.Reader, certKeyBits) if err != nil { return fmt.Errorf("failed to generate server private key: %w", err) } diff --git a/spinifex/admin/ipsec.go b/spinifex/admin/ipsec.go index 040c9114f..be535d92a 100644 --- a/spinifex/admin/ipsec.go +++ b/spinifex/admin/ipsec.go @@ -60,7 +60,7 @@ func GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, hostname, nodeIP st return err } - peerKey, err := rsa.GenerateKey(rand.Reader, 4096) + peerKey, err := rsa.GenerateKey(rand.Reader, certKeyBits) if err != nil { return fmt.Errorf("ipsec peer cert: generate key: %w", err) } diff --git a/spinifex/admin/main_test.go b/spinifex/admin/main_test.go new file mode 100644 index 000000000..e1131c80d --- /dev/null +++ b/spinifex/admin/main_test.go @@ -0,0 +1,14 @@ +package admin + +import ( + "os" + "testing" +) + +// TestMain lowers the RSA key size for the whole admin test package. 2048-bit +// keys are still valid and generate several times faster than the 4096-bit +// production default, which dominates the admin suite runtime. +func TestMain(m *testing.M) { + certKeyBits = 2048 + os.Exit(m.Run()) +} diff --git a/spinifex/utils/utils_test.go b/spinifex/utils/utils_test.go index c8c2a522c..5320164be 100644 --- a/spinifex/utils/utils_test.go +++ b/spinifex/utils/utils_test.go @@ -134,7 +134,7 @@ func TestExecProcessAndKill(t *testing.T) { err = WaitForPidFileRemoval("utilsunittest", 100*time.Millisecond) assert.Error(t, err) // Should timeout since file should still exist - time.Sleep(500 * time.Millisecond) + time.Sleep(20 * time.Millisecond) // Kill the process err = StopProcess("utilsunittest") diff --git a/spinifex/vm/lifecycle.go b/spinifex/vm/lifecycle.go index feb6b9b75..769002039 100644 --- a/spinifex/vm/lifecycle.go +++ b/spinifex/vm/lifecycle.go @@ -656,6 +656,11 @@ const ( qmpDialRetryInterval = 50 * time.Millisecond ) +// qmpSocketWaitTimeout bounds how long newQMPClientWithHandshake waits for the +// QMP socket inode to appear before dialling. Mirrors qmpDialTimeout; a test +// seam overrides it to keep dial-failure tests off the wall clock. +var qmpSocketWaitTimeout = 3 * time.Second + // removeStaleQMPSocket unlinks a leftover QMP socket inode from a prior QEMU so // the startup probe and QMP dial cannot race a dead listener. A missing file is // not an error. @@ -698,7 +703,7 @@ func isTransientDialError(err error) bool { func newQMPClientWithHandshake(ctx context.Context, v *VM) (*qmp.QMPClient, error) { // QMP socket bind lags the pidfile under recovery load; wait for the // socket inode to exist before dialling to avoid an ENOENT race. - if err := utils.WaitForUnixSocket(v.Config.QMPSocket, 3*time.Second); err != nil { + if err := utils.WaitForUnixSocket(v.Config.QMPSocket, qmpSocketWaitTimeout); err != nil { return nil, fmt.Errorf("connect QMP socket %s: %w", v.Config.QMPSocket, err) } client, err := dialQMPWithRetry(v.Config.QMPSocket) diff --git a/spinifex/vm/lifecycle_test.go b/spinifex/vm/lifecycle_test.go index 5a20ff2d9..33330c6d1 100644 --- a/spinifex/vm/lifecycle_test.go +++ b/spinifex/vm/lifecycle_test.go @@ -465,6 +465,10 @@ func startBrokenQMPListener(t *testing.T) (string, func()) { func TestAttachQMP_DialFailure_NoHeartbeatLeak(t *testing.T) { defer goleak.VerifyNone(t) + orig := qmpSocketWaitTimeout + qmpSocketWaitTimeout = 50 * time.Millisecond + t.Cleanup(func() { qmpSocketWaitTimeout = orig }) + m := NewManager() instance := &VM{ ID: "i-dial-fail", @@ -893,6 +897,8 @@ var _ NetworkPlumber = (*scriptedNetworkPlumber)(nil) func TestStartupTimeouts(t *testing.T) { assert.Equal(t, 5*time.Second, qemuStartupTimeout) assert.Equal(t, 5*time.Second, nbdReadyTimeout) + assert.Equal(t, 3*time.Second, qmpSocketWaitTimeout, + "production QMP socket wait default must stay 3s outside tests") } // TestRG4_GuestOOMTier asserts the RG-4 OOM ladder: a customer guest QEMU