Merge pull request #3687 from jsternberg/dap-filter-inputs

dap: defer inputs for a step to prevent overeager evaluation
This commit is contained in:
Tõnis Tiigi
2026-03-20 10:57:30 -07:00
committed by GitHub
2 changed files with 95 additions and 6 deletions
+38 -6
View File
@@ -2,6 +2,7 @@ package dap
import ( import (
"context" "context"
"maps"
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
@@ -138,6 +139,11 @@ type step struct {
// breakpoint resolution. // breakpoint resolution.
dgst digest.Digest 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 holds the next target when step in is used.
in *step in *step
@@ -241,10 +247,21 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
// If this branch is empty (signified by a nil return value) then // If this branch is empty (signified by a nil return value) then
// skip it. // skip it.
if head.in == nil { if head.in == nil {
// Always mark this input as deferred since it doesn't have
// an associated branch.
if entrypoint.deferred == nil {
entrypoint.deferred = make(map[int]bool)
}
entrypoint.deferred[i] = true
continue continue
} }
entrypoint.dgst = "" entrypoint.dgst = ""
// 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 entrypoint = &head
} }
@@ -256,11 +273,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
// Create a new step that refers to the direct parent. // Create a new step that refers to the direct parent.
head := &step{ head := &step{
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest), dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
in: entrypoint, deferred: entrypoint.deferred,
next: entrypoint, in: entrypoint,
out: entrypoint.out, next: entrypoint,
parent: -1, out: entrypoint.out,
parent: -1,
} }
head.frame = t.getStackFrame(head.dgst, entrypoint) head.frame = t.getStackFrame(head.dgst, entrypoint)
entrypoint = head entrypoint = head
@@ -628,6 +646,12 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
if err != nil { if err != nil {
return "", nil, err 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 refs[k] = ref
} }
return root, refs, nil return root, refs, nil
@@ -823,3 +847,11 @@ func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest
MountIndex: r.index, MountIndex: r.index,
}) })
} }
type deferredReference struct {
gateway.Reference
}
func (r *deferredReference) Evaluate(ctx context.Context) error {
return nil
}
+57
View File
@@ -7,6 +7,7 @@ import (
"path" "path"
"runtime" "runtime"
"slices" "slices"
"strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@@ -85,6 +86,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
testDapBuildStepNext, testDapBuildStepNext,
testDapBuildStepOut, testDapBuildStepOut,
testDapBuildVariables, testDapBuildVariables,
testDapBuildDeferredEval,
} }
func testDapBuild(t *testing.T, sb integration.Sandbox) { func testDapBuild(t *testing.T, sb integration.Sandbox) {
@@ -857,6 +859,61 @@ 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 := make(chan struct{}, 1)
client.RegisterEvent("output", func(em dap.EventMessage) {
e := em.(*dap.OutputEvent)
if strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar") {
select {
case seen <- struct{}{}:
default:
}
}
})
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.
select {
case <-seen:
// We should not have seen this message since the branch this
// message comes from should be deferred because we have
// not passed the breakpoint.
t.Fatal("step has been invoked before intended")
case <-time.After(100 * time.Millisecond):
}
doNext(t, client, stopped.Body.ThreadId)
stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
require.NotNil(t, stopped)
select {
case <-seen:
// Wait up to a second for the input to be seen.
case <-time.After(time.Second):
t.Fatal("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) { func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
t.Helper() t.Helper()