Skip to content

Commit

Permalink
test-explorer: integrate stack trace
Browse files Browse the repository at this point in the history
  • Loading branch information
xhd2015 committed Jul 28, 2024
1 parent 324b023 commit baca04f
Show file tree
Hide file tree
Showing 12 changed files with 428 additions and 30 deletions.
2 changes: 1 addition & 1 deletion cmd/xgo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func handleBuild(cmd string, args []string) error {
buildCacheSuffix += "-strace_" + v
}
if stackTraceDir != "" && stackTraceDir != "." && stackTraceDir != "./" {
// this affects the flags package
// this affects the trap/flags package
h := md5.New()
h.Write([]byte(stackTraceDir))
buildCacheSuffix += "-" + hex.EncodeToString(h.Sum(nil))
Expand Down
13 changes: 13 additions & 0 deletions cmd/xgo/runtime_gen/trace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,20 @@ func emitTrace(name string, root *Root, opts *ExportOptions) error {

subFile := subName + ".json"
if canUseFlagDir && flags.STRACE_DIR != "" {
// ensure strace dir exists
stat, err := os.Stat(flags.STRACE_DIR)
if err != nil {
return err
}
if !stat.IsDir() {
return fmt.Errorf("%s %w", flags.STRACE_DIR, os.ErrNotExist)
}
subFile = filepath.Join(flags.STRACE_DIR, subFile)
parentDir := filepath.Dir(subFile)
err = os.MkdirAll(parentDir, 0755)
if err != nil {
return err
}
} else {
subDir := filepath.Dir(subFile)
err := os.MkdirAll(subDir, 0755)
Expand Down
4 changes: 2 additions & 2 deletions cmd/xgo/test-explorer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</body>
<!--after body because document.body may be null-->
<!--available in CN and Global-->
<script src="https://cdn.jsdelivr.net/npm/[email protected].11/index.js"></script>
<!-- <script src="http://127.0.0.1:8080/build/index.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/[email protected].12/index.js"></script>
<!-- <script src="http://127.0.0.1:8080/build/index.js"></script> --> <!--for local dev-->
<!-- <script src="http://127.0.0.1:8080/npm-publish/index.js"></script> -->
</html>
12 changes: 9 additions & 3 deletions cmd/xgo/test-explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,16 @@ const (
RunStatus_Skip RunStatus = "skip"
)

// TestingItem describe a test item
// when invoked from sub dir, relPath
// is relative to the sub dir
type TestingItem struct {
Key string `json:"key"`
Name string `json:"name"`
BaseCaseName string `json:"baseCaseName"` // the base case's name
Key string `json:"key"`
// base name, e.g.: subcase if full name is TestSomething/subcase
Name string `json:"name"`
// base case name, e.g.: TestSomething
BaseCaseName string `json:"baseCaseName"` // the base case's name
// full name, e.g.: TestSomething/subcase
NameUnderPkg string `json:"nameUnderPkg"` // the name under pkg
RelPath string `json:"relPath"`
File string `json:"file"`
Expand Down
27 changes: 15 additions & 12 deletions cmd/xgo/test-explorer/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,23 @@ type StartSessionResult struct {
type Event string

const (
Event_ItemStatus Event = "item_status"
Event_MergeTree Event = "merge_tree"
Event_Output Event = "output"
Event_ErrorMsg Event = "error_msg"
Event_TestStart Event = "test_start"
Event_TestEnd Event = "test_end"
Event_ItemStatus Event = "item_status"
Event_MergeTree Event = "merge_tree"
Event_Output Event = "output"
Event_ErrorMsg Event = "error_msg"
Event_TestStart Event = "test_start"
Event_TestEnd Event = "test_end"
Event_UpdateTrace Event = "update_trace"
)

type TestingItemEvent struct {
Event Event `json:"event"`
Item *TestingItem `json:"item"`
Path []string `json:"path"`
Status RunStatus `json:"status"`
Msg string `json:"msg"`
LogConsole bool `json:"logConsole"`
Event Event `json:"event"`
Item *TestingItem `json:"item"`
Path []string `json:"path"`
Status RunStatus `json:"status"`
Msg string `json:"msg"`
LogConsole bool `json:"logConsole"`
TraceRecords []*CallRecord `json:"traceRecords"`
}

type PollSessionRequest struct {
Expand All @@ -63,6 +65,7 @@ type runSession struct {
item *TestingItem
path []string
debug bool
trace bool // xgo stack trace

logConsole bool

Expand Down
170 changes: 163 additions & 7 deletions cmd/xgo/test-explorer/run_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"

"github.com/xhd2015/xgo/cmd/xgo/pathsum"
"github.com/xhd2015/xgo/support/cmd"
"github.com/xhd2015/xgo/support/fileutil"
"github.com/xhd2015/xgo/support/goinfo"
"github.com/xhd2015/xgo/support/netutil"
"github.com/xhd2015/xgo/support/session"
Expand All @@ -24,6 +29,7 @@ type StartSessionRequest struct {
Item *TestingItem `json:"item"`
Path []string `json:"path"`
Debug bool `json:"debug"`
Trace bool `json:"trace"`
}

// TODO: make FE call /session/destroy
Expand All @@ -45,6 +51,9 @@ func setupRunHandler(server *http.ServeMux, projectDir string, logConsole bool,
if req.Debug && req.Item.Kind != TestingItemKind_Case {
return nil, netutil.ParamErrorf("debug not supported: %s", req.Item.Kind)
}
if req.Trace && req.Item.Kind != TestingItemKind_Case {
return nil, netutil.ParamErrorf("trace not supported: %s", req.Item.Kind)
}

config, err := getTestConfig()
if err != nil {
Expand Down Expand Up @@ -74,6 +83,7 @@ func setupRunHandler(server *http.ServeMux, projectDir string, logConsole bool,
item: req.Item,
path: req.Path,
debug: req.Debug,
trace: req.Trace,

logConsole: logConsole,
session: ses,
Expand Down Expand Up @@ -193,6 +203,11 @@ func (c *runSession) Start() error {
item := c.item
absDir := c.absDir
pathPrefix := c.pathPrefix
debug := c.debug
trace := c.trace

begin := time.Now()
_ = begin

dirPkgPath, err := resolveDirPkgPath(absDir)
if err != nil {
Expand Down Expand Up @@ -240,7 +255,6 @@ func (c *runSession) Start() error {
}}, nil
}

debug := c.debug
var singleCase bool
var eventBuilder func(line []byte) ([]*TestingItemEvent, error)
if item.Kind == TestingItemKind_Case {
Expand Down Expand Up @@ -269,6 +283,35 @@ func (c *runSession) Start() error {
if !singleCase {
defStdErrReader, defStdErr = io.Pipe()
}

// trace
var traceDir string

// in go, file is ignored under a package
// the traceDir corresponds to the test
// case's dir
// i.e.
// case = pkg/some_test.go/TestSomething/sub
// traceDir = ROOT/pkg
// caseSubPath = TestSomething/sub
var caseSubPath string
if singleCase && trace {
subPath, projectRoot, err := goinfo.FindGoModDirSubPath(absDir)
if err != nil {
return err
}
itemDir := filepath.Dir(c.item.RelPath)
if len(subPath) > 0 {
itemDir = filepath.Join(filepath.Join(subPath...), itemDir)
}
traceDir, err = getConsistentTraceDir(projectRoot, itemDir)
if err != nil {
return err
}
caseSubPath = item.NameUnderPkg
debugF("absDir=%s,itemRelPath=%s, traceDir=%s\n", absDir, c.item.RelPath, traceDir)
}

go func() {
var err error
defer func() {
Expand All @@ -284,15 +327,20 @@ func (c *runSession) Start() error {
}
pathArgs := formatPathArgs(paths)
runNames := formatRunNames(names)

testFlags := c.testFlags
if !debug && !singleCase {
testFlags = append([]string{"-json"}, testFlags...)
}
if traceDir != "" {
testFlags = append(testFlags, "--strace", "--strace-dir", traceDir)
}

if !debug {
testArgs := joinTestArgs(pathArgs, runNames)
customFlags := c.testFlags
if !singleCase {
customFlags = append([]string{"-json"}, customFlags...)
}
err = runTest(c.goCmd, c.dir, customFlags, testArgs, c.progArgs, c.env, stdout, stderr)
err = runTest(c.goCmd, c.dir, testFlags, testArgs, c.progArgs, c.env, stdout, stderr)
} else {
err = debugTest(c.goCmd, c.dir, item.File, c.testFlags, pathArgs, runNames, stdout, stderr, c.progArgs, c.env)
err = debugTest(c.goCmd, c.dir, item.File, testFlags, pathArgs, runNames, stdout, stderr, c.progArgs, c.env)
}

if err != nil {
Expand All @@ -301,6 +349,7 @@ func (c *runSession) Start() error {
fmt.Printf("test end\n")
}()

// consume stderr
if defStdErrReader != nil {
go func() {
defer defStdErr.Close()
Expand Down Expand Up @@ -330,8 +379,23 @@ func (c *runSession) Start() error {
Status: RunStatus_Success,
})
}
// read trace
if traceDir != "" {
debugF("path: %v, baseTraceCase: %v, rootPath: %v", path, caseSubPath, rootPath)
if suffix, ok := trimPrefix(path, rootPath); ok {
records := readTrace(traceDir, caseSubPath, suffix)
if records != nil {
sendEvent(&TestingItemEvent{
Event: Event_UpdateTrace,
Path: path,
TraceRecords: records,
})
}
}
}
return true
})

sendEvent(&TestingItemEvent{
Event: Event_TestEnd,
})
Expand All @@ -344,6 +408,75 @@ func (c *runSession) Start() error {
return nil
}

func trimPrefix(path []string, root []string) ([]string, bool) {
if len(path) < len(root) {
return nil, false
}
for i, p := range root {
if path[i] != p {
return nil, false
}
}
return path[len(root):], true
}

func readTrace(traceDir string, baseCaseName string, subPath []string) []*CallRecord {
traceFile := filepath.Join(traceDir, baseCaseName, filepath.Join(subPath...)) + ".json"
debugF("traceFile: %s", traceFile)
records, err := readTraceFromFile(traceFile)
if err != nil {
return []*CallRecord{
{
Pkg: "github.com/xhd2015/xgo/cmd/xgo/test-explorer",
Func: "readTraceFromFile",
Error: err.Error(),
},
}
}
return records
}

func readTraceFromFile(file string) (records []*CallRecord, err error) {
defer func() {
if e := recover(); e != nil {
if pe, ok := e.(error); ok {
err = pe
} else {
err = fmt.Errorf("panic: %v", e)
}
}
}()
data, err := fileutil.ReadFile(file)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
var rootExport *RootExport
err = json.Unmarshal(data, &rootExport)
if err != nil {
return nil, err
}
c := &traceConverter{}
if rootExport != nil {
records = c.convertStacks(rootExport.Children)
}
return records, nil
}

const debugLog = false

func debugF(format string, args ...interface{}) {
if !debugLog {
return
}
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, format, args...)
}

type pathMapping struct {
record *record
m map[string]*pathMapping
Expand Down Expand Up @@ -495,3 +628,26 @@ func (c *jsonTestEventBuilder) parse(line []byte) (*TestEvent, error) {
Output: output,
}, nil
}

func getConsistentTraceDir(projectDir string, subDir string) (string, error) {
var tmpRoot string
if runtime.GOOS != "windows" {
// prefer /tmp on unix because it is shorter
stat, statErr := os.Stat("/tmp")
if statErr == nil && stat.IsDir() {
tmpRoot = "/tmp"
}
}
pathSum, err := pathsum.PathSum("", projectDir)
if err != nil {
return "", err
}

fullPath := filepath.Join(tmpRoot, "xgo", "test-explorer", "trace", pathSum, subDir)
err = os.MkdirAll(fullPath, 0755)
if err != nil {
return "", err
}

return fullPath, nil
}
Loading

0 comments on commit baca04f

Please sign in to comment.