Repository: go-delve/delve
Pull request #2556 (2021-06-26 14:28:13)

terminal: improve 'on' command
Adds the ability to edit the list of commands executed after stopping
on a breakpoint, as well as converting a breakpoint into a tracepoint
and vice versa.

Prior to this it was possible to add commands to a breakpoint but
removing commands or changing a breakpoint into a tracepoint, or vice
versa, could only be done by removing and recreating the breakpoint.


aarzilli: Done.

Patch:
diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md
index 96ad677b..509bf719 100644
--- a/Documentation/cli/README.md
+++ b/Documentation/cli/README.md
@@ -464,9 +464,18 @@ Aliases: n
 ## on
 Executes a command when a breakpoint is hit.
 
-	on <breakpoint name or id> <command>.
+	on <breakpoint name or id> <command>
+	on <breakpoint name or id> -edit
+	
+
+Supported commands: print, stack, goroutine, trace and cond. 
+To convert a breakpoint into a tracepoint use:
+	
+	on <breakpoint name or id> trace
+
+The command 'on <bp> cond <cond-arguments>' is equivalent to 'cond <bp> <cond-arguments>'.
 
-Supported commands: print, stack and goroutine)
+The command 'on x -edit' can be used to edit the list of commands executed when the breakpoint is hit.
 
 
 ## print
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index 24b08f53..50ab47ee 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -10,6 +10,7 @@ import (
 	"go/parser"
 	"go/scanner"
 	"io"
+	"io/ioutil"
 	"math"
 	"os"
 	"os/exec"
@@ -123,7 +124,7 @@ Type "help" followed by the name of a command for more information about it.`},
 See $GOPATH/src/github.com/go-delve/delve/Documentation/cli/locspec.md for the syntax of linespec.
 
 See also: "help on", "help cond" and "help clear"`},
-		{aliases: []string{"trace", "t"}, group: breakCmds, cmdFn: tracepoint, helpMsg: `Set tracepoint.
+		{aliases: []string{"trace", "t"}, group: breakCmds, cmdFn: tracepoint, allowedPrefixes: onPrefix, helpMsg: `Set tracepoint.
 
 	trace [name] <linespec>
 
@@ -425,10 +426,19 @@ If no argument is specified the function being executed in the selected stack fr
 	-l <locspec>		disassembles the specified function`},
 		{aliases: []string{"on"}, group: breakCmds, cmdFn: c.onCmd, helpMsg: `Executes a command when a breakpoint is hit.
 
-	on <breakpoint name or id> <command>.
+	on <breakpoint name or id> <command>
+	on <breakpoint name or id> -edit
+	
+
+Supported commands: print, stack, goroutine, trace and cond. 
+To convert a breakpoint into a tracepoint use:
+	
+	on <breakpoint name or id> trace
 
-Supported commands: print, stack and goroutine)`},
-		{aliases: []string{"condition", "cond"}, group: breakCmds, cmdFn: conditionCmd, helpMsg: `Set breakpoint condition.
+The command 'on <bp> cond <cond-arguments>' is equivalent to 'cond <bp> <cond-arguments>'.
+
+The command 'on x -edit' can be used to edit the list of commands executed when the breakpoint is hit.`},
+		{aliases: []string{"condition", "cond"}, group: breakCmds, cmdFn: conditionCmd, allowedPrefixes: onPrefix, helpMsg: `Set breakpoint condition.
 
 	condition <breakpoint name or id> <boolean expression>.
 	condition -hitcount <breakpoint name or id> <operator> <argument>
