service/dap: add config expressions to evaluate requests (#2750)

We want to provide more flexibility for users to make changes to their configurations while the debug session is running. This could be accomplished by creating a custom request, but that were require a new UI as well, and every client of dlv dap to provide its own UI for this. By using the evaluate context, users can use the already existing debug console to change their configurations.

This change includes a refactor of the terminal code in order to share the code with the dap package.

This change provides a very similar to UI as the terminal package, but there are different configuration options that are DAP specific. We plan to use this same mechanism to expose a few other commands including "sources" to help users debug an ineffective substitutePath configuration.
This commit is contained in:
Suzy Mueller 2021-10-29 22:35:13 -04:00 committed by GitHub
parent b48ceec161
commit 922c4cebd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 811 additions and 153 deletions

@ -2,6 +2,11 @@ package config
import ( import (
"bytes" "bytes"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"unicode" "unicode"
) )
@ -61,3 +66,138 @@ func SplitQuotedFields(in string, quote rune) []string {
return r return r
} }
func ConfigureSetSimple(rest string, cfgname string, field reflect.Value) error {
simpleArg := func(typ reflect.Type) (reflect.Value, error) {
switch typ.Kind() {
case reflect.Int:
n, err := strconv.Atoi(rest)
if err != nil {
return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname)
}
if n < 0 {
return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number greater than zero", cfgname)
}
return reflect.ValueOf(&n), nil
case reflect.Bool:
v := rest == "true"
return reflect.ValueOf(&v), nil
case reflect.String:
return reflect.ValueOf(&rest), nil
default:
return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname)
}
}
if field.Kind() == reflect.Ptr {
val, err := simpleArg(field.Type().Elem())
if err != nil {
return err
}
field.Set(val)
} else {
val, err := simpleArg(field.Type())
if err != nil {
return err
}
field.Set(val.Elem())
}
return nil
}
func ConfigureList(w io.Writer, config interface{}, tag string) {
it := IterateConfiguration(config, tag)
for it.Next() {
fieldName, field := it.Field()
if fieldName == "" {
continue
}
writeField(w, field, fieldName)
}
}
func writeField(w io.Writer, field reflect.Value, fieldName string) {
switch field.Kind() {
case reflect.Interface:
switch field := field.Interface().(type) {
case string:
fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
default:
fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
}
case reflect.Ptr:
if !field.IsNil() {
fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem())
} else {
fmt.Fprintf(w, "%s\t<not defined>\n", fieldName)
}
case reflect.String:
fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
default:
fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
}
}
type configureIterator struct {
cfgValue reflect.Value
cfgType reflect.Type
i int
tag string
}
func IterateConfiguration(conf interface{}, tag string) *configureIterator {
cfgValue := reflect.ValueOf(conf).Elem()
cfgType := cfgValue.Type()
return &configureIterator{cfgValue, cfgType, -1, tag}
}
func (it *configureIterator) Next() bool {
it.i++
return it.i < it.cfgValue.NumField()
}
func (it *configureIterator) Field() (name string, field reflect.Value) {
name = it.cfgType.Field(it.i).Tag.Get(it.tag)
if comma := strings.Index(name, ","); comma >= 0 {
name = name[:comma]
}
field = it.cfgValue.Field(it.i)
return
}
func ConfigureListByName(conf interface{}, name, tag string) string {
if name == "" {
return ""
}
it := IterateConfiguration(conf, tag)
for it.Next() {
fieldName, field := it.Field()
if fieldName == name {
var buf bytes.Buffer
writeField(&buf, field, fieldName)
return buf.String()
}
}
return ""
}
func ConfigureFindFieldByName(conf interface{}, name, tag string) reflect.Value {
it := IterateConfiguration(conf, tag)
for it.Next() {
fieldName, field := it.Field()
if fieldName == name {
return field
}
}
return reflect.ValueOf(nil)
}
func Split2PartsBySpace(s string) []string {
v := strings.SplitN(s, " ", 2)
for i := range v {
v[i] = strings.TrimSpace(v[i])
}
return v
}

@ -35,3 +35,67 @@ func TestSplitDoubleQuotedFields(t *testing.T) {
} }
} }
} }
func TestConfigureListByName(t *testing.T) {
type testConfig struct {
boolArg bool `cfgName:"bool-arg"`
listArg []string `cfgName:"list-arg"`
}
type args struct {
sargs *testConfig
cfgname string
}
tests := []struct {
name string
args args
want string
}{
{
name: "basic bool",
args: args{
sargs: &testConfig{
boolArg: true,
listArg: []string{},
},
cfgname: "bool-arg",
},
want: "bool-arg true\n",
},
{
name: "list arg",
args: args{
sargs: &testConfig{
boolArg: true,
listArg: []string{"item 1", "item 2"},
},
cfgname: "list-arg",
},
want: "list-arg [item 1 item 2]\n",
},
{
name: "empty",
args: args{
sargs: &testConfig{},
cfgname: "",
},
want: "",
},
{
name: "invalid",
args: args{
sargs: &testConfig{},
cfgname: "nonexistent",
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ConfigureListByName(tt.args.sargs, tt.args.cfgname, "cfgName"); got != tt.want {
t.Errorf("ConfigureListByName() = %v, want %v", got, tt.want)
}
})
}
}

