Files
buildx/dap/adapter_test.go
T
2026-03-10 18:24:55 -07:00

285 lines
8.0 KiB
Go

package dap
import (
"context"
"fmt"
"io"
"path/filepath"
"testing"
"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"
"golang.org/x/sync/errgroup"
)
func TestLaunch(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(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 = daptest.DoRequest[*dap.ConfigurationDoneResponse](t, client, &dap.ConfigurationDoneRequest{
Request: dap.Request{Command: "configurationDone"},
})
close(initialized)
})
eg.Go(func() error {
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 := <-daptest.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:
case <-ctx.Done():
assert.Fail(t, "did not receive initialized event")
}
select {
case <-configurationDone:
case <-ctx.Done():
assert.Fail(t, "did not receive configurationDone response")
}
return nil
})
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(conn)
assert.NoError(t, err)
return nil
})
var (
initialized = make(chan struct{})
setBreakpoints <-chan *dap.SetBreakpointsResponse
)
client.RegisterEvent("initialized", func(em dap.EventMessage) {
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")},
Breakpoints: []dap.SourceBreakpoint{},
},
})
close(initialized)
})
eg.Go(func() error {
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 := <-daptest.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:
case <-ctx.Done():
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 <-ctx.Done():
assert.Fail(t, "did not receive setBreakpoints response")
}
return nil
})
eg.Wait()
}
func TestBreakpointMapIntersectVerified(t *testing.T) {
t.Parallel()
ws := t.TempDir()
filename := "Dockerfile"
fpath := filepath.Join(ws, filename)
type breakpointCase struct {
desc string
sbp dap.SourceBreakpoint
expectVerified bool
}
docRanges := []*pb.Range{
{Start: &pb.Position{Line: 10, Character: 0}, End: &pb.Position{Line: 10, Character: 10}},
{Start: &pb.Position{Line: 20, Character: 5}, End: &pb.Position{Line: 20, Character: 5}},
{Start: &pb.Position{Line: 30, Character: 0}, End: &pb.Position{Line: 30, Character: 10}},
{Start: &pb.Position{Line: 35, Character: 2}, End: &pb.Position{Line: 35, Character: 7}},
}
breakpointCases := []breakpointCase{
{desc: "inside range 0", sbp: dap.SourceBreakpoint{Line: 10, Column: 5}, expectVerified: true},
{desc: "before range 0", sbp: dap.SourceBreakpoint{Line: 10, Column: -1}},
{desc: "range 1 point", sbp: dap.SourceBreakpoint{Line: 20, Column: 5}, expectVerified: true},
{desc: "before range 1 point", sbp: dap.SourceBreakpoint{Line: 20, Column: 4}},
{desc: "range 2 end", sbp: dap.SourceBreakpoint{Line: 30, Column: 10}, expectVerified: true},
{desc: "after range 2", sbp: dap.SourceBreakpoint{Line: 30, Column: 11}},
{desc: "inside range 3", sbp: dap.SourceBreakpoint{Line: 35, Column: 4}, expectVerified: true},
{desc: "different line", sbp: dap.SourceBreakpoint{Line: 40, Column: 0}},
}
bm := newBreakpointMap()
sbps := make([]dap.SourceBreakpoint, len(breakpointCases))
for i, bc := range breakpointCases {
sbps[i] = bc.sbp
}
bm.Set(fpath, sbps)
srcLocs := make(map[string]*pb.Locations, len(docRanges))
for i, rng := range docRanges {
srcLocs[fmt.Sprintf("doc-%d", i)] = &pb.Locations{
Locations: []*pb.Location{{
SourceIndex: 0,
Ranges: []*pb.Range{rng},
}},
}
}
src := &pb.Source{
Locations: srcLocs,
Infos: []*pb.SourceInfo{
{Filename: fpath},
},
}
ctx := newBreakpointTestContext(t)
digests := bm.Intersect(ctx, src)
wantMatches := 0
for _, bc := range breakpointCases {
if bc.expectVerified {
wantMatches++
}
}
assert.Len(t, digests, wantMatches)
expectedEvents := make(map[int]struct{})
for i, bp := range bm.getByPath(fpath) {
if breakpointCases[i].expectVerified {
expectedEvents[bp.Id] = struct{}{}
}
}
for len(expectedEvents) > 0 {
select {
case msg := <-ctx.messages:
evt, ok := msg.(*dap.BreakpointEvent)
if !assert.True(t, ok, "expected breakpoint event message") {
continue
}
if _, ok := expectedEvents[evt.Body.Breakpoint.Id]; ok {
delete(expectedEvents, evt.Body.Breakpoint.Id)
assert.True(t, evt.Body.Breakpoint.Verified)
} else {
t.Fatalf("unexpected breakpoint event for id %d", evt.Body.Breakpoint.Id)
}
case <-time.After(time.Second):
t.Fatalf("expected %d more breakpoint events", len(expectedEvents))
}
}
stored := bm.getByPath(fpath)
if assert.Len(t, stored, len(breakpointCases)) {
for i, bc := range breakpointCases {
assert.Equal(t, bc.expectVerified, stored[i].Verified, "breakpoint %d (%s) mismatch", i, bc.desc)
}
}
}
func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *daptest.Client) {
t.Helper()
rd1, wr1 := io.Pipe()
rd2, wr2 := io.Pipe()
srvConn := daptest.LogConn(t, "server", NewConn(rd1, wr2))
t.Cleanup(func() {
srvConn.Close()
})
clientConn := daptest.LogConn(t, "client", NewConn(rd2, wr1))
t.Cleanup(func() { clientConn.Close() })
adapter := New[C]()
client := daptest.NewClient(clientConn)
t.Cleanup(func() { client.Close() })
t.Cleanup(func() { adapter.Stop() })
return adapter, srvConn, client
}
type breakpointTestContext struct {
context.Context
messages chan dap.Message
}
func newBreakpointTestContext(t *testing.T) *breakpointTestContext {
t.Helper()
return &breakpointTestContext{
Context: context.Background(),
messages: make(chan dap.Message, 16),
}
}
func (c *breakpointTestContext) C() chan<- dap.Message {
return c.messages
}
func (c *breakpointTestContext) Go(f func(Context)) bool {
go f(c)
return true
}
func (c *breakpointTestContext) Request(dap.RequestMessage) dap.ResponseMessage {
return nil
}