service/dap: Add support for debug and test modes (#1901)

* service/dap: Add support for debug and test modes

* Address code review comments

* Remove //dap comment

* OptFlags() => optfalgs()

* If mode => switch mode
This commit is contained in:
polinasok 2020-03-04 09:22:51 -08:00 committed by GitHub
parent d1c63500e8
commit f863be0a17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 250 additions and 91 deletions

@ -0,0 +1,10 @@
package main
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
os.Exit(m.Run())
}

@ -13,6 +13,7 @@ import (
"syscall" "syscall"
"github.com/go-delve/delve/pkg/config" "github.com/go-delve/delve/pkg/config"
"github.com/go-delve/delve/pkg/gobuild"
"github.com/go-delve/delve/pkg/goversion" "github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/terminal" "github.com/go-delve/delve/pkg/terminal"
@ -354,14 +355,6 @@ and dap modes.
return RootCommand return RootCommand
} }
// Remove the file at path and issue a warning to stderr if this fails.
func remove(path string) {
err := os.Remove(path)
if err != nil {
fmt.Fprintf(os.Stderr, "could not remove %v: %v\n", path, err)
}
}
func dapCmd(cmd *cobra.Command, args []string) { func dapCmd(cmd *cobra.Command, args []string) {
status := func() int { status := func() int {
if err := logflags.Setup(Log, LogOutput, LogDest); err != nil { if err := logflags.Setup(Log, LogOutput, LogDest); err != nil {
@ -428,12 +421,12 @@ func debugCmd(cmd *cobra.Command, args []string) {
} }
dlvArgs, targetArgs := splitArgs(cmd, args) dlvArgs, targetArgs := splitArgs(cmd, args)
err = gobuild(debugname, dlvArgs) err = gobuild.GoBuild(debugname, dlvArgs, BuildFlags)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return 1 return 1
} }
defer remove(debugname) defer gobuild.Remove(debugname)
processArgs := append([]string{debugname}, targetArgs...) processArgs := append([]string{debugname}, targetArgs...)
return execute(0, processArgs, conf, "", executingGeneratedFile) return execute(0, processArgs, conf, "", executingGeneratedFile)
}() }()
@ -484,17 +477,17 @@ func traceCmd(cmd *cobra.Command, args []string) {
return 1 return 1
} }
if traceTestBinary { if traceTestBinary {
if err := gotestbuild(debugname, dlvArgs); err != nil { if err := gobuild.GoTestBuild(debugname, dlvArgs, BuildFlags); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return 1 return 1
} }
} else { } else {
if err := gobuild(debugname, dlvArgs); err != nil { if err := gobuild.GoBuild(debugname, dlvArgs, BuildFlags); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return 1 return 1
} }
} }
defer remove(debugname) defer gobuild.Remove(debugname)
} }
processArgs = append([]string{debugname}, targetArgs...) processArgs = append([]string{debugname}, targetArgs...)
@ -576,11 +569,11 @@ func testCmd(cmd *cobra.Command, args []string) {
} }
dlvArgs, targetArgs := splitArgs(cmd, args) dlvArgs, targetArgs := splitArgs(cmd, args)
err = gotestbuild(debugname, dlvArgs) err = gobuild.GoTestBuild(debugname, dlvArgs, BuildFlags)
if err != nil { if err != nil {
return 1 return 1
} }
defer remove(debugname) defer gobuild.Remove(debugname)
processArgs := append([]string{debugname}, targetArgs...) processArgs := append([]string{debugname}, targetArgs...)
return execute(0, processArgs, conf, "", executingGeneratedTest) return execute(0, processArgs, conf, "", executingGeneratedTest)
@ -795,48 +788,3 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
return connect(listener.Addr().String(), clientConn, conf, kind) return connect(listener.Addr().String(), clientConn, conf, kind)
} }
func optflags(args []string) []string {
// after go1.9 building with -gcflags='-N -l' and -a simultaneously works.
// after go1.10 specifying -a is unnecessary because of the new caching strategy, but we should pass -gcflags=all=-N -l to have it applied to all packages
// see https://github.com/golang/go/commit/5993251c015dfa1e905bdf44bdb41572387edf90
ver, _ := goversion.Installed()
switch {
case ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}):
args = append(args, "-gcflags", "all=-N -l")
case ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}):
args = append(args, "-gcflags", "-N -l", "-a")
default:
args = append(args, "-gcflags", "-N -l")
}
return args
}
func gobuild(debugname string, pkgs []string) error {
args := []string{"-o", debugname}
args = optflags(args)
if BuildFlags != "" {
args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)
}
args = append(args, pkgs...)
return gocommand("build", args...)
}
func gotestbuild(debugname string, pkgs []string) error {
args := []string{"-c", "-o", debugname}
args = optflags(args)
if BuildFlags != "" {
args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)
}
args = append(args, pkgs...)
return gocommand("test", args...)
}
func gocommand(command string, args ...string) error {
allargs := []string{command}
allargs = append(allargs, args...)
goBuild := exec.Command("go", allargs...)
goBuild.Stderr = os.Stderr
return goBuild.Run()
}