@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/cosiner/argv" "github.com/cosiner/argv"
"github.com/go-delve/delve/pkg/config"
"github.com/go-delve/delve/pkg/locspec" "github.com/go-delve/delve/pkg/locspec"
"github.com/go-delve/delve/pkg/terminal/colorize" "github.com/go-delve/delve/pkg/terminal/colorize"
"github.com/go-delve/delve/service" "github.com/go-delve/delve/service"
@ -985,16 +986,8 @@ func selectedGID(state *api.DebuggerState) int {
return state.SelectedGoroutine.ID return state.SelectedGoroutine.ID
} }
func split2PartsBySpace(s string) []string {
v := strings.SplitN(s, " ", 2)
for i := range v {
v[i] = strings.TrimSpace(v[i])
}
return v
}
func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error { func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
args := split2PartsBySpace(argstr) args := config.Split2PartsBySpace(argstr)
if ctx.Prefix == onPrefix { if ctx.Prefix == onPrefix {
if len(args) != 1 || args[0] != "" { if len(args) != 1 || args[0] != "" {
@ -1043,7 +1036,7 @@ func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, directi
return errors.New("not enough arguments") return errors.New("not enough arguments")
} }
} else { } else {
args := split2PartsBySpace(argstr) args := config.Split2PartsBySpace(argstr)
var err error var err error
if frame, err = strconv.Atoi(args[0]); err != nil { if frame, err = strconv.Atoi(args[0]); err != nil {
return err return err
@ -1258,7 +1251,7 @@ func restart(t *Term, ctx callContext, args string) error {
} }
func restartRecorded(t *Term, ctx callContext, args string) error { func restartRecorded(t *Term, ctx callContext, args string) error {
v := split2PartsBySpace(args) v := config.Split2PartsBySpace(args)
rerecord := false rerecord := false
resetArgs := false resetArgs := false
@ -1810,7 +1803,7 @@ func formatBreakpointAttrs(prefix string, bp *api.Breakpoint, includeTrace bool)
} }
func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]*api.Breakpoint, error) { func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]*api.Breakpoint, error) {
args := split2PartsBySpace(argstr) args := config.Split2PartsBySpace(argstr)
requestedBp := &api.Breakpoint{} requestedBp := &api.Breakpoint{}
spec := "" spec := ""
@ -2212,7 +2205,7 @@ func types(t *Term, ctx callContext, args string) error {
} }
func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) { func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) {
if v := split2PartsBySpace(args); len(v) >= 1 && v[0] == "-v" { if v := config.Split2PartsBySpace(args); len(v) >= 1 && v[0] == "-v" {
if len(v) == 2 { if len(v) == 2 {
return v[1], t.loadConfig() return v[1], t.loadConfig()
} else { } else {
@ -2486,7 +2479,7 @@ func disassCommand(t *Term, ctx callContext, args string) error {
var cmd, rest string var cmd, rest string
if args != "" { if args != "" {
argv := split2PartsBySpace(args) argv := config.Split2PartsBySpace(args)
if len(argv) != 2 { if len(argv) != 2 {
return errDisasmUsage return errDisasmUsage
} }
@ -2517,7 +2510,7 @@ func disassCommand(t *Term, ctx callContext, args string) error {
} }
disasm, disasmErr = t.client.DisassemblePC(ctx.Scope, locs[0].PC, flavor) disasm, disasmErr = t.client.DisassemblePC(ctx.Scope, locs[0].PC, flavor)
case "-a": case "-a":
v := split2PartsBySpace(rest) v := config.Split2PartsBySpace(rest)
if len(v) != 2 { if len(v) != 2 {
return errDisasmUsage return errDisasmUsage
} }
@ -2839,7 +2832,7 @@ func getBreakpointByIDOrName(t *Term, arg string) (*api.Breakpoint, error) {
} }
func (c *Commands) onCmd(t *Term, ctx callContext, argstr string) error { func (c *Commands) onCmd(t *Term, ctx callContext, argstr string) error {
args := split2PartsBySpace(argstr) args := config.Split2PartsBySpace(argstr)
if len(args) < 2 { if len(args) < 2 {
return errors.New("not enough arguments") return errors.New("not enough arguments")
@ -2916,7 +2909,7 @@ func (c *Commands) parseBreakpointAttrs(t *Term, ctx callContext, r io.Reader) e
} }
func conditionCmd(t *Term, ctx callContext, argstr string) error { func conditionCmd(t *Term, ctx callContext, argstr string) error {
args := split2PartsBySpace(argstr) args := config.Split2PartsBySpace(argstr)
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf("not enough arguments") return fmt.Errorf("not enough arguments")
@ -2930,7 +2923,7 @@ func conditionCmd(t *Term, ctx callContext, argstr string) error {
return nil return nil
} }
args = split2PartsBySpace(args[1]) args = config.Split2PartsBySpace(args[1])
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf("not enough arguments") return fmt.Errorf("not enough arguments")
} }

@ -4,8 +4,6 @@ import (
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
"strconv"
"strings"
"text/tabwriter" "text/tabwriter"
"github.com/go-delve/delve/pkg/config" "github.com/go-delve/delve/pkg/config"
@ -32,80 +30,15 @@ func configureCmd(t *Term, ctx callContext, args string) error {
} }
} }
type configureIterator struct {
cfgValue reflect.Value
cfgType reflect.Type
i int
}
func iterateConfiguration(conf *config.Config) *configureIterator {
cfgValue := reflect.ValueOf(conf).Elem()
cfgType := cfgValue.Type()
return &configureIterator{cfgValue, cfgType, -1}
}
func (it *configureIterator) Next() bool {
it.i++
return it.i < it.cfgValue.NumField()
}
func (it *configureIterator) Field() (name string, field reflect.Value) {
name = it.cfgType.Field(it.i).Tag.Get("yaml")
if comma := strings.Index(name, ","); comma >= 0 {
name = name[:comma]
}
field = it.cfgValue.Field(it.i)
return
}
func configureFindFieldByName(conf *config.Config, name string) reflect.Value {
it := iterateConfiguration(conf)
for it.Next() {
fieldName, field := it.Field()
if fieldName == name {
return field
}
}
return reflect.ValueOf(nil)
}
func configureList(t *Term) error { func configureList(t *Term) error {
w := new(tabwriter.Writer) w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 1, ' ', 0) w.Init(os.Stdout, 0, 8, 1, ' ', 0)
config.ConfigureList(w, t.conf, "yaml")
it := iterateConfiguration(t.conf)
for it.Next() {
fieldName, field := it.Field()
if fieldName == "" {
continue
}
switch field.Kind() {
case reflect.Interface:
switch field := field.Interface().(type) {
case string:
fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
default:
fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
}
case reflect.Ptr:
if !field.IsNil() {
fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem())
} else {
fmt.Fprintf(w, "%s\t<not defined>\n", fieldName)
}
case reflect.String:
fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
default:
fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
}
}
return w.Flush() return w.Flush()
} }
func configureSet(t *Term, args string) error { func configureSet(t *Term, args string) error {
v := split2PartsBySpace(args) v := config.Split2PartsBySpace(args)
cfgname := v[0] cfgname := v[0]
var rest string var rest string
@ -117,7 +50,7 @@ func configureSet(t *Term, args string) error {
return configureSetAlias(t, rest) return configureSetAlias(t, rest)
} }
field := configureFindFieldByName(t.conf, cfgname) field := config.ConfigureFindFieldByName(t.conf, cfgname, "yaml")
if !field.CanAddr() { if !field.CanAddr() {
return fmt.Errorf("%q is not a configuration parameter", cfgname) return fmt.Errorf("%q is not a configuration parameter", cfgname)
} }
@ -126,41 +59,7 @@ func configureSet(t *Term, args string) error {
return configureSetSubstitutePath(t, rest) return configureSetSubstitutePath(t, rest)
} }
simpleArg := func(typ reflect.Type) (reflect.Value, error) { return config.ConfigureSetSimple(rest, cfgname, field)
switch typ.Kind() {
case reflect.Int:
n, err := strconv.Atoi(rest)
if err != nil {
return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname)
}
if n < 0 {
return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number greater than zero", cfgname)
}
return reflect.ValueOf(&n), nil
case reflect.Bool:
v := rest == "true"
return reflect.ValueOf(&v), nil
case reflect.String:
return reflect.ValueOf(&rest), nil
default:
return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname)
}
}
if field.Kind() == reflect.Ptr {
val, err := simpleArg(field.Type().Elem())
if err != nil {
return err
}
field.Set(val)
} else {
val, err := simpleArg(field.Type())
if err != nil {
return err
}
field.Set(val.Elem())
}
return nil
} }
func configureSetSubstitutePath(t *Term, rest string) error { func configureSetSubstitutePath(t *Term, rest string) error {

115
service/dap/command.go Normal file

@ -0,0 +1,115 @@
package dap
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/go-delve/delve/pkg/config"
)
func (s *Session) delveCmd(goid, frame int, cmdstr string) (string, error) {
vals := strings.SplitN(strings.TrimSpace(cmdstr), " ", 2)
cmdname := vals[0]
var args string
if len(vals) > 1 {
args = strings.TrimSpace(vals[1])
}
for _, cmd := range debugCommands(s) {
for _, alias := range cmd.aliases {
if alias == cmdname {
return cmd.cmdFn(goid, frame, args)
}
}
}
return "", errNoCmd
}
type cmdfunc func(goid, frame int, args string) (string, error)
type command struct {
aliases []string
helpMsg string
cmdFn cmdfunc
}
const (
msgHelp = `Prints the help message.
help [command]
Type "help" followed by the name of a command for more information about it.`
msgConfig = `Changes configuration parameters.
config -list
Show all configuration parameters.
config <parameter> <value>
Changes the value of a configuration parameter.
config substitutePath <from> <to>
config substitutePath <from>
Adds or removes a path substitution rule.`
)
// debugCommands returns a list of commands with default commands defined.
func debugCommands(s *Session) []command {
return []command{
{aliases: []string{"help", "h"}, cmdFn: s.helpMessage, helpMsg: msgHelp},
{aliases: []string{"config"}, cmdFn: s.evaluateConfig, helpMsg: msgConfig},
}
}
var errNoCmd = errors.New("command not available")
func (s *Session) helpMessage(_, _ int, args string) (string, error) {
var buf bytes.Buffer
if args != "" {
for _, cmd := range debugCommands(s) {
for _, alias := range cmd.aliases {
if alias == args {
return cmd.helpMsg, nil
}
}
}
return "", errNoCmd
}
fmt.Fprintln(&buf, "The following commands are available:")
for _, cmd := range debugCommands(s) {
h := cmd.helpMsg
if idx := strings.Index(h, "\n"); idx >= 0 {
h = h[:idx]
}
if len(cmd.aliases) > 1 {
fmt.Fprintf(&buf, " %s (alias: %s) \t %s\n", cmd.aliases[0], strings.Join(cmd.aliases[1:], " | "), h)
} else {
fmt.Fprintf(&buf, " %s \t %s\n", cmd.aliases[0], h)
}
}
fmt.Fprintln(&buf)
fmt.Fprintln(&buf, "Type help followed by a command for full documentation.")
return buf.String(), nil
}
func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) {
argv := config.Split2PartsBySpace(expr)
name := argv[0]
switch name {
case "-list":
return listConfig(&s.args), nil
default:
res, err := configureSet(&s.args, expr)
if err != nil {
return "", err
}
return res, nil
}
}

