dap: add debug adapter implementation

Adds a simple implementation of the debug adapter that supports the very
basics of a debug adapter.

It supports the launch request, the configuration done request, the
creation of threads, stopping, resuming, and disconnecting from server.

It does not support custom breakpoints, stack traces, or variable
inspection yet. These are planned to be added in the future.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2025-06-30 10:51:20 -05:00
parent 43e2f27cac
commit 42599a7d49
27 changed files with 4034 additions and 213 deletions
+129
View File
@@ -0,0 +1,129 @@
package dap
import (
"context"
"encoding/json"
"io"
"testing"
"time"
"github.com/google/go-dap"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
func TestLaunch(t *testing.T) {
adapter, conn, client := NewTestAdapter[any](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(time.Second):
assert.Fail(t, "did not receive configurationDone response")
}
return nil
})
eg.Wait()
}
func NewTestAdapter[T any](t *testing.T) (*Adapter[T], 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[T](nil)
t.Cleanup(func() { adapter.Stop() })
client := NewClient(clientConn)
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
}