From adb60963c3ae1897256fff2d8662f8e8b436f705 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:43:58 +1000 Subject: [PATCH] fix: back QMP test mocks with a unix socket instead of net.Pipe Go 1.27's encoding/json v2 decoder stops at the closing brace and leaves Encoder.Encode's trailing newline unread. net.Pipe is unbuffered and synchronous, so the writer blocks on that undelivered byte while the peer blocks writing its reply, deadlocking on the first QMP command. TestTryBlockdevDel hung until the 180s package timeout. A real unix socket is kernel-buffered and matches production QMP transport, which is already the pattern newMockQMPClientWithEvents uses. --- spinifex/daemon/daemon_test.go | 34 +++++++++++++++++++++++++--------- spinifex/vm/volumes_test.go | 32 ++++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/spinifex/daemon/daemon_test.go b/spinifex/daemon/daemon_test.go index 3c553d0ff..6d88e288a 100644 --- a/spinifex/daemon/daemon_test.go +++ b/spinifex/daemon/daemon_test.go @@ -2213,24 +2213,31 @@ func TestHandleEC2Events_DetachVolume(t *testing.T) { }) } -// newMockQMPClient creates a QMPClient backed by an in-memory pipe. +// newMockQMPClient creates a QMPClient backed by a unix socket. // The returned cancel function stops the mock server goroutine. // responseFunc is called for each received QMP command and should return the // JSON object to send back (e.g. `{"return": {}}`). If nil, all commands // get a success response. +// +// A real socket rather than net.Pipe, which is unbuffered and synchronous: +// the decoder stops at the closing brace and leaves the encoder's trailing +// newline unread, wedging the writer while the peer replies. func newMockQMPClient(t *testing.T, responseFunc func(cmd qmp.QMPCommand) map[string]any) (*qmp.QMPClient, func()) { t.Helper() - clientConn, serverConn := net.Pipe() - client := &qmp.QMPClient{ - Conn: clientConn, - Decoder: json.NewDecoder(clientConn), - Encoder: json.NewEncoder(clientConn), - } + sockPath := filepath.Join(t.TempDir(), "qmp.sock") + ln, err := net.Listen("unix", sockPath) + require.NoError(t, err) done := make(chan struct{}) go func() { defer close(done) + serverConn, err := ln.Accept() + if err != nil { + return + } + defer serverConn.Close() + dec := json.NewDecoder(serverConn) enc := json.NewEncoder(serverConn) for { @@ -2250,9 +2257,18 @@ func newMockQMPClient(t *testing.T, responseFunc func(cmd qmp.QMPCommand) map[st } }() + clientConn, err := net.Dial("unix", sockPath) + require.NoError(t, err) + + client := &qmp.QMPClient{ + Conn: clientConn, + Decoder: json.NewDecoder(clientConn), + Encoder: json.NewEncoder(clientConn), + } + cancel := func() { - clientConn.Close() - serverConn.Close() + _ = ln.Close() + _ = clientConn.Close() <-done } return client, cancel diff --git a/spinifex/vm/volumes_test.go b/spinifex/vm/volumes_test.go index e7e8514a1..dc923f2a3 100644 --- a/spinifex/vm/volumes_test.go +++ b/spinifex/vm/volumes_test.go @@ -24,23 +24,30 @@ import ( "github.com/stretchr/testify/require" ) -// newMockQMPClient creates a QMPClient backed by an in-memory pipe. The +// newMockQMPClient creates a QMPClient backed by a unix socket. The // responder is invoked for every command and returns the JSON object the // server should reply with (e.g. {"return": {}} or {"error": {...}}). A // nil responder reply defaults to an empty success. +// +// A real socket rather than net.Pipe, which is unbuffered and synchronous: +// the decoder stops at the closing brace and leaves the encoder's trailing +// newline unread, wedging the writer while the peer replies. func newMockQMPClient(t *testing.T, responder func(qmp.QMPCommand) map[string]any) (*qmp.QMPClient, func()) { t.Helper() - clientConn, serverConn := net.Pipe() - client := &qmp.QMPClient{ - Conn: clientConn, - Decoder: json.NewDecoder(clientConn), - Encoder: json.NewEncoder(clientConn), - } + sockPath := filepath.Join(t.TempDir(), "qmp.sock") + ln, err := net.Listen("unix", sockPath) + require.NoError(t, err) done := make(chan struct{}) go func() { defer close(done) + serverConn, err := ln.Accept() + if err != nil { + return + } + defer serverConn.Close() + dec := json.NewDecoder(serverConn) enc := json.NewEncoder(serverConn) for { @@ -61,9 +68,18 @@ func newMockQMPClient(t *testing.T, responder func(qmp.QMPCommand) map[string]an } }() + clientConn, err := net.Dial("unix", sockPath) + require.NoError(t, err) + + client := &qmp.QMPClient{ + Conn: clientConn, + Decoder: json.NewDecoder(clientConn), + Encoder: json.NewEncoder(clientConn), + } + cancel := func() { + _ = ln.Close() _ = clientConn.Close() - _ = serverConn.Close() <-done } return client, cancel