Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
69 changes: 62 additions & 7 deletions cmd/rerunfails.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"sort"
"strings"

"golang.org/x/tools/cover"
"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 +27,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 @@ -56,18 +68,31 @@ func rerunFailed(ctx context.Context, opts *options, scanConfig testjson.ScanCon
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, mainProfilePath := coverprofile.ParseCoverProfile(opts.args)
rerunProfiles := []*cover.Profile{}

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)
rerunProfilePath := ""
if isCoverprofile {
// create a new unique coverprofile filenames for each rerun
rerunProfilePath = fmt.Sprintf("%s.%d.%d", mainProfilePath, attempts, i)
rerunOpts = rerunOpts.withCoverprofile(rerunProfilePath)
}
goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, rerunOpts))
if err != nil {
return err
}

cfg := testjson.ScanConfig{
RunID: attempts + 1,
Stdout: goTestProc.stdout,
Expand All @@ -83,12 +108,42 @@ 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 before checking for errors. Even if
// there is errors, we still need to combine the coverprofile files.
if isCoverprofile {
rerunProfile, err := cover.ParseProfiles(rerunProfilePath)
if err != nil {
return fmt.Errorf("failed to parse coverprofile %s: %v", rerunProfilePath, err)
}

rerunProfiles = append(rerunProfiles, rerunProfile...)

// Once we read the rerun profiles from files to memory, we can
// safely delete the rerun profile. This will allow us to avoid
// extra clean up in the case that we error out in future
// attempts.
if err := os.Remove(rerunProfilePath); err != nil {
return fmt.Errorf("failed to remove coverprofile %s after combined with the main profile: %v",
rerunProfilePath, err)
}
}

if err := hasErrors(exitErr, scanConfig.Execution); err != nil {
return err
}
}
rec = nextRec
}

// Write the combined coverprofile files with the main coverprofile file
if isCoverprofile {
if err := coverprofile.Combine(mainProfilePath, rerunProfiles); err != nil {
return fmt.Errorf("failed to combine coverprofiles: %v", err)
}
}

return rec.lastErr
}

Expand Down
118 changes: 118 additions & 0 deletions internal/coverprofile/covermerge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) 2015, Wade Simmons
// All rights reserved.

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:

// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.

// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may need to copy the license file
(https://github.com/wadey/gocovmerge/blob/master/LICENSE) into internal/coverprofile to comply with the license.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the LICENSE to the file header since it is only the file we copied over. Please let me know if this is good enough.


package coverprofile

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)
}
}
}
78 changes: 78 additions & 0 deletions internal/coverprofile/coverprofile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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, ""
}

// WriteCoverProfile writes the cover profile to the file
func WriteCoverProfile(profiles []*cover.Profile, filename string) error {
// 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", filename)
f, err := os.Create(tempFile)
if err != nil {
return fmt.Errorf("failed to create coverprofile file: %v", err)
}

dumpProfiles(profiles, f)
if err := f.Close(); err != nil {
return fmt.Errorf("failed to close temp file %s: %v", tempFile, err)
}

if err := os.Rename(tempFile, filename); err != nil {
return fmt.Errorf("failed to rename temp file %s to %s: %v", tempFile, filename, err)
}

return nil
}

// CombineProfiles combines the cover profiles together
func CombineProfiles(this []*cover.Profile, others ...*cover.Profile) []*cover.Profile {
merged := this
for _, p := range others {
merged = addProfile(merged, p)
}
return merged
}

// A helper function to expose the destination of the merged profile so we
// testing is easier.
func combine(main string, others []*cover.Profile, out string) error {
mainProfiles, err := cover.ParseProfiles(main)
if err != nil {
return fmt.Errorf("failed to parse coverprofile %s: %v", main, err)
}

merged := mainProfiles

for _, other := range others {
merged = CombineProfiles(mainProfiles, other)
}

return WriteCoverProfile(merged, out)
}

// Combine merges the `others` cover profile with the main cover profile file
func Combine(main string, others []*cover.Profile) error {
return combine(main, others, main)
}
Loading