Merge pull request #3691 from jsternberg/dap-entrypoint-breakpoint

dap: fix skipped breakpoint when the breakpoint and the entrypoint were the same
This commit is contained in:
Tõnis Tiigi
2026-03-16 17:58:47 -07:00
committed by GitHub
2 changed files with 88 additions and 20 deletions
+46 -14
View File
@@ -71,18 +71,20 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
} }
defer t.reset() defer t.reset()
var next *step
action := stepContinue action := stepContinue
if cfg.StopOnEntry { if cfg.StopOnEntry {
action = stepNext // If we are stopping on entry, automatically advance to the
// entrypoint.
action, next = stepNext, t.entrypoint
} }
var ( var (
k string k string
refs map[string]gateway.Reference refs map[string]gateway.Reference
next = t.entrypoint
err error err error
) )
for next != nil { for {
event := t.needsDebug(next, action, err) event := t.needsDebug(next, action, err)
if event.Reason != "" { if event.Reason != "" {
select { select {
@@ -98,7 +100,9 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
} }
t.setBreakpoints(ctx) 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 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) { 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 // Determine how we are going to limit the scan for the next step.
// we should conclude debugging. var limit func(s *step) *step
var target *step
switch action { switch action {
case stepNext: case stepNext:
target = t.continueDigest(from, from.next) limit = func(s *step) *step {
return s.next
}
case stepIn: case stepIn:
target = from.in limit = func(s *step) *step {
return s.in
}
case stepOut: case stepOut:
target = t.continueDigest(from, from.out) limit = func(s *step) *step {
case stepContinue: return s.out
target = t.continueDigest(from, nil) }
} }
target := t.continueDigest(from, limit)
return t.seek(ctx, target) 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 return k, result, refs, nil
} }
func (t *thread) continueDigest(from, until *step) *step { func (t *thread) continueDigest(from *step, limit func(*step) *step) *step {
if len(t.bps) == 0 && until == nil { // 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 return nil
} }
@@ -561,6 +572,27 @@ func (t *thread) continueDigest(from, until *step) *step {
return ok 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 { next := func(s *step) *step {
cur := s.in cur := s.in
for cur != nil && cur != until { for cur != nil && cur != until {
+42 -6
View File
@@ -78,6 +78,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
testDapBuild, testDapBuild,
testDapBuildStopOnEntry, testDapBuildStopOnEntry,
testDapBuildSetBreakpoints, testDapBuildSetBreakpoints,
testDapBuildEntryBreakpoint,
testDapBuildVerifiedBreakpoints, testDapBuildVerifiedBreakpoints,
testDapBuildLoadedSource, testDapBuildLoadedSource,
testDapBuildStepIn, testDapBuildStepIn,
@@ -121,17 +122,19 @@ func testDapBuildStopOnEntry(t *testing.T, sb integration.Sandbox) {
}) })
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh) stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
require.Equal(t, "step", stopped.Body.Reason)
threads := doThreads(t, client) threads := doThreads(t, client)
require.ElementsMatch(t, []int{stopped.Body.ThreadId}, threads) require.ElementsMatch(t, []int{stopped.Body.ThreadId}, threads)
stackTraceResp := <-daptest.DoRequest[*dap.StackTraceResponse](t, client, &dap.StackTraceRequest{ stackFrames := doStackTrace(t, client, stopped.Body.ThreadId)
Request: dap.Request{Command: "stackTrace"}, assertStackTrace(t, stackFrames, []stackFrameMatcher{
Arguments: dap.StackTraceArguments{ {
ThreadId: stopped.Body.ThreadId, SourceName: "Dockerfile",
Line: 7,
Name: `^\[stage-1 .*\] COPY`,
}, },
}) })
require.True(t, stackTraceResp.Success)
require.Len(t, stackTraceResp.Body.StackFrames, 1)
var exitErr *exec.ExitError var exitErr *exec.ExitError
require.ErrorAs(t, done(true), &exitErr) require.ErrorAs(t, done(true), &exitErr)
@@ -202,6 +205,39 @@ func testDapBuildSetBreakpoints(t *testing.T, sb integration.Sandbox) {
require.NoError(t, done(false)) 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) { func testDapBuildVerifiedBreakpoints(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t) dir := createTestProject(t)
client, done, err := dapBuildCmd(t, sb, withArgs(dir)) client, done, err := dapBuildCmd(t, sb, withArgs(dir))