Skip to content

Commit

Permalink
Rough draft for combining the coverage files
Browse files Browse the repository at this point in the history
  • Loading branch information
yihuaf committed Feb 16, 2024
1 parent bc98120 commit 55e31ce
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 7 deletions.
9 changes: 9 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,15 @@ func goTestCmdArgs(opts *options, rerunOpts rerunOpts) []string {
result = append(result, rerunOpts.runFlag)
}

if rerunOpts.coverprofileFlag != "" {
// Replace the existing coverprofile arg with our new one in the re-run case.
coverprofileIndex, coverprofileIndexEnd := argIndex("coverprofile", args)
if coverprofileIndex >= 0 && coverprofileIndexEnd < len(args) {
args = append(args[:coverprofileIndex], args[coverprofileIndexEnd+1:]...)
}
result = append(result, rerunOpts.coverprofileFlag)
}

pkgArgIndex := findPkgArgPosition(args)
result = append(result, args[:pkgArgIndex]...)
result = append(result, cmdArgPackageList(opts, rerunOpts)...)
Expand Down
58 changes: 51 additions & 7 deletions cmd/rerunfails.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (
"sort"
"strings"

"gotest.tools/gotestsum/internal/coverprofile"
"gotest.tools/gotestsum/testjson"
)

type rerunOpts struct {
runFlag string
pkg string
runFlag string
pkg string
coverprofileFlag string
}

func (o rerunOpts) Args() []string {
Expand All @@ -24,13 +26,22 @@ func (o rerunOpts) Args() []string {
if o.pkg != "" {
result = append(result, o.pkg)
}
if o.coverprofileFlag != "" {
result = append(result, o.coverprofileFlag)
}
return result
}

func (o rerunOpts) withCoverprofile(coverprofile string) rerunOpts {
o.coverprofileFlag = "-coverprofile=" + coverprofile
return o
}

func newRerunOptsFromTestCase(tc testjson.TestCase) rerunOpts {
return rerunOpts{
runFlag: goTestRunFlagForTestCase(tc.Test),
pkg: tc.Package,
runFlag: goTestRunFlagForTestCase(tc.Test),
pkg: tc.Package,
coverprofileFlag: "",
}
}

Expand All @@ -51,23 +62,37 @@ func rerunFailsFilter(o *options) testCaseFilter {
return testjson.FilterFailedUnique
}

// Need to add the cov file context and etc...
func rerunFailed(ctx context.Context, opts *options, scanConfig testjson.ScanConfig) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
tcFilter := rerunFailsFilter(opts)

// We need to take special care for the coverprofile file in the rerun
// failed case. If we pass the same `-coverprofile` flag to the `go test`
// command, it will overwrite the file. We need to combine the coverprofile
// files from the original run and the rerun.

isCoverprofile, mainProfile := coverprofile.ParseCoverProfile(opts.args)

rec := newFailureRecorderFromExecution(scanConfig.Execution)
for attempts := 0; rec.count() > 0 && attempts < opts.rerunFailsMaxAttempts; attempts++ {
testjson.PrintSummary(opts.stdout, scanConfig.Execution, testjson.SummarizeNone)
opts.stdout.Write([]byte("\n")) // nolint: errcheck

nextRec := newFailureRecorder(scanConfig.Handler)
for _, tc := range tcFilter(rec.failures) {
goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, newRerunOptsFromTestCase(tc)))
for i, tc := range tcFilter(rec.failures) {
rerunOpts := newRerunOptsFromTestCase(tc)
var newCoverprofile string
if isCoverprofile {
// create a new unique coverprofile filenames for each rerun
newCoverprofile = fmt.Sprintf("%s.%d.%d", mainProfile, attempts, i)
rerunOpts = rerunOpts.withCoverprofile(newCoverprofile)
}
goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, rerunOpts))
if err != nil {
return err
}

