From 8e8b8b85c261d219ed3b37638b6cd96d47da79d6 Mon Sep 17 00:00:00 2001 From: Rob Pike Date: Tue, 18 Jun 2013 14:18:25 -0700 Subject: [PATCH] cmd/go: write coverage to file, add percentage statistic Move the data dumper to the testing package, where it has access to file I/O. Print a percentage value at the end of the run. R=rsc, adg CC=golang-dev https://golang.org/cl/10264045 --- src/cmd/go/test.go | 62 +++++++++----------------- src/cmd/go/testflag.go | 9 +++- src/pkg/testing/cover.go | 91 ++++++++++++++++++++++++++++++++++++++ src/pkg/testing/testing.go | 5 +++ 4 files changed, 124 insertions(+), 43 deletions(-) create mode 100644 src/pkg/testing/cover.go diff --git a/src/cmd/go/test.go b/src/cmd/go/test.go index f33e56e0f3b..f20e1efad32 100644 --- a/src/cmd/go/test.go +++ b/src/cmd/go/test.go @@ -126,8 +126,6 @@ control the execution of any test: -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: @@ -135,6 +133,11 @@ control the execution of any test: count: integer: how many times does this statement execute? atomic: integer: like count, but correct in multithreaded tests; significantly more expensive. + Sets -v. TODO: This will change. + + -coverprofile cover.out + Write a coverage profile to the specified file after all tests + have passed. -cpu 1,2,4 Specify a list of GOMAXPROCS values for which the tests or @@ -534,7 +537,7 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, if testCover != "" { p.coverMode = testCover - p.coverVars = declareCoverVars(p.GoFiles...) + p.coverVars = declareCoverVars(p.ImportPath, p.GoFiles...) } if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), p, p.coverVars); err != nil { @@ -678,11 +681,11 @@ 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 { +func declareCoverVars(importPath string, files ...string) map[string]*CoverVar { coverVars := make(map[string]*CoverVar) for _, file := range files { coverVars[file] = &CoverVar{ - File: file, + File: filepath.Join(importPath, file), Var: fmt.Sprintf("GoCover_%d", coverIndex), } coverIndex++ @@ -934,9 +937,6 @@ import ( {{if .NeedXtest}} _xtest {{.Package.ImportPath | printf "%s_test" | printf "%q"}} {{end}} -{{if .CoverEnabled}} - _fmt "fmt" -{{end}} ) var tests = []testing.InternalTest{ @@ -972,66 +972,46 @@ func matchString(pat, str string) (result bool, err error) { } {{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) + coverBlocks = make(map[string][]testing.CoverBlock) ) func init() { {{range $file, $cover := .CoverVars}} - coverRegisterFile({{printf "%q" $file}}, _test.{{$cover.Var}}.Count[:], _test.{{$cover.Var}}.Pos[:]...) + coverRegisterFile({{printf "%q" $cover.File}}, _test.{{$cover.Var}}.Count[:], _test.{{$cover.Var}}.Pos[:], _test.{{$cover.Var}}.NumStmt[:]) {{end}} } -func coverRegisterFile(fileName string, counter []uint32, pos ...uint32) { - if 3*len(counter) != len(pos) { +func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) { + if 3*len(counter) != len(pos) || len(counter) != len(numStmts) { panic("coverage: mismatched sizes") } if coverCounters[fileName] != nil { panic("coverage: duplicate counter array for " + fileName) } coverCounters[fileName] = counter - block := make([]coverBlock, len(counter)) + block := make([]testing.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), + block[i] = testing.CoverBlock{ + Line0: pos[3*i+0], + Col0: uint16(pos[3*i+2]), + Line1: pos[3*i+1], + Col1: uint16(pos[3*i+2]>>16), + Stmts: numStmts[i], } } 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() + testing.RegisterCover(coverCounters, coverBlocks) {{end}} + testing.Main(matchString, tests, benchmarks, examples) } `)) diff --git a/src/cmd/go/testflag.go b/src/cmd/go/testflag.go index 7326b907622..cd90a2f174c 100644 --- a/src/cmd/go/testflag.go +++ b/src/cmd/go/testflag.go @@ -27,6 +27,8 @@ var usageMessage = `Usage of go test: -bench="": passes -test.bench to test -benchmem=false: print memory allocation statistics for benchmarks -benchtime=1s: passes -test.benchtime to test + -cover="": passes -test.cover to test + -coverprofile="": passes -test.coverprofile to test -cpu="": passes -test.cpu to test -cpuprofile="": passes -test.cpuprofile to test -memprofile="": passes -test.memprofile to test @@ -63,7 +65,6 @@ var testFlagDefn = []*testFlagSpec{ {name: "c", boolVar: &testC}, {name: "file", multiOK: true}, {name: "i", boolVar: &testI}, - {name: "cover"}, // build flags. {name: "a", boolVar: &buildA}, @@ -82,6 +83,8 @@ var testFlagDefn = []*testFlagSpec{ {name: "bench", passToTest: true}, {name: "benchmem", boolVar: new(bool), passToTest: true}, {name: "benchtime", passToTest: true}, + {name: "cover", passToTest: true}, + {name: "coverprofile", passToTest: true}, {name: "cpu", passToTest: true}, {name: "cpuprofile", passToTest: true}, {name: "memprofile", passToTest: true}, @@ -171,7 +174,7 @@ func testFlags(args []string) (packageNames, passToTest []string) { testBench = true case "timeout": testTimeout = value - case "blockprofile", "cpuprofile", "memprofile": + case "blockprofile", "coverprofile", "cpuprofile", "memprofile": testProfile = true case "outputdir": outputDir = value @@ -182,6 +185,8 @@ func testFlags(args []string) (packageNames, passToTest []string) { default: fatalf("invalid flag argument for -cover: %q", value) } + // Guarantee we see the coverage statistics. Doesn't turn -v on generally; tricky. TODO? + testV = true } if extraWord { i++ diff --git a/src/pkg/testing/cover.go b/src/pkg/testing/cover.go new file mode 100644 index 00000000000..24198706acd --- /dev/null +++ b/src/pkg/testing/cover.go @@ -0,0 +1,91 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Support for test coverage. + +package testing + +import ( + "fmt" + "os" +) + +// CoverBlock records the coverage data for a single basic block. +// NOTE: This struct is internal to the testing infrastructure and may change. +// It is not covered (yet) by the Go 1 compatibility guidelines. +type CoverBlock struct { + Line0 uint32 + Col0 uint16 + Line1 uint32 + Col1 uint16 + Stmts uint16 +} + +var ( + coverCounters map[string][]uint32 + coverBlocks map[string][]CoverBlock +) + +// RegisterCover records the coverage data accumulators for the tests. +// NOTE: This struct is internal to the testing infrastructure and may change. +// It is not covered (yet) by the Go 1 compatibility guidelines. +func RegisterCover(c map[string][]uint32, b map[string][]CoverBlock) { + coverCounters = c + coverBlocks = b +} + +// mustBeNil checks the error and, if present, reports it and exits. +func mustBeNil(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "testing: %s\n", err) + os.Exit(2) + } +} + +// coverReport reports the coverage percentage and writes a coverage profile if requested. +func coverReport() { + var f *os.File + var err error + if *coverProfile != "" { + f, err = os.Create(toOutputDir(*coverProfile)) + mustBeNil(err) + defer func() { mustBeNil(f.Close()) }() + } + + var active, total int64 + packageName := "" + for name, counts := range coverCounters { + if packageName == "" { + // Package name ends at last slash. + for i, c := range name { + if c == '/' { + packageName = name[:i] + } + } + } + blocks := coverBlocks[name] + for i, count := range counts { + stmts := int64(blocks[i].Stmts) + total += stmts + if count > 0 { + active += stmts + } + if f != nil { + _, err := fmt.Fprintf(f, "%s:%d.%d,%d.%d %d %d\n", name, + blocks[i].Line0, blocks[i].Col0, + blocks[i].Line1, blocks[i].Col1, + stmts, + count) + mustBeNil(err) + } + } + } + if total == 0 { + total = 1 + } + if packageName == "" { + packageName = "package" + } + fmt.Printf("test coverage for %s: %.1f%% of statements\n", packageName, 100*float64(active)/float64(total)) +} diff --git a/src/pkg/testing/testing.go b/src/pkg/testing/testing.go index 7ef47b6875a..ef8c77b4976 100644 --- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -122,6 +122,8 @@ var ( // Report as tests are run; default is silent for success. chatty = flag.Bool("test.v", false, "verbose: print additional output") + cover = flag.String("test.cover", "", "cover mode: set, count, atomic; default is none") + coverProfile = flag.String("test.coverprofile", "", "write a coveraage profile to the named file after execution") match = flag.String("test.run", "", "regular expression to select tests and examples to run") memProfile = flag.String("test.memprofile", "", "write a memory profile to the named file after execution") memProfileRate = flag.Int("test.memprofilerate", 0, "if >=0, sets runtime.MemProfileRate") @@ -518,6 +520,9 @@ func after() { } f.Close() } + if *cover != "" { + coverReport() + } } // toOutputDir returns the file name relocated, if required, to outputDir.