72
pkg/gobuild/gobuild.go Normal file

@ -0,0 +1,72 @@
// Package gobuild provides utilities for building programs and tests
// for the debugging session.
package gobuild
import (
"fmt"
"os"
"os/exec"
"github.com/go-delve/delve/pkg/config"
"github.com/go-delve/delve/pkg/goversion"
)
// Remove the file at path and issue a warning to stderr if this fails.
// This can be used to remove the temporary binary generated for the session.
func Remove(path string) {
err := os.Remove(path)
if err != nil {
fmt.Fprintf(os.Stderr, "could not remove %v: %v\n", path, err)
}
}
// optflags generates default build flags to turn off optimization and inlining.
func optflags(args []string) []string {
// after go1.9 building with -gcflags='-N -l' and -a simultaneously works.
// after go1.10 specifying -a is unnecessary because of the new caching strategy,
// but we should pass -gcflags=all=-N -l to have it applied to all packages
// see https://github.com/golang/go/commit/5993251c015dfa1e905bdf44bdb41572387edf90
ver, _ := goversion.Installed()
switch {
case ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}):
args = append(args, "-gcflags", "all=-N -l")
case ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}):
args = append(args, "-gcflags", "-N -l", "-a")
default:
args = append(args, "-gcflags", "-N -l")
}
return args
}
// GoBuild builds non-test files in 'pkgs' with the specified 'buildflags'
// and writes the output at 'debugname'.
func GoBuild(debugname string, pkgs []string, buildflags string) error {
args := []string{"-o", debugname}
args = optflags(args)
if buildflags != "" {
args = append(args, config.SplitQuotedFields(buildflags, '\'')...)
}
args = append(args, pkgs...)
return gocommand("build", args...)
}
// GoBuild builds test files 'pkgs' with the specified 'buildflags'
// and writes the output at 'debugname'.
func GoTestBuild(debugname string, pkgs []string, buildflags string) error {
args := []string{"-c", "-o", debugname}
args = optflags(args)
if buildflags != "" {
args = append(args, config.SplitQuotedFields(buildflags, '\'')...)
}
args = append(args, pkgs...)
return gocommand("test", args...)
}
func gocommand(command string, args ...string) error {
allargs := []string{command}
allargs = append(allargs, args...)
goBuild := exec.Command("go", allargs...)
goBuild.Stderr = os.Stderr
return goBuild.Run()
}

