dap: Fix breakpoint overlap check

The old logic wasn't quite right, and a little hard to follow.
Before this, because breakpoints sent from setBreakpoints don't have an
end line or column they are just a single point the comparison would
fail the check due to the column check.

E.g. if I have a breakpoint on line 30.
The content lines range from 29-31.
Now lets say the end column is at postion 10, but my breakpoint is at
L30 column 11 (still within range).
The overlap check would fail because 11 > 10 but these shouldn't be
compared at all in this case.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff
2025-11-24 17:03:26 -08:00
parent 4826295631
commit 8d822ebf71
2 changed files with 136 additions and 1 deletions
+10 -1
View File
@@ -612,7 +612,16 @@ func (b *breakpointMap) Intersect(ctx Context, src *pb.Source, ws string) map[di
func (b *breakpointMap) intersect(ctx Context, src *pb.Source, locs *pb.Locations, ws string) int {
overlaps := func(r *pb.Range, bp *dap.Breakpoint) bool {
return r.Start.Line <= int32(bp.Line) && r.Start.Character <= int32(bp.Column) && r.End.Line >= int32(bp.EndLine) && r.End.Character >= int32(bp.EndColumn)
if bp.Line < int(r.Start.Line) || bp.Line > int(r.End.Line) {
return false
}
if bp.Line == int(r.Start.Line) && bp.Column < int(r.Start.Character) {
return false
}
if bp.Line == int(r.End.Line) && bp.Column > int(r.End.Character) {
return false
}
return true
}
for _, loc := range locs.Locations {
+126
View File
@@ -3,6 +3,7 @@ package dap
import (
"context"
"encoding/json"
"fmt"
"io"
"path/filepath"
"testing"
@@ -10,6 +11,7 @@ import (
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap"
"github.com/moby/buildkit/solver/pb"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
@@ -134,6 +136,104 @@ func TestSetBreakpoints(t *testing.T) {
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: filename},
},
}
ctx := newBreakpointTestContext(t)
digests := bm.Intersect(ctx, src, ws)
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.byPath[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.byPath[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, *Client) {
t.Helper()
@@ -193,3 +293,29 @@ func (c *loggingConn) RecvMsg(ctx context.Context) (dap.Message, error) {
c.t.Logf("[%s] recv: %v", c.prefix, string(b))
return m, nil
}
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
}