mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
b3f7c2cb30
### Why are the changes in this PR needed? The DAP debugger currently truncates variable values to a hardcoded limit of 100 characters, with no way for callers to configure it. Long values cannot be inspected or copied whole from a debugger UI. ### What are the changes in this PR? - Add a new `SetMaxVariableLength` Debugger option (in both `v1/debug` and the top-level `debug` package) - A value of 0 disables truncation; the default stays 100 characters for backward compatibility - Plumb the limit through `variableManager` and `namedVar`, so it applies to top-level and nested (object/array/set) variables alike - Relax `truncatedString` to return the original string unchanged when the limit is <= 0 - Add `TestTruncatedString` and `TestVariableValueLengthLimit` covering the default limit, unlimited (0), negative, custom limits, and boundary cases ### Notes - `go build ./...` and `go test ./v1/debug/...` both pass - Default behavior is unchanged; the new option is opt-in - This PR was developed with AI assistance (Claude Code) ### Further comments Refs #8890 --------- Signed-off-by: summy wu <summy.wu81@gmail.com>
2307 lines
46 KiB
Go
2307 lines
46 KiB
Go
// Copyright 2024 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package debug
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/ast/location"
|
|
"github.com/open-policy-agent/opa/v1/logging"
|
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
"github.com/open-policy-agent/opa/v1/storage"
|
|
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
"github.com/open-policy-agent/opa/v1/topdown"
|
|
"github.com/open-policy-agent/opa/v1/topdown/builtins"
|
|
"github.com/open-policy-agent/opa/v1/types"
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
)
|
|
|
|
func TestDebuggerEvalStepOver(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
brRow int
|
|
brHits int
|
|
expRow int
|
|
}{
|
|
{
|
|
note: "rule",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
q # breakpoint
|
|
true # step-over to here
|
|
}
|
|
|
|
q := y if {
|
|
y := 1 * 2
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
expRow: 6,
|
|
},
|
|
{
|
|
note: "partial rule (success)",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p contains 1 if { # breakpoint
|
|
true
|
|
}
|
|
|
|
p contains 2 if { # step-over to here, the next partial rule 'p'
|
|
true
|
|
}
|
|
`,
|
|
brRow: 4,
|
|
brHits: 2, // First enter, then exit
|
|
expRow: 8,
|
|
},
|
|
{
|
|
note: "partial rule (fail)",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p contains 1 if {
|
|
false # breakpoint
|
|
}
|
|
|
|
p contains 2 if { # step-over to here, the next partial rule 'p'
|
|
true
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
brHits: 2, // First eval, then fail
|
|
expRow: 8,
|
|
},
|
|
{
|
|
note: "function",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
f(1) # breakpoint
|
|
true # step-over to here
|
|
}
|
|
|
|
f(x) := y if {
|
|
y := x * 2
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
expRow: 6,
|
|
},
|
|
{
|
|
note: "every",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
every x in [1, 2, 3] { # breakpoint
|
|
f(x)
|
|
}
|
|
true # step-over to here
|
|
}
|
|
|
|
f(x) := y if {
|
|
y := x * 2
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
brHits: 4, // every "header" is composed of multiple expressions that will all cause a breakpoint hit.
|
|
expRow: 8,
|
|
},
|
|
{
|
|
note: "comprehension",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
l := [x | # breakpoint
|
|
x := ["a", "b", "c"][_]
|
|
]
|
|
true # step-over to here
|
|
}
|
|
|
|
f(x) := y if {
|
|
y := x * 2
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
expRow: 8,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
modName := "test1.rego"
|
|
files := map[string]string{
|
|
modName: tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootDir string) {
|
|
eh := newTestEventHandler()
|
|
d := NewDebugger(SetEventHandler(eh.HandleEvent))
|
|
|
|
launchProps := LaunchEvalProperties{
|
|
LaunchProperties: LaunchProperties{
|
|
BundlePaths: []string{rootDir},
|
|
},
|
|
Query: "x = data.test.p",
|
|
}
|
|
|
|
// There is only one thread
|
|
thr := ThreadID(1)
|
|
|
|
s, err := d.LaunchEval(ctx, launchProps)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error launching debgug session: %v", err)
|
|
}
|
|
|
|
if _, err := s.AddBreakpoint(location.Location{File: path.Join(rootDir, modName), Row: tc.brRow}); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
expBrHits := 1
|
|
if tc.brHits > 0 {
|
|
expBrHits = tc.brHits
|
|
}
|
|
|
|
for range expBrHits {
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error resuming threads: %v", err)
|
|
}
|
|
|
|
// wait for breakpoint
|
|
if e := eh.WaitFor(ctx, StoppedEventType); e == nil {
|
|
t.Fatal("Expected stopped event")
|
|
} else if e.stackEvent.Location.Row != tc.brRow {
|
|
t.Fatalf("Expected to stop on row 5, got %d", e.stackEvent.Location.Row)
|
|
}
|
|
}
|
|
|
|
// Release the event handler, so we don't block the debugger
|
|
eh.IgnoreAll(ctx)
|
|
|
|
// step over
|
|
if err := s.StepOver(thr); err != nil {
|
|
t.Fatalf("Unexpected error stepping over: %v", err)
|
|
}
|
|
|
|
if frame := topOfStack(t, s); frame == nil {
|
|
t.Fatal("Expected frame")
|
|
} else if frame.location.Row != tc.expRow {
|
|
t.Fatalf("Expected to stop on row %d, got %d", tc.expRow, frame.location.Row)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerEvalStepIn(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
brRow int
|
|
brHits int
|
|
expRow int
|
|
}{
|
|
{
|
|
note: "rule",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
q # breakpoint
|
|
true
|
|
}
|
|
|
|
q := y if { # step-in to here
|
|
y := 2 * 2
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
expRow: 9,
|
|
},
|
|
{
|
|
note: "function",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
f(1) # breakpoint
|
|
true
|
|
}
|
|
|
|
f(x) := y if { # step-in to here
|
|
y := x * 2
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
expRow: 9,
|
|
},
|
|
{
|
|
note: "every",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
every x in [1, 2, 3] { # breakpoint
|
|
x < 4 # step-in to here
|
|
}
|
|
true
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
brHits: 4, // every "header" is composed of multiple expressions that will all cause a breakpoint hit.
|
|
expRow: 6,
|
|
},
|
|
{
|
|
note: "comprehension",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
l := [x | # breakpoint
|
|
x := ["a", "b", "c"][_] # step-in to here
|
|
]
|
|
count(l) == 3
|
|
}
|
|
`,
|
|
brRow: 5,
|
|
expRow: 6,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
modName := "test1.rego"
|
|
files := map[string]string{
|
|
modName: tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootDir string) {
|
|
eh := newTestEventHandler()
|
|
d := NewDebugger(SetEventHandler(eh.HandleEvent))
|
|
|
|
launchProps := LaunchEvalProperties{
|
|
LaunchProperties: LaunchProperties{
|
|
BundlePaths: []string{rootDir},
|
|
},
|
|
Query: "x = data.test.p",
|
|
}
|
|
|
|
// There is only one thread
|
|
thr := ThreadID(1)
|
|
|
|
s, err := d.LaunchEval(ctx, launchProps)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error launching debgug session: %v", err)
|
|
}
|
|
|
|
if _, err := s.AddBreakpoint(location.Location{File: path.Join(rootDir, modName), Row: tc.brRow}); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
expBrHits := 1
|
|
if tc.brHits > 0 {
|
|
expBrHits = tc.brHits
|
|
}
|
|
for range expBrHits {
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error resuming threads: %v", err)
|
|
}
|
|
|
|
// wait for breakpoint
|
|
if e := eh.WaitFor(ctx, StoppedEventType); e == nil {
|
|
t.Fatal("Expected stopped event")
|
|
} else if e.stackEvent.Location.Row != tc.brRow {
|
|
t.Fatalf("Expected to stop on row 5, got %d", e.stackEvent.Location.Row)
|
|
}
|
|
}
|
|
|
|
// Release the event handler, so we don't block the debugger
|
|
eh.IgnoreAll(ctx)
|
|
|
|
// step into function
|
|
if err := s.StepIn(thr); err != nil {
|
|
t.Fatalf("Unexpected error stepping in: %v", err)
|
|
}
|
|
|
|
if frame := topOfStack(t, s); frame == nil {
|
|
t.Fatal("Expected frame")
|
|
} else if frame.location.Row != tc.expRow {
|
|
t.Fatalf("Expected to stop on row %d, got %d", tc.expRow, frame.location.Row)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerEvalStepOut(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
brRow int
|
|
brHits int
|
|
expRow int
|
|
}{
|
|
{
|
|
note: "rule",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
q
|
|
true # step-out to here
|
|
}
|
|
|
|
q := y if {
|
|
true # breakpoint
|
|
y := 2 * 2
|
|
}
|
|
`,
|
|
brRow: 10,
|
|
expRow: 6,
|
|
},
|
|
{
|
|
note: "function",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
f(1)
|
|
true # step-out to here
|
|
}
|
|
|
|
f(x) := y if {
|
|
true # breakpoint
|
|
y := x * 2
|
|
}
|
|
`,
|
|
brRow: 10,
|
|
expRow: 6,
|
|
},
|
|
{
|
|
note: "every",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
every x in [1, 2, 3] {
|
|
true
|
|
true # breakpoint
|
|
x < 4
|
|
}
|
|
true # step-out to here
|
|
}
|
|
`,
|
|
brRow: 7,
|
|
brHits: 3, // every expr enumeration
|
|
expRow: 10,
|
|
},
|
|
{
|
|
note: "comprehension",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
l := [x |
|
|
true
|
|
true # breakpoint
|
|
x := ["a", "b", "c"][_]
|
|
]
|
|
true # step-out to here
|
|
}
|
|
`,
|
|
brRow: 7,
|
|
expRow: 10,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
modName := "test1.rego"
|
|
files := map[string]string{
|
|
modName: tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootDir string) {
|
|
eh := newTestEventHandler()
|
|
d := NewDebugger(SetEventHandler(eh.HandleEvent))
|
|
|
|
launchProps := LaunchEvalProperties{
|
|
LaunchProperties: LaunchProperties{
|
|
BundlePaths: []string{rootDir},
|
|
},
|
|
Query: "x = data.test.p",
|
|
}
|
|
|
|
// There is only one thread
|
|
thr := ThreadID(1)
|
|
|
|
s, err := d.LaunchEval(ctx, launchProps)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error launching debgug session: %v", err)
|
|
}
|
|
|
|
if _, err := s.AddBreakpoint(location.Location{File: path.Join(rootDir, modName), Row: tc.brRow}); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// wait for breakpoint
|
|
expBrHits := 1
|
|
if tc.brHits > 0 {
|
|
expBrHits = tc.brHits
|
|
}
|
|
for range expBrHits {
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error resuming threads: %v", err)
|
|
}
|
|
|
|
if e := eh.WaitFor(ctx, StoppedEventType); e == nil {
|
|
t.Fatal("Expected stopped event")
|
|
} else if e.stackEvent.Location.Row != tc.brRow {
|
|
t.Fatalf("Expected to stop on row 10, got %d", e.stackEvent.Location.Row)
|
|
}
|
|
}
|
|
|
|
// Release the event handler, so we don't block the debugger
|
|
eh.IgnoreAll(ctx)
|
|
|
|
// step out of function
|
|
if err := s.StepOut(thr); err != nil {
|
|
t.Fatalf("Unexpected error stepping in: %v", err)
|
|
}
|
|
|
|
if frame := topOfStack(t, s); frame == nil {
|
|
t.Fatal("Expected frame")
|
|
} else if frame.location.Row != tc.expRow {
|
|
t.Fatalf("Expected to stop on row %d, got %d", tc.expRow, frame.location.Row)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerEvalPrint(t *testing.T) {
|
|
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(10*time.Second))
|
|
defer cancel()
|
|
|
|
files := map[string]string{
|
|
"test1.rego": `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
print("hello")
|
|
}
|
|
`,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootDir string) {
|
|
eh := newTestEventHandler()
|
|
d := NewDebugger(SetEventHandler(eh.HandleEvent))
|
|
|
|
launchProps := LaunchEvalProperties{
|
|
LaunchProperties: LaunchProperties{
|
|
BundlePaths: []string{rootDir},
|
|
EnablePrint: true,
|
|
},
|
|
Query: "x = data.test.p",
|
|
}
|
|
|
|
s, err := d.LaunchEval(ctx, launchProps)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error launching debgug session: %v", err)
|
|
}
|
|
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error resuming threads: %v", err)
|
|
}
|
|
|
|
// print output
|
|
e := eh.WaitFor(ctx, StdoutEventType)
|
|
if e.Message != "hello" {
|
|
t.Fatalf("Expected message to be 'hello', got %q", e.Message)
|
|
}
|
|
|
|
// result output
|
|
exp := `[
|
|
{
|
|
"expressions": [
|
|
{
|
|
"value": true,
|
|
"text": "x = data.test.p",
|
|
"location": {
|
|
"row": 1,
|
|
"col": 1
|
|
}
|
|
}
|
|
],
|
|
"bindings": {
|
|
"x": true
|
|
}
|
|
}
|
|
]`
|
|
e = eh.WaitFor(ctx, StdoutEventType)
|
|
if e.Message != exp {
|
|
t.Fatalf("Expected message to be:\n\n%s\n\ngot:\n\n%s", exp, e.Message)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFiles(t *testing.T) {
|
|
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(10*time.Second))
|
|
defer cancel()
|
|
|
|
files := map[string]string{
|
|
"mod.rego": `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
input.foo == "a"
|
|
input.bar == "b"
|
|
data.baz == "c"
|
|
data.qux == "d"
|
|
}
|
|
`,
|
|
"input.json": `{
|
|
"foo": "a",
|
|
"bar": "b"
|
|
}`,
|
|
"input.yaml": `
|
|
foo: a
|
|
bar: b
|
|
`,
|
|
"data.json": `{
|
|
"baz": "c",
|
|
"qux": "d"
|
|
}`,
|
|
"data.yaml": `
|
|
baz: c
|
|
qux: d
|
|
`,
|
|
}
|
|
|
|
for _, ext := range []string{"json", "yaml"} {
|
|
t.Run(ext, func(t *testing.T) {
|
|
test.WithTempFS(files, func(rootDir string) {
|
|
eh := newTestEventHandler()
|
|
d := NewDebugger(SetEventHandler(eh.HandleEvent))
|
|
|
|
launchProps := LaunchEvalProperties{
|
|
LaunchProperties: LaunchProperties{
|
|
DataPaths: []string{
|
|
path.Join(rootDir, "mod.rego"),
|
|
path.Join(rootDir, "data."+ext),
|
|
},
|
|
EnablePrint: true,
|
|
},
|
|
Query: "x = data.test.p",
|
|
InputPath: path.Join(rootDir, "input."+ext),
|
|
}
|
|
|
|
s, err := d.LaunchEval(ctx, launchProps)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error launching debgug session: %v", err)
|
|
}
|
|
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error resuming threads: %v", err)
|
|
}
|
|
|
|
// result output
|
|
exp := `[
|
|
{
|
|
"expressions": [
|
|
{
|
|
"value": true,
|
|
"text": "x = data.test.p",
|
|
"location": {
|
|
"row": 1,
|
|
"col": 1
|
|
}
|
|
}
|
|
],
|
|
"bindings": {
|
|
"x": true
|
|
}
|
|
}
|
|
]`
|
|
e := eh.WaitFor(ctx, StdoutEventType)
|
|
if e.Message != exp {
|
|
t.Fatalf("Expected message to be:\n\n%s\n\ngot:\n\n%s", exp, e.Message)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func topOfStack(t *testing.T, s Session) *stackFrame {
|
|
t.Helper()
|
|
stk, err := s.StackTrace(ThreadID(1))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
frame, ok := stk[0].(*stackFrame)
|
|
if !ok {
|
|
t.Fatalf("Expected stackFrame, got %T", stk[0])
|
|
}
|
|
return frame
|
|
}
|
|
|
|
func TestDebuggerAutomaticStop(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
props LaunchProperties
|
|
expEventType EventType
|
|
expEventIndex int
|
|
}{
|
|
{
|
|
note: "No automatic stop",
|
|
expEventType: TerminatedEventType,
|
|
expEventIndex: -1,
|
|
},
|
|
{
|
|
note: "Stop on entry",
|
|
props: LaunchProperties{
|
|
StopOnEntry: true,
|
|
},
|
|
expEventType: StoppedEventType,
|
|
expEventIndex: 1,
|
|
},
|
|
{
|
|
note: "Stop on end of trace",
|
|
props: LaunchProperties{
|
|
StopOnResult: true,
|
|
},
|
|
expEventType: StoppedEventType,
|
|
expEventIndex: -1,
|
|
},
|
|
{
|
|
note: "Stop on fail",
|
|
props: LaunchProperties{
|
|
StopOnFail: true,
|
|
},
|
|
expEventType: ExceptionEventType,
|
|
expEventIndex: 3,
|
|
},
|
|
}
|
|
|
|
testEvents := []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EnterOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 3
|
|
Op: topdown.FailOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 5
|
|
Op: topdown.ExitOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
stk := newTestStack(testEvents...)
|
|
eh := newTestEventHandler()
|
|
_, s, _ := setupDebuggerSession(ctx, stk, tc.props, eh.HandleEvent, nil, nil, nil)
|
|
|
|
if err := s.start(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
test.EventuallyOrFatal(t, 5*time.Second, func() bool {
|
|
e := eh.Next(10 * time.Millisecond)
|
|
if e != nil && e.Type == tc.expEventType {
|
|
i, _ := stk.Current()
|
|
return i == tc.expEventIndex
|
|
}
|
|
return false
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerStopOnBreakpoint(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
breakpoint location.Location
|
|
events []*topdown.Event
|
|
expEventIndices []int
|
|
}{
|
|
{
|
|
note: "breakpoint on line with single event",
|
|
breakpoint: location.Location{File: "test.rego", Row: 1},
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EnterOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
},
|
|
expEventIndices: []int{1},
|
|
},
|
|
{
|
|
note: "breakpoint on line with single event (2)",
|
|
breakpoint: location.Location{File: "test.rego", Row: 2},
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EnterOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
},
|
|
expEventIndices: []int{2},
|
|
},
|
|
{
|
|
note: "breakpoint on line with multiple consecutive events",
|
|
breakpoint: location.Location{File: "test.rego", Row: 2},
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EnterOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 3
|
|
Op: topdown.UnifyOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 4
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 3,
|
|
},
|
|
},
|
|
},
|
|
expEventIndices: []int{2, 3},
|
|
},
|
|
{
|
|
note: "breakpoint on reoccurring line",
|
|
breakpoint: location.Location{File: "test.rego", Row: 2},
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EnterOp,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 3
|
|
Op: topdown.EvalOp,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 3,
|
|
},
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{ // 5
|
|
Op: topdown.RedoOp,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 1,
|
|
},
|
|
},
|
|
},
|
|
expEventIndices: []int{2, 4},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
stk := newTestStack(tc.events...)
|
|
eh := newTestEventHandler()
|
|
_, s, _ := setupDebuggerSession(ctx, stk, LaunchProperties{}, eh.HandleEvent, nil, nil, nil)
|
|
|
|
bp, err := s.AddBreakpoint(tc.breakpoint)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if bp.Location().File != tc.breakpoint.File {
|
|
t.Errorf("Expected breakpoint file %s, got %s", tc.breakpoint.File, bp.Location().File)
|
|
}
|
|
|
|
if bp.Location().Row != tc.breakpoint.Row {
|
|
t.Errorf("Expected breakpoint row %d, got %d", tc.breakpoint.Row, bp.Location().Row)
|
|
}
|
|
|
|
if err := s.start(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
var stoppedAt []int
|
|
test.EventuallyOrFatal(t, 5*time.Second, func() bool {
|
|
for {
|
|
e := eh.NextBlocking()
|
|
if e == nil || e.Type == TerminatedEventType {
|
|
return true
|
|
}
|
|
if e.Type == StoppedEventType {
|
|
stoppedAt = append(stoppedAt, e.stackIndex)
|
|
if err := s.Resume(e.Thread); err != nil {
|
|
t.Fatalf("Unexpected error resuming: %v", err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if !slices.Equal(stoppedAt, tc.expEventIndices) {
|
|
t.Errorf("Expected to stop at event indices %v, got %v", tc.expEventIndices, stoppedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerStepIn(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
events []*topdown.Event
|
|
expEventIndices []int
|
|
}{
|
|
{
|
|
note: "single query",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 2},
|
|
},
|
|
{
|
|
note: "nested queries",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 3
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 5
|
|
Op: topdown.RedoOp,
|
|
QueryID: 0,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 2, 3, 4, 5},
|
|
},
|
|
{
|
|
note: "sequential queries",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 3
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 5
|
|
Op: topdown.RedoOp,
|
|
QueryID: 0,
|
|
},
|
|
{ // 6
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 7
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 2, 3, 4, 5, 6, 7},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
stk := newTestStack(tc.events...)
|
|
eh := newTestEventHandler()
|
|
_, s, thr := setupDebuggerSession(ctx, stk, LaunchProperties{}, eh.HandleEvent, nil, nil, nil)
|
|
|
|
var stoppedAt []int
|
|
doneCh := make(chan struct{})
|
|
defer close(doneCh)
|
|
go func() {
|
|
for {
|
|
e := eh.NextBlocking()
|
|
|
|
if e == nil || e.Type == TerminatedEventType {
|
|
break
|
|
}
|
|
|
|
if e.Type == StoppedEventType {
|
|
stoppedAt = append(stoppedAt, e.stackIndex)
|
|
}
|
|
}
|
|
doneCh <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
if err := s.StepIn(thr.id); err != nil {
|
|
t.Errorf("Unexpected error stepping in: %v", err)
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Timed out waiting for debugger to finish")
|
|
case <-doneCh:
|
|
}
|
|
|
|
if !slices.Equal(stoppedAt, tc.expEventIndices) {
|
|
t.Errorf("Expected to stop at event indices %v, got %v", tc.expEventIndices, stoppedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerStepOver(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
events []*topdown.Event
|
|
expEventIndices []int
|
|
}{
|
|
{
|
|
note: "single query",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 2},
|
|
},
|
|
{
|
|
note: "nested queries",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 3
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 5
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 4, 5},
|
|
},
|
|
{
|
|
note: "multiple nested queries",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 3
|
|
Op: topdown.EvalOp,
|
|
QueryID: 3,
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 3,
|
|
},
|
|
{ // 5
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 6
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 7
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 6, 7},
|
|
},
|
|
{
|
|
note: "sequential queries",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 3
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 5
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 6
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 1, 4, 6},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
stk := newTestStack(tc.events...)
|
|
eh := newTestEventHandler()
|
|
_, s, thr := setupDebuggerSession(ctx, stk, LaunchProperties{}, eh.HandleEvent, nil, nil, nil)
|
|
|
|
var stoppedAt []int
|
|
doneCh := make(chan struct{})
|
|
defer close(doneCh)
|
|
go func() {
|
|
for {
|
|
e := eh.NextBlocking()
|
|
|
|
if e == nil || e.Type == TerminatedEventType {
|
|
break
|
|
}
|
|
|
|
if e.Type == StoppedEventType {
|
|
stoppedAt = append(stoppedAt, e.stackIndex)
|
|
}
|
|
}
|
|
doneCh <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
if err := s.StepOver(thr.id); err != nil {
|
|
t.Errorf("Unexpected error stepping over: %v", err)
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Timed out waiting for debugger to finish")
|
|
case <-doneCh:
|
|
}
|
|
|
|
if !slices.Equal(stoppedAt, tc.expEventIndices) {
|
|
t.Errorf("Expected to stop at event indices %v, got %v", tc.expEventIndices, stoppedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerStepOut(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
events []*topdown.Event
|
|
expEventIndices []int
|
|
}{
|
|
{
|
|
note: "single query",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
// We always expect to stop on the first stack event
|
|
expEventIndices: []int{0},
|
|
},
|
|
{
|
|
note: "single query to step out of",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 3
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
|
|
expEventIndices: []int{0, 2},
|
|
},
|
|
{
|
|
note: "multiple queries to step out of",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 3,
|
|
},
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 3,
|
|
},
|
|
{ // 2
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 3
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 4
|
|
Op: topdown.RedoOp,
|
|
QueryID: 1,
|
|
},
|
|
{ // 5
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
},
|
|
},
|
|
|
|
expEventIndices: []int{0, 2, 4},
|
|
},
|
|
{
|
|
note: "step-out also steps-over",
|
|
events: []*topdown.Event{
|
|
{ // 0
|
|
Op: topdown.EvalOp,
|
|
QueryID: 3,
|
|
},
|
|
// Extra query to step over
|
|
{ // 1
|
|
Op: topdown.EvalOp,
|
|
QueryID: 4,
|
|
},
|
|
{ // 2
|
|
Op: topdown.RedoOp,
|
|
QueryID: 4,
|
|
},
|
|
{ // 3
|
|
Op: topdown.EvalOp,
|
|
QueryID: 3,
|
|
},
|
|
{ // 4
|
|
Op: topdown.EvalOp,
|
|
QueryID: 2,
|
|
},
|
|
{ // 5
|
|
Op: topdown.RedoOp,
|
|
QueryID: 2,
|
|
},
|
|
},
|
|
expEventIndices: []int{0, 4},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
stk := newTestStack(tc.events...)
|
|
eh := newTestEventHandler()
|
|
_, s, thr := setupDebuggerSession(ctx, stk, LaunchProperties{}, eh.HandleEvent, nil, nil, nil)
|
|
|
|
var stoppedAt []int
|
|
doneCh := make(chan struct{})
|
|
defer close(doneCh)
|
|
go func() {
|
|
for {
|
|
e := eh.NextBlocking()
|
|
|
|
if e == nil || e.Type == TerminatedEventType {
|
|
break
|
|
}
|
|
|
|
if e.Type == StoppedEventType {
|
|
stoppedAt = append(stoppedAt, e.stackIndex)
|
|
}
|
|
}
|
|
doneCh <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
if err := s.StepOut(thr.id); err != nil {
|
|
t.Errorf("Unexpected error stepping over: %v", err)
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Timed out waiting for debugger to finish")
|
|
case <-doneCh:
|
|
}
|
|
|
|
if !slices.Equal(stoppedAt, tc.expEventIndices) {
|
|
t.Errorf("Expected to stop at event indices %v, got %v", tc.expEventIndices, stoppedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerStackTrace(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
events []*topdown.Event
|
|
expTrace []*stackFrame
|
|
}{
|
|
{
|
|
note: "empty stack",
|
|
expTrace: []*stackFrame{},
|
|
},
|
|
{
|
|
note: "single stack frame, no event node",
|
|
events: []*topdown.Event{
|
|
{
|
|
Op: topdown.EvalOp,
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 42,
|
|
},
|
|
},
|
|
},
|
|
expTrace: []*stackFrame{
|
|
{
|
|
id: 1,
|
|
name: "#1: 1 Eval, test.rego:42",
|
|
thread: 1,
|
|
location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 42,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "single stack frame, event node",
|
|
events: []*topdown.Event{
|
|
{
|
|
Op: topdown.EvalOp,
|
|
Node: ast.MustParseExpr("data.test.p[x]"),
|
|
QueryID: 1,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 42,
|
|
},
|
|
},
|
|
},
|
|
expTrace: []*stackFrame{
|
|
{
|
|
id: 1,
|
|
name: "#1: 1 | Eval data.test.p[x]",
|
|
thread: 1,
|
|
location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 42,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "multiple stack frames",
|
|
events: []*topdown.Event{
|
|
{
|
|
Op: topdown.EvalOp,
|
|
Node: ast.MustParseExpr("y := data.test.p[x]"),
|
|
QueryID: 5,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
{
|
|
Op: topdown.UnifyOp,
|
|
Node: ast.MustParseExpr("y = 1"),
|
|
QueryID: 5,
|
|
Location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 3,
|
|
},
|
|
},
|
|
},
|
|
// Reversed order
|
|
expTrace: []*stackFrame{
|
|
{
|
|
id: 2,
|
|
name: "#2: 5 | Unify y = 1",
|
|
thread: 1,
|
|
location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 3,
|
|
},
|
|
},
|
|
{
|
|
id: 1,
|
|
name: "#1: 5 | Eval assign(y, data.test.p[x])",
|
|
thread: 1,
|
|
location: &location.Location{
|
|
File: "test.rego",
|
|
Row: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(5*time.Second))
|
|
defer cancel()
|
|
|
|
stk := newTestStack(tc.events...)
|
|
eh := newTestEventHandler()
|
|
_, s, thr := setupDebuggerSession(ctx, stk, LaunchProperties{}, eh.HandleEvent, nil, nil, nil)
|
|
|
|
if err := s.start(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if e := eh.WaitFor(ctx, TerminatedEventType); e == nil {
|
|
t.Fatal("Run never terminated")
|
|
}
|
|
|
|
trace, err := s.StackTrace(thr.id)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(trace) != len(tc.expTrace) {
|
|
t.Fatalf("Expected %d stack frames, got %d", len(tc.expTrace), len(trace))
|
|
}
|
|
|
|
if len(trace) != len(tc.expTrace) {
|
|
t.Errorf("Expected stack trace:\n\n%v\n\ngot:\n\n%v", tc.expTrace, trace)
|
|
}
|
|
for i := range trace {
|
|
if !trace[i].Equal(tc.expTrace[i]) {
|
|
t.Errorf("Expected stack frame (%d):\n\n%v\n\ngot:\n\n%v", i, tc.expTrace[i], trace[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebuggerScopeVariables(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
input *ast.Term
|
|
locals map[ast.Var]ast.Value
|
|
virtualCache map[string]ast.Value
|
|
data map[string]any
|
|
result *rego.ResultSet
|
|
expScopes map[string]scopeInfo
|
|
}{
|
|
{
|
|
note: "no variables",
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 0,
|
|
},
|
|
"Input (not provided)": {
|
|
name: "Input (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "input (object)",
|
|
input: ast.ObjectTerm(
|
|
ast.Item(ast.StringTerm("x"), ast.NumberTerm("1")),
|
|
ast.Item(ast.StringTerm("y"), ast.BooleanTerm(true))),
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 0,
|
|
},
|
|
"Input": {
|
|
name: "Input",
|
|
namedVariables: 1,
|
|
variables: map[string]varInfo{
|
|
"input": {
|
|
typ: "object",
|
|
val: `{"x": 1, "y": true}`,
|
|
children: map[string]varInfo{
|
|
`"x"`: {
|
|
typ: "number",
|
|
val: "1",
|
|
},
|
|
`"y"`: {
|
|
typ: "boolean",
|
|
val: "true",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "input (array)",
|
|
input: ast.ArrayTerm(
|
|
ast.StringTerm("foo"),
|
|
ast.NumberTerm("1"),
|
|
ast.BooleanTerm(true)),
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 0,
|
|
},
|
|
"Input": {
|
|
name: "Input",
|
|
namedVariables: 1,
|
|
variables: map[string]varInfo{
|
|
"input": {
|
|
typ: "array",
|
|
val: `["foo", 1, true]`,
|
|
children: map[string]varInfo{
|
|
"0": {
|
|
typ: "string",
|
|
val: `"foo"`,
|
|
},
|
|
"1": {
|
|
typ: "number",
|
|
val: "1",
|
|
},
|
|
"2": {
|
|
typ: "boolean",
|
|
val: "true",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "local vars",
|
|
locals: map[ast.Var]ast.Value{
|
|
ast.Var("x"): ast.Number("42"),
|
|
ast.Var("y"): ast.Boolean(true),
|
|
ast.Var("z"): ast.String("foo"),
|
|
ast.Var("obj"): ast.NewObject(
|
|
ast.Item(ast.StringTerm("a"), ast.NumberTerm("1")),
|
|
ast.Item(ast.StringTerm("b"), ast.NumberTerm("2"))),
|
|
ast.Var("arr"): ast.NewArray(ast.NumberTerm("1"), ast.NumberTerm("2"), ast.NumberTerm("3")),
|
|
},
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 5,
|
|
variables: map[string]varInfo{
|
|
"x": {
|
|
typ: "number",
|
|
val: "42",
|
|
},
|
|
"y": {
|
|
typ: "boolean",
|
|
val: "true",
|
|
},
|
|
"z": {
|
|
typ: "string",
|
|
val: `"foo"`,
|
|
},
|
|
"obj": {
|
|
typ: "object",
|
|
val: `{"a": 1, "b": 2}`,
|
|
children: map[string]varInfo{
|
|
`"a"`: {
|
|
typ: "number",
|
|
val: "1",
|
|
},
|
|
`"b"`: {
|
|
typ: "number",
|
|
val: "2",
|
|
},
|
|
},
|
|
},
|
|
"arr": {
|
|
typ: "array",
|
|
val: "[1, 2, 3]",
|
|
children: map[string]varInfo{
|
|
"0": {
|
|
typ: "number",
|
|
val: "1",
|
|
},
|
|
"1": {
|
|
typ: "number",
|
|
val: "2",
|
|
},
|
|
"2": {
|
|
typ: "number",
|
|
val: "3",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"Input (not provided)": {
|
|
name: "Input (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "local var with long text description",
|
|
locals: map[ast.Var]ast.Value{
|
|
ast.Var("x"): ast.String(strings.Repeat("x", 1000)),
|
|
},
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 1,
|
|
variables: map[string]varInfo{
|
|
"x": {
|
|
typ: "string",
|
|
val: fmt.Sprintf(`"%s...`, strings.Repeat("x", 97)),
|
|
},
|
|
},
|
|
},
|
|
"Input (not provided)": {
|
|
name: "Input (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "result",
|
|
result: ®o.ResultSet{
|
|
rego.Result{
|
|
Expressions: []*rego.ExpressionValue{
|
|
{
|
|
Value: ast.Boolean(true),
|
|
Text: "x = data.test.allow",
|
|
},
|
|
},
|
|
Bindings: map[string]any{
|
|
"x": ast.Boolean(true),
|
|
},
|
|
},
|
|
},
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 0,
|
|
},
|
|
"Input (not provided)": {
|
|
name: "Input (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Result Set": {
|
|
name: "Result Set",
|
|
namedVariables: 1,
|
|
variables: map[string]varInfo{
|
|
"0": {
|
|
typ: "object",
|
|
val: `{"bindings": {"x": true}, "expressions": [{"text": "x = data.test.allow", "value": true}]}`,
|
|
children: map[string]varInfo{
|
|
`"bindings"`: {
|
|
typ: "object",
|
|
val: `{"x": true}`,
|
|
children: map[string]varInfo{
|
|
`"x"`: {
|
|
typ: "boolean",
|
|
val: "true",
|
|
},
|
|
},
|
|
},
|
|
`"expressions"`: {
|
|
typ: "array",
|
|
val: `[{"text": "x = data.test.allow", "value": true}]`,
|
|
children: map[string]varInfo{
|
|
"0": {
|
|
typ: "object",
|
|
val: `{"text": "x = data.test.allow", "value": true}`,
|
|
children: map[string]varInfo{
|
|
`"text"`: {
|
|
typ: "string",
|
|
val: `"x = data.test.allow"`,
|
|
},
|
|
`"value"`: {
|
|
typ: "boolean",
|
|
val: "true",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "virtual cache",
|
|
virtualCache: map[string]ast.Value{
|
|
"data.foo": ast.String("bar"),
|
|
"data.baz": ast.Number("42"),
|
|
},
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 0,
|
|
},
|
|
"Input (not provided)": {
|
|
name: "Input (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Data (not provided)": {
|
|
name: "Data (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Virtual Cache": {
|
|
name: "Virtual Cache",
|
|
namedVariables: 2,
|
|
variables: map[string]varInfo{
|
|
"data.foo": {
|
|
typ: "string",
|
|
val: `"bar"`,
|
|
},
|
|
"data.baz": {
|
|
typ: "number",
|
|
val: "42",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "data",
|
|
data: map[string]any{
|
|
"foo": "bar",
|
|
"baz": 42,
|
|
},
|
|
expScopes: map[string]scopeInfo{
|
|
"Locals": {
|
|
name: "Locals",
|
|
namedVariables: 0,
|
|
},
|
|
"Input (not provided)": {
|
|
name: "Input (not provided)",
|
|
namedVariables: 0,
|
|
},
|
|
"Data": {
|
|
name: "Data",
|
|
namedVariables: 1,
|
|
variables: map[string]varInfo{
|
|
"data": {
|
|
typ: "object",
|
|
val: `{"baz": 42, "foo": "bar"}`,
|
|
children: map[string]varInfo{
|
|
`"foo"`: {
|
|
typ: "string",
|
|
val: `"bar"`,
|
|
},
|
|
`"baz"`: {
|
|
typ: "number",
|
|
val: "42",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
locals := ast.NewValueMap()
|
|
for k, v := range tc.locals {
|
|
locals.Put(k, v)
|
|
}
|
|
|
|
e := topdown.Event{
|
|
Op: topdown.EvalOp,
|
|
Locals: locals,
|
|
}
|
|
|
|
e.WithInput(tc.input)
|
|
events := []*topdown.Event{&e}
|
|
|
|
stk := newTestStack(events...)
|
|
|
|
if tc.result != nil {
|
|
stk.result = *tc.result
|
|
}
|
|
|
|
stk.Next() // Move forward to the first event
|
|
eh := newTestEventHandler()
|
|
|
|
var vc topdown.VirtualCache
|
|
if tc.virtualCache != nil {
|
|
vc = topdown.NewVirtualCache()
|
|
for k, v := range tc.virtualCache {
|
|
vc.Put(ast.MustParseRef(k), ast.NewTerm(v))
|
|
}
|
|
}
|
|
|
|
var store storage.Store
|
|
if tc.data != nil {
|
|
store = inmem.NewFromObject(tc.data)
|
|
}
|
|
|
|
_, s, thr := setupDebuggerSession(ctx, stk, LaunchProperties{}, eh.HandleEvent, vc, store, nil)
|
|
|
|
trace, err := s.StackTrace(thr.id)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if len(trace) != 1 {
|
|
t.Fatalf("Expected 1 stack frame, got %d", len(trace))
|
|
}
|
|
|
|
scopes, err := s.Scopes(trace[0].ID())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(scopes) != len(tc.expScopes) {
|
|
t.Fatalf("Expected %d scopes, got %d", len(tc.expScopes), len(scopes))
|
|
}
|
|
|
|
for i, scope := range scopes {
|
|
expScope, ok := tc.expScopes[scope.Name()]
|
|
if !ok {
|
|
t.Errorf("Unexpected scope: %s", scopes[i].Name())
|
|
continue
|
|
}
|
|
|
|
if scope.Name() != expScope.name {
|
|
t.Errorf("Expected scope name %s, got %s", expScope.name, scope.Name())
|
|
}
|
|
|
|
if scope.NamedVariables() != expScope.namedVariables {
|
|
t.Errorf("Expected %d named variables, got %d", expScope.namedVariables, scope.NamedVariables())
|
|
}
|
|
|
|
if scope.NamedVariables() > 0 && scope.VariablesReference() == 0 {
|
|
t.Errorf("Expected non-zero variables reference")
|
|
}
|
|
|
|
if expScope.namedVariables > 0 {
|
|
vars, err := s.Variables(scope.VariablesReference())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(vars) != expScope.namedVariables {
|
|
t.Fatalf("Expected nuber of variables to equal named variables for scope (%d), got %d", expScope.namedVariables, len(vars))
|
|
}
|
|
|
|
assertVariables(t, s, vars, expScope.variables)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type varInfo struct {
|
|
typ string
|
|
val string
|
|
children map[string]varInfo
|
|
}
|
|
|
|
type scopeInfo struct {
|
|
name string
|
|
namedVariables int
|
|
variables map[string]varInfo
|
|
}
|
|
|
|
func assertVariables(t *testing.T, s Session, variables []Variable, exp map[string]varInfo) {
|
|
for _, v := range variables {
|
|
expVar, ok := exp[v.Name()]
|
|
if !ok {
|
|
t.Errorf("Unexpected variable: %s", v.Name())
|
|
continue
|
|
}
|
|
|
|
if v.Type() != expVar.typ {
|
|
t.Errorf("Expected variable type %s, got %s", expVar.typ, v.Type())
|
|
}
|
|
|
|
if v.Value() != expVar.val {
|
|
t.Errorf("Expected variable value %s, got %s", expVar.val, v.Value())
|
|
}
|
|
|
|
if len(expVar.children) != 0 {
|
|
if v.VariablesReference() == 0 {
|
|
t.Errorf("Expected non-zero variables reference")
|
|
}
|
|
|
|
vars, err := s.Variables(v.VariablesReference())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
assertVariables(t, s, vars, expVar.children)
|
|
} else if v.VariablesReference() != 0 {
|
|
t.Errorf("Expected zero variables reference")
|
|
}
|
|
}
|
|
}
|
|
|
|
func setupDebuggerSession(ctx context.Context, stk stack, launchProperties LaunchProperties, eh EventHandler,
|
|
vc topdown.VirtualCache, store storage.Store, l logging.Logger) (*debugger, *session, *thread) {
|
|
if l == nil {
|
|
l = logging.NewNoOpLogger()
|
|
}
|
|
|
|
opts := []DebuggerOption{SetLogger(l)}
|
|
if eh != nil {
|
|
opts = append(opts, SetEventHandler(eh))
|
|
}
|
|
|
|
varManager := newVariableManager(100)
|
|
d := newDebugger(opts...)
|
|
t := newThread(1, "test", stk, varManager, vc, store, l)
|
|
s := newSession(ctx, d, varManager, launchProperties, []*thread{t})
|
|
|
|
return d, s, t
|
|
}
|
|
|
|
type testEventHandler struct {
|
|
ch chan *Event
|
|
}
|
|
|
|
func newTestEventHandler() *testEventHandler {
|
|
return &testEventHandler{
|
|
ch: make(chan *Event),
|
|
}
|
|
}
|
|
|
|
func (eh *testEventHandler) HandleEvent(event Event) {
|
|
eh.ch <- &event
|
|
}
|
|
|
|
func (eh *testEventHandler) Next(duration time.Duration) *Event {
|
|
select {
|
|
case e := <-eh.ch:
|
|
return e
|
|
case <-time.After(duration):
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (eh *testEventHandler) NextBlocking() *Event {
|
|
return <-eh.ch
|
|
}
|
|
|
|
func (eh *testEventHandler) WaitFor(ctx context.Context, eventType EventType) *Event {
|
|
for {
|
|
select {
|
|
case e := <-eh.ch:
|
|
if e.Type == eventType {
|
|
return e
|
|
}
|
|
case <-ctx.Done():
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (eh *testEventHandler) IgnoreAll(ctx context.Context) {
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-eh.ch:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (eh *testEventHandler) Do(task func() error) error {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
eh.IgnoreAll(ctx)
|
|
|
|
return task()
|
|
}
|
|
|
|
type testStack struct {
|
|
events []*topdown.Event
|
|
index int
|
|
result rego.ResultSet
|
|
closed bool
|
|
}
|
|
|
|
func newTestStack(events ...*topdown.Event) *testStack {
|
|
return &testStack{
|
|
events: events,
|
|
index: -1,
|
|
}
|
|
}
|
|
|
|
func (*testStack) Enabled() bool {
|
|
return true
|
|
}
|
|
|
|
func (*testStack) TraceEvent(_ topdown.Event) {
|
|
}
|
|
|
|
func (*testStack) Config() topdown.TraceConfig {
|
|
return topdown.TraceConfig{}
|
|
}
|
|
|
|
func (ts *testStack) Current() (int, *topdown.Event) {
|
|
if ts.index < 0 || ts.index >= len(ts.events) {
|
|
return -1, nil
|
|
}
|
|
return ts.index, ts.events[ts.index]
|
|
}
|
|
|
|
func (ts *testStack) Event(i int) *topdown.Event {
|
|
if i >= 0 && i < len(ts.events) {
|
|
return ts.events[i]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ts *testStack) Next() (int, *topdown.Event) {
|
|
if ts.closed || ts.index >= len(ts.events)-1 {
|
|
ts.index++
|
|
return -1, nil
|
|
}
|
|
ts.index++
|
|
return ts.Current()
|
|
}
|
|
|
|
func (ts *testStack) Result() rego.ResultSet {
|
|
return ts.result
|
|
}
|
|
|
|
func (ts *testStack) Close() error {
|
|
ts.closed = true
|
|
return nil
|
|
}
|
|
|
|
func TestDebuggerCustomBuiltIn(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
decl := ®o.Function{
|
|
Name: "my.builtin",
|
|
Description: "My built-in",
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S, types.S),
|
|
types.S,
|
|
),
|
|
}
|
|
|
|
fn := func(_ rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
|
|
aStr, err := builtins.StringOperand(a.Value, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bStr, err := builtins.StringOperand(b.Value, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ast.StringTerm(fmt.Sprintf("%s+%s", aStr, bStr)), nil
|
|
}
|
|
|
|
props := LaunchEvalProperties{
|
|
Query: `x := my.builtin("hello", "world")`,
|
|
}
|
|
|
|
exp := `[{"expressions":[{"value":true,"text":"x := my.builtin(\"hello\", \"world\")","location":{"row":1,"col":1}}],"bindings":{"x":"\"hello\"+\"world\""}}]`
|
|
|
|
eh := newTestEventHandler()
|
|
|
|
d := NewDebugger(SetEventHandler(eh.HandleEvent))
|
|
|
|
s, err := d.LaunchEval(ctx, props, RegoOption(rego.Function2(decl, fn)))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if err := s.ResumeAll(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// wait for result
|
|
if e := eh.WaitFor(ctx, TerminatedEventType); e == nil {
|
|
t.Fatal("Expected terminated event")
|
|
}
|
|
|
|
ts, err := s.Threads()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
res := ts[0].(*thread).stack.Result()
|
|
bs, err := json.Marshal(res)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
actual := string(bs)
|
|
if actual != exp {
|
|
t.Fatalf("Expected:\n\n%v\n\nbut got:\n\n%v", exp, actual)
|
|
}
|
|
}
|