Merge pull request #3566 from jsternberg/dap-build-integration-tests

tests: add integration tests for `dap build`
This commit is contained in:
Tõnis Tiigi
2025-12-17 09:41:34 -08:00
committed by GitHub
10 changed files with 1151 additions and 72 deletions
+2 -2
View File
@@ -400,7 +400,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, debugOpts debuggerOpti
desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term)
}
if options.imageIDFile != "" {
if err := os.WriteFile(options.imageIDFile, []byte(getImageID(resp.ExporterResponse)), 0644); err != nil {
if err := os.WriteFile(options.imageIDFile, []byte(getImageID(resp.ExporterResponse)), 0o644); err != nil {
return errors.Wrap(err, "writing image ID file")
}
}
@@ -655,7 +655,7 @@ func writeMetadataFile(filename string, dt any) error {
if err != nil {
return err
}
return atomicwriter.WriteFile(filename, b, 0644)
return atomicwriter.WriteFile(filename, b, 0o644)
}
func decodeExporterResponse(exporterResponse map[string]string) map[string]any {
+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
+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
+1002
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -34,6 +34,7 @@ func TestIntegration(t *testing.T) {
tests = append(tests, dialstdioTests...)
tests = append(tests, composeTests...)
tests = append(tests, diskusageTests...)
tests = append(tests, dapBuildTests...)
testIntegration(t, tests...)
}
+24 -15
View File
@@ -1,24 +1,26 @@
package dap
package daptest
import (
"context"
"errors"
"io"
"sync"
"sync/atomic"
"testing"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
type Client struct {
conn Conn
conn common.Conn
requests map[int]chan<- dap.ResponseMessage
requestsMu sync.Mutex
events map[string]func(dap.EventMessage)
events map[string][]func(dap.EventMessage)
eventsMu sync.RWMutex
seq atomic.Int64
@@ -26,11 +28,11 @@ type Client struct {
cancel context.CancelCauseFunc
}
func NewClient(conn Conn) *Client {
func NewClient(conn common.Conn) *Client {
c := &Client{
conn: conn,
requests: make(map[int]chan<- dap.ResponseMessage),
events: make(map[string]func(dap.EventMessage)),
events: make(map[string][]func(dap.EventMessage)),
}
var ctx context.Context
@@ -41,7 +43,7 @@ func NewClient(conn Conn) *Client {
for {
m, err := conn.RecvMsg(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) {
return nil
}
return err
@@ -83,15 +85,22 @@ func (c *Client) Do(t *testing.T, req dap.RequestMessage) <-chan dap.ResponseMes
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
}
// We need to set the channel before we send the message
// because it's otherwise possible for us to receive the response
// before we've registered the original request.
c.requestsMu.Lock()
c.requests[req.GetSeq()] = ch
c.requestsMu.Unlock()
if err := c.conn.SendMsg(req); err != nil {
assert.NoError(t, err)
close(ch)
c.requestsMu.Lock()
delete(c.requests, req.GetSeq())
c.requestsMu.Unlock()
}
return ch
}
@@ -111,15 +120,15 @@ func (c *Client) RegisterEvent(event string, fn func(dap.EventMessage)) {
c.eventsMu.Lock()
defer c.eventsMu.Unlock()
c.events[event] = fn
c.events[event] = append(c.events[event], fn)
}
func (c *Client) invokeEventCallback(event dap.EventMessage) {
c.eventsMu.RLock()
fn := c.events[event.GetEvent().Event]
fns := c.events[event.GetEvent().Event]
c.eventsMu.RUnlock()
if fn != nil {
for _, fn := range fns {
fn(event)
}
}
+91
View File
@@ -0,0 +1,91 @@
package daptest
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"testing"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap"
)
func LogConn(t *testing.T, prefix string, conn common.Conn) common.Conn {
return &loggingConn{
Conn: conn,
t: t,
prefix: prefix,
}
}
type loggingConn struct {
common.Conn
t *testing.T
prefix string
outBuf []byte
}
func (c *loggingConn) SendMsg(m dap.Message) error {
c.t.Helper()
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) {
c.t.Helper()
m, err := c.Conn.RecvMsg(ctx)
if err != nil {
if !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
c.t.Logf("[%s] recv error: %v", c.prefix, err)
}
return nil, err
}
if e, ok := m.(dap.EventMessage); ok {
if drop := c.handleEvent(e); drop {
return m, nil
}
}
b, _ := json.Marshal(m)
c.t.Logf("[%s] recv: %v", c.prefix, string(b))
return m, nil
}
func (c *loggingConn) handleEvent(e dap.EventMessage) bool {
switch e.GetEvent().Event {
case "output":
m := e.(*dap.OutputEvent)
c.outBuf = append(c.outBuf, []byte(m.Body.Output)...)
for len(c.outBuf) > 0 {
i := bytes.IndexRune(c.outBuf, '\n')
if i < 0 {
break
}
c.t.Log(string(c.outBuf[:i]))
c.outBuf = c.outBuf[i+1:]
}
return true
case "terminated":
if len(c.outBuf) > 0 {
c.t.Log(string(c.outBuf))
c.outBuf = nil
}
return false
default:
return false
}
}