From 19ce116bb2c14140b0286041cdd72b12f64da590 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Fri, 29 Oct 2021 22:41:30 -0400 Subject: [PATCH] service/dap: allow expression evaluation in log messages (#2747) From the DAP spec: If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. This change parses the log messages and stores the parsed values as a format string and list of expressions to evaluate and get the string value of. Updates golang/vscode-go#123 --- service/dap/server.go | 124 +++++++++++++++++++++++++++++++------ service/dap/server_test.go | 81 +++++++++++++++++++++++- 2 files changed, 185 insertions(+), 20 deletions(-) diff --git a/service/dap/server.go b/service/dap/server.go index 3bf160fa..cee513ae 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1355,9 +1355,10 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func( } else { got.Cond = want.condition got.HitCond = want.hitCondition - got.Tracepoint = want.logMessage != "" - got.UserData = want.logMessage - err = s.debugger.AmendBreakpoint(got) + err = setLogMessage(got, want.logMessage) + if err == nil { + err = s.debugger.AmendBreakpoint(got) + } } createdBps[want.name] = struct{}{} s.updateBreakpointsResponse(breakpoints, i, err, got) @@ -1380,19 +1381,20 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func( if _, ok := createdBps[want.name]; ok { err = fmt.Errorf("breakpoint already exists") } else { - // Create new breakpoints. - got, err = s.debugger.CreateBreakpoint( - &api.Breakpoint{ - Name: want.name, - File: wantLoc.file, - Line: wantLoc.line, - Addr: wantLoc.addr, - Addrs: wantLoc.addrs, - Cond: want.condition, - HitCond: want.hitCondition, - Tracepoint: want.logMessage != "", - UserData: want.logMessage, - }) + bp := &api.Breakpoint{ + Name: want.name, + File: wantLoc.file, + Line: wantLoc.line, + Addr: wantLoc.addr, + Addrs: wantLoc.addrs, + Cond: want.condition, + HitCond: want.hitCondition, + } + err = setLogMessage(bp, want.logMessage) + if err == nil { + // Create new breakpoints. + got, err = s.debugger.CreateBreakpoint(bp) + } } } createdBps[want.name] = struct{}{} @@ -1401,6 +1403,18 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func( return breakpoints } +func setLogMessage(bp *api.Breakpoint, msg string) error { + tracepoint, userdata, err := parseLogPoint(msg) + if err != nil { + return err + } + bp.Tracepoint = tracepoint + if userdata != nil { + bp.UserData = *userdata + } + return nil +} + func (s *Session) updateBreakpointsResponse(breakpoints []dap.Breakpoint, i int, err error, got *api.Breakpoint) { breakpoints[i].Verified = (err == nil) if err != nil { @@ -3601,8 +3615,8 @@ func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool { if !bp.Tracepoint { return false } - // TODO(suzmue): allow evaluate expressions within log points. - if msg, ok := bp.UserData.(string); ok { + if lMsg, ok := bp.UserData.(logMessage); ok { + msg := lMsg.evaluate(s, goid) s.send(&dap.OutputEvent{ Event: *newEvent("output"), Body: dap.OutputEventBody{ @@ -3618,6 +3632,19 @@ func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool { return true } +func (msg *logMessage) evaluate(s *Session, goid int) string { + evaluated := make([]interface{}, len(msg.args)) + for i := range msg.args { + exprVar, err := s.debugger.EvalVariableInScope(goid, 0, 0, msg.args[i], DefaultLoadConfig) + if err != nil { + evaluated[i] = fmt.Sprintf("{eval err: %e}", err) + continue + } + evaluated[i] = s.convertVariableToString(exprVar) + } + return fmt.Sprintf(msg.format, evaluated...) +} + func (s *Session) toClientPath(path string) string { if len(s.args.substitutePathServerToClient) == 0 { return path @@ -3639,3 +3666,64 @@ func (s *Session) toServerPath(path string) string { } return serverPath } + +type logMessage struct { + format string + args []string +} + +// parseLogPoint parses a log message according to the DAP spec: +// "Expressions within {} are interpolated." +func parseLogPoint(msg string) (bool, *logMessage, error) { + // Note: All braces *must* come in pairs, even those within an + // expression to be interpolated. + // TODO(suzmue): support individual braces in string values in + // eval expressions. + var args []string + + var isArg bool + var formatSlice, argSlice []rune + braceCount := 0 + for _, r := range msg { + if isArg { + switch r { + case '}': + if braceCount--; braceCount == 0 { + argStr := strings.TrimSpace(string(argSlice)) + if len(argStr) == 0 { + return false, nil, fmt.Errorf("empty evaluation string") + } + args = append(args, argStr) + formatSlice = append(formatSlice, '%', 's') + isArg = false + continue + } + case '{': + braceCount += 1 + } + argSlice = append(argSlice, r) + continue + } + + switch r { + case '}': + return false, nil, fmt.Errorf("invalid log point format, unexpected '}'") + case '{': + if braceCount++; braceCount == 1 { + isArg, argSlice = true, []rune{} + continue + } + } + formatSlice = append(formatSlice, r) + } + if isArg { + return false, nil, fmt.Errorf("invalid log point format") + } + if len(formatSlice) == 0 { + return false, nil, nil + } + return true, &logMessage{ + format: string(formatSlice), + args: args, + }, nil +} diff --git a/service/dap/server_test.go b/service/dap/server_test.go index d1d8e508..cbd75b3a 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -3126,7 +3126,7 @@ func TestLogPoints(t *testing.T) { execute: func() { checkStop(t, client, 1, "main.main", 23) bps := []int{6, 25, 27, 16} - logMessages := map[int]string{6: "in callme!", 16: "in callme2!"} + logMessages := map[int]string{6: "{i*2}: in callme!", 16: "in callme2!"} client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages) client.ExpectSetBreakpointsResponse(t) @@ -3142,7 +3142,7 @@ func TestLogPoints(t *testing.T) { client.ContinueRequest(1) client.ExpectContinueResponse(t) - checkLogMessage(t, client.ExpectOutputEvent(t), 1, "in callme!", fixture.Source, 6) + checkLogMessage(t, client.ExpectOutputEvent(t), 1, fmt.Sprintf("%d: in callme!", i*2), fixture.Source, 6) } se := client.ExpectStoppedEvent(t) if se.Body.Reason != "breakpoint" || se.Body.ThreadId != 1 { @@ -6528,6 +6528,83 @@ func TestBadlyFormattedMessageToServer(t *testing.T) { }) } +func TestParseLogPoint(t *testing.T) { + tests := []struct { + name string + msg string + wantTracepoint bool + wantFormat string + wantArgs []string + wantErr bool + }{ + // Test simple log messages. + {name: "simple string", msg: "hello, world!", wantTracepoint: true, wantFormat: "hello, world!"}, + {name: "empty string", msg: "", wantTracepoint: false, wantErr: false}, + // Test parse eval expressions. + { + name: "simple eval", + msg: "{x}", + wantTracepoint: true, + wantFormat: "%s", + wantArgs: []string{"x"}, + }, + { + name: "type cast", + msg: "hello {string(x)}", + wantTracepoint: true, + wantFormat: "hello %s", + wantArgs: []string{"string(x)"}, + }, + { + name: "multiple eval", + msg: "{x} {y} {z}", + wantTracepoint: true, + wantFormat: "%s %s %s", + wantArgs: []string{"x", "y", "z"}, + }, + { + name: "eval expressions contain braces", + msg: "{interface{}(x)} {myType{y}} {[]myType{{z}}}", + wantTracepoint: true, + wantFormat: "%s %s %s", + wantArgs: []string{"interface{}(x)", "myType{y}", "[]myType{{z}}"}, + }, + // Test parse errors. + {name: "empty evaluation", msg: "{}", wantErr: true}, + {name: "empty space evaluation", msg: "{ \n}", wantErr: true}, + {name: "open brace missing closed", msg: "{", wantErr: true}, + {name: "closed brace missing open", msg: "}", wantErr: true}, + {name: "open brace in expression", msg: `{m["{"]}`, wantErr: true}, + {name: "closed brace in expression", msg: `{m["}"]}`, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTracepoint, gotLogMessage, err := parseLogPoint(tt.msg) + if gotTracepoint != tt.wantTracepoint { + t.Errorf("parseLogPoint() tracepoint = %v, wantTracepoint %v", gotTracepoint, tt.wantTracepoint) + return + } + if (err != nil) != tt.wantErr { + t.Errorf("parseLogPoint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantTracepoint { + return + } + if gotLogMessage == nil { + t.Errorf("parseLogPoint() gotLogMessage = nil, want log message") + return + } + if gotLogMessage.format != tt.wantFormat { + t.Errorf("parseLogPoint() gotFormat = %v, want %v", gotLogMessage.format, tt.wantFormat) + } + if !reflect.DeepEqual(gotLogMessage.args, tt.wantArgs) { + t.Errorf("parseLogPoint() gotArgs = %v, want %v", gotLogMessage.args, tt.wantArgs) + } + }) + } +} + func TestDisassemble(t *testing.T) { runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) { runDebugSessionWithBPs(t, client, "launch",