dap: fix skipped breakpoint when the breakpoint and the entrypoint were the same

We erroneously skipped a breakpoint when that breakpoint was the same as
the entrypoint and we did not use stop on entry. This is because we only
started evaluating breakpoints after the first step on the entrypoint
instead of at the entrypoint.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2026-03-11 10:52:50 -05:00
parent c4b5d6766e
commit 724afbb867
2 changed files with 88 additions and 20 deletions
+48 -16
View File
@@ -71,18 +71,20 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
}
defer t.reset()
var next *step
action := stepContinue
if cfg.StopOnEntry {
action = stepNext
// If we are stopping on entry, automatically advance to the
// entrypoint.
action, next = stepNext, t.entrypoint
}
var (
k string
refs map[string]gateway.Reference
next = t.entrypoint
err error
)
for next != nil {
for {
event := t.needsDebug(next, action, err)
if event.Reason != "" {
select {
@@ -98,7 +100,9 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
}
t.setBreakpoints(ctx)
k, next, refs, err = t.seekNext(ctx, next, action)
if k, next, refs, err = t.seekNext(ctx, next, action); next == nil {
break
}
}
return nil
}
@@ -509,19 +513,24 @@ func (t *thread) setBreakpoints(ctx Context) {
}
func (t *thread) seekNext(ctx Context, from *step, action stepType) (string, *step, map[string]gateway.Reference, error) {
// If we're at the end, return no digest to signal that
// we should conclude debugging.
var target *step
// Determine how we are going to limit the scan for the next step.
var limit func(s *step) *step
switch action {
case stepNext:
target = t.continueDigest(from, from.next)
case stepIn:
target = from.in
case stepOut:
target = t.continueDigest(from, from.out)
case stepContinue:
target = t.continueDigest(from, nil)
limit = func(s *step) *step {
return s.next
}
case stepIn:
limit = func(s *step) *step {
return s.in
}
case stepOut:
limit = func(s *step) *step {
return s.out
}
}
target := t.continueDigest(from, limit)
return t.seek(ctx, target)
}
@@ -547,8 +556,10 @@ func (t *thread) seek(ctx Context, target *step) (k string, result *step, mounts
return k, result, refs, nil
}
func (t *thread) continueDigest(from, until *step) *step {
if len(t.bps) == 0 && until == nil {
func (t *thread) continueDigest(from *step, limit func(*step) *step) *step {
// First chance to exit early. If there's no function for limiting
// the until step and no breakpoints then just go directly to the end step.
if len(t.bps) == 0 && limit == nil {
return nil
}
@@ -561,6 +572,27 @@ func (t *thread) continueDigest(from, until *step) *step {
return ok
}
// Special case. When we aren't coming from any step we consider
// whether the entrypoint itself is a breakpoint. If it is, we stop
// there. Otherwise, we treat the entrypoint as the from location.
if from == nil {
if isBreakpoint(t.entrypoint.dgst) {
return t.entrypoint
}
from = t.entrypoint
}
var until *step
if limit != nil {
until = limit(from)
}
// Second chance to exit early. If we've fully resolved from and the
// limit function doesn't return an end step, just go directly to the end.
if len(t.bps) == 0 && until == nil {
return nil
}
next := func(s *step) *step {
cur := s.in
for cur != nil && cur != until {
+42 -6
View File
@@ -78,6 +78,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
testDapBuild,
testDapBuildStopOnEntry,
testDapBuildSetBreakpoints,
testDapBuildEntryBreakpoint,
testDapBuildVerifiedBreakpoints,
testDapBuildLoadedSource,
testDapBuildStepIn,
@@ -121,17 +122,19 @@ func testDapBuildStopOnEntry(t *testing.T, sb integration.Sandbox) {
})
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
require.Equal(t, "step", stopped.Body.Reason)
threads := doThreads(t, client)
require.ElementsMatch(t, []int{stopped.Body.ThreadId}, threads)
stackTraceResp := <-daptest.DoRequest[*dap.StackTraceResponse](t, client, &dap.StackTraceRequest{
Request: dap.Request{Command: "stackTrace"},
Arguments: dap.StackTraceArguments{
ThreadId: stopped.Body.ThreadId,
stackFrames := doStackTrace(t, client, stopped.Body.ThreadId)
assertStackTrace(t, stackFrames, []stackFrameMatcher{
{
SourceName: "Dockerfile",
Line: 7,
Name: `^\[stage-1 .*\] COPY`,
},
})
require.True(t, stackTraceResp.Success)
require.Len(t, stackTraceResp.Body.StackFrames, 1)
var exitErr *exec.ExitError
require.ErrorAs(t, done(true), &exitErr)
@@ -202,6 +205,39 @@ func testDapBuildSetBreakpoints(t *testing.T, sb integration.Sandbox) {
require.NoError(t, done(false))
}
// testDapBuildEntryBreakpoint checks that the entrypoint is a valid breakpoint.
func testDapBuildEntryBreakpoint(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
client, done, err := dapBuildCmd(t, sb, withArgs(dir))
require.NoError(t, err)
interruptCh := pollInterruptEvents(client)
doLaunch(t, client, commands.LaunchConfig{
Dockerfile: path.Join(dir, "Dockerfile"),
ContextPath: dir,
},
dap.SourceBreakpoint{Line: 7},
)
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
require.Equal(t, "breakpoint", stopped.Body.Reason)
threads := doThreads(t, client)
require.ElementsMatch(t, []int{stopped.Body.ThreadId}, threads)
stackFrames := doStackTrace(t, client, stopped.Body.ThreadId)
assertStackTrace(t, stackFrames, []stackFrameMatcher{
{
SourceName: "Dockerfile",
Line: 7,
Name: `^\[stage-1 .*\] COPY`,
},
})
var exitErr *exec.ExitError
require.ErrorAs(t, done(true), &exitErr)
}
func testDapBuildVerifiedBreakpoints(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
client, done, err := dapBuildCmd(t, sb, withArgs(dir))