Skip to content

Commit

Permalink
add xgo tool coverage serve
Browse files Browse the repository at this point in the history
  • Loading branch information
xhd2015 committed May 14, 2024
1 parent 960c3c4 commit 7527db4
Show file tree
Hide file tree
Showing 19 changed files with 600 additions and 122 deletions.
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@

**English | [简体中文](./README_zh_cn.md)**

Enable function Trap in `go`, provide tools like Mock and Trace to help go developers write unit test and debug both easier and faster.
`xgo` provides *all-in-one* test utilities for golang, including:

`xgo` works as a preprocessor for `go run`,`go build`, and `go test`(see our [blog](https://blog.xhd2015.xyz/posts/xgo-monkey-patching-in-go-using-toolexec)).

It **preprocess** the source code and IR(Intermediate Representation) before invoking `go`, adding missing abilities to go program by cooperating with(or hacking) the go compiler.

These abilities include:
- [Trap](#trap)
- [Mock](#mock)
- [Trace](#trace)
- [Incremental Coverage](#incremental-coverage)

As for the monkey patching part, `xgo` works as a preprocessor for `go run`,`go build`, and `go test`(see our [blog](https://blog.xhd2015.xyz/posts/xgo-monkey-patching-in-go-using-toolexec)).

See [Quick Start](#quick-start) and [Documentation](./doc) for more details.

Expand Down Expand Up @@ -136,6 +134,9 @@ The above demo can be found at [doc/demo](./doc/demo).

# API
## Trap
It **preprocess** the source code and IR(Intermediate Representation) before invoking `go`, adding missing abilities to go program by cooperating with(or hacking) the go compiler.


Trap allows developer to intercept function execution on the fly.

Trap is the core of `xgo` as it is the basis of other abilities like Mock and Trace.
Expand Down Expand Up @@ -417,6 +418,29 @@ func TestTrace(t *testing.T) {
```
The trace will only include `B()` and `C()`.

## Incremental Coverage
The `xgo tool coverage` sub command extends go's builtin `go tool cover` for better visualization.

First, run `go test` or `xgo test` to get a coverage profile:
```sh
go test -cover -coverpkg ./... -coverprofile cover.out ./...
```

Then, use `xgo` to display the coverage:
```sh
xgo tool coverage serve cover.out
```

Output:

![coverage](doc/img/coverage.jpg "Coverage")

The displayed coverage is a combination of coverage and git diff. By default, only modified lines were shown:
- Covered lines shown as light blue,
- Uncovered lines shown as light yellow

This helps to quickly locate changes that were not covered, and add tests for them incrementally.

# Concurrent safety
I know you guys from other monkey patching library suffer from the unsafety implied by these frameworks.

Expand Down
37 changes: 30 additions & 7 deletions README_zh_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@

**[English](./README.md) | 简体中文**

允许对`go`的函数进行拦截, 并提供Mock和Trace等工具帮助开发者编写测试和快速调试。
`xgo`提供了一个*全功能*的Golang测试工具集, 包括:

`xgo`作为一个预处理器工作在`go run`,`go build`,和`go test`之上(查看[blog](https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec))。

`xgo`对源代码和IR(中间码)进行预处理之后, 再调用`go`进行后续的编译工作。通过这种方式, `xgo`实现了一些在`go`中缺乏的能力。

这些能力包括:
- [Trap](#trap)
- [Mock](#mock)
- [Trace](#trace).
- [Trace](#trace)
- [增量覆盖率](#增量覆盖率)

就Mock而言,`xgo`作为一个预处理器工作在`go run`,`go build`,和`go test`之上(查看[blog](https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec))。

`xgo`对源代码和IR(中间码)进行预处理之后, 再调用`go`进行后续的编译工作。通过这种方式, `xgo`实现了一些在`go`中缺乏的能力。

更多细节, 参见[快速开始](#快速开始)[文档](./doc)

Expand Down Expand Up @@ -406,6 +406,29 @@ func TestTrace(t *testing.T) {
```
结果中只会包含`B()``C()`.

## 增量覆盖率
子命令`xgo tool coverage`扩展了go内置的`go tool cover`, 提供了更好的覆盖率可视化体验。

首先,运行`go test``xgo test`来生成覆盖率文件:
```sh
go test -cover -coverpkg ./... -coverprofile cover.out ./...
```

然后,使用`xgo`来展示覆盖率:
```sh
xgo tool coverage serve cover.out
```

展示效果:

![coverage](doc/img/coverage.jpg "Coverage")

展示结果是覆盖率和git diff的组合。默认情况下,只有变更的行会被展示:
- 已经覆盖的行展示为浅蓝色,
- 未覆盖的行展示为浅黄色

这个工具可以帮助我们快速定位未覆盖的变更代码,从而增量地为它们添加测试用例。

# 并发安全
我知道大部分人认为Monkey Patching不是并发安全的,但那是现有的库的实现方式决定的。

Expand Down
13 changes: 11 additions & 2 deletions cmd/xgo/coverage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ func Main(args []string) {
os.Exit(1)
}
cmd := args[0]
args = args[1:]
if cmd == "help" {
fmt.Print(strings.TrimPrefix(help, "\n"))
return
}
if cmd != "merge" && cmd != "compact" {
if cmd != "merge" && cmd != "compact" && cmd != "serve" {
fmt.Fprintf(os.Stderr, "unrecognized cmd: %s\n", cmd)
return
}
args = args[1:]
if cmd == "serve" {
err := handleServe(args)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
return
}

var remainArgs []string
var outFile string
Expand Down
242 changes: 242 additions & 0 deletions cmd/xgo/coverage/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package coverage

import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"

"github.com/xhd2015/xgo/support/cmd"
"github.com/xhd2015/xgo/support/httputil"
"github.com/xhd2015/xgo/support/osinfo"
)

func handleServe(args []string) error {
// download binary from coverage-visualizer/xgo-tool-coverage-serve
toolDir, err := getToolDir()
if err != nil {
return err
}
serveTool := filepath.Join(toolDir, "xgo-tool-coverage-serve") + osinfo.EXE_SUFFIX

ok, err := checkAndFixFile(serveTool)
if err != nil {
return err
}

err = os.MkdirAll(filepath.Dir(serveTool), 0755)
if err != nil {
return err
}

serveToolLastCheck := serveTool + ".last-check"

s, err := readFileOrEmpty(serveToolLastCheck)
if err != nil {
return err
}
var lastCheckTime time.Time
if s != "" {
lastCheckTime, _ = time.Parse(time.DateTime, s)
}

// if tool does not exist, download it

if !ok {
// download
fmt.Fprintf(os.Stderr, "downloading coverage-visualizer/xgo-tool-coverage-serve\n")
err := downloadTool(serveTool, serveToolLastCheck)
if err != nil {
return err
}
} else if lastCheckTime.IsZero() || time.Since(lastCheckTime) > 7*24*time.Hour {
go func() {
defer func() {
if e := recover(); e != nil {
fmt.Fprintf(os.Stderr, "checking update: panic %v\n", e)
}
}()
err := downloadTool(serveTool, serveToolLastCheck)
if err != nil {
fmt.Fprintf(os.Stderr, "checking update: %v\n", err)
}
}()
}

serveArgs := []string{"serve"}
var hasBuildArg bool
for _, arg := range args {
if arg == "--build-arg" {
hasBuildArg = true
break
}
}
if !hasBuildArg {
serveArgs = append(serveArgs, "--build-arg", "./")
}
serveArgs = append(serveArgs, args...)
stripOut := &stripWriter{w: os.Stdout}
defer stripOut.Close()
stripErr := &stripWriter{w: os.Stderr}
defer stripErr.Close()
return cmd.New().Stdout(stripOut).Stderr(stripErr).
Run(serveTool, serveArgs...)
}

func downloadTool(serveTool string, recordFile string) error {
goos, err := getGOOS()
if err != nil {
return err
}
goarch, err := getGOARCH()
if err != nil {
return err
}
downloadURL := fmt.Sprintf("https://github.com/xhd2015/coverage-visualizer/releases/download/xgo-tool-coverage-serve-v0.0.1/xgo-tool-coverage-serve-%s-%s", goos, goarch)

ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

err = httputil.DownloadFile(ctx, downloadURL, serveTool)
if err != nil {
return err
}
recordTime := time.Now().Format(time.DateTime)
os.WriteFile(recordFile, []byte(recordTime), 0755)

return chmodExec(serveTool)
}

// TODO: make executable on windows
func chmodExec(file string) error {
return os.Chmod(file, 0755)
}

func getGOOS() (string, error) {
goos := runtime.GOOS
if goos != "" {
return goos, nil
}
goos, err := cmd.Output("go", "env", "GOOS")
if err != nil {
return "", err
}
return strings.TrimSpace(goos), nil
}
func getGOARCH() (string, error) {
goarch := runtime.GOARCH
if goarch != "" {
return goarch, nil
}
goarch, err := cmd.Output("go", "env", "GOARCH")
if err != nil {
return "", err
}
return strings.TrimSpace(goarch), nil
}

func readFileOrEmpty(file string) (string, error) {
data, err := os.ReadFile(file)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
return "", err
}
return string(data), nil
}

func checkAndFixFile(file string) (bool, error) {
statInfo, err := os.Stat(file)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, err
}
if statInfo.IsDir() {
err = os.RemoveAll(file)
if err != nil {
return false, err
}
return false, nil
}
return true, nil
}

func getToolDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".xgo", "tool"), nil
}

type stripWriter struct {
buf []byte
w io.Writer
}

func (c *stripWriter) Write(p []byte) (int, error) {
n := len(p)
for i := 0; i < n; i++ {
if p[i] != '\n' {
c.buf = append(c.buf, p[i])
continue
}
err := c.sendLine()
if err != nil {
return 0, err
}
}
return len(p), nil
}

func (c *stripWriter) sendLine() error {
str := string(c.buf)
c.buf = nil
for _, stripPair := range stripPairs {
from := string(stripPair[0])
to := string(stripPair[1])
str = strings.ReplaceAll(str, from, to)
}

_, err := c.w.Write([]byte(str))
if err != nil {
return err
}
_, err = c.w.Write([]byte{'\n'})
return err
}

var stripPairs = [][2][]byte{
{
[]byte{103, 105, 116, 46, 103, 97, 114, 101, 110, 97, 46, 99, 111, 109},
[]byte{115, 111, 109, 101, 45, 103, 105, 116, 46, 99, 111, 109},
},
{
[]byte{115, 104, 111, 112, 101, 101},
[]byte{115, 111, 109, 101, 45, 99, 111, 114, 112},
},
{
[]byte{108, 111, 97, 110, 45, 115, 101, 114, 118, 105, 99, 101},
[]byte{115, 111, 109, 101, 45, 115, 101, 114, 118, 105, 99, 101},
},
{
[]byte{99, 114, 101, 100, 105, 116, 95, 98, 97, 99, 107, 101, 110, 100},
[]byte{115, 111, 109, 101, 45, 108, 118, 49},
},
}

func (c *stripWriter) Close() error {
if len(c.buf) > 0 {
c.sendLine()
}
return nil
}
7 changes: 7 additions & 0 deletions cmd/xgo/coverage/serve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package coverage

import "testing"

func TestStrip(t *testing.T) {

}
Loading

0 comments on commit 7527db4

Please sign in to comment.