80
service/dap/config.go Normal file

@ -0,0 +1,80 @@
package dap
import (
"bytes"
"fmt"
"github.com/go-delve/delve/pkg/config"
)
func listConfig(args *launchAttachArgs) string {
var buf bytes.Buffer
config.ConfigureList(&buf, args, "cfgName")
return buf.String()
}
func configureSet(sargs *launchAttachArgs, args string) (string, error) {
v := config.Split2PartsBySpace(args)
cfgname := v[0]
var rest string
if len(v) == 2 {
rest = v[1]
}
field := config.ConfigureFindFieldByName(sargs, cfgname, "cfgName")
if !field.CanAddr() {
return "", fmt.Errorf("%q is not a configuration parameter", cfgname)
}
// If there were no arguments provided, just list the value.
if len(v) == 1 {
return config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
}
if cfgname == "substitutePath" {
err := configureSetSubstitutePath(sargs, rest)
if err != nil {
return "", err
}
// Print the updated client to server and server to client maps.
return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
}
err := config.ConfigureSetSimple(rest, cfgname, field)
if err != nil {
return "", err
}
return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
}
func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {
argv := config.SplitQuotedFields(rest, '"')
switch len(argv) {
case 1: // delete substitute-path rule
for i := range args.substitutePathClientToServer {
if args.substitutePathClientToServer[i][0] == argv[0] {
copy(args.substitutePathClientToServer[i:], args.substitutePathClientToServer[i+1:])
args.substitutePathClientToServer = args.substitutePathClientToServer[:len(args.substitutePathClientToServer)-1]
copy(args.substitutePathServerToClient[i:], args.substitutePathServerToClient[i+1:])
args.substitutePathServerToClient = args.substitutePathServerToClient[:len(args.substitutePathServerToClient)-1]
return nil
}
}
return fmt.Errorf("could not find rule for %q", argv[0])
case 2: // add substitute-path rule
for i := range args.substitutePathClientToServer {
if args.substitutePathClientToServer[i][0] == argv[0] {
args.substitutePathClientToServer[i][1] = argv[1]
args.substitutePathServerToClient[i][0] = argv[1]
return nil
}
}
args.substitutePathClientToServer = append(args.substitutePathClientToServer, [2]string{argv[0], argv[1]})
args.substitutePathServerToClient = append(args.substitutePathServerToClient, [2]string{argv[1], argv[0]})
default:
return fmt.Errorf("too many arguments to \"config substitute-path\"")
}
return nil
}