@@ -1700,36 +1710,8 @@ func breakpoints(t *Term, ctx callContext, args string) error {
 	for _, bp := range breakPoints {
 		fmt.Printf("%s at %v (%d)\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp), bp.TotalHitCount)
 
-		var attrs []string
-		if bp.Cond != "" {
-			attrs = append(attrs, fmt.Sprintf("\tcond %s", bp.Cond))
-		}
-		if bp.HitCond != "" {
-			attrs = append(attrs, fmt.Sprintf("\tcond -hitcount %s", bp.HitCond))
-		}
-		if bp.Stacktrace > 0 {
-			attrs = append(attrs, fmt.Sprintf("\tstack %d", bp.Stacktrace))
-		}
-		if bp.Goroutine {
-			attrs = append(attrs, "\tgoroutine")
-		}
-		if bp.LoadArgs != nil {
-			if *(bp.LoadArgs) == longLoadConfig {
-				attrs = append(attrs, "\targs -v")
-			} else {
-				attrs = append(attrs, "\targs")
-			}
-		}
-		if bp.LoadLocals != nil {
-			if *(bp.LoadLocals) == longLoadConfig {
-				attrs = append(attrs, "\tlocals -v")
-			} else {
-				attrs = append(attrs, "\tlocals")
-			}
-		}
-		for i := range bp.Variables {
-			attrs = append(attrs, fmt.Sprintf("\tprint %s", bp.Variables[i]))
-		}
+		attrs := formatBreakpointAttrs("\t", bp, false)
+
 		if len(attrs) > 0 {
 			fmt.Printf("%s\n", strings.Join(attrs, "\n"))
 		}
@@ -1737,6 +1719,43 @@ func breakpoints(t *Term, ctx callContext, args string) error {
 	return nil
 }
 
+func formatBreakpointAttrs(prefix string, bp *api.Breakpoint, includeTrace bool) []string {
+	var attrs []string
+	if bp.Cond != "" {
+		attrs = append(attrs, fmt.Sprintf("%scond %s", prefix, bp.Cond))
+	}
+	if bp.HitCond != "" {
+		attrs = append(attrs, fmt.Sprintf("%scond -hitcount %s", prefix, bp.HitCond))
+	}
+	if bp.Stacktrace > 0 {
+		attrs = append(attrs, fmt.Sprintf("%sstack %d", prefix, bp.Stacktrace))
+	}
+	if bp.Goroutine {
+		attrs = append(attrs, fmt.Sprintf("%sgoroutine", prefix))
+	}
+	if bp.LoadArgs != nil {
+		if *(bp.LoadArgs) == longLoadConfig {
+			attrs = append(attrs, fmt.Sprintf("%sargs -v", prefix))
+		} else {
+			attrs = append(attrs, fmt.Sprintf("%sargs", prefix))
+		}
+	}
+	if bp.LoadLocals != nil {
+		if *(bp.LoadLocals) == longLoadConfig {
+			attrs = append(attrs, fmt.Sprintf("%slocals -v", prefix))
+		} else {
+			attrs = append(attrs, fmt.Sprintf("%slocals", prefix))
+		}
+	}
+	for i := range bp.Variables {
+		attrs = append(attrs, fmt.Sprintf("%sprint %s", prefix, bp.Variables[i]))
+	}
+	if includeTrace && bp.Tracepoint {
+		attrs = append(attrs, fmt.Sprintf("%strace", prefix))
+	}
+	return attrs
+}
+
 func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]*api.Breakpoint, error) {
 	args := split2PartsBySpace(argstr)
 
@@ -1829,16 +1848,18 @@ func breakpoint(t *Term, ctx callContext, args string) error {
 }
 
 func tracepoint(t *Term, ctx callContext, args string) error {
+	if ctx.Prefix == onPrefix {
+		if args != "" {
+			return errors.New("too many arguments to trace")
+		}
+		ctx.Breakpoint.Tracepoint = true
+		return nil
+	}
 	_, err := setBreakpoint(t, ctx, true, args)
 	return err
 }
 
-func edit(t *Term, ctx callContext, args string) error {
-	file, lineno, _, err := getLocation(t, ctx, args, false)
-	if err != nil {
-		return err
-	}
-
+func runEditor(args ...string) error {
 	var editor string
 	if editor = os.Getenv("DELVE_EDITOR"); editor == "" {
 		if editor = os.Getenv("EDITOR"); editor == "" {
@@ -1846,13 +1867,21 @@ func edit(t *Term, ctx callContext, args string) error {
 		}
 	}
 
-	cmd := exec.Command(editor, fmt.Sprintf("+%d", lineno), file)
+	cmd := exec.Command(editor, args...)
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	return cmd.Run()
 }
 
+func edit(t *Term, ctx callContext, args string) error {
+	file, lineno, _, err := getLocation(t, ctx, args, false)
+	if err != nil {
+		return err
+	}
+	return runEditor(fmt.Sprintf("+%d", lineno), file)
+}
+
 func watchpoint(t *Term, ctx callContext, args string) error {
 	v := strings.SplitN(args, " ", 2)
 	if len(v) != 2 {
@@ -2766,13 +2795,69 @@ func (c *Commands) onCmd(t *Term, ctx callContext, argstr string) error {
 
 	ctx.Prefix = onPrefix
 	ctx.Breakpoint = bp
-	err = c.CallWithContext(args[1], t, ctx)
-	if err != nil {
-		return err
+
+	if args[1] == "-edit" {
+		f, err := ioutil.TempFile("", "dlv-on-cmd-")
+		if err != nil {
+			return err
+		}
+		defer func() {
+			_ = os.Remove(f.Name())
+		}()
+		attrs := formatBreakpointAttrs("", ctx.Breakpoint, true)
+		_, err = f.Write([]byte(strings.Join(attrs, "\n")))
+		if err != nil {
+			return err
+		}
+		err = f.Close()
+		if err != nil {
+			return err
+		}
+
+		err = runEditor(f.Name())
+		if err != nil {
+			return err
+		}
+
+		fin, err := os.Open(f.Name())
+		if err != nil {
+			return err
+		}
+		defer fin.Close()
+
+		err = c.parseBreakpointAttrs(t, ctx, fin)
+		if err != nil {
+			return err
+		}
+	} else {
+		err = c.CallWithContext(args[1], t, ctx)
+		if err != nil {
+			return err
+		}
 	}
 	return t.client.AmendBreakpoint(ctx.Breakpoint)
 }
 
+func (c *Commands) parseBreakpointAttrs(t *Term, ctx callContext, r io.Reader) error {
+	ctx.Breakpoint.Tracepoint = false
+	ctx.Breakpoint.Goroutine = false
+	ctx.Breakpoint.Stacktrace = 0
+	ctx.Breakpoint.Variables = ctx.Breakpoint.Variables[:0]
+	ctx.Breakpoint.Cond = ""
+	ctx.Breakpoint.HitCond = ""
+
+	scan := bufio.NewScanner(r)
+	lineno := 0
+	for scan.Scan() {
+		lineno++
+		err := c.CallWithContext(scan.Text(), t, ctx)
+		if err != nil {
+			fmt.Printf("%d: %s\n", lineno, err.Error())
+		}
+	}
+	return scan.Err()
+}
+
 func conditionCmd(t *Term, ctx callContext, argstr string) error {
 	args := split2PartsBySpace(argstr)
 
@@ -2782,10 +2867,17 @@ func conditionCmd(t *Term, ctx callContext, argstr string) error {
 
 	if args[0] == "-hitcount" {
 		// hitcount breakpoint
+
+		if ctx.Prefix == onPrefix {
+			ctx.Breakpoint.HitCond = args[1]
+			return nil
+		}
+
 		args = split2PartsBySpace(args[1])
 		if len(args) < 2 {
 			return fmt.Errorf("not enough arguments")
 		}
+
 		bp, err := getBreakpointByIDOrName(t, args[0])
 		if err != nil {
 			return err
@@ -2796,6 +2888,11 @@ func conditionCmd(t *Term, ctx callContext, argstr string) error {
 		return t.client.AmendBreakpoint(bp)
 	}
 
+	if ctx.Prefix == onPrefix {
+		ctx.Breakpoint.Cond = argstr
+		return nil
+	}
+
 	bp, err := getBreakpointByIDOrName(t, args[0])
 	if err != nil {
 		return err
diff --git a/service/dap/server.go b/service/dap/server.go
index 34c8a579..4a5d5b68 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -772,11 +772,9 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 		return
 	}
 
-
 	if mode == "replay" {
 		traceDirPath, _ := request.Arguments["traceDirPath"].(string)
 
-
 		// Validate trace directory
 		if traceDirPath == "" {
 			s.sendErrorResponse(request.Request,
@@ -785,10 +783,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 			return
 		}
 
-
-
-
-
 		// Assign the rr trace directory path to debugger configuration
 		s.config.Debugger.CoreFile = traceDirPath
 		s.config.Debugger.Backend = "rr"
@@ -797,7 +791,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 	if mode == "core" {
 		coreFilePath, _ := request.Arguments["coreFilePath"].(string)
 
-
 		// Validate core dump path
 		if coreFilePath == "" {
 
@@ -807,7 +800,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 			return
 		}
 
-
 		// Assign the non-empty core file path to debugger configuration. This will
 		// trigger a native core file replay instead of an rr trace replay
 		s.config.Debugger.CoreFile = coreFilePath
@@ -815,7 +807,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 	}
 
 	s.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend)
-	
+
 	// Prepare the debug executable filename, build flags and build it
 	if mode == "debug" || mode == "test" {
 		output, ok := request.Arguments["output"].(string)
@@ -970,7 +962,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 	}
 	// Enable StepBack controls on supported backends
 	if s.config.Debugger.Backend == "rr" {
-		s.send(&dap.CapabilitiesEvent{ Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{ SupportsStepBack: true }}})
+		s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportsStepBack: true}}})
 	}
 
 	// Notify the client that the debugger is ready to start accepting
@@ -1126,7 +1118,7 @@ func (s *Server) stopDebugSession(killProcess bool) error {
 			s.logToConsole(exited.Error())
 			err = nil
 		default:
-			s.log.Error(err)
+			s.log.Error("detach returned error: ", err)
 		}
 	}
 	return err
@@ -1452,49 +1444,53 @@ func fnPackageName(loc *proc.Location) string {
 // onThreadsRequest handles 'threads' request.
 // This is a mandatory request to support.
 // It is sent in response to configurationDone response and stopped events.
+// Depending on the debug session stage, goroutines information
+// might not be available. However, the DAP spec states that
+// "even if a debug adapter does not support multiple threads,
+// it must implement the threads request and return a single
+// (dummy) thread". Therefore, this handler never returns
+// an error response. If the dummy thread is returned in its place,
+// the next waterfall request for its stackTrace will return the error.
 func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
-	if s.debugger == nil {
-		s.sendErrorResponse(request.Request, UnableToDisplayThreads, "Unable to display threads", "debugger is nil")
-		return
+	var err error
+	var gs []*proc.G
+	var next int
+	if s.debugger != nil {
+		gs, next, err = s.debugger.Goroutines(0, maxGoroutines)
 	}
+	threads := make([]dap.Thread, len(gs))
 
-	gs, next, err := s.debugger.Goroutines(0, maxGoroutines)
 	if err != nil {
 		switch err.(type) {
 		case proc.ErrProcessExited:
 			// If the program exits very quickly, the initial threads request will complete after it has exited.
 			// A TerminatedEvent has already been sent. Ignore the err returned in this case.
-			s.send(&dap.ThreadsResponse{Response: *newResponse(request.Request)})
+			s.log.Debug(err)
 		default:
-			s.sendErrorResponse(request.Request, UnableToDisplayThreads, "Unable to display threads", err.Error())
+			s.send(&dap.OutputEvent{
+				Event: *newEvent("output"),
+				Body: dap.OutputEventBody{
+					Output:   fmt.Sprintf("Unable to retrieve goroutines: %s\n", err.Error()),
+					Category: "stderr",
+				}})
 		}
-		return
-	}
-
-	if next >= 0 {
-		s.logToConsole(fmt.Sprintf("too many goroutines, only loaded %d", len(gs)))
-	}
-
-	threads := make([]dap.Thread, len(gs))
-	if len(threads) == 0 {
-		// Depending on the debug session stage, goroutines information
-		// might not be available. However, the DAP spec states that
-		// "even if a debug adapter does not support multiple threads,
-		// it must implement the threads request and return a single
-		// (dummy) thread".
+		threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
+	} else if len(threads) == 0 {
 		threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
 	} else {
+		if next >= 0 {
+			s.logToConsole(fmt.Sprintf("too many goroutines, only loaded %d", len(gs)))
+		}
 		state, err := s.debugger.State( /*nowait*/ true)
 		if err != nil {
-			s.sendErrorResponse(request.Request, UnableToDisplayThreads, "Unable to display threads", err.Error())
-			return
+			s.log.Debug("Unable to get debugger state: ", err)
 		}
 		s.debugger.LockTarget()
 		defer s.debugger.UnlockTarget()
 
 		for i, g := range gs {
 			selected := ""
-			if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
+			if state != nil && state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
 				selected = "* "
 			}
 			thread := ""
@@ -1674,6 +1670,11 @@ type stackFrame struct {
 // As per DAP spec, this request only gets triggered as a follow-up
 // to a successful threads request as part of the "request waterfall".
 func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
+	if s.debugger == nil {
+		s.sendErrorResponse(request.Request, UnableToProduceStackTrace, "Unable to produce stack trace", "debugger is nil")
+		return
+	}
+
 	goroutineID := request.Arguments.ThreadId
 	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
 	if err != nil {
@@ -2392,7 +2393,7 @@ func (s *Server) doCall(goid, frame int, expr string) (*api.DebuggerState, []*pr
 		UnsafeCall:           false,
 		GoroutineID:          goid,
 	}, nil)
-	if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited {
+	if processExited(state, err) {
 		e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
 		s.send(e)
 		return nil, nil, errors.New("terminated")
@@ -2825,6 +2826,11 @@ func (s *Server) resetHandlesForStoppedEvent() {
 	s.exceptionErr = nil
 }
 
+func processExited(state *api.DebuggerState, err error) bool {
+	_, isexited := err.(proc.ErrProcessExited)
+	return isexited || err == nil && state.Exited
+}
+
 // doRunCommand runs a debugger command until it stops on
 // termination, error, breakpoint, etc, when an appropriate
 // event needs to be sent to the client. asyncSetupDone is
@@ -2837,7 +2843,7 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
 	// So we should always close it ourselves just in case.
 	defer s.asyncCommandDone(asyncSetupDone)
 	state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone)
-	if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited {
+	if processExited(state, err) {
 		s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
 		return
 	}
@@ -2929,4 +2935,4 @@ func (s *Server) toServerPath(path string) string {
 		s.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
 	}
 	return serverPath
-}
\ No newline at end of file
+}


Test patch:
diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go
index cd985f7c..e23b95d6 100644
--- a/pkg/terminal/command_test.go
+++ b/pkg/terminal/command_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
+	"reflect"
 	"regexp"
 	"runtime"
 	"strconv"
@@ -1192,3 +1193,90 @@ func TestHitCondBreakpoint(t *testing.T) {
 		}
 	})
 }
+
+func TestBreakpointEditing(t *testing.T) {
+	term := &FakeTerminal{
+		t:    t,
+		Term: New(nil, &config.Config{}),
+	}
+	_ = term
+
+	var testCases = []struct {
+		inBp    *api.Breakpoint
+		inBpStr string
+		edit    string
+		outBp   *api.Breakpoint
+	}{
+		{ // tracepoint -> breakpoint
+			&api.Breakpoint{Tracepoint: true},
+			"trace",
+			"",
+			&api.Breakpoint{}},
+		{ // breakpoint -> tracepoint
+			&api.Breakpoint{Variables: []string{"a"}},
+			"print a",
+			"print a\ntrace",
+			&api.Breakpoint{Tracepoint: true, Variables: []string{"a"}}},
+		{ // add print var
+			&api.Breakpoint{Variables: []string{"a"}},
+			"print a",
+			"print b\nprint a\n",
+			&api.Breakpoint{Variables: []string{"b", "a"}}},
+		{ // add goroutine flag
+			&api.Breakpoint{},
+			"",
+			"goroutine",
+			&api.Breakpoint{Goroutine: true}},
+		{ // remove goroutine flag
+			&api.Breakpoint{Goroutine: true},
+			"goroutine",
+			"",
+			&api.Breakpoint{}},
+		{ // add stack directive
+			&api.Breakpoint{},
+			"",
+			"stack 10",
+			&api.Breakpoint{Stacktrace: 10}},
+		{ // remove stack directive
+			&api.Breakpoint{Stacktrace: 20},
+			"stack 20",
+			"print a",
+			&api.Breakpoint{Variables: []string{"a"}}},
+		{ // add condition
+			&api.Breakpoint{Variables: []string{"a"}},
+			"print a",
+			"print a\ncond a < b",
+			&api.Breakpoint{Variables: []string{"a"}, Cond: "a < b"}},
+		{ // remove condition
+			&api.Breakpoint{Cond: "a < b"},
+			"cond a < b",
+			"",
+			&api.Breakpoint{}},
+		{ // change condition
+			&api.Breakpoint{Cond: "a < b"},
+			"cond a < b",
+			"cond a < 5",
+			&api.Breakpoint{Cond: "a < 5"}},
+		{ // change hitcount condition
+			&api.Breakpoint{HitCond: "% 2"},
+			"cond -hitcount % 2",
+			"cond -hitcount = 2",
+			&api.Breakpoint{HitCond: "= 2"}},
+	}
+
+	for _, tc := range testCases {
+		bp := *tc.inBp
+		bpStr := strings.Join(formatBreakpointAttrs("", &bp, true), "\n")
+		if bpStr != tc.inBpStr {
+			t.Errorf("Expected %q got %q for:\n%#v", tc.inBpStr, bpStr, tc.inBp)
+		}
+		ctx := callContext{Prefix: onPrefix, Scope: api.EvalScope{GoroutineID: -1, Frame: 0, DeferredCall: 0}, Breakpoint: &bp}
+		err := term.cmds.parseBreakpointAttrs(nil, ctx, strings.NewReader(tc.edit))
+		if err != nil {
+			t.Errorf("Unexpected error during edit %q", tc.edit)
+		}
+		if !reflect.DeepEqual(bp, *tc.outBp) {
+			t.Errorf("mismatch after edit\nexpected: %#v\ngot: %#v", tc.outBp, bp)
+		}
+	}
+}
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index e1c0d93b..63422f38 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -438,8 +438,11 @@ func TestContinueOnEntry(t *testing.T) {
 		// 7 >> threads, << threads
 		client.ThreadsRequest()
 		tResp := client.ExpectThreadsResponse(t)
-		if tResp.Seq != 0 || tResp.RequestSeq != 7 || len(tResp.Body.Threads) != 0 {
-			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7 len(Threads)=0", tResp)
+		if tResp.Seq != 0 || tResp.RequestSeq != 7 || len(tResp.Body.Threads) != 1 {
+			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7 len(Threads)=1", tResp)
+		}
+		if tResp.Body.Threads[0].Id != 1 || tResp.Body.Threads[0].Name != "Dummy" {
+			t.Errorf("\ngot %#v\nwant Id=1, Name=\"Dummy\"", tResp)
 		}
 
 		// 8 >> disconnect, << disconnect