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>
This commit is contained in:
Remy Suen
2025-10-23 05:31:37 -04:00
parent f478741671
commit 7c4dd41cb0
2 changed files with 70 additions and 0 deletions
+5
View File
@@ -590,6 +590,11 @@ func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoi
breakpoints = append(breakpoints, bp)
}
b.byPath[fname] = breakpoints
if breakpoints == nil {
// explicitly initialize breakpoints so that
// we do not send a null back in the JSON if there are no breakpoints
breakpoints = []dap.Breakpoint{}
}
return breakpoints
}
+65
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"io"
"path/filepath"
"testing"
"time"
@@ -69,6 +70,70 @@ func TestLaunch(t *testing.T) {
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()