Skip to content

Commit

Permalink
testscript: merge coverage from all test binary executions
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdan committed Jan 28, 2021
1 parent d773b4c commit eebd5f2
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 86 deletions.
5 changes: 4 additions & 1 deletion cmd/testscript/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ func dropgofrompath(ts *testscript.TestScript, neg bool, args []string) {
var newPath []string
for _, d := range filepath.SplitList(ts.Getenv("PATH")) {
getenv := func(k string) string {
if k == "PATH" {
// Note that Windows and Plan9 use lowercase "path".
if strings.ToUpper(k) == "PATH" {
return d
}
return ts.Getenv(k)
Expand All @@ -79,7 +80,9 @@ func dropgofrompath(ts *testscript.TestScript, neg bool, args []string) {
newPath = append(newPath, d)
}
}
ts.Logf("PATH=%s", ts.Getenv("PATH"))
ts.Setenv("PATH", strings.Join(newPath, string(filepath.ListSeparator)))
ts.Logf("PATH=%s", ts.Getenv("PATH"))
}

func setfilegoproxy(ts *testscript.TestScript, neg bool, args []string) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/testscript/testdata/nogo.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# should support skip
unquote file.txt

env PATH=
dropgofrompath
! testscript -v file.txt
stdout 'unknown command "go"'
stderr 'error running file.txt in'
Expand Down
14 changes: 14 additions & 0 deletions gotooltest/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package gotooltest_test

import (
"os"
"path/filepath"
"testing"

"github.com/rogpeppe/go-internal/gotooltest"
Expand All @@ -14,7 +16,19 @@ import (
func TestSimple(t *testing.T) {
p := testscript.Params{
Dir: "testdata",
Setup: func(env *testscript.Env) error {
// cover.txt will need testscript as a dependency.
// Tell it where our module is, via an absolute path.
wd, err := os.Getwd()
if err != nil {
return err
}
modPath := filepath.Dir(wd)
env.Setenv("GOINTERNAL_MODULE", modPath)
return nil
},
}

if err := gotooltest.Setup(&p); err != nil {
t.Fatal(err)
}
Expand Down
82 changes: 82 additions & 0 deletions gotooltest/testdata/cover.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
unquote scripts/exec.txt

# The module uses testscript itself.
# Use the checkec out module, based on where the test binary ran.
go mod edit -replace=github.com/rogpeppe/go-internal=${GOINTERNAL_MODULE}
go mod tidy

# First, a 'go test' run without coverage.
go test -vet=off
stdout 'PASS'
! stdout 'total coverage'

# Then, a 'go test' run with -coverprofile.
# Assuming testscript works well, this results in the basic coverage being 0%,
# since the test binary does not directly run any non-test code.
# The total coverage after merging profiles should end up being 100%,
# as long as all three sub-profiles are accounted for.
# Marking all printlns as covered requires all edge cases to work well.
go test -vet=off -coverprofile=cover.out -v
stdout 'PASS'
stdout '^coverage: 0\.0%'
stdout '^total coverage: 100\.0%'
! stdout 'malformed coverage' # written by "go test" if cover.out is invalid
exists cover.out

-- go.mod --
module test

go 1.15
-- foo.go --
package foo

import "os"

func foo1() int {
switch os.Args[1] {
case "1":
println("first path")
case "2":
println("second path")
default:
println("third path")
}
return 1
}
-- foo_test.go --
package foo

import (
"os"
"testing"

"github.com/rogpeppe/go-internal/gotooltest"
"github.com/rogpeppe/go-internal/testscript"
)

func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string] func() int{
"foo": foo1,
}))
}

func TestFoo(t *testing.T) {
p := testscript.Params{
Dir: "scripts",
}
if err := gotooltest.Setup(&p); err != nil {
t.Fatal(err)
}
testscript.Run(t, p)
}
-- scripts/exec.txt --
># Note that foo always fails, to prevent "go build" from doing anything.
>
># Running the command directly; trigger the first path.
>! foo 1
>
># Running the command via exec; trigger the second path.
>! exec foo 2
>
># Running the command indirectly, via toolexec; trigger the third path.
>! go build -a -toolexec=foo runtime
58 changes: 29 additions & 29 deletions testscript/cover.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -83,44 +83,44 @@ func mergeCoverProfile(cover *testing.Cover, r io.Reader) error {
return nil
}

var (
coverChan chan *os.File
coverDone chan testing.Cover
)

func goCoverProfileMerge() {
if coverChan != nil {
panic("RunMain called twice!")
}
coverChan = make(chan *os.File)
coverDone = make(chan testing.Cover)
go mergeCoverProfiles()
}

func mergeCoverProfiles() {
func finalizeCoverProfile(dir string) error {
// Merge all the coverage profiles written by test binary sub-processes,
// such as those generated by executions of commands.
var cover testing.Cover
for f := range coverChan {
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if err := mergeCoverProfile(&cover, f); err != nil {
log.Printf("cannot merge coverage profile from %v: %v", f.Name(), err)
return fmt.Errorf("cannot merge coverage profile from %v: %v", f.Name(), err)
}
f.Close()
os.Remove(f.Name())
return nil
}); err != nil {
return errors.Wrap(err)
}
if err := os.RemoveAll(dir); err != nil {
return errors.Wrap(err)
}
coverDone <- cover
}

func finalizeCoverProfile() error {
// We need to include our own top-level coverage profile too.
cprof := coverProfile()
if cprof == "" {
return nil
}
f, err := os.Open(cprof)
if err != nil {
return errors.Notef(err, nil, "cannot open existing cover profile")
}
coverChan <- f
close(coverChan)
cover := <-coverDone
if err := mergeCoverProfile(&cover, f); err != nil {
return fmt.Errorf("cannot merge coverage profile from %v: %v", f.Name(), err)
}

// Finally, write the resulting merged profile.
f, err = os.Create(cprof)
if err != nil {
return errors.Notef(err, nil, "cannot create cover profile")
Expand Down
Loading

0 comments on commit eebd5f2

Please sign in to comment.