tests: add integration tests for dap build

This adds integration tests for the `dap build` command to test various
behavior associated with the command. We start the build and the
integration test acts as a dap client to send requests and check that
the output is what we expect.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2025-12-17 10:56:30 -06:00
parent 150cc839f2
commit fdfba3014d
10 changed files with 1151 additions and 72 deletions
+3 -2
View File
@@ -568,8 +568,9 @@ func newBreakpointMap() *breakpointMap {
func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoints []dap.Breakpoint) {
b.mu.Lock()
defer b.mu.Unlock()
// explicitly initialize breakpoints so that
// we do not send a null back in the JSON if there are no breakpoints
// Explicitly initialize breakpoints so that we do not send a
// null back in the JSON if there are no breakpoints
breakpoints = []dap.Breakpoint{}
prev := b.byPath[fname]
+11 -48
View File
@@ -2,7 +2,6 @@ package dap
import (
"context"
"encoding/json"
"fmt"
"io"
"path/filepath"
@@ -10,6 +9,7 @@ import (
"time"
"github.com/docker/buildx/dap/common"
"github.com/docker/buildx/util/daptest"
"github.com/google/go-dap"
"github.com/moby/buildkit/solver/pb"
"github.com/stretchr/testify/assert"
@@ -36,20 +36,20 @@ func TestLaunch(t *testing.T) {
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{
configurationDone = daptest.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{
initializeResp := <-daptest.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{
launchResp := <-daptest.DoRequest[*dap.LaunchResponse](t, client, &dap.LaunchRequest{
Request: dap.Request{Command: "launch"},
})
assert.True(t, launchResp.Success)
@@ -93,7 +93,7 @@ func TestSetBreakpoints(t *testing.T) {
)
client.RegisterEvent("initialized", func(em dap.EventMessage) {
setBreakpoints = DoRequest[*dap.SetBreakpointsResponse](t, client, &dap.SetBreakpointsRequest{
setBreakpoints = daptest.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")},
@@ -104,13 +104,13 @@ func TestSetBreakpoints(t *testing.T) {
})
eg.Go(func() error {
initializeResp := <-DoRequest[*dap.InitializeResponse](t, client, &dap.InitializeRequest{
initializeResp := <-daptest.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{
launchResp := <-daptest.DoRequest[*dap.LaunchResponse](t, client, &dap.LaunchRequest{
Request: dap.Request{Command: "launch"},
})
assert.True(t, launchResp.Success)
@@ -234,66 +234,29 @@ func TestBreakpointMapIntersectVerified(t *testing.T) {
}
}
func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *Client) {
func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *daptest.Client) {
t.Helper()
rd1, wr1 := io.Pipe()
rd2, wr2 := io.Pipe()
srvConn := logConn(t, "server", NewConn(rd1, wr2))
srvConn := daptest.LogConn(t, "server", NewConn(rd1, wr2))
t.Cleanup(func() {
srvConn.Close()
})
clientConn := logConn(t, "client", NewConn(rd2, wr1))
clientConn := daptest.LogConn(t, "client", NewConn(rd2, wr1))
t.Cleanup(func() { clientConn.Close() })
adapter := New[C]()
t.Cleanup(func() { adapter.Stop() })
client := NewClient(clientConn)
client := daptest.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
}
type breakpointTestContext struct {
context.Context
messages chan dap.Message
+1
View File
@@ -0,0 +1 @@
package dap
-135
View File
@@ -1,135 +0,0 @@
package dap
import (
"context"
"sync"
"sync/atomic"
"testing"
"github.com/google/go-dap"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
type Client struct {
conn Conn
requests map[int]chan<- dap.ResponseMessage
requestsMu sync.Mutex
events map[string]func(dap.EventMessage)
eventsMu sync.RWMutex
seq atomic.Int64
eg *errgroup.Group
cancel context.CancelCauseFunc
}
func NewClient(conn Conn) *Client {
c := &Client{
conn: conn,
requests: make(map[int]chan<- dap.ResponseMessage),
events: make(map[string]func(dap.EventMessage)),
}
var ctx context.Context
ctx, c.cancel = context.WithCancelCause(context.Background())
c.eg, _ = errgroup.WithContext(context.Background())
c.eg.Go(func() error {
for {
m, err := conn.RecvMsg(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
return err
}
switch m := m.(type) {
case dap.RequestMessage:
// TODO: no reverse requests are currently supported
conn.SendMsg(&dap.Response{
ProtocolMessage: dap.ProtocolMessage{
Seq: c.nextSeq(),
Type: "response",
},
RequestSeq: m.GetRequest().GetSeq(),
Success: false,
Command: m.GetRequest().Command,
Message: "not implemented",
})
case dap.ResponseMessage:
c.requestsMu.Lock()
req := m.GetResponse().GetResponse().RequestSeq
ch := c.requests[req]
delete(c.requests, req)
c.requestsMu.Unlock()
if ch != nil {
ch <- m
}
case dap.EventMessage:
c.invokeEventCallback(m)
}
}
})
return c
}
func (c *Client) Do(t *testing.T, req dap.RequestMessage) <-chan dap.ResponseMessage {
req.GetRequest().Type = "request"
req.GetRequest().Seq = c.nextSeq()
ch := make(chan dap.ResponseMessage, 1)
if err := c.conn.SendMsg(req); err != nil {
assert.NoError(t, err)
close(ch)
return ch
}
c.requestsMu.Lock()
c.requests[req.GetSeq()] = ch
c.requestsMu.Unlock()
return ch
}
func DoRequest[ResponseMessage dap.ResponseMessage, RequestMessage dap.RequestMessage](t *testing.T, c *Client, req RequestMessage) <-chan ResponseMessage {
ch := make(chan ResponseMessage, 1)
go func() {
defer close(ch)
if m := <-c.Do(t, req); m != nil {
ch <- m.(ResponseMessage)
}
}()
return ch
}
func (c *Client) RegisterEvent(event string, fn func(dap.EventMessage)) {
c.eventsMu.Lock()
defer c.eventsMu.Unlock()
c.events[event] = fn
}
func (c *Client) invokeEventCallback(event dap.EventMessage) {
c.eventsMu.RLock()
fn := c.events[event.GetEvent().Event]
c.eventsMu.RUnlock()
if fn != nil {
fn(event)
}
}
func (c *Client) Close() error {
c.cancel(context.Canceled)
return c.eg.Wait()
}
func (c *Client) nextSeq() int {
seq := c.seq.Add(1)
return int(seq)
}
+14
View File
@@ -0,0 +1,14 @@
package common
import (
"context"
"io"
"github.com/google/go-dap"
)
type Conn interface {
SendMsg(m dap.Message) error
RecvMsg(ctx context.Context) (dap.Message, error)
io.Closer
}
+2 -5
View File
@@ -6,16 +6,13 @@ import (
"io"
"sync"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
type Conn interface {
SendMsg(m dap.Message) error
RecvMsg(ctx context.Context) (dap.Message, error)
io.Closer
}
type Conn = common.Conn
type conn struct {
recvCh <-chan dap.Message