197
service/dap/config_test.go Normal file

@ -0,0 +1,197 @@
package dap
import (
"testing"
)
func TestListConfig(t *testing.T) {
type args struct {
args *launchAttachArgs
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{
args: &launchAttachArgs{},
},
want: formatConfig(0, false, false, false, [][2]string{}),
},
{
name: "default values",
args: args{
args: &defaultArgs,
},
want: formatConfig(50, false, false, false, [][2]string{}),
},
{
name: "custom values",
args: args{
args: &launchAttachArgs{
StackTraceDepth: 35,
ShowGlobalVariables: true,
substitutePathClientToServer: [][2]string{{"hello", "world"}},
substitutePathServerToClient: [][2]string{{"world", "hello"}},
},
},
want: formatConfig(35, true, false, false, [][2]string{{"hello", "world"}}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := listConfig(tt.args.args); got != tt.want {
t.Errorf("listConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfigureSetSubstitutePath(t *testing.T) {
type args struct {
args *launchAttachArgs
rest string
}
tests := []struct {
name string
args args
wantRules [][2]string
wantErr bool
}{
// Test add rule.
{
name: "add rule",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{},
substitutePathServerToClient: [][2]string{},
},
rest: "/path/to/client/dir /path/to/server/dir",
},
wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
wantErr: false,
},
{
name: "add rule (multiple)",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
},
substitutePathServerToClient: [][2]string{
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
},
},
rest: "/path/to/client/dir/c /path/to/server/dir/b",
},
wantRules: [][2]string{
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
},
wantErr: false,
},
// Test modify rule.
{
name: "modify rule",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
},
rest: "/path/to/client/dir /new/path/to/server/dir",
},
wantRules: [][2]string{{"/path/to/client/dir", "/new/path/to/server/dir"}},
wantErr: false,
},
{
name: "modify rule (multiple)",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
},
substitutePathServerToClient: [][2]string{
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
{"/path/to/server/dir/b", "/path/to/client/dir/c"},
},
},
rest: "/path/to/client/dir/b /new/path",
},
wantRules: [][2]string{
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
{"/path/to/client/dir/b", "/new/path"},
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
},
wantErr: false,
},
// Test delete rule.
{
name: "delete rule",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
},
rest: "/path/to/client/dir",
},
wantRules: [][2]string{},
wantErr: false,
},
// Test invalid input.
{
name: "error on empty args",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{},
substitutePathServerToClient: [][2]string{},
},
rest: " \n\r ",
},
wantErr: true,
},
{
name: "error on delete nonexistent rule",
args: args{
args: &launchAttachArgs{
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
},
rest: "/path/to/server/dir",
},
wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := configureSetSubstitutePath(tt.args.args, tt.args.rest)
if (err != nil) != tt.wantErr {
t.Errorf("configureSetSubstitutePath() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(tt.args.args.substitutePathClientToServer) != len(tt.wantRules) {
t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer=%v, want %d rules", tt.args.args.substitutePathClientToServer, len(tt.wantRules))
return
}
gotClient2Server := tt.args.args.substitutePathClientToServer
gotServer2Client := tt.args.args.substitutePathServerToClient
for i, rule := range tt.wantRules {
if gotClient2Server[i][0] != rule[0] || gotClient2Server[i][1] != rule[1] {
t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], rule)
}
if gotServer2Client[i][1] != rule[0] || gotServer2Client[i][0] != rule[1] {
reverseRule := [2]string{rule[1], rule[0]}
t.Errorf("configureSetSubstitutePath() got substitutePathServerToClient[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], reverseRule)
}
}
})
}
}

