Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test-explorer: add debug button #152

Merged
merged 1 commit into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/xgo/runtime_gen/core/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

const VERSION = "1.0.37"
const REVISION = "310d0d44809c8f2ad26761138fb8eb3cc4db75c9+1"
const NUMBER = 238
const REVISION = "62c6c037c1f57c371e227dd1bb8b8e141367f1c6+1"
const NUMBER = 239

// these fields will be filled by compiler
const XGO_VERSION = ""
Expand Down
7 changes: 7 additions & 0 deletions cmd/xgo/test-explorer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ func (c *TestConfig) CmdEnv() []string {
return env
}

func (c *TestConfig) GetGoCmd() string {
if c.GoCmd != "" {
return c.GoCmd
}
return "go"
}

type GoConfig struct {
Min string `json:"min"`
Max string `json:"max"`
Expand Down
221 changes: 221 additions & 0 deletions cmd/xgo/test-explorer/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package test_explorer

import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"

"github.com/xhd2015/xgo/support/cmd"
"github.com/xhd2015/xgo/support/fileutil"
"github.com/xhd2015/xgo/support/netutil"
"github.com/xhd2015/xgo/support/session"
"github.com/xhd2015/xgo/support/strutil"
)

type DebugRequest struct {
Item *TestingItem `json:"item"`
}
type DebugResponse struct {
ID string `json:"id"`
}

type DebugPollRequest struct {
ID string `json:"id"`
}

type DebugPollResponse struct {
Events []*TestingItemEvent `json:"events"`
}
type DebugDestroyRequest struct {
ID string `json:"id"`
}

func setupDebugHandler(server *http.ServeMux, projectDir string, getTestConfig func() (*TestConfig, error)) {
sessionManager := session.NewSessionManager()

server.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) {
netutil.SetCORSHeaders(w)
netutil.HandleJSON(w, r, func(ctx context.Context, r *http.Request) (interface{}, error) {
var req *DebugRequest
err := parseBody(r.Body, &req)
if err != nil {
return nil, err
}
if req == nil || req.Item == nil || req.Item.File == "" {
return nil, netutil.ParamErrorf("requires file")
}

file := req.Item.File
isFile, err := fileutil.IsFile(file)
if err != nil {
return nil, err
}
if !isFile {
return nil, fmt.Errorf("cannot debug multiple tests")
}
absDir, err := filepath.Abs(projectDir)
if err != nil {
return nil, err
}

relPath, err := filepath.Rel(absDir, file)
if err != nil {
return nil, err
}

config, err := getTestConfig()
if err != nil {
return nil, err
}

id, session, err := sessionManager.Start()
if err != nil {
return nil, err
}

pr, pw := io.Pipe()

// go func() { xxx }
// - build with gcflags="all=-N -l"
// - start dlv
// - output prompt
go func() {
defer session.SendEvents(&TestingItemEvent{
Event: Event_TestEnd,
})
debug := func(projectDir string, file string, stdout io.Writer, stderr io.Writer) error {
goCmd := config.GetGoCmd()
tmpDir, err := os.MkdirTemp("", "go-test-debug")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
binName := "debug.bin"
baseName := filepath.Base(file)
if baseName != "" {
binName = baseName + "-" + binName
}

// TODO: find a way to automatically set breakpoint
// dlvInitFile := filepath.Join(tmpDir, "dlv-init.txt")
// err = ioutil.WriteFile(dlvInitFile, []byte(fmt.Sprintf("break %s:%d\n", file, req.Item.Line)), 0755)
// if err != nil {
// return err
// }
relPathDir := filepath.Dir(relPath)
tmpBin := filepath.Join(tmpDir, binName)
err = cmd.Dir(projectDir).Debug().Stderr(stderr).Stdout(stdout).Run(goCmd, "test", "-c", "-o", tmpBin, "-gcflags=all=-N -l", "./"+relPathDir)
if err != nil {
return err
}
err = netutil.ServePort(2345, true, 500*time.Millisecond, func(port int) {
// user need to set breakpoint explicitly
fmt.Fprintf(stderr, "dlv listen on localhost:%d\n", port)
fmt.Fprintf(stderr, "Debug with IDEs:\n")
fmt.Fprintf(stderr, " > VSCode: add the following config to .vscode/launch.json configurations:")
fmt.Fprintf(stderr, "\n%s\n", strutil.IndentLines(formatVscodeConfig(port), " "))
fmt.Fprintf(stderr, " > GoLand: click Add Configuration > Go Remote > localhost:%d\n", port)
fmt.Fprintf(stderr, " > Terminal: dlv connect localhost:%d\n", port)
}, func(port int) error {
// dlv exec --api-version=2 --listen=localhost:2345 --accept-multiclient --headless ./debug.bin
return cmd.Dir(filepath.Dir(file)).Debug().Stderr(stderr).Stdout(stdout).Run("dlv", "exec",
"--api-version=2",
fmt.Sprintf("--listen=localhost:%d", port),
// NOTE: --init is ignored if --headless
// "--init", dlvInitFile,
"--headless",
// "--allow-non-terminal-interactive=true",
tmpBin, "-test.v", "-test.run", fmt.Sprintf("$%s^", req.Item.Name))
})
if err != nil {
return err
}
return nil
}
err := debug(projectDir, file, io.MultiWriter(os.Stdout, pw), io.MultiWriter(os.Stderr, pw))
if err != nil {
session.SendEvents(&TestingItemEvent{
Event: Event_Output,
Msg: "err: " + err.Error(),
})
}
}()

go func() {
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
data := scanner.Bytes()
session.SendEvents(&TestingItemEvent{
Event: Event_Output,
Msg: string(data),
})
}
}()
return &DebugResponse{ID: id}, nil
})
})

server.HandleFunc("/debug/pollStatus", func(w http.ResponseWriter, r *http.Request) {
netutil.SetCORSHeaders(w)
netutil.HandleJSON(w, r, func(ctx context.Context, r *http.Request) (interface{}, error) {
var req *DebugPollRequest
err := parseBody(r.Body, &req)
if err != nil {
return nil, err
}
if req.ID == "" {
return nil, netutil.ParamErrorf("requires id")
}
session, err := sessionManager.Get(req.ID)
if err != nil {
return nil, err
}

events, err := session.PollEvents()
if err != nil {
return nil, err
}
return &DebugPollResponse{
Events: convTestingEvents(events),
}, nil
})
})
server.HandleFunc("/debug/destroy", func(w http.ResponseWriter, r *http.Request) {
netutil.SetCORSHeaders(w)
netutil.HandleJSON(w, r, func(ctx context.Context, r *http.Request) (interface{}, error) {
var req *DebugDestroyRequest
err := parseBody(r.Body, &req)
if err != nil {
return nil, err
}
if req.ID == "" {
return nil, netutil.ParamErrorf("requires id")
}
err = sessionManager.Destroy(req.ID)
if err != nil {
return nil, err
}
return nil, nil
})
})
}

func formatVscodeConfig(port int) string {
return fmt.Sprintf(`{
"configurations": [
{
"name": "Debug dlv localhost:%d",
"type": "go",
"request": "attach",
"mode": "remote",
"port": %d,
"host": "127.0.0.1"
}
}
}`, port, port)
}
2 changes: 1 addition & 1 deletion cmd/xgo/test-explorer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
</body>
<!--after body because document.body may be null-->
<!--available in CN and Global-->
<script src="https://cdn.jsdelivr.net/npm/[email protected].7/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].8/index.js"></script>
<!-- <script src="http://127.0.0.1:8081/npm-publish/index.js"></script> -->
</html>
Loading
Loading