diff --git a/debug/debugger.go b/debug/debugger.go index 33f46fb677..87030f905a 100644 --- a/debug/debugger.go +++ b/debug/debugger.go @@ -36,6 +36,13 @@ func SetEventHandler(handler EventHandler) DebuggerOption { return v1.SetEventHandler(handler) } +// SetMaxVariableLength sets the maximum length of variable values displayed in the debugger. +// Values longer than this limit will be truncated. A value of 0 disables truncation. +// If not set, the default is 100 characters. +func SetMaxVariableLength(maxVariableLength int) DebuggerOption { + return v1.SetMaxVariableLength(maxVariableLength) +} + type LaunchEvalProperties = v1.LaunchEvalProperties type LaunchTestProperties = v1.LaunchTestProperties diff --git a/v1/debug/debugger.go b/v1/debug/debugger.go index 99beccc701..135613b836 100644 --- a/v1/debug/debugger.go +++ b/v1/debug/debugger.go @@ -41,9 +41,10 @@ type Debugger interface { } type debugger struct { - logger logging.Logger - printHook *printHook - eventHandler EventHandler + logger logging.Logger + printHook *printHook + eventHandler EventHandler + maxVariableLength int } type Session interface { @@ -171,8 +172,9 @@ func NewDebugger(options ...DebuggerOption) Debugger { func newDebugger(options ...DebuggerOption) *debugger { d := &debugger{ - eventHandler: newNopEventHandler(), - logger: logging.NewNoOpLogger(), + eventHandler: newNopEventHandler(), + logger: logging.NewNoOpLogger(), + maxVariableLength: 100, } d.printHook = &printHook{d: d} @@ -195,6 +197,15 @@ func SetEventHandler(handler EventHandler) DebuggerOption { } } +// SetMaxVariableLength sets the maximum length of variable values displayed in the debugger. +// Values longer than this limit will be truncated. A value of 0 disables truncation. +// If not set, the default is 100 characters. +func SetMaxVariableLength(maxVariableLength int) DebuggerOption { + return func(d *debugger) { + d.maxVariableLength = maxVariableLength + } +} + type LaunchEvalProperties struct { LaunchProperties Query string @@ -326,7 +337,7 @@ func (d *debugger) LaunchEval(ctx context.Context, props LaunchEvalProperties, o rego.EvalVirtualCache(vc), } - varManager := newVariableManager() + varManager := newVariableManager(d.maxVariableLength) // Threads are 1-indexed. t := newThread(1, "main", tracer, varManager, vc, store, d.logger) s := newSession(ctx, d, varManager, props.LaunchProperties, []*thread{t}) diff --git a/v1/debug/debugger_test.go b/v1/debug/debugger_test.go index 1148f2cf84..d75788cfcb 100644 --- a/v1/debug/debugger_test.go +++ b/v1/debug/debugger_test.go @@ -2113,7 +2113,7 @@ func setupDebuggerSession(ctx context.Context, stk stack, launchProperties Launc opts = append(opts, SetEventHandler(eh)) } - varManager := newVariableManager() + varManager := newVariableManager(100) d := newDebugger(opts...) t := newThread(1, "test", stk, varManager, vc, store, l) s := newSession(ctx, d, varManager, launchProperties, []*thread{t}) diff --git a/v1/debug/variable.go b/v1/debug/variable.go index bccb4b54a0..d800bd49ce 100644 --- a/v1/debug/variable.go +++ b/v1/debug/variable.go @@ -29,8 +29,9 @@ type Variable interface { } type namedVar struct { - name string - value ast.Value + name string + value ast.Value + maxVariableLength int } func (nv namedVar) Name() string { @@ -42,17 +43,18 @@ func (nv namedVar) Type() string { } func (nv namedVar) Value() string { - return truncatedString(nv.value.String(), 100) + return truncatedString(nv.value.String(), nv.maxVariableLength) } type variableGetter func() []namedVar type variableManager struct { - getters []variableGetter + getters []variableGetter + maxVariableLength int } -func newVariableManager() *variableManager { - return &variableManager{} +func newVariableManager(maxVariableLength int) *variableManager { + return &variableManager{maxVariableLength: maxVariableLength} } func (vs *variableManager) addVars(getter variableGetter) VarRef { @@ -93,6 +95,7 @@ func (vs *variableManager) vars(varRef VarRef) ([]Variable, error) { vars := make([]Variable, len(namedVar)) for i, nv := range namedVar { + nv.maxVariableLength = vs.maxVariableLength vars[i] = variable{ v: nv, ref: vs.subVars(nv.value), @@ -106,7 +109,14 @@ func (vs *variableManager) vars(varRef VarRef) ([]Variable, error) { return vars, nil } +// truncatedString truncates s to at most max characters. When max <= 3 it returns +// s unchanged to avoid a negative slice index (s[:max-2]) and to prevent producing +// output longer than max. Otherwise strings longer than max are truncated to +// s[:max-2] + "...". func truncatedString(s string, max int) string { + if max <= 3 { + return s + } if len(s) > max { return s[:max-2] + "..." } diff --git a/v1/debug/variable_test.go b/v1/debug/variable_test.go new file mode 100644 index 0000000000..7221e3dd79 --- /dev/null +++ b/v1/debug/variable_test.go @@ -0,0 +1,161 @@ +// Copyright 2026 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 ( + "strings" + "testing" + + "github.com/open-policy-agent/opa/v1/ast" +) + +func TestTruncatedString(t *testing.T) { + tests := []struct { + name string + s string + max int + expected string + }{ + { + name: "no truncation when max is 0", + s: strings.Repeat("a", 200), + max: 0, + expected: strings.Repeat("a", 200), + }, + { + name: "no truncation when max is negative", + s: strings.Repeat("a", 200), + max: -1, + expected: strings.Repeat("a", 200), + }, + { + name: "no truncation when max is 1 (avoids negative slice)", + s: "abcdefghij", + max: 1, + expected: "abcdefghij", + }, + { + name: "no truncation when max is 2", + s: "abcdefghij", + max: 2, + expected: "abcdefghij", + }, + { + name: "no truncation when max is 3", + s: "abcdefghij", + max: 3, + expected: "abcdefghij", + }, + { + name: "truncation at default limit", + s: strings.Repeat("a", 200), + max: 100, + expected: strings.Repeat("a", 98) + "...", + }, + { + name: "no truncation when value is under limit", + s: strings.Repeat("a", 50), + max: 100, + expected: strings.Repeat("a", 50), + }, + { + name: "no truncation when value equals limit", + s: strings.Repeat("a", 100), + max: 100, + expected: strings.Repeat("a", 100), + }, + { + name: "truncation when value exceeds limit by one", + s: strings.Repeat("a", 101), + max: 100, + expected: strings.Repeat("a", 98) + "...", + }, + { + name: "small truncation limit", + s: "abcdefghij", + max: 5, + expected: "abc...", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := truncatedString(tc.s, tc.max) + if got != tc.expected { + t.Errorf("truncatedString(%q, %d) = %q, want %q", tc.s, tc.max, got, tc.expected) + } + }) + } +} + +func TestVariableValueLengthLimit(t *testing.T) { + // A string value longer than the default 100-character limit. + // ast.String.String() renders strings with surrounding quotes, so the + // expected values below include those quote characters. + longValue := strings.Repeat("x", 150) + // A string value under the limit. + shortValue := strings.Repeat("y", 50) + tests := []struct { + name string + maxVariableLength int + expectedLong string + expectedShort string + }{ + { + name: "default limit truncates long values", + maxVariableLength: 100, + expectedLong: `"` + strings.Repeat("x", 97) + "...", + expectedShort: `"` + shortValue + `"`, + }, + { + name: "zero limit disables truncation", + maxVariableLength: 0, + expectedLong: `"` + longValue + `"`, + expectedShort: `"` + shortValue + `"`, + }, + { + name: "negative limit disables truncation", + maxVariableLength: -1, + expectedLong: `"` + longValue + `"`, + expectedShort: `"` + shortValue + `"`, + }, + { + name: "custom limit", + maxVariableLength: 120, + expectedLong: `"` + strings.Repeat("x", 117) + "...", + expectedShort: `"` + shortValue + `"`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + vm := newVariableManager(tc.maxVariableLength) + ref := vm.addVars(func() []namedVar { + return []namedVar{ + {name: "long", value: ast.StringTerm(longValue).Value}, + {name: "short", value: ast.StringTerm(shortValue).Value}, + } + }) + + vars, err := vm.vars(ref) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + got := map[string]string{} + for _, v := range vars { + got[v.Name()] = v.Value() + } + + if got["long"] != tc.expectedLong { + t.Errorf("long variable value = %q (len %d), want %q (len %d)", + got["long"], len(got["long"]), tc.expectedLong, len(tc.expectedLong)) + } + if got["short"] != tc.expectedShort { + t.Errorf("short variable value = %q, want %q", got["short"], tc.expectedShort) + } + }) + } +}