Skip to content

Commit ba04f8f

Browse files
authored
Merge pull request #3687 from jsternberg/dap-filter-inputs
dap: defer inputs for a step to prevent overeager evaluation
2 parents a0a8f63 + c790615 commit ba04f8f

2 files changed

Lines changed: 95 additions & 6 deletions

File tree

dap/thread.go

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

33
import (
44
"context"
5+
"maps"
56
"path/filepath"
67
"slices"
78
"strings"
@@ -138,6 +139,11 @@ type step struct {
138139
// breakpoint resolution.
139140
dgst digest.Digest
140141

142+
// deferred holds the inputs that should have its evaluation deferred.
143+
// These inputs are still included in the references but will only be
144+
// evaluated when needed.
145+
deferred map[int]bool
146+
141147
// in holds the next target when step in is used.
142148
in *step
143149

@@ -241,10 +247,21 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
241247
// If this branch is empty (signified by a nil return value) then
242248
// skip it.
243249
if head.in == nil {
250+
// Always mark this input as deferred since it doesn't have
251+
// an associated branch.
252+
if entrypoint.deferred == nil {
253+
entrypoint.deferred = make(map[int]bool)
254+
}
255+
entrypoint.deferred[i] = true
244256
continue
245257
}
246-
247258
entrypoint.dgst = ""
259+
260+
// Filter this input from the target so it doesn't get solved
261+
// when moving to this step.
262+
head.deferred = make(map[int]bool)
263+
maps.Copy(head.deferred, entrypoint.deferred)
264+
head.deferred[i] = true
248265
entrypoint = &head
249266
}
250267

@@ -256,11 +273,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
256273

257274
// Create a new step that refers to the direct parent.
258275
head := &step{
259-
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
260-
in: entrypoint,
261-
next: entrypoint,
262-
out: entrypoint.out,
263-
parent: -1,
276+
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
277+
deferred: entrypoint.deferred,
278+
in: entrypoint,
279+
next: entrypoint,
280+
out: entrypoint.out,
281+
parent: -1,
264282
}
265283
head.frame = t.getStackFrame(head.dgst, entrypoint)
266284
entrypoint = head
@@ -628,6 +646,12 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
628646
if err != nil {
629647
return "", nil, err
630648
}
649+
650+
// If we have marked this input to be deferred, wrap it in a reference
651+
// that suppresses the evaluate call.
652+
if target.deferred[i] {
653+
ref = &deferredReference{Reference: ref}
654+
}
631655
refs[k] = ref
632656
}
633657
return root, refs, nil
@@ -823,3 +847,11 @@ func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest
823847
MountIndex: r.index,
824848
})
825849
}
850+
851+
type deferredReference struct {
852+
gateway.Reference
853+
}
854+
855+
func (r *deferredReference) Evaluate(ctx context.Context) error {
856+
return nil
857+
}

tests/dap_build.go

Lines changed: 57 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"
@@ -85,6 +86,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
8586
testDapBuildStepNext,
8687
testDapBuildStepOut,
8788
testDapBuildVariables,
89+
testDapBuildDeferredEval,
8890
}
8991

9092
func testDapBuild(t *testing.T, sb integration.Sandbox) {
@@ -857,6 +859,61 @@ func testDapBuildVariables(t *testing.T, sb integration.Sandbox) {
857859
}
858860
}
859861

862+
func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
863+
dir := createTestProject(t)
864+
client, done, err := dapBuildCmd(t, sb)
865+
require.NoError(t, err)
866+
867+
// Track when we see this message.
868+
seen := make(chan struct{}, 1)
869+
client.RegisterEvent("output", func(em dap.EventMessage) {
870+
e := em.(*dap.OutputEvent)
871+
if strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar") {
872+
select {
873+
case seen <- struct{}{}:
874+
default:
875+
}
876+
}
877+
})
878+
879+
interruptCh := pollInterruptEvents(client)
880+
doLaunch(t, client, commands.LaunchConfig{
881+
Dockerfile: path.Join(dir, "Dockerfile"),
882+
ContextPath: dir,
883+
},
884+
dap.SourceBreakpoint{Line: 7},
885+
)
886+
887+
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
888+
require.NotNil(t, stopped)
889+
890+
// The output event is usually immediate but it can sometimes be delayed due to
891+
// the multithreading in the printer. Just wait for a little bit.
892+
select {
893+
case <-seen:
894+
// We should not have seen this message since the branch this
895+
// message comes from should be deferred because we have
896+
// not passed the breakpoint.
897+
t.Fatal("step has been invoked before intended")
898+
case <-time.After(100 * time.Millisecond):
899+
}
900+
901+
doNext(t, client, stopped.Body.ThreadId)
902+
903+
stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
904+
require.NotNil(t, stopped)
905+
906+
select {
907+
case <-seen:
908+
// Wait up to a second for the input to be seen.
909+
case <-time.After(time.Second):
910+
t.Fatal("step should have been seen")
911+
}
912+
913+
var exitErr *exec.ExitError
914+
require.ErrorAs(t, done(true), &exitErr)
915+
}
916+
860917
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
861918
t.Helper()
862919

0 commit comments

Comments
 (0)