Files
buildx/dap/adapter_test.go
T
Remy Suen 7c4dd41cb0 dap: stop sending null to conform to the specification
The setBreakpoints response body is defined to be an array of
breakpoints but the debug adapter incorrectly serialized an empty array
as null in the JSON message. Explicitly initializing the array will
force the JSON serialization process to send an empty array back instead
of null.

Signed-off-by: Remy Suen <remy.suen@docker.com>
2025-10-23 05:31:37 -04:00

196 lines
5.0 KiB
Go

package dap
import (
"context"
"encoding/json"
"io"
"path/filepath"
"testing"
"time"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
func TestLaunch(t *testing.T) {
adapter, conn, client := NewTestAdapter[common.Config](t)
ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded)
defer cancel()
eg, _ := errgroup.WithContext(ctx)
eg.Go(func() error {
_, err := adapter.Start(ctx, conn)
assert.NoError(t, err)
return nil
})
var (
initialized = make(chan struct{})
configurationDone <-chan *dap.ConfigurationDoneResponse
)
client.RegisterEvent("initialized", func(em dap.EventMessage) {
// Send configuration done since we don't do any configuration.
configurationDone = DoRequest[*dap.ConfigurationDoneResponse](t, client, &dap.ConfigurationDoneRequest{
Request: dap.Request{Command: "configurationDone"},
})
close(initialized)
})
eg.Go(func() error {
initializeResp := <-DoRequest[*dap.InitializeResponse](t, client, &dap.InitializeRequest{
Request: dap.Request{Command: "initialize"},
})
assert.True(t, initializeResp.Success)
assert.True(t, initializeResp.Body.SupportsConfigurationDoneRequest)
launchResp := <-DoRequest[*dap.LaunchResponse](t, client, &dap.LaunchRequest{
Request: dap.Request{Command: "launch"},
})
assert.True(t, launchResp.Success)
// We should have received the initialized event.
select {
case <-initialized:
default:
assert.Fail(t, "did not receive initialized event")
}
select {
case <-configurationDone:
case <-time.After(10 * time.Second):
assert.Fail(t, "did not receive configurationDone response")
}
return nil
})
eg.Wait()
}
// TestSetBreakpoints will test sending a setBreakpoints request with no breakpoints.
// The response should be an empty array instead of null in the JSON.
func TestSetBreakpoints(t *testing.T) {
adapter, conn, client := NewTestAdapter[common.Config](t)
ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded)
defer cancel()
eg, _ := errgroup.WithContext(ctx)
eg.Go(func() error {
_, err := adapter.Start(ctx, conn)
assert.NoError(t, err)
return nil
})
var (
initialized = make(chan struct{})
setBreakpoints <-chan *dap.SetBreakpointsResponse
)
client.RegisterEvent("initialized", func(em dap.EventMessage) {
setBreakpoints = DoRequest[*dap.SetBreakpointsResponse](t, client, &dap.SetBreakpointsRequest{
Request: dap.Request{Command: "setBreakpoints"},
Arguments: dap.SetBreakpointsArguments{
Source: dap.Source{Name: "Dockerfile", Path: filepath.Join(t.TempDir(), "Dockerfile")},
Breakpoints: []dap.SourceBreakpoint{},
},
})
close(initialized)
})
eg.Go(func() error {
initializeResp := <-DoRequest[*dap.InitializeResponse](t, client, &dap.InitializeRequest{
Request: dap.Request{Command: "initialize"},
})
assert.True(t, initializeResp.Success)
assert.True(t, initializeResp.Body.SupportsConfigurationDoneRequest)
launchResp := <-DoRequest[*dap.LaunchResponse](t, client, &dap.LaunchRequest{
Request: dap.Request{Command: "launch"},
})
assert.True(t, launchResp.Success)
// We should have received the initialized event.
select {
case <-initialized:
default:
assert.Fail(t, "did not receive initialized event")
}
select {
case setBreakpointsResp := <-setBreakpoints:
assert.True(t, setBreakpointsResp.Success)
assert.Len(t, setBreakpointsResp.Body.Breakpoints, 0)
assert.NotNil(t, setBreakpointsResp.Body.Breakpoints, "breakpoints should be an empty array instead of null in the JSON")
case <-time.After(10 * time.Second):
assert.Fail(t, "did not receive setBreakpoints response")
}
return nil
})
eg.Wait()
}
func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *Client) {
t.Helper()
rd1, wr1 := io.Pipe()
rd2, wr2 := io.Pipe()
srvConn := logConn(t, "server", NewConn(rd1, wr2))
t.Cleanup(func() {
srvConn.Close()
})
clientConn := logConn(t, "client", NewConn(rd2, wr1))
t.Cleanup(func() { clientConn.Close() })
adapter := New[C]()
t.Cleanup(func() { adapter.Stop() })
client := NewClient(clientConn)
t.Cleanup(func() { client.Close() })
return adapter, srvConn, client
}
func logConn(t *testing.T, prefix string, conn Conn) Conn {
return &loggingConn{
Conn: conn,
t: t,
prefix: prefix,
}
}
type loggingConn struct {
Conn
t *testing.T
prefix string
}
func (c *loggingConn) SendMsg(m dap.Message) error {
b, _ := json.Marshal(m)
c.t.Logf("[%s] send: %v", c.prefix, string(b))
err := c.Conn.SendMsg(m)
if err != nil {
c.t.Logf("[%s] send error: %v", c.prefix, err)
}
return err
}
func (c *loggingConn) RecvMsg(ctx context.Context) (dap.Message, error) {
m, err := c.Conn.RecvMsg(ctx)
if err != nil {
c.t.Logf("[%s] recv error: %v", c.prefix, err)
return nil, err
}
b, _ := json.Marshal(m)
c.t.Logf("[%s] recv: %v", c.prefix, string(b))
return m, nil
}