cmd/go: add coverage analysis

This feature is not yet ready for real use. The CL marks a bite-sized
piece that is ready for review. TODOs that remain:
        provide control over output
        produce output without setting -v
        make work on reflect, sync and time packages
                (fail now due to link errors caused by inlining)
        better documentation
Almost all packages work now, though, if clumsily; try:
        go test -v -cover=count encoding/binary

R=rsc
CC=gobot, golang-dev, remyoudompheng
https://golang.org/cl/10050045
This commit is contained in:
Rob Pike 2013-06-11 09:35:10 -07:00
parent e4b5cbde46
commit caefc5d0ca
6 changed files with 184 additions and 20 deletions

View file

@ -786,7 +786,26 @@ func (b *builder) build(a *action) (err error) {
}
var gofiles, cfiles, sfiles, objects, cgoObjects []string
gofiles = append(gofiles, a.p.GoFiles...)
// If we're doing coverage, preprocess the .go files and put them in the work directory
if a.p.coverMode != "" {
for _, file := range a.p.GoFiles {
sourceFile := filepath.Join(a.p.Dir, file)
cover := a.p.coverVars[file]
if cover == nil {
// Not covering this file
gofiles = append(gofiles, file)
continue
}
coverFile := filepath.Join(obj, file)
if err := b.cover(a, coverFile, sourceFile, 0666, cover.Count, cover.Pos); err != nil {
return err
}
gofiles = append(gofiles, coverFile)
}
} else {
gofiles = append(gofiles, a.p.GoFiles...)
}
cfiles = append(cfiles, a.p.CFiles...)
sfiles = append(sfiles, a.p.SFiles...)
@ -1090,6 +1109,17 @@ func (b *builder) copyFile(a *action, dst, src string, perm os.FileMode) error {
return nil
}
// cover runs, in effect,
// go tool cover -mode=b.coverMode -count="count" -pos="pos" src.go >dst.go
func (b *builder) cover(a *action, dst, src string, perm os.FileMode, count, pos string) error {
out, err := b.runOut(a.objdir, "cover "+a.p.ImportPath, nil, tool("cover"), "-mode="+a.p.coverMode, "-count="+count, "-pos="+pos, src)
if err != nil {
return err
}
// Output is processed source code. Write it to destination.
return ioutil.WriteFile(dst, out, perm)
}
var objectMagic = [][]byte{
{'!', '<', 'a', 'r', 'c', 'h', '>', '\n'}, // Package archive
{'\x7F', 'E', 'L', 'F'}, // ELF

View file

@ -738,6 +738,18 @@ control the execution of any test:
if -test.blockprofile is set without this flag, all blocking events
are recorded, equivalent to -test.blockprofilerate=1.
-cover set,count,atomic
TODO: This feature is not yet fully implemented.
TODO: Must run with -v to see output.
TODO: Need control over output format,
Set the mode for coverage analysis for the package[s] being tested.
The default is to do none.
The values:
set: boolean: does this statement execute?
count: integer: how many times does this statement execute?
atomic: integer: like count, but correct in multithreaded tests;
significantly more expensive.
-cpu 1,2,4
Specify a list of GOMAXPROCS values for which the tests or
benchmarks should be executed. The default is the current value

View file

@ -76,14 +76,23 @@ type Package struct {
deps []*Package
gofiles []string // GoFiles+CgoFiles+TestGoFiles+XTestGoFiles files, absolute paths
sfiles []string
allgofiles []string // gofiles + IgnoredGoFiles, absolute paths
target string // installed file for this package (may be executable)
fake bool // synthesized package
forceBuild bool // this package must be rebuilt
forceLibrary bool // this package is a library (even if named "main")
local bool // imported via local path (./ or ../)
localPrefix string // interpret ./ and ../ imports relative to this prefix
exeName string // desired name for temporary executable
allgofiles []string // gofiles + IgnoredGoFiles, absolute paths
target string // installed file for this package (may be executable)
fake bool // synthesized package
forceBuild bool // this package must be rebuilt
forceLibrary bool // this package is a library (even if named "main")
local bool // imported via local path (./ or ../)
localPrefix string // interpret ./ and ../ imports relative to this prefix
exeName string // desired name for temporary executable
coverMode string // preprocess Go source files with the coverage tool in this mode
coverVars map[string]*CoverVar // variables created by coverage analysis
}
// CoverVar holds the name of the generated coverage variables targeting the named file.
type CoverVar struct {
File string // local file name
Count string // name of count array
Pos string // name of position array
}
func (p *Package) copyBuild(pp *build.Package) {
@ -278,11 +287,12 @@ func reusePackage(p *Package, stk *importStack) *Package {
// isGoTool is the list of directories for Go programs that are installed in
// $GOROOT/pkg/tool.
var isGoTool = map[string]bool{
"cmd/api": true,
"cmd/cgo": true,
"cmd/fix": true,
"cmd/yacc": true,
"code.google.com/p/go.tools/cmd/vet": true,
"cmd/api": true,
"cmd/cgo": true,
"cmd/fix": true,
"cmd/yacc": true,
"code.google.com/p/go.tools/cmd/cover": true,
"code.google.com/p/go.tools/cmd/vet": true,
}
// expandScanner expands a scanner.List error into all the errors in the list.

View file

@ -124,6 +124,18 @@ control the execution of any test:
if -test.blockprofile is set without this flag, all blocking events
are recorded, equivalent to -test.blockprofilerate=1.
-cover set,count,atomic
TODO: This feature is not yet fully implemented.
TODO: Must run with -v to see output.
TODO: Need control over output format,
Set the mode for coverage analysis for the package[s] being tested.
The default is to do none.
The values:
set: boolean: does this statement execute?
count: integer: how many times does this statement execute?
atomic: integer: like count, but correct in multithreaded tests;
significantly more expensive.
-cpu 1,2,4
Specify a list of GOMAXPROCS values for which the tests or
benchmarks should be executed. The default is the current value
@ -235,6 +247,7 @@ See the documentation of the testing package for more information.
var (
testC bool // -c flag
testCover string // -cover flag
testProfile bool // some profiling flag
testI bool // -i flag
testV bool // -v flag
@ -492,12 +505,18 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
if err := b.mkdir(ptestDir); err != nil {
return nil, nil, nil, err
}
if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), p); err != nil {
if testCover != "" {
p.coverMode = testCover
p.coverVars = declareCoverVars(p.GoFiles...)
}
if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), p, p.coverVars); err != nil {
return nil, nil, nil, err
}
// Test package.
if len(p.TestGoFiles) > 0 {
if len(p.TestGoFiles) > 0 || testCover != "" {
ptest = new(Package)
*ptest = *p
ptest.GoFiles = nil
@ -629,6 +648,23 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
return pmainAction, runAction, printAction, nil
}
var coverIndex = 0
// declareCoverVars attaches the required cover variables names
// to the files, to be used when annotating the files.
func declareCoverVars(files ...string) map[string]*CoverVar {
coverVars := make(map[string]*CoverVar)
for _, file := range files {
coverVars[file] = &CoverVar{
File: file,
Count: fmt.Sprintf("GoCoverCount_%d", coverIndex),
Pos: fmt.Sprintf("GoCoverPos_%d", coverIndex),
}
coverIndex++
}
return coverVars
}
// runTest is the action for running a test binary.
func (b *builder) runTest(a *action) error {
args := stringList(a.deps[0].target, testArgs)
@ -767,9 +803,10 @@ func isTest(name, prefix string) bool {
// writeTestmain writes the _testmain.go file for package p to
// the file named out.
func writeTestmain(out string, p *Package) error {
func writeTestmain(out string, p *Package, coverVars map[string]*CoverVar) error {
t := &testFuncs{
Package: p,
Package: p,
CoverVars: coverVars,
}
for _, file := range p.TestGoFiles {
if err := t.load(filepath.Join(p.Dir, file), "_test", &t.NeedTest); err != nil {
@ -802,6 +839,11 @@ type testFuncs struct {
Package *Package
NeedTest bool
NeedXtest bool
CoverVars map[string]*CoverVar
}
func (t *testFuncs) CoverEnabled() bool {
return testCover != ""
}
type testFunc struct {
@ -861,12 +903,15 @@ import (
"regexp"
"testing"
{{if .NeedTest}}
{{if or .CoverEnabled .NeedTest}}
_test {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .NeedXtest}}
_xtest {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
{{if .CoverEnabled}}
_fmt "fmt"
{{end}}
)
var tests = []testing.InternalTest{
@ -901,8 +946,67 @@ func matchString(pat, str string) (result bool, err error) {
return matchRe.MatchString(str), nil
}
{{if .CoverEnabled}}
type coverBlock struct {
line0 uint32
col0 uint16
line1 uint32
col1 uint16
}
// Only updated by init functions, so no need for atomicity.
var (
coverCounters = make(map[string][]uint32)
coverBlocks = make(map[string][]coverBlock)
)
func init() {
{{range $file, $cover := .CoverVars}}
coverRegisterFile({{printf "%q" $file}}, _test.{{$cover.Count}}[:], _test.{{$cover.Pos}}[:]...)
{{end}}
}
func coverRegisterFile(fileName string, counter []uint32, pos ...uint32) {
if 3*len(counter) != len(pos) {
panic("coverage: mismatched sizes")
}
if coverCounters[fileName] != nil {
panic("coverage: duplicate counter array for " + fileName)
}
coverCounters[fileName] = counter
block := make([]coverBlock, len(counter))
for i := range counter {
block[i] = coverBlock{
line0: pos[3*i+0],
col0: uint16(pos[3*i+2]),
line1: pos[3*i+1],
col1: uint16(pos[3*i+2]>>16),
}
}
coverBlocks[fileName] = block
}
func coverDump() {
for name, counts := range coverCounters {
blocks := coverBlocks[name]
for i, count := range counts {
_, err := _fmt.Printf("%s:%d.%d,%d.%d %d\n", name,
blocks[i].line0, blocks[i].col0,
blocks[i].line1, blocks[i].col1,
count)
if err != nil {
panic(err)
}
}
}
}
{{end}}
func main() {
testing.Main(matchString, tests, benchmarks, examples)
{{if .CoverEnabled}}
coverDump()
{{end}}
}
`))

View file

@ -62,6 +62,7 @@ var testFlagDefn = []*testFlagSpec{
{name: "c", boolVar: &testC},
{name: "file", multiOK: true},
{name: "i", boolVar: &testI},
{name: "cover"},
// build flags.
{name: "a", boolVar: &buildA},
@ -169,6 +170,13 @@ func testFlags(args []string) (packageNames, passToTest []string) {
testTimeout = value
case "blockprofile", "cpuprofile", "memprofile":
testProfile = true
case "cover":
switch value {
case "set", "count", "atomic":
testCover = value
default:
fatalf("invalid flag argument for -cover: %q", value)
}
}
if extraWord {
i++

View file

@ -65,7 +65,7 @@ func tool(toolName string) string {
func isInGoToolsRepo(toolName string) bool {
switch toolName {
case "vet":
case "cover", "vet":
return true
}
return false