Files
releases/v1/debug/debugger_test.go
T
Anders Eknert afb30d3f9d Add gocritic linter, fix a bunch of stuff (#7377)
Brace yourselves! For there are many touched files here. No changes
in semantics however.

Spent a long time trying out the various optional rules gocritic
provides, and settled for a few of them. There are more I really
like, but that would take many hours to address across the codebase.

Perhaps others find gocritic too pedantic? If so, we can merge the
fixes without enabling the rule.

Signed-off-by: Anders Eknert <anders@styra.com>
2025-02-24 16:28:41 +01:00

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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background())
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(context.Background())
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(context.Background())
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(context.Background())
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(context.Background())
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(context.Background(), 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]interface{}
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: &rego.ResultSet{
rego.Result{
Expressions: []*rego.ExpressionValue{
{
Value: ast.Boolean(true),
Text: "x = data.test.allow",
},
},
Bindings: map[string]interface{}{
"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]interface{}{
"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(context.Background())
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()
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 (ts *testStack) Enabled() bool {
return true
}
func (ts *testStack) TraceEvent(_ topdown.Event) {
}
func (ts *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 := context.Background()
decl := &rego.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)
}
}