mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
debug: allow configuring variable value length limit (#8907)
### 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>
This commit is contained in:
@@ -36,6 +36,13 @@ func SetEventHandler(handler EventHandler) DebuggerOption {
|
|||||||
return v1.SetEventHandler(handler)
|
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 LaunchEvalProperties = v1.LaunchEvalProperties
|
||||||
|
|
||||||
type LaunchTestProperties = v1.LaunchTestProperties
|
type LaunchTestProperties = v1.LaunchTestProperties
|
||||||
|
|||||||
+17
-6
@@ -41,9 +41,10 @@ type Debugger interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type debugger struct {
|
type debugger struct {
|
||||||
logger logging.Logger
|
logger logging.Logger
|
||||||
printHook *printHook
|
printHook *printHook
|
||||||
eventHandler EventHandler
|
eventHandler EventHandler
|
||||||
|
maxVariableLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session interface {
|
type Session interface {
|
||||||
@@ -171,8 +172,9 @@ func NewDebugger(options ...DebuggerOption) Debugger {
|
|||||||
|
|
||||||
func newDebugger(options ...DebuggerOption) *debugger {
|
func newDebugger(options ...DebuggerOption) *debugger {
|
||||||
d := &debugger{
|
d := &debugger{
|
||||||
eventHandler: newNopEventHandler(),
|
eventHandler: newNopEventHandler(),
|
||||||
logger: logging.NewNoOpLogger(),
|
logger: logging.NewNoOpLogger(),
|
||||||
|
maxVariableLength: 100,
|
||||||
}
|
}
|
||||||
d.printHook = &printHook{d: d}
|
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 {
|
type LaunchEvalProperties struct {
|
||||||
LaunchProperties
|
LaunchProperties
|
||||||
Query string
|
Query string
|
||||||
@@ -326,7 +337,7 @@ func (d *debugger) LaunchEval(ctx context.Context, props LaunchEvalProperties, o
|
|||||||
rego.EvalVirtualCache(vc),
|
rego.EvalVirtualCache(vc),
|
||||||
}
|
}
|
||||||
|
|
||||||
varManager := newVariableManager()
|
varManager := newVariableManager(d.maxVariableLength)
|
||||||
// Threads are 1-indexed.
|
// Threads are 1-indexed.
|
||||||
t := newThread(1, "main", tracer, varManager, vc, store, d.logger)
|
t := newThread(1, "main", tracer, varManager, vc, store, d.logger)
|
||||||
s := newSession(ctx, d, varManager, props.LaunchProperties, []*thread{t})
|
s := newSession(ctx, d, varManager, props.LaunchProperties, []*thread{t})
|
||||||
|
|||||||
@@ -2113,7 +2113,7 @@ func setupDebuggerSession(ctx context.Context, stk stack, launchProperties Launc
|
|||||||
opts = append(opts, SetEventHandler(eh))
|
opts = append(opts, SetEventHandler(eh))
|
||||||
}
|
}
|
||||||
|
|
||||||
varManager := newVariableManager()
|
varManager := newVariableManager(100)
|
||||||
d := newDebugger(opts...)
|
d := newDebugger(opts...)
|
||||||
t := newThread(1, "test", stk, varManager, vc, store, l)
|
t := newThread(1, "test", stk, varManager, vc, store, l)
|
||||||
s := newSession(ctx, d, varManager, launchProperties, []*thread{t})
|
s := newSession(ctx, d, varManager, launchProperties, []*thread{t})
|
||||||
|
|||||||
+16
-6
@@ -29,8 +29,9 @@ type Variable interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type namedVar struct {
|
type namedVar struct {
|
||||||
name string
|
name string
|
||||||
value ast.Value
|
value ast.Value
|
||||||
|
maxVariableLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nv namedVar) Name() string {
|
func (nv namedVar) Name() string {
|
||||||
@@ -42,17 +43,18 @@ func (nv namedVar) Type() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (nv namedVar) Value() string {
|
func (nv namedVar) Value() string {
|
||||||
return truncatedString(nv.value.String(), 100)
|
return truncatedString(nv.value.String(), nv.maxVariableLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
type variableGetter func() []namedVar
|
type variableGetter func() []namedVar
|
||||||
|
|
||||||
type variableManager struct {
|
type variableManager struct {
|
||||||
getters []variableGetter
|
getters []variableGetter
|
||||||
|
maxVariableLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVariableManager() *variableManager {
|
func newVariableManager(maxVariableLength int) *variableManager {
|
||||||
return &variableManager{}
|
return &variableManager{maxVariableLength: maxVariableLength}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vs *variableManager) addVars(getter variableGetter) VarRef {
|
func (vs *variableManager) addVars(getter variableGetter) VarRef {
|
||||||
@@ -93,6 +95,7 @@ func (vs *variableManager) vars(varRef VarRef) ([]Variable, error) {
|
|||||||
vars := make([]Variable, len(namedVar))
|
vars := make([]Variable, len(namedVar))
|
||||||
|
|
||||||
for i, nv := range namedVar {
|
for i, nv := range namedVar {
|
||||||
|
nv.maxVariableLength = vs.maxVariableLength
|
||||||
vars[i] = variable{
|
vars[i] = variable{
|
||||||
v: nv,
|
v: nv,
|
||||||
ref: vs.subVars(nv.value),
|
ref: vs.subVars(nv.value),
|
||||||
@@ -106,7 +109,14 @@ func (vs *variableManager) vars(varRef VarRef) ([]Variable, error) {
|
|||||||
return vars, nil
|
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 {
|
func truncatedString(s string, max int) string {
|
||||||
|
if max <= 3 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
if len(s) > max {
|
if len(s) > max {
|
||||||
return s[:max-2] + "..."
|
return s[:max-2] + "..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user