Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 79 additions & 19 deletions dap/thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dap

import (
"context"
"maps"
"path"
"path/filepath"
"slices"
Expand Down Expand Up @@ -71,18 +72,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 {
Expand All @@ -98,7 +101,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
}
Expand Down Expand Up @@ -127,6 +132,11 @@ type step struct {
// breakpoint resolution.
dgst digest.Digest

// deferred holds the inputs that should have its evaluation deferred.
// These inputs are still included in the references but will only be
// evaluated when needed.
deferred map[int]bool

// in holds the next target when step in is used.
in *step

Expand Down Expand Up @@ -221,6 +231,13 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
// Create the routine associated with this input.
// Associate it with the entrypoint in step.
head.in = t.createBranch(digest.Digest(inp.Digest), entrypoint)

// Filter this input from the target so it doesn't get solved
// when moving to this step.
head.deferred = make(map[int]bool)
maps.Copy(head.deferred, entrypoint.deferred)
head.deferred[i] = true

entrypoint = &head
}

Expand All @@ -232,11 +249,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *

// Create a new step that refers to the direct parent.
head := &step{
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
in: entrypoint,
next: entrypoint,
out: entrypoint.out,
parent: -1,
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
deferred: entrypoint.deferred,
in: entrypoint,
next: entrypoint,
out: entrypoint.out,
parent: -1,
}
head.frame = t.getStackFrame(head.dgst, entrypoint)
entrypoint = head
Expand Down Expand Up @@ -487,19 +505,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)
limit = func(s *step) *step {
return s.next
}
case stepIn:
target = from.in
limit = func(s *step) *step {
return s.in
}
case stepOut:
target = t.continueDigest(from, from.out)
case stepContinue:
target = t.continueDigest(from, nil)
limit = func(s *step) *step {
return s.out
}
}

target := t.continueDigest(from, limit)
return t.seek(ctx, target)
}

Expand All @@ -525,8 +548,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
}

Expand All @@ -539,6 +564,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 {
Expand Down Expand Up @@ -574,6 +620,12 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
if err != nil {
return "", nil, err
}

// If we have marked this input to be deferred, wrap it in a reference
// that suppresses the evaluate call.
if target.deferred[i] {
ref = &deferredReference{Reference: ref}
}
refs[k] = ref
}
return root, refs, nil
Expand Down Expand Up @@ -769,3 +821,11 @@ func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest
MountIndex: r.index,
})
}

type deferredReference struct {
gateway.Reference
}

func (r *deferredReference) Evaluate(ctx context.Context) error {
return nil
}
81 changes: 81 additions & 0 deletions tests/dap_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path"
"runtime"
"slices"
"strings"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -77,11 +78,13 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
testDapBuild,
testDapBuildStopOnEntry,
testDapBuildSetBreakpoints,
testDapBuildEntryBreakpoint,
testDapBuildVerifiedBreakpoints,
testDapBuildStepIn,
testDapBuildStepNext,
testDapBuildStepOut,
testDapBuildVariables,
testDapBuildDeferredEval,
}

func testDapBuild(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -200,6 +203,37 @@ 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)
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,
},
})
require.True(t, stackTraceResp.Success)
require.Len(t, stackTraceResp.Body.StackFrames, 1)

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))
Expand Down Expand Up @@ -762,6 +796,53 @@ func testDapBuildVariables(t *testing.T, sb integration.Sandbox) {
}
}

func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
client, done, err := dapBuildCmd(t, sb)
require.NoError(t, err)

// Track when we see this message.
seen := false
client.RegisterEvent("output", func(em dap.EventMessage) {
e := em.(*dap.OutputEvent)
seen = seen || strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar")
})

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.NotNil(t, stopped)

// The output event is usually immediate but it can sometimes be delayed due to
// the multithreading in the printer. Just wait for a little bit.
<-time.After(100 * time.Millisecond)

// We should not have seen this message since the branch this
// message comes from should be deferred because we have
// not passed the breakpoint.
require.False(t, seen, "step has been invoked before intended")
doNext(t, client, stopped.Body.ThreadId)

stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
require.NotNil(t, stopped)

if !seen {
// If we haven't seen the output then wait for a little bit
// due to the printer being potentially delayed.
<-time.After(100 * time.Millisecond)
}
require.True(t, seen, "step should have been seen")

var exitErr *exec.ExitError
require.ErrorAs(t, done(true), &exitErr)
}

func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
t.Helper()

Expand Down