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:
+2
-2
@@ -400,7 +400,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, debugOpts debuggerOpti
|
|||||||
desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term)
|
desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term)
|
||||||
}
|
}
|
||||||
if options.imageIDFile != "" {
|
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")
|
return errors.Wrap(err, "writing image ID file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,7 +655,7 @@ func writeMetadataFile(filename string, dt any) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return atomicwriter.WriteFile(filename, b, 0644)
|
return atomicwriter.WriteFile(filename, b, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeExporterResponse(exporterResponse map[string]string) map[string]any {
|
func decodeExporterResponse(exporterResponse map[string]string) map[string]any {
|
||||||
|
|||||||
+3
-2
@@ -568,8 +568,9 @@ func newBreakpointMap() *breakpointMap {
|
|||||||
func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoints []dap.Breakpoint) {
|
func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoints []dap.Breakpoint) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
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{}
|
breakpoints = []dap.Breakpoint{}
|
||||||
|
|
||||||
prev := b.byPath[fname]
|
prev := b.byPath[fname]
|
||||||
|
|||||||
+11
-48
@@ -2,7 +2,6 @@ package dap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -10,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/buildx/dap/common"
|
"github.com/docker/buildx/dap/common"
|
||||||
|
"github.com/docker/buildx/util/daptest"
|
||||||
"github.com/google/go-dap"
|
"github.com/google/go-dap"
|
||||||
"github.com/moby/buildkit/solver/pb"
|
"github.com/moby/buildkit/solver/pb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -36,20 +36,20 @@ func TestLaunch(t *testing.T) {
|
|||||||
|
|
||||||
client.RegisterEvent("initialized", func(em dap.EventMessage) {
|
client.RegisterEvent("initialized", func(em dap.EventMessage) {
|
||||||
// Send configuration done since we don't do any configuration.
|
// 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"},
|
Request: dap.Request{Command: "configurationDone"},
|
||||||
})
|
})
|
||||||
close(initialized)
|
close(initialized)
|
||||||
})
|
})
|
||||||
|
|
||||||
eg.Go(func() error {
|
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"},
|
Request: dap.Request{Command: "initialize"},
|
||||||
})
|
})
|
||||||
assert.True(t, initializeResp.Success)
|
assert.True(t, initializeResp.Success)
|
||||||
assert.True(t, initializeResp.Body.SupportsConfigurationDoneRequest)
|
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"},
|
Request: dap.Request{Command: "launch"},
|
||||||
})
|
})
|
||||||
assert.True(t, launchResp.Success)
|
assert.True(t, launchResp.Success)
|
||||||
@@ -93,7 +93,7 @@ func TestSetBreakpoints(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
client.RegisterEvent("initialized", func(em dap.EventMessage) {
|
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"},
|
Request: dap.Request{Command: "setBreakpoints"},
|
||||||
Arguments: dap.SetBreakpointsArguments{
|
Arguments: dap.SetBreakpointsArguments{
|
||||||
Source: dap.Source{Name: "Dockerfile", Path: filepath.Join(t.TempDir(), "Dockerfile")},
|
Source: dap.Source{Name: "Dockerfile", Path: filepath.Join(t.TempDir(), "Dockerfile")},
|
||||||
@@ -104,13 +104,13 @@ func TestSetBreakpoints(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
eg.Go(func() error {
|
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"},
|
Request: dap.Request{Command: "initialize"},
|
||||||
})
|
})
|
||||||
assert.True(t, initializeResp.Success)
|
assert.True(t, initializeResp.Success)
|
||||||
assert.True(t, initializeResp.Body.SupportsConfigurationDoneRequest)
|
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"},
|
Request: dap.Request{Command: "launch"},
|
||||||
})
|
})
|
||||||
assert.True(t, launchResp.Success)
|
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()
|
t.Helper()
|
||||||
|
|
||||||
rd1, wr1 := io.Pipe()
|
rd1, wr1 := io.Pipe()
|
||||||
rd2, wr2 := io.Pipe()
|
rd2, wr2 := io.Pipe()
|
||||||
|
|
||||||
srvConn := logConn(t, "server", NewConn(rd1, wr2))
|
srvConn := daptest.LogConn(t, "server", NewConn(rd1, wr2))
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
srvConn.Close()
|
srvConn.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
clientConn := logConn(t, "client", NewConn(rd2, wr1))
|
clientConn := daptest.LogConn(t, "client", NewConn(rd2, wr1))
|
||||||
t.Cleanup(func() { clientConn.Close() })
|
t.Cleanup(func() { clientConn.Close() })
|
||||||
|
|
||||||
adapter := New[C]()
|
adapter := New[C]()
|
||||||
t.Cleanup(func() { adapter.Stop() })
|
t.Cleanup(func() { adapter.Stop() })
|
||||||
|
|
||||||
client := NewClient(clientConn)
|
client := daptest.NewClient(clientConn)
|
||||||
t.Cleanup(func() { client.Close() })
|
t.Cleanup(func() { client.Close() })
|
||||||
|
|
||||||
return adapter, srvConn, client
|
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 {
|
type breakpointTestContext struct {
|
||||||
context.Context
|
context.Context
|
||||||
messages chan dap.Message
|
messages chan dap.Message
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package dap
|
||||||
@@ -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
@@ -6,16 +6,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/dap/common"
|
||||||
"github.com/google/go-dap"
|
"github.com/google/go-dap"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Conn interface {
|
type Conn = common.Conn
|
||||||
SendMsg(m dap.Message) error
|
|
||||||
RecvMsg(ctx context.Context) (dap.Message, error)
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type conn struct {
|
type conn struct {
|
||||||
recvCh <-chan dap.Message
|
recvCh <-chan dap.Message
|
||||||
|
|||||||
+1002
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ func TestIntegration(t *testing.T) {
|
|||||||
tests = append(tests, dialstdioTests...)
|
tests = append(tests, dialstdioTests...)
|
||||||
tests = append(tests, composeTests...)
|
tests = append(tests, composeTests...)
|
||||||
tests = append(tests, diskusageTests...)
|
tests = append(tests, diskusageTests...)
|
||||||
|
tests = append(tests, dapBuildTests...)
|
||||||
testIntegration(t, tests...)
|
testIntegration(t, tests...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
package dap
|
package daptest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/dap/common"
|
||||||
"github.com/google/go-dap"
|
"github.com/google/go-dap"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn Conn
|
conn common.Conn
|
||||||
|
|
||||||
requests map[int]chan<- dap.ResponseMessage
|
requests map[int]chan<- dap.ResponseMessage
|
||||||
requestsMu sync.Mutex
|
requestsMu sync.Mutex
|
||||||
|
|
||||||
events map[string]func(dap.EventMessage)
|
events map[string][]func(dap.EventMessage)
|
||||||
eventsMu sync.RWMutex
|
eventsMu sync.RWMutex
|
||||||
|
|
||||||
seq atomic.Int64
|
seq atomic.Int64
|
||||||
@@ -26,11 +28,11 @@ type Client struct {
|
|||||||
cancel context.CancelCauseFunc
|
cancel context.CancelCauseFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(conn Conn) *Client {
|
func NewClient(conn common.Conn) *Client {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
requests: make(map[int]chan<- dap.ResponseMessage),
|
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
|
var ctx context.Context
|
||||||
@@ -41,7 +43,7 @@ func NewClient(conn Conn) *Client {
|
|||||||
for {
|
for {
|
||||||
m, err := conn.RecvMsg(ctx)
|
m, err := conn.RecvMsg(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -83,15 +85,22 @@ func (c *Client) Do(t *testing.T, req dap.RequestMessage) <-chan dap.ResponseMes
|
|||||||
req.GetRequest().Seq = c.nextSeq()
|
req.GetRequest().Seq = c.nextSeq()
|
||||||
|
|
||||||
ch := make(chan dap.ResponseMessage, 1)
|
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.requestsMu.Lock()
|
||||||
c.requests[req.GetSeq()] = ch
|
c.requests[req.GetSeq()] = ch
|
||||||
c.requestsMu.Unlock()
|
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
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,15 +120,15 @@ func (c *Client) RegisterEvent(event string, fn func(dap.EventMessage)) {
|
|||||||
c.eventsMu.Lock()
|
c.eventsMu.Lock()
|
||||||
defer c.eventsMu.Unlock()
|
defer c.eventsMu.Unlock()
|
||||||
|
|
||||||
c.events[event] = fn
|
c.events[event] = append(c.events[event], fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) invokeEventCallback(event dap.EventMessage) {
|
func (c *Client) invokeEventCallback(event dap.EventMessage) {
|
||||||
c.eventsMu.RLock()
|
c.eventsMu.RLock()
|
||||||
fn := c.events[event.GetEvent().Event]
|
fns := c.events[event.GetEvent().Event]
|
||||||
c.eventsMu.RUnlock()
|
c.eventsMu.RUnlock()
|
||||||
|
|
||||||
if fn != nil {
|
for _, fn := range fns {
|
||||||
fn(event)
|
fn(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user