cfg := testjson.ScanConfig{
RunID: attempts + 1,
Stdout: goTestProc.stdout,
Expand All @@ -83,6 +108,25 @@ func rerunFailed(ctx context.Context, opts *options, scanConfig testjson.ScanCon
if exitErr != nil {
nextRec.lastErr = exitErr
}

// Need to wait for the go test command to finish before combining
// the coverprofile files. But need to before checking for errors.
// Even if there is errors, we still need to combine the
// coverprofile files.
if isCoverprofile {
if err := coverprofile.Combine(mainProfile, newCoverprofile); err != nil {
return fmt.Errorf("failed to combine coverprofiles %s and %s: %v", mainProfile, newCoverprofile, err)
}

if err := os.Remove(newCoverprofile); err != nil {
return fmt.Errorf(
"failed to remove coverprofile %s after combined with the main profile: %v",
newCoverprofile,
err,
)
}
}

if err := hasErrors(exitErr, scanConfig.Execution); err != nil {
return err
}
Expand Down
95 changes: 95 additions & 0 deletions internal/coverprofile/covermerge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package coverprofile

// gocovmerge takes the results from multiple `go test -coverprofile` runs and
// merges them into one profile

// Taken from: https://github.com/wadey/gocovmerge @ b5bfa59ec0adc420475f97f89b58045c721d761c

import (
"fmt"
"io"
"log"
"sort"

"golang.org/x/tools/cover"
)

func mergeProfiles(p *cover.Profile, merge *cover.Profile) {
if p.Mode != merge.Mode {
log.Fatalf("cannot merge profiles with different modes")
}
// Since the blocks are sorted, we can keep track of where the last block
// was inserted and only look at the blocks after that as targets for merge
startIndex := 0
for _, b := range merge.Blocks {
startIndex = mergeProfileBlock(p, b, startIndex)
}
}

func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) int {
sortFunc := func(i int) bool {
pi := p.Blocks[i+startIndex]
return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol)
}

i := 0
if !sortFunc(i) {
i = sort.Search(len(p.Blocks)-startIndex, sortFunc)
}
i += startIndex
if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol {
if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol {
log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb)
}
switch p.Mode {
case "set":
p.Blocks[i].Count |= pb.Count
case "count", "atomic":
p.Blocks[i].Count += pb.Count
default:
log.Fatalf("unsupported covermode: '%s'", p.Mode)
}
} else {
if i > 0 {
pa := p.Blocks[i-1]
if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) {
log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb)
}
}
if i < len(p.Blocks)-1 {
pa := p.Blocks[i+1]
if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) {
log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb)
}
}
p.Blocks = append(p.Blocks, cover.ProfileBlock{})
copy(p.Blocks[i+1:], p.Blocks[i:])
p.Blocks[i] = pb
}
return i + 1
}

func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile {
i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName })
if i < len(profiles) && profiles[i].FileName == p.FileName {
mergeProfiles(profiles[i], p)
} else {
profiles = append(profiles, nil)
copy(profiles[i+1:], profiles[i:])
profiles[i] = p
}
return profiles
}

func dumpProfiles(profiles []*cover.Profile, out io.Writer) {
if len(profiles) == 0 {
return
}
fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode)
for _, p := range profiles {
for _, b := range p.Blocks {
fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine,
b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count)
}
}
}
64 changes: 64 additions & 0 deletions internal/coverprofile/coverprofile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package coverprofile

import (
"fmt"
"os"
"strings"

"golang.org/x/tools/cover"
)

// ParseCoverProfile parse the coverprofile file from the flag
func ParseCoverProfile(args []string) (bool, string) {
for _, arg := range args {
if strings.HasPrefix(arg, "-coverprofile=") {
return true, strings.TrimPrefix(arg, "-coverprofile=")
}
}

return false, ""
}

func Combine(main string, other string) error {
var merged []*cover.Profile

mainProfiles, err := cover.ParseProfiles(main)
if err != nil {
return fmt.Errorf("failed to parse coverprofile %s: %v", main, err)
}

for _, p := range mainProfiles {
merged = addProfile(merged, p)
}

otherProfiles, err := cover.ParseProfiles(other)
if err != nil {
return fmt.Errorf("failed to parse coverprofile %s: %v", other, err)
}

for _, p := range otherProfiles {
merged = addProfile(merged, p)
}

// Create a tmp file to write the merged profiles to. Then use os.Rename to
// atomically move the file to the main profile to mimic the effect of
// atomic replacement of the file. Note, we can't put the file on tempfs
// using the all the nice utilities around tempfiles. In places like docker
// containers, calling os.Rename on a file that is on tempfs to a file on
// normal filesystem partition will fail with errno 18 invalid cross device
// link.
tempFile := fmt.Sprintf("%s.tmp", other)
f, err := os.Create(tempFile)
if err != nil {
return fmt.Errorf("failed to create temp file %s: %v", tempFile, err)
}
dumpProfiles(merged, f)
if err := f.Close(); err != nil {
return fmt.Errorf("failed to close temp file %s: %v", tempFile, err)
}
if err := os.Rename(tempFile, main); err != nil {
return fmt.Errorf("failed to rename temp file %s to %s: %v", tempFile, main, err)
}

return nil
}

0 comments on commit 55e31ce

Please sign in to comment.