Skip to content

Commit 067eaf4

Browse files
committed
dap: filter inputs for a step to prevent overeager evaluation
When the debug thread was updated to always solve inputs from the operation that it was tied to it became a bit overeager to evaluate them. The intention of the steps is to have a single direct parent and then potentially multiple "function calls" that can be evaluated with step into and step out to leave. With the change, that logic stayed in, but the inputs were always being evaluated before they were stepped into or over. Now, when we construct the steps, we also attach a list of inputs that we should defer evaluation on to ensure we don't execute inputs that haven't been executed yet. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
1 parent e5aff80 commit 067eaf4

File tree

2 files changed

+71
-5
lines changed

2 files changed

+71
-5
lines changed

dap/thread.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dap
22

33
import (
44
"context"
5+
"maps"
56
"path"
67
"path/filepath"
78
"slices"
@@ -131,6 +132,10 @@ type step struct {
131132
// breakpoint resolution.
132133
dgst digest.Digest
133134

135+
// filtered holds the inputs that should be filtered out of the
136+
// solved inputs.
137+
filtered map[int]bool
138+
134139
// in holds the next target when step in is used.
135140
in *step
136141

@@ -225,6 +230,13 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
225230
// Create the routine associated with this input.
226231
// Associate it with the entrypoint in step.
227232
head.in = t.createBranch(digest.Digest(inp.Digest), entrypoint)
233+
234+
// Filter this input from the target so it doesn't get solved
235+
// when moving to this step.
236+
head.filtered = make(map[int]bool)
237+
maps.Copy(head.filtered, entrypoint.filtered)
238+
head.filtered[i] = true
239+
228240
entrypoint = &head
229241
}
230242

@@ -236,11 +248,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
236248

237249
// Create a new step that refers to the direct parent.
238250
head := &step{
239-
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
240-
in: entrypoint,
241-
next: entrypoint,
242-
out: entrypoint.out,
243-
parent: -1,
251+
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
252+
filtered: entrypoint.filtered,
253+
in: entrypoint,
254+
next: entrypoint,
255+
out: entrypoint.out,
256+
parent: -1,
244257
}
245258
head.frame = t.getStackFrame(head.dgst, entrypoint)
246259
entrypoint = head
@@ -593,6 +606,10 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
593606
var root string
594607
refs := make(map[string]gateway.Reference)
595608
for i, input := range op.Inputs {
609+
if target.filtered[i] {
610+
continue
611+
}
612+
596613
k := t.determineInputName(op, i, input)
597614
if _, ok := refs[k]; ok || k == "" {
598615
continue

tests/dap_build.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path"
88
"runtime"
99
"slices"
10+
"strings"
1011
"syscall"
1112
"testing"
1213
"time"
@@ -83,6 +84,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
8384
testDapBuildStepNext,
8485
testDapBuildStepOut,
8586
testDapBuildVariables,
87+
testDapBuildDeferredEval,
8688
}
8789

8890
func testDapBuild(t *testing.T, sb integration.Sandbox) {
@@ -794,6 +796,53 @@ func testDapBuildVariables(t *testing.T, sb integration.Sandbox) {
794796
}
795797
}
796798

799+
func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
800+
dir := createTestProject(t)
801+
client, done, err := dapBuildCmd(t, sb)
802+
require.NoError(t, err)
803+
804+
// Track when we see this message.
805+
seen := false
806+
client.RegisterEvent("output", func(em dap.EventMessage) {
807+
e := em.(*dap.OutputEvent)
808+
seen = seen || strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar")
809+
})
810+
811+
interruptCh := pollInterruptEvents(client)
812+
doLaunch(t, client, commands.LaunchConfig{
813+
Dockerfile: path.Join(dir, "Dockerfile"),
814+
ContextPath: dir,
815+
},
816+
dap.SourceBreakpoint{Line: 7},
817+
)
818+
819+
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
820+
require.NotNil(t, stopped)
821+
822+
// The output event is usually immediate but it can sometimes be delayed due to
823+
// the multithreading in the printer. Just wait for a little bit.
824+
<-time.After(100 * time.Millisecond)
825+
826+
// We should not have seen this message since the branch this
827+
// message comes from should be deferred because we have
828+
// not passed the breakpoint.
829+
require.False(t, seen, "step has been invoked before intended")
830+
doNext(t, client, stopped.Body.ThreadId)
831+
832+
stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
833+
require.NotNil(t, stopped)
834+
835+
if !seen {
836+
// If we haven't seen the output then wait for a little bit
837+
// due to the printer being potentially delayed.
838+
<-time.After(100 * time.Millisecond)
839+
}
840+
require.True(t, seen, "step should have been seen")
841+
842+
var exitErr *exec.ExitError
843+
require.ErrorAs(t, done(true), &exitErr)
844+
}
845+
797846
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
798847
t.Helper()
799848

0 commit comments

Comments
 (0)