diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6fdc7701..73b22a88 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -43,7 +43,7 @@ jobs: run: go run ./script/run-test --reset-instrument --debug -v -cover -coverpkg github.com/xhd2015/xgo/runtime/... -coverprofile cover.out - name: Merge Coverages - run: go run ./script/cover merge ./cover-runtime.out ./cover-runtime-sub.out -o cover-runtime-merged.out + run: go run ./cmd/coverage merge ./cover-runtime.out ./cover-runtime-sub.out -o cover-runtime-merged.out --exclude-prefix github.com/xhd2015/xgo/runtime/test - name: Print coverage run: cd runtime && go tool cover --func ../cover-runtime-merged.out diff --git a/cmd/coverage/main.go b/cmd/coverage/main.go new file mode 100644 index 00000000..56823055 --- /dev/null +++ b/cmd/coverage/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/xhd2015/xgo/cmd/xgo/coverage" +) + +func main() { + // os.Arg[0] = coverage + // os.Arg[1] = args + args := os.Args[1:] + coverage.Main(args) +} diff --git a/cmd/xgo/coverage/help.go b/cmd/xgo/coverage/help.go new file mode 100644 index 00000000..6826f76c --- /dev/null +++ b/cmd/xgo/coverage/help.go @@ -0,0 +1,22 @@ +package coverage + +const help = ` +Xgo tool coverage is a tool to view and merge go coverage profiles, just like an extension to go tool cover. + +Usage: + xgo tool coverage [arguments] + +The commands are: + merge merge coverage profiles + help show help message + +Options for merge: + -o output to file instead of stdout + --exclude-prefix exclude coverage of a specific package and sub packages + +Examples: + xgo tool coverage merge -o cover.a cover-a.out cover-b.out merge multiple files into one + +See https://github.com/xhd2015/xgo for documentation. + +` diff --git a/cmd/xgo/coverage/main.go b/cmd/xgo/coverage/main.go new file mode 100644 index 00000000..acff2a55 --- /dev/null +++ b/cmd/xgo/coverage/main.go @@ -0,0 +1,135 @@ +package coverage + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/xhd2015/xgo/support/coverage" +) + +func Main(args []string) { + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "requires cmd\n") + os.Exit(1) + } + cmd := args[0] + if cmd == "help" { + fmt.Print(strings.TrimPrefix(help, "\n")) + return + } + args = args[1:] + + var remainArgs []string + var outFile string + n := len(args) + + var flagHelp bool + var excludePrefix []string + for i := 0; i < n; i++ { + arg := args[i] + if arg == "--" { + remainArgs = append(remainArgs, args[i+1:]...) + break + } + if arg == "-o" { + if i+1 >= n { + fmt.Fprintf(os.Stderr, "%s requires file\n", arg) + os.Exit(1) + } + outFile = args[i+1] + i++ + continue + } + if arg == "--exclude-prefix" { + if i+1 >= n { + fmt.Fprintf(os.Stderr, "%s requires argument\n", arg) + os.Exit(1) + } + if args[i+1] == "" { + fmt.Fprintf(os.Stderr, "%s requires non empty argument\n", arg) + os.Exit(1) + } + excludePrefix = append(excludePrefix, args[i+1]) + i++ + continue + } + if arg == "--help" || arg == "-h" { + flagHelp = true + continue + } + if !strings.HasPrefix(arg, "-") { + remainArgs = append(remainArgs, arg) + continue + } + fmt.Fprintf(os.Stderr, "unrecognized flag: %s\n", arg) + os.Exit(1) + } + if flagHelp { + fmt.Print(strings.TrimPrefix(help, "\n")) + return + } + + switch cmd { + case "merge": + if len(remainArgs) == 0 { + fmt.Fprintf(os.Stderr, "requires files\n") + os.Exit(1) + } + err := mergeCover(remainArgs, outFile, excludePrefix) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + default: + fmt.Fprintf(os.Stderr, "unrecognized cmd: %s\n", cmd) + os.Exit(1) + } +} + +func mergeCover(files []string, outFile string, excludePrefix []string) error { + var mode string + covs := make([][]*coverage.CovLine, 0, len(files)) + for _, file := range files { + content, err := os.ReadFile(file) + if err != nil { + return err + } + covMode, cov := coverage.Parse(string(content)) + covs = append(covs, cov) + if mode == "" { + mode = covMode + } + } + res := coverage.Merge(covs...) + res = coverage.Filter(res, func(line *coverage.CovLine) bool { + return !hasAnyPrefix(line.Prefix, excludePrefix) + }) + + if mode == "" { + mode = "set" + } + mergedCov := coverage.Format(mode, res) + + var out io.Writer = os.Stdout + if outFile != "" { + file, err := os.Create(outFile) + if err != nil { + return err + } + defer file.Close() + out = file + } + _, err := io.WriteString(out, mergedCov) + return err +} + +func hasAnyPrefix(s string, prefixes []string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return true + } + } + return false +} diff --git a/cmd/xgo/runtime_gen/core/version.go b/cmd/xgo/runtime_gen/core/version.go index b9ae3d7d..17938bbf 100755 --- a/cmd/xgo/runtime_gen/core/version.go +++ b/cmd/xgo/runtime_gen/core/version.go @@ -6,9 +6,9 @@ import ( "os" ) -const VERSION = "1.0.30" -const REVISION = "83fdf348a92806bbdc0b6746fc7d55ae1671dfab+1" -const NUMBER = 209 +const VERSION = "1.0.31" +const REVISION = "12ae038b4e052f42cd97af0721488c1b5f77eb6e+1" +const NUMBER = 210 // these fields will be filled by compiler const XGO_VERSION = "" diff --git a/cmd/xgo/tool.go b/cmd/xgo/tool.go index 776701c7..06671a44 100644 --- a/cmd/xgo/tool.go +++ b/cmd/xgo/tool.go @@ -8,13 +8,19 @@ import ( "runtime" "strings" + "github.com/xhd2015/xgo/cmd/xgo/coverage" "github.com/xhd2015/xgo/cmd/xgo/trace" "github.com/xhd2015/xgo/support/cmd" ) func handleTool(tool string, args []string) error { if tool == "trace" { - return trace.Main(args) + trace.Main(args) + return nil + } + if tool == "coverage" { + coverage.Main(args) + return nil } tools := []string{ tool, diff --git a/cmd/xgo/trace/main.go b/cmd/xgo/trace/main.go index 1dfbaf48..f50e79b2 100644 --- a/cmd/xgo/trace/main.go +++ b/cmd/xgo/trace/main.go @@ -20,13 +20,13 @@ import ( "github.com/xhd2015/xgo/support/cmd" ) -func Main(args []string) error { +func Main(args []string) { if len(args) == 0 { - return errors.New("requires file") + fmt.Fprintf(os.Stderr, "requires file\n") + os.Exit(1) } file := args[0] serveFile(file) - return nil } func serveFile(file string) { diff --git a/cmd/xgo/version.go b/cmd/xgo/version.go index 542c738a..c967cbc4 100644 --- a/cmd/xgo/version.go +++ b/cmd/xgo/version.go @@ -2,9 +2,9 @@ package main import "fmt" -const VERSION = "1.0.30" -const REVISION = "83fdf348a92806bbdc0b6746fc7d55ae1671dfab+1" -const NUMBER = 209 +const VERSION = "1.0.31" +const REVISION = "12ae038b4e052f42cd97af0721488c1b5f77eb6e+1" +const NUMBER = 210 func getRevision() string { revSuffix := "" diff --git a/runtime/core/version.go b/runtime/core/version.go index b9ae3d7d..17938bbf 100644 --- a/runtime/core/version.go +++ b/runtime/core/version.go @@ -6,9 +6,9 @@ import ( "os" ) -const VERSION = "1.0.30" -const REVISION = "83fdf348a92806bbdc0b6746fc7d55ae1671dfab+1" -const NUMBER = 209 +const VERSION = "1.0.31" +const REVISION = "12ae038b4e052f42cd97af0721488c1b5f77eb6e+1" +const NUMBER = 210 // these fields will be filled by compiler const XGO_VERSION = "" diff --git a/script/cover/cover.go b/script/cover/cover.go index a9e7a1f6..7bf7fe18 100644 --- a/script/cover/cover.go +++ b/script/cover/cover.go @@ -4,8 +4,9 @@ import ( "fmt" "io" "os" - "strconv" "strings" + + "github.com/xhd2015/xgo/support/coverage" ) func main() { @@ -54,23 +55,23 @@ func main() { } func mergeCover(files []string, outFile string) error { - covs := make([][]*covLine, 0, len(files)) + covs := make([][]*coverage.CovLine, 0, len(files)) for _, file := range files { content, err := os.ReadFile(file) if err != nil { return err } - covs = append(covs, parseCover(string(content))) + covs = append(covs, coverage.Parse(string(content))) } - res := merge(covs) - res = filter(res, func(line *covLine) bool { - if strings.HasPrefix(line.prefix, "github.com/xhd2015/xgo/runtime/test") { + res := coverage.Merge(covs...) + res = coverage.Filter(res, func(line *coverage.CovLine) bool { + if strings.HasPrefix(line.Prefix, "github.com/xhd2015/xgo/runtime/test") { return false } return true }) - mergedCov := formatCoverage("set", res) + mergedCov := coverage.Format("set", res) var out io.Writer = os.Stdout if outFile != "" { @@ -84,93 +85,3 @@ func mergeCover(files []string, outFile string) error { _, err := io.WriteString(out, mergedCov) return err } - -func filter(covs []*covLine, check func(line *covLine) bool) []*covLine { - n := len(covs) - j := 0 - for i := 0; i < n; i++ { - if check(covs[i]) { - covs[j] = covs[i] - j++ - } - } - return covs[:j] -} - -func formatCoverage(mode string, lines []*covLine) string { - strs := make([]string, 0, len(lines)+1) - strs = append(strs, "mode: "+mode) - for _, line := range lines { - strs = append(strs, line.prefix+" "+strconv.FormatInt(line.count, 10)) - } - return strings.Join(strs, "\n") -} - -func merge(covs [][]*covLine) []*covLine { - if len(covs) == 0 { - return nil - } - if len(covs) == 1 { - return covs[0] - } - result := covs[0] - for i := 1; i < len(covs); i++ { - result = mergeCov(result, covs[i]) - } - return result -} - -func mergeCov(a []*covLine, b []*covLine) []*covLine { - for _, line := range b { - idx := -1 - for i := 0; i < len(a); i++ { - if a[i].prefix == line.prefix { - idx = i - break - } - } - if idx < 0 { - a = append(a, line) - } else { - // fmt.Printf("add %s %d %d\n", a[idx].prefix, a[idx].count, line.count) - a[idx].count += line.count - } - } - return a -} - -type covLine struct { - prefix string - count int64 -} - -func parseCover(content string) []*covLine { - lines := strings.Split(content, "\n") - if len(lines) > 0 && strings.HasPrefix(lines[0], "mode:") { - lines = lines[1:] - } - covLines := make([]*covLine, 0, len(lines)) - for _, line := range lines { - covLine := parseCovLine(line) - if covLine == nil { - continue - } - covLines = append(covLines, covLine) - } - return covLines -} - -func parseCovLine(line string) *covLine { - idx := strings.LastIndex(line, " ") - if idx < 0 { - return nil - } - cnt, err := strconv.ParseInt(line[idx+1:], 10, 64) - if err != nil { - cnt = 0 - } - return &covLine{ - prefix: line[:idx], - count: cnt, - } -} diff --git a/support/coverage/format.go b/support/coverage/format.go new file mode 100644 index 00000000..f5ccc079 --- /dev/null +++ b/support/coverage/format.go @@ -0,0 +1,16 @@ +package coverage + +import ( + "strconv" + "strings" +) + +// mode can be: set +func Format(mode string, lines []*CovLine) string { + strs := make([]string, 0, len(lines)+1) + strs = append(strs, "mode: "+mode) + for _, line := range lines { + strs = append(strs, line.Prefix+" "+strconv.FormatInt(line.Count, 10)) + } + return strings.Join(strs, "\n") +} diff --git a/support/coverage/merge.go b/support/coverage/merge.go new file mode 100644 index 00000000..96d3ae6a --- /dev/null +++ b/support/coverage/merge.go @@ -0,0 +1,46 @@ +package coverage + +func Merge(covs ...[]*CovLine) []*CovLine { + if len(covs) == 0 { + return nil + } + if len(covs) == 1 { + return covs[0] + } + result := covs[0] + for i := 1; i < len(covs); i++ { + result = doMerge(result, covs[i]) + } + return result +} + +func Filter(covs []*CovLine, check func(line *CovLine) bool) []*CovLine { + n := len(covs) + j := 0 + for i := 0; i < n; i++ { + if check(covs[i]) { + covs[j] = covs[i] + j++ + } + } + return covs[:j] +} + +func doMerge(linesA []*CovLine, linesB []*CovLine) []*CovLine { + for _, line := range linesB { + idx := -1 + for i := 0; i < len(linesA); i++ { + if linesA[i].Prefix == line.Prefix { + idx = i + break + } + } + if idx < 0 { + linesA = append(linesA, line) + } else { + // fmt.Printf("add %s %d %d\n", a[idx].prefix, a[idx].count, line.count) + linesA[idx].Count += line.Count + } + } + return linesA +} diff --git a/support/coverage/parse.go b/support/coverage/parse.go new file mode 100644 index 00000000..6a2ac874 --- /dev/null +++ b/support/coverage/parse.go @@ -0,0 +1,45 @@ +package coverage + +import ( + "strconv" + "strings" +) + +type CovLine struct { + Prefix string + Count int64 +} + +const modePrefix = "mode:" + +func Parse(content string) (mode string, covLines []*CovLine) { + lines := strings.Split(content, "\n") + if len(lines) > 0 && strings.HasPrefix(lines[0], "mode:") { + mode = strings.TrimSpace(lines[0][len(modePrefix):]) + lines = lines[1:] + } + covLines = make([]*CovLine, 0, len(lines)) + for _, line := range lines { + covLine := ParseCovLine(line) + if covLine == nil { + continue + } + covLines = append(covLines, covLine) + } + return mode, covLines +} + +func ParseCovLine(line string) *CovLine { + idx := strings.LastIndex(line, " ") + if idx < 0 { + return nil + } + cnt, err := strconv.ParseInt(line[idx+1:], 10, 64) + if err != nil { + cnt = 0 + } + return &CovLine{ + Prefix: line[:idx], + Count: cnt, + } +} diff --git a/support/coverage/parse_test.go b/support/coverage/parse_test.go new file mode 100644 index 00000000..a866ac8f --- /dev/null +++ b/support/coverage/parse_test.go @@ -0,0 +1,15 @@ +package coverage + +import "testing" + +func TestParseCoverage(t *testing.T) { + mode, lines := Parse(`mode: set +github.com/xhd2015/xgo/runtime/core/func.go:44.41,45.22 1 0`) + if mode != "set" { + t.Fatalf("expect mode to be %s, actual: %s", "set", mode) + } + if len(lines) != 1 { + t.Fatalf("len(lines): %d", len(lines)) + } + // t.Logf("lines[0]: %v", lines[0]) +}