From 90c8456261171cb11d338ea9f6db6ff94dad8ef3 Mon Sep 17 00:00:00 2001 From: xhd2015 Date: Wed, 29 May 2024 09:50:01 +0800 Subject: [PATCH] test-explorer: make run streaming output --- cmd/xgo/runtime_gen/core/version.go | 4 +- cmd/xgo/test-explorer/debug.go | 256 +++++++------------------ cmd/xgo/test-explorer/index.html | 2 +- cmd/xgo/test-explorer/poll.go | 178 +++++++++++++++++ cmd/xgo/test-explorer/run.go | 72 +++---- cmd/xgo/test-explorer/test_explorer.go | 18 +- cmd/xgo/version.go | 4 +- runtime/core/version.go | 4 +- 8 files changed, 284 insertions(+), 254 deletions(-) create mode 100644 cmd/xgo/test-explorer/poll.go diff --git a/cmd/xgo/runtime_gen/core/version.go b/cmd/xgo/runtime_gen/core/version.go index 3403fb7f..180278e2 100755 --- a/cmd/xgo/runtime_gen/core/version.go +++ b/cmd/xgo/runtime_gen/core/version.go @@ -7,8 +7,8 @@ import ( ) const VERSION = "1.0.37" -const REVISION = "5d0b62062accb5c87ec7643e925d351ce65e3b59+1" -const NUMBER = 241 +const REVISION = "da25b0b8838244b76b23707349c5a2b343abc5d9+1" +const NUMBER = 242 // these fields will be filled by compiler const XGO_VERSION = "" diff --git a/cmd/xgo/test-explorer/debug.go b/cmd/xgo/test-explorer/debug.go index 39e3dc58..0e13e624 100644 --- a/cmd/xgo/test-explorer/debug.go +++ b/cmd/xgo/test-explorer/debug.go @@ -1,19 +1,14 @@ 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" ) @@ -36,191 +31,78 @@ type DebugDestroyRequest struct { } 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") - } - if req.Item.Name == "" { - return nil, netutil.ParamErrorf("requires name") - } - - 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 - } - - parsedFlags, parsedArgs, err := getTestFlags(absDir, file, req.Item.Name) - 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) - - flags := []string{"test", "-c", "-o", tmpBin, "-gcflags=all=-N -l"} - flags = append(flags, config.Flags...) - flags = append(flags, parsedFlags...) - flags = append(flags, "./"+relPathDir) - err = cmd.Dir(projectDir).Debug().Stderr(stderr).Stdout(stdout).Run(goCmd, flags...) - 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", - append([]string{ - "exec", - "--api-version=2", - "--check-go-version=false", - // NOTE: --init is ignored if --headless - // "--init", dlvInitFile, - "--headless", - // "--allow-non-terminal-interactive=true", - fmt.Sprintf("--listen=localhost:%d", port), - tmpBin, "--", "-test.v", "-test.run", fmt.Sprintf("^%s$", req.Item.Name), - }, parsedArgs...)..., - ) - }) - 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 - }) - }) + setupPollHandler(server, "/debug", projectDir, getTestConfig, debug) +} - 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 - } +func debug(ctx *RunContext) error { + projectDir := ctx.ProjectDir + file := ctx.File + relPath := ctx.RelPath + name := ctx.Name + stdout := ctx.Stdout + stderr := ctx.Stderr + goCmd := ctx.GoCmd + buildFlags := ctx.BuildFlags + args := ctx.Args + env := ctx.Env + + 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 + } - 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 - }) + // 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) + + flags := []string{"test", "-c", "-o", tmpBin, "-gcflags=all=-N -l"} + flags = append(flags, buildFlags...) + flags = append(flags, "./"+relPathDir) + err = cmd.Dir(projectDir).Debug().Stderr(stderr).Stdout(stdout).Run(goCmd, flags...) + 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). + Env(env). + Run("dlv", + append([]string{ + "exec", + "--api-version=2", + "--check-go-version=false", + // NOTE: --init is ignored if --headless + // "--init", dlvInitFile, + "--headless", + // "--allow-non-terminal-interactive=true", + fmt.Sprintf("--listen=localhost:%d", port), + tmpBin, "--", "-test.v", "-test.run", fmt.Sprintf("^%s$", name), + }, args...)..., + ) }) + if err != nil { + return err + } + return nil } func formatVscodeConfig(port int) string { diff --git a/cmd/xgo/test-explorer/index.html b/cmd/xgo/test-explorer/index.html index ecc3daf1..3631463f 100644 --- a/cmd/xgo/test-explorer/index.html +++ b/cmd/xgo/test-explorer/index.html @@ -20,6 +20,6 @@ - + \ No newline at end of file diff --git a/cmd/xgo/test-explorer/poll.go b/cmd/xgo/test-explorer/poll.go new file mode 100644 index 00000000..09e3edef --- /dev/null +++ b/cmd/xgo/test-explorer/poll.go @@ -0,0 +1,178 @@ +package test_explorer + +import ( + "bufio" + "context" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "github.com/xhd2015/xgo/support/fileutil" + "github.com/xhd2015/xgo/support/netutil" + "github.com/xhd2015/xgo/support/session" +) + +func setupPollHandler(server *http.ServeMux, prefix string, projectDir string, getTestConfig func() (*TestConfig, error), runner func(ctx *RunContext) error) { + sessionManager := session.NewSessionManager() + + server.HandleFunc(prefix, 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") + } + if req.Item.Name == "" { + return nil, netutil.ParamErrorf("requires name") + } + + 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 + } + + parsedFlags, parsedArgs, err := getTestFlags(absDir, file, req.Item.Name) + 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, sess, err := sessionManager.Start() + if err != nil { + return nil, err + } + + startRun(sess, projectDir, absDir, file, relPath, req.Item.Name, config, parsedFlags, parsedArgs, runner) + return &DebugResponse{ID: id}, nil + }) + }) + + server.HandleFunc(prefix+"/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(prefix+"/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 startRun(sess session.Session, projectDir string, absDir string, file string, relPath string, name string, config *TestConfig, testFlags []string, testArgs []string, runner func(ctx *RunContext) error) { + pr, pw := io.Pipe() + // go func() { xxx } + // - build with gcflags="all=-N -l" + // - start dlv + // - output prompt + go func() { + defer sess.SendEvents(&TestingItemEvent{ + Event: Event_TestEnd, + }) + + ctx := &RunContext{ + ProjectDir: projectDir, + AbsProjectDir: absDir, + File: file, + RelPath: relPath, + Name: name, + Stdout: io.MultiWriter(os.Stdout, pw), + Stderr: io.MultiWriter(os.Stderr, pw), + + GoCmd: config.GetGoCmd(), + BuildFlags: append(config.Flags, testFlags...), + Env: config.CmdEnv(), + Args: testArgs, + } + err := runner(ctx) + if err != nil { + sess.SendEvents(&TestingItemEvent{ + Event: Event_Output, + Msg: "err: " + err.Error(), + }) + } + }() + + go func() { + scanner := bufio.NewScanner(pr) + for scanner.Scan() { + data := scanner.Bytes() + sess.SendEvents(&TestingItemEvent{ + Event: Event_Output, + Msg: string(data), + }) + } + }() +} + +type RunContext struct { + ProjectDir string + AbsProjectDir string + File string + RelPath string + Name string + Stdout io.Writer + Stderr io.Writer + + GoCmd string + BuildFlags []string + + Env []string + Args []string +} diff --git a/cmd/xgo/test-explorer/run.go b/cmd/xgo/test-explorer/run.go index 36f539c0..2cd91c95 100644 --- a/cmd/xgo/test-explorer/run.go +++ b/cmd/xgo/test-explorer/run.go @@ -22,6 +22,14 @@ import ( "github.com/xhd2015/xgo/support/session" ) +func setupTestHandler(server *http.ServeMux, projectDir string, getTestConfig func() (*TestConfig, error)) { + setupPollHandler(server, "/run", projectDir, getTestConfig, run) +} + +func setupRunHandler(server *http.ServeMux, projectDir string, getTestConfig func() (*TestConfig, error)) { + doSetupRunHandler(server, projectDir, getTestConfig) +} + type StartSessionRequest struct { *TestingItem } @@ -363,7 +371,7 @@ func (c *runSession) sendEvent(event *TestingItemEvent) { } // TODO: make FE call /session/destroy -func setupRunHandler(server *http.ServeMux, projectDir string, getTestConfig func() (*TestConfig, error)) { +func doSetupRunHandler(server *http.ServeMux, projectDir string, getTestConfig func() (*TestConfig, error)) { sessionManager := session.NewSessionManager() server.HandleFunc("/session/start", func(w http.ResponseWriter, r *http.Request) { @@ -453,59 +461,37 @@ func setupRunHandler(server *http.ServeMux, projectDir string, getTestConfig fun }) } -func run(req *RunRequest, projectDir string, goCmd string, env []string, testFlags []string) (*RunResult, error) { - if req == nil || req.BaseRequest == nil || req.File == "" { - return nil, fmt.Errorf("requires file") - } - if req.Name == "" { - return nil, fmt.Errorf("requires name") - } - absDir, err := filepath.Abs(projectDir) - if err != nil { - return nil, err - } - relPath, err := filepath.Rel(absDir, req.File) - if err != nil { - return nil, err - } - parsedFlags, parsedArgs, err := getTestFlags(absDir, req.File, req.Name) - if err != nil { - return nil, err - } - - // fmt.Printf("run:%v\n", req) - var buf bytes.Buffer - args := []string{"test", "-run", fmt.Sprintf("^%s$", req.Name)} - if req.Verbose { +func run(ctx *RunContext) error { + projectDir := ctx.ProjectDir + relPath := ctx.RelPath + name := ctx.Name + buildFlags := ctx.BuildFlags + runArgs := ctx.Args + env := ctx.Env + goCmd := ctx.GoCmd + stderr := ctx.Stderr + stdout := ctx.Stdout + verbose := true + + args := []string{"test", "-run", fmt.Sprintf("^%s$", name)} + if verbose { args = append(args, "-v") } - args = append(args, testFlags...) - args = append(args, parsedFlags...) + args = append(args, buildFlags...) args = append(args, "./"+filepath.Dir(relPath)) - if len(parsedArgs) > 0 { + if len(runArgs) > 0 { args = append(args, "-args") - args = append(args, parsedArgs...) + args = append(args, runArgs...) } if goCmd == "" { goCmd = "go" } - runErr := cmd.Debug().Dir(projectDir). + return cmd.Debug().Dir(projectDir). Env(env). - Stderr(io.MultiWriter(os.Stderr, &buf)). - Stdout(io.MultiWriter(os.Stdout, &buf)). + Stdout(stdout). + Stderr(stderr). Run(goCmd, args...) - if runErr != nil { - return &RunResult{ - Status: RunStatus_Fail, - Msg: buf.String(), - }, nil - } - - return &RunResult{ - Status: RunStatus_Success, - Msg: buf.String(), - }, nil } func getTestFlags(absProjectDir string, file string, name string) (flags []string, args []string, err error) { diff --git a/cmd/xgo/test-explorer/test_explorer.go b/cmd/xgo/test-explorer/test_explorer.go index 16acbb65..44b1a262 100644 --- a/cmd/xgo/test-explorer/test_explorer.go +++ b/cmd/xgo/test-explorer/test_explorer.go @@ -243,25 +243,9 @@ func handle(opts *Options) error { }) }) - server.HandleFunc("/run", 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 *RunRequest - err := parseBody(r.Body, &req) - if err != nil { - return nil, err - } - config, err := getTestConfig() - if err != nil { - return nil, err - } - - return run(req, opts.ProjectDir, config.GoCmd, config.CmdEnv(), config.Flags) - }) - }) - setupRunHandler(server, opts.ProjectDir, getTestConfig) setupDebugHandler(server, opts.ProjectDir, getTestConfig) + setupTestHandler(server, opts.ProjectDir, getTestConfig) setupOpenHandler(server) return netutil.ServePortHTTP(server, 7070, true, 500*time.Millisecond, func(port int) { diff --git a/cmd/xgo/version.go b/cmd/xgo/version.go index b7d477d5..b2f956ee 100644 --- a/cmd/xgo/version.go +++ b/cmd/xgo/version.go @@ -3,8 +3,8 @@ package main import "fmt" const VERSION = "1.0.37" -const REVISION = "5d0b62062accb5c87ec7643e925d351ce65e3b59+1" -const NUMBER = 241 +const REVISION = "da25b0b8838244b76b23707349c5a2b343abc5d9+1" +const NUMBER = 242 func getRevision() string { revSuffix := "" diff --git a/runtime/core/version.go b/runtime/core/version.go index 3403fb7f..180278e2 100644 --- a/runtime/core/version.go +++ b/runtime/core/version.go @@ -7,8 +7,8 @@ import ( ) const VERSION = "1.0.37" -const REVISION = "5d0b62062accb5c87ec7643e925d351ce65e3b59+1" -const NUMBER = 241 +const REVISION = "da25b0b8838244b76b23707349c5a2b343abc5d9+1" +const NUMBER = 242 // these fields will be filled by compiler const XGO_VERSION = ""