@ -44,7 +44,15 @@ func (c *Client) Check{{.}}(t *testing.T, m dap.Message) *dap.{{.}} {
if !ok { if !ok {
t.Fatalf("got %#v, want *dap.ContinuedEvent", m) t.Fatalf("got %#v, want *dap.ContinuedEvent", m)
} }
m = c.ExpectMessage(t){{end}} m = c.ExpectMessage(t){{else}}{{if (eq . "ConfigurationDoneResponse") }}
oe, ok := m.(*dap.OutputEvent)
if !ok {
t.Fatalf("got %#v, want *dap.OutputEvent", m)
}
if oe.Body.Output != "Type 'dlv help' for list of commands.\n" {
t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n")
}
m = c.ExpectMessage(t){{end}}{{end}}
r, ok := m.(*dap.{{.}}) r, ok := m.(*dap.{{.}})
if !ok { if !ok {
t.Fatalf("got %#v, want *dap.{{.}}", m) t.Fatalf("got %#v, want *dap.{{.}}", m)

@ -128,6 +128,14 @@ func (c *Client) ExpectConfigurationDoneResponse(t *testing.T) *dap.Configuratio
// CheckConfigurationDoneResponse fails the test if m is not *ConfigurationDoneResponse. // CheckConfigurationDoneResponse fails the test if m is not *ConfigurationDoneResponse.
func (c *Client) CheckConfigurationDoneResponse(t *testing.T, m dap.Message) *dap.ConfigurationDoneResponse { func (c *Client) CheckConfigurationDoneResponse(t *testing.T, m dap.Message) *dap.ConfigurationDoneResponse {
t.Helper() t.Helper()
oe, ok := m.(*dap.OutputEvent)
if !ok {
t.Fatalf("got %#v, want *dap.OutputEvent", m)
}
if oe.Body.Output != "Type 'dlv help' for list of commands.\n" {
t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n")
}
m = c.ExpectMessage(t)
r, ok := m.(*dap.ConfigurationDoneResponse) r, ok := m.(*dap.ConfigurationDoneResponse)
if !ok { if !ok {
t.Fatalf("got %#v, want *dap.ConfigurationDoneResponse", m) t.Fatalf("got %#v, want *dap.ConfigurationDoneResponse", m)
@ -441,6 +449,24 @@ func (c *Client) CheckLoadedSourcesResponse(t *testing.T, m dap.Message) *dap.Lo
return r return r
} }
// ExpectMemoryEvent reads a protocol message from the connection
// and fails the test if the read message is not *MemoryEvent.
func (c *Client) ExpectMemoryEvent(t *testing.T) *dap.MemoryEvent {
t.Helper()
m := c.ExpectMessage(t)
return c.CheckMemoryEvent(t, m)
}
// CheckMemoryEvent fails the test if m is not *MemoryEvent.
func (c *Client) CheckMemoryEvent(t *testing.T, m dap.Message) *dap.MemoryEvent {
t.Helper()
r, ok := m.(*dap.MemoryEvent)
if !ok {
t.Fatalf("got %#v, want *dap.MemoryEvent", m)
}
return r
}
// ExpectModuleEvent reads a protocol message from the connection // ExpectModuleEvent reads a protocol message from the connection
// and fails the test if the read message is not *ModuleEvent. // and fails the test if the read message is not *ModuleEvent.
func (c *Client) ExpectModuleEvent(t *testing.T) *dap.ModuleEvent { func (c *Client) ExpectModuleEvent(t *testing.T) *dap.ModuleEvent {
@ -1085,3 +1111,21 @@ func (c *Client) CheckVariablesResponse(t *testing.T, m dap.Message) *dap.Variab
} }
return r return r
} }
// ExpectWriteMemoryResponse reads a protocol message from the connection
// and fails the test if the read message is not *WriteMemoryResponse.
func (c *Client) ExpectWriteMemoryResponse(t *testing.T) *dap.WriteMemoryResponse {
t.Helper()
m := c.ExpectMessage(t)
return c.CheckWriteMemoryResponse(t, m)
}
// CheckWriteMemoryResponse fails the test if m is not *WriteMemoryResponse.
func (c *Client) CheckWriteMemoryResponse(t *testing.T, m dap.Message) *dap.WriteMemoryResponse {
t.Helper()
r, ok := m.(*dap.WriteMemoryResponse)
if !ok {
t.Fatalf("got %#v, want *dap.WriteMemoryResponse", m)
}
return r
}

@ -26,6 +26,7 @@ const (
UnableToSetVariable = 2012 UnableToSetVariable = 2012
UnableToDisassemble = 2013 UnableToDisassemble = 2013
UnableToListRegisters = 2014 UnableToListRegisters = 2014
UnableToRunDlvCommand = 2015
// Add more codes as we support more requests // Add more codes as we support more requests
NoDebugIsRunning = 3000 NoDebugIsRunning = 3000
DebuggeeIsRunning = 4000 DebuggeeIsRunning = 4000

@ -178,21 +178,22 @@ type process struct {
// launchAttachArgs captures arguments from launch/attach request that // launchAttachArgs captures arguments from launch/attach request that
// impact handling of subsequent requests. // impact handling of subsequent requests.
// The fields with cfgName tag can be updated through an evaluation request.
type launchAttachArgs struct { type launchAttachArgs struct {
// stopOnEntry is set to automatically stop the debugee after start. // stopOnEntry is set to automatically stop the debugee after start.
stopOnEntry bool stopOnEntry bool
// stackTraceDepth is the maximum length of the returned list of stack frames. // StackTraceDepth is the maximum length of the returned list of stack frames.
stackTraceDepth int StackTraceDepth int `cfgName:"stackTraceDepth"`
// showGlobalVariables indicates if global package variables should be loaded. // ShowGlobalVariables indicates if global package variables should be loaded.
showGlobalVariables bool ShowGlobalVariables bool `cfgName:"showGlobalVariables"`
// hideSystemGoroutines indicates if system goroutines should be removed from threads // ShowRegisters indicates if register values should be loaded.
ShowRegisters bool `cfgName:"showRegisters"`
// HideSystemGoroutines indicates if system goroutines should be removed from threads
// responses. // responses.
hideSystemGoroutines bool HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
// showRegisters indicates if register values should be loaded.
showRegisters bool
// substitutePathClientToServer indicates rules for converting file paths between client and debugger. // substitutePathClientToServer indicates rules for converting file paths between client and debugger.
// These must be directory paths. // These must be directory paths.
substitutePathClientToServer [][2]string substitutePathClientToServer [][2]string `cfgName:"substitutePath"`
// substitutePathServerToClient indicates rules for converting file paths between debugger and client. // substitutePathServerToClient indicates rules for converting file paths between debugger and client.
// These must be directory paths. // These must be directory paths.
substitutePathServerToClient [][2]string substitutePathServerToClient [][2]string
@ -203,10 +204,10 @@ type launchAttachArgs struct {
// in favor of default*Config variables defined in types.go. // in favor of default*Config variables defined in types.go.
var defaultArgs = launchAttachArgs{ var defaultArgs = launchAttachArgs{
stopOnEntry: false, stopOnEntry: false,
stackTraceDepth: 50, StackTraceDepth: 50,
showGlobalVariables: false, ShowGlobalVariables: false,
hideSystemGoroutines: false, HideSystemGoroutines: false,
showRegisters: false, ShowRegisters: false,
substitutePathClientToServer: [][2]string{}, substitutePathClientToServer: [][2]string{},
substitutePathServerToClient: [][2]string{}, substitutePathServerToClient: [][2]string{},
} }
@ -310,11 +311,11 @@ func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debu
func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error { func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
s.args.stopOnEntry = args.StopOnEntry s.args.stopOnEntry = args.StopOnEntry
if depth := args.StackTraceDepth; depth > 0 { if depth := args.StackTraceDepth; depth > 0 {
s.args.stackTraceDepth = depth s.args.StackTraceDepth = depth
} }
s.args.showGlobalVariables = args.ShowGlobalVariables s.args.ShowGlobalVariables = args.ShowGlobalVariables
s.args.hideSystemGoroutines = args.HideSystemGoroutines s.args.ShowRegisters = args.ShowRegisters
s.args.showRegisters = args.ShowRegisters s.args.HideSystemGoroutines = args.HideSystemGoroutines
if paths := args.SubstitutePath; len(paths) > 0 { if paths := args.SubstitutePath; len(paths) > 0 {
clientToServer := make([][2]string, 0, len(paths)) clientToServer := make([][2]string, 0, len(paths))
serverToClient := make([][2]string, 0, len(paths)) serverToClient := make([][2]string, 0, len(paths))
@ -1555,7 +1556,9 @@ func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneReque
} }
s.debugger.Target().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints s.debugger.Target().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints
s.logToConsole("Type 'dlv help' for list of commands.")
s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)}) s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)})
if !s.args.stopOnEntry { if !s.args.stopOnEntry {
s.runUntilStopAndNotify(api.Continue, allowNextStateChange) s.runUntilStopAndNotify(api.Continue, allowNextStateChange)
} }
@ -1601,7 +1604,7 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
var next int var next int
if s.debugger != nil { if s.debugger != nil {
gs, next, err = s.debugger.Goroutines(0, maxGoroutines) gs, next, err = s.debugger.Goroutines(0, maxGoroutines)
if err == nil && s.args.hideSystemGoroutines { if err == nil && s.args.HideSystemGoroutines {
gs = s.debugger.FilterGoroutines(gs, []api.ListGoroutinesFilter{{ gs = s.debugger.FilterGoroutines(gs, []api.ListGoroutinesFilter{{
Kind: api.GoroutineUser, Kind: api.GoroutineUser,
Negated: false, Negated: false,
@ -1916,7 +1919,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
if start < 0 { if start < 0 {
start = 0 start = 0
} }
levels := s.args.stackTraceDepth levels := s.args.StackTraceDepth
if request.Arguments.Levels > 0 { if request.Arguments.Levels > 0 {
levels = request.Arguments.Levels levels = request.Arguments.Levels
} }
@ -1960,7 +1963,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
// We don't know the exact number of available stack frames, so // We don't know the exact number of available stack frames, so
// add an arbitrary number so the client knows to request additional // add an arbitrary number so the client knows to request additional
// frames. // frames.
totalFrames += s.args.stackTraceDepth totalFrames += s.args.StackTraceDepth
} }
response := &dap.StackTraceResponse{ response := &dap.StackTraceResponse{
Response: *newResponse(request.Request), Response: *newResponse(request.Request),
@ -2011,7 +2014,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)} scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
scopes := []dap.Scope{scopeLocals} scopes := []dap.Scope{scopeLocals}
if s.args.showGlobalVariables { if s.args.ShowGlobalVariables {
// Limit what global variables we will return to the current package only. // Limit what global variables we will return to the current package only.
// TODO(polina): This is how vscode-go currently does it to make // TODO(polina): This is how vscode-go currently does it to make
// the amount of the returned data manageable. In fact, this is // the amount of the returned data manageable. In fact, this is
@ -2046,7 +2049,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
scopes = append(scopes, scopeGlobals) scopes = append(scopes, scopeGlobals)
} }
if s.args.showRegisters { if s.args.ShowRegisters {
// Retrieve registers // Retrieve registers
regs, err := s.debugger.ScopeRegisters(goid, frame, 0, false) regs, err := s.debugger.ScopeRegisters(goid, frame, 0, false)
if err != nil { if err != nil {
@ -2565,6 +2568,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
// Support the following expressions: // Support the following expressions:
// -- {expression} - evaluates the expression and returns the result as a variable // -- {expression} - evaluates the expression and returns the result as a variable
// -- call {function} - injects a function call and returns the result as a variable // -- call {function} - injects a function call and returns the result as a variable
// -- config {expression} - updates configuration paramaters
// TODO(polina): users have complained about having to click to expand multi-level // TODO(polina): users have complained about having to click to expand multi-level
// variables, so consider also adding the following: // variables, so consider also adding the following:
// -- print {expression} - return the result as a string like from dlv cli // -- print {expression} - return the result as a string like from dlv cli
@ -2584,9 +2588,20 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
} }
response := &dap.EvaluateResponse{Response: *newResponse(request.Request)} response := &dap.EvaluateResponse{Response: *newResponse(request.Request)}
isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression) expr := request.Arguments.Expression
if err == nil && isCall { // call {expression}
expr := strings.Replace(request.Arguments.Expression, "call ", "", 1) if isConfig, err := regexp.MatchString(`^\s*dlv\s+\S+`, expr); err == nil && isConfig { // dlv {command}
expr := strings.Replace(expr, "dlv ", "", 1)
result, err := s.delveCmd(goid, frame, expr)
if err != nil {
s.sendErrorResponseWithOpts(request.Request, UnableToRunDlvCommand, "Unable to run dlv command", err.Error(), showErrorToUser)
return
}
response.Body = dap.EvaluateResponseBody{
Result: result,
}
} else if isCall, err := regexp.MatchString(`^\s*call\s+\S+`, expr); err == nil && isCall { // call {expression}
expr := strings.Replace(expr, "call ", "", 1)
_, retVars, err := s.doCall(goid, frame, expr) _, retVars, err := s.doCall(goid, frame, expr)
if err != nil { if err != nil {
s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
@ -2608,7 +2623,7 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
} }
} }
} else { // {expression} } else { // {expression}
exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, DefaultLoadConfig) exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, expr, DefaultLoadConfig)
if err != nil { if err != nil {
s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
return return
@ -3201,7 +3216,7 @@ func (s *Session) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
} }
func (s *Session) stacktrace(goroutineID int, g *proc.G) (string, error) { func (s *Session) stacktrace(goroutineID int, g *proc.G) (string, error) {
frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0) frames, err := s.debugger.Stacktrace(goroutineID, s.args.StackTraceDepth, 0)
if err != nil { if err != nil {
return "", err return "", err
} }

@ -844,14 +844,14 @@ func checkStackFramesNamed(testName string, t *testing.T, got *dap.StackTraceRes
// checkScope is a helper for verifying the values within a ScopesResponse. // checkScope is a helper for verifying the values within a ScopesResponse.
// i - index of the scope within ScopesRespose.Body.Scopes array // i - index of the scope within ScopesRespose.Body.Scopes array
// name - name of the scope // name - name of the scope
// varRef - reference to retrieve variables of this scope // varRef - reference to retrieve variables of this scope. If varRef is negative, the reference is not checked.
func checkScope(t *testing.T, got *dap.ScopesResponse, i int, name string, varRef int) { func checkScope(t *testing.T, got *dap.ScopesResponse, i int, name string, varRef int) {
t.Helper() t.Helper()
if len(got.Body.Scopes) <= i { if len(got.Body.Scopes) <= i {
t.Errorf("\ngot %d\nwant len(Scopes)>%d", len(got.Body.Scopes), i) t.Errorf("\ngot %d\nwant len(Scopes)>%d", len(got.Body.Scopes), i)
} }
goti := got.Body.Scopes[i] goti := got.Body.Scopes[i]
if goti.Name != name || goti.VariablesReference != varRef || goti.Expensive { if goti.Name != name || (varRef >= 0 && goti.VariablesReference != varRef) || goti.Expensive {
t.Errorf("\ngot %#v\nwant Name=%q VariablesReference=%d Expensive=false", goti, name, varRef) t.Errorf("\ngot %#v\nwant Name=%q VariablesReference=%d Expensive=false", goti, name, varRef)
} }
} }
@ -1063,6 +1063,7 @@ func TestStackTraceRequest(t *testing.T) {
client.StackTraceRequest(1, 0, 0) client.StackTraceRequest(1, 0, 0)
stResp = client.ExpectStackTraceResponse(t) stResp = client.ExpectStackTraceResponse(t)
checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3) checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3)
}, },
disconnect: false, disconnect: false,
}}) }})
@ -2842,7 +2843,9 @@ func TestHitBreakpointIds(t *testing.T) {
client.ContinueRequest(1) client.ContinueRequest(1)
client.ExpectContinueResponse(t) client.ExpectContinueResponse(t)
se = client.ExpectStoppedEvent(t) se = client.ExpectStoppedEvent(t)
checkHitBreakpointIds(t, se, "function breakpoint", functionBps[1].Id) checkHitBreakpointIds(t, se, "function breakpoint", functionBps[1].Id)
checkStop(t, client, 1, "main.anotherFunction", 27) checkStop(t, client, 1, "main.anotherFunction", 27)
}, },
disconnect: true, disconnect: true,
@ -3805,6 +3808,105 @@ func TestEvaluateRequest(t *testing.T) {
}) })
} }
func formatConfig(depth int, showGlobals, showRegisters, hideSystemGoroutines bool, substitutePath [][2]string) string {
formatStr := `stackTraceDepth %d
showGlobalVariables %v
showRegisters %v
hideSystemGoroutines %v
substitutePath %v
`
return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, hideSystemGoroutines, substitutePath)
}
func TestEvaluateCommandRequest(t *testing.T) {
runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
fixture.Source, []int{}, // Breakpoint set in the program
[]onBreakpoint{{ // Stop at first breakpoint
execute: func() {
checkStop(t, client, 1, "main.foobar", 66)
// Request help.
const dlvHelp = `The following commands are available:
help (alias: h) Prints the help message.
config Changes configuration parameters.
Type help followed by a command for full documentation.
`
client.EvaluateRequest("dlv help", 1000, "repl")
got := client.ExpectEvaluateResponse(t)
checkEval(t, got, dlvHelp, noChildren)
client.EvaluateRequest("dlv help config", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, msgConfig, noChildren)
// Test config.
client.EvaluateRequest("dlv config -list", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, formatConfig(50, false, false, false, [][2]string{}), noChildren)
// Read and modify showGlobalVariables.
client.EvaluateRequest("dlv config showGlobalVariables", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "showGlobalVariables\tfalse\n", noChildren)
client.ScopesRequest(1000)
scopes := client.ExpectScopesResponse(t)
if len(scopes.Body.Scopes) > 1 {
t.Errorf("\ngot %#v\nwant len(scopes)=1 (Locals)", scopes)
}
checkScope(t, scopes, 0, "Locals", -1)
client.EvaluateRequest("dlv config showGlobalVariables true", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "showGlobalVariables\ttrue\n\nUpdated", noChildren)
client.EvaluateRequest("dlv config -list", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, formatConfig(50, true, false, false, [][2]string{}), noChildren)
client.ScopesRequest(1000)
scopes = client.ExpectScopesResponse(t)
if len(scopes.Body.Scopes) < 2 {
t.Errorf("\ngot %#v\nwant len(scopes)=2 (Locals & Globals)", scopes)
}
checkScope(t, scopes, 0, "Locals", -1)
checkScope(t, scopes, 1, "Globals (package main)", -1)
// Read and modify substitutePath.
client.EvaluateRequest("dlv config substitutePath", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "substitutePath\t[]\n", noChildren)
client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "your/server/path"), 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "substitutePath\t[[my/client/path your/server/path]]\n\nUpdated", noChildren)
client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "new/your/server/path"), 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "substitutePath\t[[my/client/path new/your/server/path]]\n\nUpdated", noChildren)
client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q", "my/client/path"), 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "substitutePath\t[]\n\nUpdated", noChildren)
// Test bad inputs.
client.EvaluateRequest("dlv help bad", 1000, "repl")
client.ExpectErrorResponse(t)
client.EvaluateRequest("dlv bad", 1000, "repl")
client.ExpectErrorResponse(t)
},
disconnect: true,
}})
})
}
// From testvariables2 fixture // From testvariables2 fixture
const ( const (
// As defined in the code // As defined in the code