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:
Summy Wu
2026-07-31 22:36:34 +08:00
committed by GitHub
parent c93c18f424
commit b3f7c2cb30
5 changed files with 202 additions and 13 deletions
+7
View File
@@ -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
View File
@@ -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})
+1 -1
View File
@@ -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
View File
@@ -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] + "..."
} }
+161
View File
@@ -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)
}
})
}
}