@ -136,18 +136,27 @@ func (c *Client) InitializeRequest() {
c.send(request) c.send(request)
} }
// LaunchRequest sends a 'launch' request. // LaunchRequest sends a 'launch' request with the specified args.
func (c *Client) LaunchRequest(program string, stopOnEntry bool) { func (c *Client) LaunchRequest(mode string, program string, stopOnEntry bool) {
request := &dap.LaunchRequest{Request: *c.newRequest("launch")} request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
request.Arguments = map[string]interface{}{ request.Arguments = map[string]interface{}{
"request": "launch", "request": "launch",
"mode": "exec", "mode": mode,
"program": program, "program": program,
"stopOnEntry": stopOnEntry, "stopOnEntry": stopOnEntry,
} }
c.send(request) c.send(request)
} }
// LaunchRequestWithArgs takes a map of untyped implementation-specific
// arguments to send a 'launch' request. This version can be used to
// test for values of unexpected types or unspecified values.
func (c *Client) LaunchRequestWithArgs(arguments map[string]interface{}) {
request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
request.Arguments = arguments
c.send(request)
}
// DisconnectRequest sends a 'disconnect' request. // DisconnectRequest sends a 'disconnect' request.
func (c *Client) DisconnectRequest() { func (c *Client) DisconnectRequest() {
request := &dap.DisconnectRequest{Request: *c.newRequest("disconnect")} request := &dap.DisconnectRequest{Request: *c.newRequest("disconnect")}

@ -16,6 +16,7 @@ import (
"net" "net"
"path/filepath" "path/filepath"
"github.com/go-delve/delve/pkg/gobuild"
"github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/service" "github.com/go-delve/delve/service"
@ -52,6 +53,8 @@ type Server struct {
log *logrus.Entry log *logrus.Entry
// stopOnEntry is set to automatically stop the debugee after start. // stopOnEntry is set to automatically stop the debugee after start.
stopOnEntry bool stopOnEntry bool
// binaryToRemove is the compiled binary to be removed on disconnect.
binaryToRemove string
} }
// NewServer creates a new DAP Server. It takes an opened Listener // NewServer creates a new DAP Server. It takes an opened Listener
@ -115,6 +118,9 @@ func (s *Server) signalDisconnect() {
close(s.config.DisconnectChan) close(s.config.DisconnectChan)
s.config.DisconnectChan = nil s.config.DisconnectChan = nil
} }
if s.binaryToRemove != "" {
gobuild.Remove(s.binaryToRemove)
}
} }
// Run launches a new goroutine where it accepts a client connection // Run launches a new goroutine where it accepts a client connection
@ -286,34 +292,72 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
s.send(response) s.send(response)
} }
// Output path for the compiled binary in debug or test modes.
const debugBinary string = "./__debug_bin"
func (s *Server) onLaunchRequest(request *dap.LaunchRequest) { func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
// TODO(polina): Respond with an error if debug session is in progress? // TODO(polina): Respond with an error if debug session is in progress?
program, ok := request.Arguments["program"]
program, ok := request.Arguments["program"].(string)
if !ok || program == "" { if !ok || program == "" {
s.sendErrorResponse(request.Request, s.sendErrorResponse(request.Request,
FailedToContinue, "Failed to launch", FailedToContinue, "Failed to launch",
"The program attribute is missing in debug configuration.") "The program attribute is missing in debug configuration.")
return return
} }
s.config.ProcessArgs = []string{program.(string)}
s.config.WorkingDir = filepath.Dir(program.(string))
// TODO: support program args
stop, ok := request.Arguments["stopOnEntry"]
s.stopOnEntry = (ok && stop == true)
mode, ok := request.Arguments["mode"] mode, ok := request.Arguments["mode"]
if !ok || mode == "" { if !ok || mode == "" {
mode = "debug" mode = "debug"
} }
// TODO(polina): support "debug", "test" and "remote" modes
if mode != "exec" { if mode == "debug" || mode == "test" {
output, ok := request.Arguments["output"].(string)
if !ok || output == "" {
output = debugBinary
}
debugname, err := filepath.Abs(output)
if err != nil {
s.sendInternalErrorResponse(request.Seq, err.Error())
return
}
buildFlags, ok := request.Arguments["buildFlags"].(string)
if !ok {
buildFlags = ""
}
switch mode {
case "debug":
err = gobuild.GoBuild(debugname, []string{program}, buildFlags)
case "test":
err = gobuild.GoTestBuild(debugname, []string{program}, buildFlags)
}
if err != nil {
s.sendErrorResponse(request.Request,
FailedToContinue, "Failed to launch",
fmt.Sprintf("Build error: %s", err.Error()))
return
}
program = debugname
s.binaryToRemove = debugname
}
// TODO(polina): support "remote" mode
if mode != "exec" && mode != "debug" && mode != "test" {
s.sendErrorResponse(request.Request, s.sendErrorResponse(request.Request,
FailedToContinue, "Failed to launch", FailedToContinue, "Failed to launch",
fmt.Sprintf("Unsupported 'mode' value %q in debug configuration.", mode)) fmt.Sprintf("Unsupported 'mode' value %q in debug configuration.", mode))
return return
} }
stop, ok := request.Arguments["stopOnEntry"]
s.stopOnEntry = (ok && stop == true)
// TODO(polina): support target args
s.config.ProcessArgs = []string{program}
s.config.WorkingDir = filepath.Dir(program)
config := &debugger.Config{ config := &debugger.Config{
WorkingDir: s.config.WorkingDir, WorkingDir: s.config.WorkingDir,
AttachPid: 0, AttachPid: 0,

@ -5,6 +5,7 @@ import (
"io" "io"
"net" "net"
"os" "os"
"path/filepath"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -16,6 +17,8 @@ import (
"github.com/google/go-dap" "github.com/google/go-dap"
) )
const stopOnEntry bool = true
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
var logOutput string var logOutput string
flag.StringVar(&logOutput, "log-output", "", "configures log output") flag.StringVar(&logOutput, "log-output", "", "configures log output")
@ -73,7 +76,7 @@ func TestStopOnEntry(t *testing.T) {
t.Errorf("got %#v, want Seq=0, RequestSeq=0", initResp) t.Errorf("got %#v, want Seq=0, RequestSeq=0", initResp)
} }
client.LaunchRequest(fixture.Path, true /*stopOnEntry*/) client.LaunchRequest("exec", fixture.Path, stopOnEntry)
initEv := client.ExpectInitializedEvent(t) initEv := client.ExpectInitializedEvent(t)
if initEv.Seq != 0 { if initEv.Seq != 0 {
t.Errorf("got %#v, want Seq=0", initEv) t.Errorf("got %#v, want Seq=0", initEv)
@ -128,7 +131,7 @@ func TestSetBreakpoint(t *testing.T) {
client.InitializeRequest() client.InitializeRequest()
client.ExpectInitializeResponse(t) client.ExpectInitializeResponse(t)
client.LaunchRequest(fixture.Path, false /*stopOnEntry*/) client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
client.ExpectInitializedEvent(t) client.ExpectInitializedEvent(t)
launchResp := client.ExpectLaunchResponse(t) launchResp := client.ExpectLaunchResponse(t)
if launchResp.RequestSeq != 1 { if launchResp.RequestSeq != 1 {
@ -172,14 +175,54 @@ func TestSetBreakpoint(t *testing.T) {
}) })
} }
// runDebugSesion is a helper for executing the standard init and shutdown
// sequences while specifying unique launch criteria via parameters.
func runDebugSession(t *testing.T, client *daptest.Client, launchRequest func()) {
client.InitializeRequest()
client.ExpectInitializeResponse(t)
launchRequest()
client.ExpectInitializedEvent(t)
client.ExpectLaunchResponse(t)
client.ConfigurationDoneRequest()
client.ExpectConfigurationDoneResponse(t)
client.ExpectTerminatedEvent(t)
client.DisconnectRequest()
client.ExpectDisconnectResponse(t)
}
func TestLaunchDebugRequest(t *testing.T) {
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
// We reuse the harness that builds, but ignore the actual binary.
runDebugSession(t, client, func() {
// Use the default output directory.
client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "debug", "program": fixture.Source})
})
})
}
func TestLaunchTestRequest(t *testing.T) {
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSession(t, client, func() {
// We reuse the harness that builds, but ignore the actual binary.
fixtures := protest.FindFixturesDir()
testdir, _ := filepath.Abs(filepath.Join(fixtures, "buildtest"))
client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "test", "program": testdir, "output": "__mytestdir"})
})
})
}
func TestBadLaunchRequests(t *testing.T) { func TestBadLaunchRequests(t *testing.T) {
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) { runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
client.LaunchRequest("", true) seqCnt := 0
expectFailedToLaunch := func(response *dap.ErrorResponse) {
expectFailedToLaunch := func(response *dap.ErrorResponse, seq int) {
t.Helper() t.Helper()
if response.RequestSeq != seq { if response.RequestSeq != seqCnt {
t.Errorf("RequestSeq got %d, want %d", seq, response.RequestSeq) t.Errorf("RequestSeq got %d, want %d", seqCnt, response.RequestSeq)
} }
if response.Command != "launch" { if response.Command != "launch" {
t.Errorf("Command got %q, want \"launch\"", response.Command) t.Errorf("Command got %q, want \"launch\"", response.Command)
@ -190,28 +233,61 @@ func TestBadLaunchRequests(t *testing.T) {
if response.Body.Error.Id != 3000 { if response.Body.Error.Id != 3000 {
t.Errorf("Id got %d, want 3000", response.Body.Error.Id) t.Errorf("Id got %d, want 3000", response.Body.Error.Id)
} }
seqCnt++
} }
resp := client.ExpectErrorResponse(t) expectFailedToLaunchWithMessage := func(response *dap.ErrorResponse, errmsg string) {
expectFailedToLaunch(resp, 0) t.Helper()
// Test for the DAP-specific detailed error message. expectFailedToLaunch(response)
wantErrorFormat := "Failed to launch: The program attribute is missing in debug configuration." if response.Body.Error.Format != errmsg {
if resp.Body.Error.Format != wantErrorFormat { t.Errorf("\ngot %q\nwant %q", response.Body.Error.Format, errmsg)
t.Errorf("got %q, want %q", resp.Body.Error.Format, wantErrorFormat)
} }
}
// Test for the DAP-specific detailed error message.
client.LaunchRequest("exec", "", stopOnEntry)
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: The program attribute is missing in debug configuration.")
client.LaunchRequestWithArgs(map[string]interface{}{"program": 12345})
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: The program attribute is missing in debug configuration.")
client.LaunchRequestWithArgs(map[string]interface{}{"program": nil})
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: The program attribute is missing in debug configuration.")
client.LaunchRequestWithArgs(map[string]interface{}{})
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: The program attribute is missing in debug configuration.")
client.LaunchRequest("remote", fixture.Path, stopOnEntry)
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: Unsupported 'mode' value \"remote\" in debug configuration.")
client.LaunchRequest("notamode", fixture.Path, stopOnEntry)
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: Unsupported 'mode' value \"notamode\" in debug configuration.")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": 12345, "program": fixture.Path})
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: Unsupported 'mode' value %!q(float64=12345) in debug configuration.")
// Skip detailed message checks for potentially different OS-specific errors. // Skip detailed message checks for potentially different OS-specific errors.
client.LaunchRequest(fixture.Path+"_does_not_exist", false) client.LaunchRequest("exec", fixture.Path+"_does_not_exist", stopOnEntry)
expectFailedToLaunch(client.ExpectErrorResponse(t), 1) expectFailedToLaunch(client.ExpectErrorResponse(t))
client.LaunchRequest(fixture.Source, true) // Not an executable client.LaunchRequest("debug", fixture.Path+"_does_not_exist", stopOnEntry)
expectFailedToLaunch(client.ExpectErrorResponse(t), 2) expectFailedToLaunch(client.ExpectErrorResponse(t)) // Build error
client.LaunchRequest("exec", fixture.Source, stopOnEntry)
expectFailedToLaunch(client.ExpectErrorResponse(t)) // Not an executable
// We failed to launch the program. Make sure shutdown still works. // We failed to launch the program. Make sure shutdown still works.
client.DisconnectRequest() client.DisconnectRequest()
dresp := client.ExpectDisconnectResponse(t) dresp := client.ExpectDisconnectResponse(t)
if dresp.RequestSeq != 3 { if dresp.RequestSeq != seqCnt {
t.Errorf("got %#v, want RequestSeq=3", dresp) t.Errorf("got %#v, want RequestSeq=%d", dresp, seqCnt)
} }
}) })
} }