cmd/go: support new hybrid coverage instrumentation

If GOEXPERIMENT=coverageredesign is in effect, introduce a new
top-level '-cover' option to "go build" to turn on new-style hybrid
code coverage instrumentation. Similarly, use the new instrumentation
for "go test -cover".

The main effects of "-cover" under the hood are to instrument files at
the package level using cmd/cover and to pass additional options to
the compiler when building instrumented packages.

The previous workflow for "go tool -cover mypkg" would expand to a
series of "go tool cover" commands (one per file) followed by a single
package compilation command to build the rewritten sources.

With the new workflow, the Go command will pass all of the Go files in
a package to the cover tool as a chunk (along with a config file
containing other parameters), then the cover tool will write
instrumented versions of the sources along with another "output"
config with info on coverage variable names for the the compiler. The
Go command will then kick off the compiler on the modified source
files, also passing in the config file generated by cmd/cover.

Updates #51430.

Change-Id: Id65621ff6a8c70a30168c1412c2d6f805ff3b9e7
Reviewed-on: https://go-review.googlesource.com/c/go/+/355452
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Than McIntosh <thanm@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
This commit is contained in:
Than McIntosh 2021-10-11 11:26:19 -04:00
parent b32689f6c3
commit 53773a5d08
23 changed files with 1027 additions and 242 deletions

View file

@ -125,6 +125,15 @@
// Supported only on linux/arm64, linux/amd64.
// Supported only on linux/amd64 or linux/arm64 and only with GCC 7 and higher
// or Clang/LLVM 9 and higher.
// -cover
// enable code coverage instrumentation (requires
// that GOEXPERIMENT=coverageredesign be set).
// -coverpkg pattern1,pattern2,pattern3
// For a build that targets package 'main' (e.g. building a Go
// executable), apply coverage analysis to each package matching
// the patterns. The default is to apply coverage analysis to
// packages in the main Go module. See 'go help packages' for a
// description of package patterns. Sets -cover.
// -v
// print the names of packages as they are compiled.
// -work
@ -2176,6 +2185,13 @@
// For GOARCH=wasm, comma-separated list of experimental WebAssembly features to use.
// Valid values are satconv, signext.
//
// Environment variables for use with code coverage:
//
// GOCOVERDIR
// Directory into which to write code coverage data files
// generated by running a "go build -cover" binary.
// Requires that GOEXPERIMENT=coverageredesign is enabled.
//
// Special-purpose environment variables:
//
// GCCGOTOOLDIR

View file

@ -882,6 +882,7 @@ func TestNewReleaseRebuildsStalePackagesInGOPATH(t *testing.T) {
"src/runtime",
"src/internal/abi",
"src/internal/bytealg",
"src/internal/coverage/rtcov",
"src/internal/cpu",
"src/internal/goarch",
"src/internal/goexperiment",

View file

@ -74,6 +74,9 @@ var (
BuildLinkshared bool // -linkshared flag
BuildMSan bool // -msan flag
BuildASan bool // -asan flag
BuildCover bool // -cover flag
BuildCoverMode string // -covermode flag
BuildCoverPkg []string // -coverpkg flag
BuildN bool // -n flag
BuildO string // -o flag
BuildP = runtime.GOMAXPROCS(0) // -p flag

View file

@ -619,6 +619,13 @@ Architecture-specific environment variables:
For GOARCH=wasm, comma-separated list of experimental WebAssembly features to use.
Valid values are satconv, signext.
Environment variables for use with code coverage:
GOCOVERDIR
Directory into which to write code coverage data files
generated by running a "go build -cover" binary.
Requires that GOEXPERIMENT=coverageredesign is enabled.
Special-purpose environment variables:
GCCGOTOOLDIR

View file

@ -8,6 +8,7 @@ package load
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@ -222,6 +223,7 @@ type PackageInternal struct {
FuzzInstrument bool // package should be instrumented for fuzzing
CoverMode string // preprocess Go source files with the coverage tool in this mode
CoverVars map[string]*CoverVar // variables created by coverage analysis
CoverageCfg string // coverage info config file path (passed to compiler)
OmitDebug bool // tell linker not to write debug information
GobinSubdir bool // install target would be subdir of GOBIN
BuildInfo string // add this info to package main
@ -2570,6 +2572,10 @@ func LinkerDeps(p *Package) []string {
if cfg.BuildASan {
deps = append(deps, "runtime/asan")
}
// Building for coverage forces an import of runtime/coverage.
if cfg.BuildCover && cfg.Experiment.CoverageRedesign {
deps = append(deps, "runtime/coverage")
}
return deps
}
@ -3207,3 +3213,209 @@ func PackagesAndErrorsOutsideModule(ctx context.Context, opts PackageOpts, args
}
return pkgs, nil
}
// EnsureImport ensures that package p imports the named package.
func EnsureImport(p *Package, pkg string) {
for _, d := range p.Internal.Imports {
if d.Name == pkg {
return
}
}
p1 := LoadImportWithFlags(pkg, p.Dir, p, &ImportStack{}, nil, 0)
if p1.Error != nil {
base.Fatalf("load %s: %v", pkg, p1.Error)
}
p.Internal.Imports = append(p.Internal.Imports, p1)
}
// PrepareForCoverageBuild is a helper invoked for "go install -cover"
// and "go build -cover"; it walks through the packages being built
// (and dependencies) and marks them for coverage instrumentation
// when appropriate, and adding dependencies where needed.
func PrepareForCoverageBuild(pkgs []*Package) {
var match []func(*Package) bool
matchMainMod := func(p *Package) bool {
return !p.Standard && p.Module != nil && p.Module.Main
}
// The set of packages instrumented by default varies depending on
// options and the nature of the build. If "-coverpkg" has been
// set, then match packages below using that value; if we're
// building with a module in effect, then default to packages in
// the main module. If no module is in effect and we're building
// in GOPATH mode, instrument the named packages and their
// dependencies in GOPATH. Otherwise, for "go run ..." and for the
// "go build ..." case, instrument just the packages named on the
// command line.
if len(cfg.BuildCoverPkg) == 0 {
if modload.Enabled() {
// Default is main module.
match = []func(*Package) bool{matchMainMod}
} else {
// These matchers below are intended to handle the cases of:
//
// 1. "go run ..." and "go build ..."
// 2. building in gopath mode with GO111MODULE=off
//
// In case 2 above, the assumption here is that (in the
// absence of a -coverpkg flag) we will be instrumenting
// the named packages only.
matchMain := func(p *Package) bool { return p.Internal.CmdlineFiles || p.Internal.CmdlinePkg }
match = []func(*Package) bool{matchMain}
}
} else {
match = make([]func(*Package) bool, len(cfg.BuildCoverPkg))
for i := range cfg.BuildCoverPkg {
match[i] = MatchPackage(cfg.BuildCoverPkg[i], base.Cwd())
}
}
// Visit the packages being built or installed, along with all
// of their dependencies, and mark them to be instrumented,
// taking into account the value of -coverpkg.
SelectCoverPackages(PackageList(pkgs), match, "build")
}
func SelectCoverPackages(roots []*Package, match []func(*Package) bool, op string) []*Package {
var warntag string
var includeMain bool
switch op {
case "build":
warntag = "built"
includeMain = true
case "test":
warntag = "tested"
default:
panic("internal error, bad mode passed to SelectCoverPackages")
}
covered := []*Package{}
matched := make([]bool, len(match))
for _, p := range roots {
haveMatch := false
for i := range match {
if match[i](p) {
matched[i] = true
haveMatch = true
}
}
if !haveMatch {
continue
}
// There is nothing to cover in package unsafe; it comes from
// the compiler.
if p.ImportPath == "unsafe" {
continue
}
// A package which only has test files can't be imported as a
// dependency, and at the moment we don't try to instrument it
// for coverage. There isn't any technical reason why
// *_test.go files couldn't be instrumented, but it probably
// doesn't make much sense to lump together coverage metrics
// (ex: percent stmts covered) of *_test.go files with
// non-test Go code.
if len(p.GoFiles)+len(p.CgoFiles) == 0 {
continue
}
// Silently ignore attempts to run coverage on sync/atomic
// and/or runtime/internal/atomic when using atomic coverage
// mode. Atomic coverage mode uses sync/atomic, so we can't
// also do coverage on it.
if cfg.BuildCoverMode == "atomic" && p.Standard &&
(p.ImportPath == "sync/atomic" || p.ImportPath == "runtime/internal/atomic") {
continue
}
// If using the race detector, silently ignore attempts to run
// coverage on the runtime packages. It will cause the race
// detector to be invoked before it has been initialized. Note
// the use of "regonly" instead of just ignoring the package
// completely-- we do this due to the requirements of the
// package ID numbering scheme. See the comment in
// $GOROOT/src/internal/coverage/pkid.go dealing with
// hard-coding of runtime package IDs.
cmode := cfg.BuildCoverMode
if cfg.BuildRace && p.Standard && (p.ImportPath == "runtime" || strings.HasPrefix(p.ImportPath, "runtime/internal")) {
cmode = "regonly"
}
// If -coverpkg is in effect and for some reason we don't want
// coverage data for the main package, make sure that we at
// least process it for registration hooks.
if includeMain && p.Name == "main" && !haveMatch {
haveMatch = true
cmode = "regonly"
}
// Mark package for instrumentation.
p.Internal.CoverMode = cmode
covered = append(covered, p)
// Force import of sync/atomic into package if atomic mode.
if cfg.BuildCoverMode == "atomic" {
EnsureImport(p, "sync/atomic")
}
// Generate covervars if using legacy coverage design.
if !cfg.Experiment.CoverageRedesign {
var coverFiles []string
coverFiles = append(coverFiles, p.GoFiles...)
coverFiles = append(coverFiles, p.CgoFiles...)
p.Internal.CoverVars = DeclareCoverVars(p, coverFiles...)
}
}
// Warn about -coverpkg arguments that are not actually used.
for i := range cfg.BuildCoverPkg {
if !matched[i] {
fmt.Fprintf(os.Stderr, "warning: no packages being %s depend on matches for pattern %s\n", warntag, cfg.BuildCoverPkg[i])
}
}
return covered
}
// declareCoverVars attaches the required cover variables names
// to the files, to be used when annotating the files. This
// function only called when using legacy coverage test/build
// (e.g. GOEXPERIMENT=coverageredesign is off).
func DeclareCoverVars(p *Package, files ...string) map[string]*CoverVar {
coverVars := make(map[string]*CoverVar)
coverIndex := 0
// We create the cover counters as new top-level variables in the package.
// We need to avoid collisions with user variables (GoCover_0 is unlikely but still)
// and more importantly with dot imports of other covered packages,
// so we append 12 hex digits from the SHA-256 of the import path.
// The point is only to avoid accidents, not to defeat users determined to
// break things.
sum := sha256.Sum256([]byte(p.ImportPath))
h := fmt.Sprintf("%x", sum[:6])
for _, file := range files {
if base.IsTestFile(file) {
continue
}
// For a package that is "local" (imported via ./ import or command line, outside GOPATH),
// we record the full path to the file name.
// Otherwise we record the import path, then a forward slash, then the file name.
// This makes profiles within GOPATH file system-independent.
// These names appear in the cmd/cover HTML interface.
var longFile string
if p.Internal.Local {
longFile = filepath.Join(p.Dir, file)
} else {
longFile = pathpkg.Join(p.ImportPath, file)
}
coverVars[file] = &CoverVar{
File: longFile,
Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h),
}
coverIndex++
}
return coverVars
}

View file

@ -21,6 +21,7 @@ import (
"unicode"
"unicode/utf8"
"cmd/go/internal/cfg"
"cmd/go/internal/fsys"
"cmd/go/internal/str"
"cmd/go/internal/trace"
@ -35,12 +36,11 @@ var TestMainDeps = []string{
}
type TestCover struct {
Mode string
Local bool
Pkgs []*Package
Paths []string
Vars []coverInfo
DeclVars func(*Package, ...string) map[string]*CoverVar
Mode string
Local bool
Pkgs []*Package
Paths []string
Vars []coverInfo
}
// TestPackagesFor is like TestPackagesAndErrors but it returns
@ -287,7 +287,7 @@ func TestPackagesAndErrors(ctx context.Context, opts PackageOpts, p *Package, co
}
stk.Pop()
if cover != nil && cover.Pkgs != nil {
if cover != nil && cover.Pkgs != nil && !cfg.Experiment.CoverageRedesign {
// Add imports, but avoid duplicates.
seen := map[*Package]bool{p: true, ptest: true}
for _, p1 := range pmain.Internal.Imports {
@ -346,21 +346,36 @@ func TestPackagesAndErrors(ctx context.Context, opts PackageOpts, p *Package, co
// Replace pmain's transitive dependencies with test copies, as necessary.
recompileForTest(pmain, p, ptest, pxtest)
// Should we apply coverage analysis locally,
// only for this package and only for this test?
// Yes, if -cover is on but -coverpkg has not specified
// a list of packages for global coverage.
if cover != nil && cover.Local {
ptest.Internal.CoverMode = cover.Mode
var coverFiles []string
coverFiles = append(coverFiles, ptest.GoFiles...)
coverFiles = append(coverFiles, ptest.CgoFiles...)
ptest.Internal.CoverVars = cover.DeclVars(ptest, coverFiles...)
}
if cover != nil {
if cfg.Experiment.CoverageRedesign {
// Here ptest needs to inherit the proper coverage mode (since
// it contains p's Go files), whereas pmain contains only
// test harness code (don't want to instrument it, and
// we don't want coverage hooks in the pkg init).
ptest.Internal.CoverMode = p.Internal.CoverMode
pmain.Internal.CoverMode = "testmain"
}
// Should we apply coverage analysis locally, only for this
// package and only for this test? Yes, if -cover is on but
// -coverpkg has not specified a list of packages for global
// coverage.
if cover.Local {
ptest.Internal.CoverMode = cover.Mode
for _, cp := range pmain.Internal.Imports {
if len(cp.Internal.CoverVars) > 0 {
t.Cover.Vars = append(t.Cover.Vars, coverInfo{cp, cp.Internal.CoverVars})
if !cfg.Experiment.CoverageRedesign {
var coverFiles []string
coverFiles = append(coverFiles, ptest.GoFiles...)
coverFiles = append(coverFiles, ptest.CgoFiles...)
ptest.Internal.CoverVars = DeclareCoverVars(ptest, coverFiles...)
}
}
if !cfg.Experiment.CoverageRedesign {
for _, cp := range pmain.Internal.Imports {
if len(cp.Internal.CoverVars) > 0 {
t.Cover.Vars = append(t.Cover.Vars, coverInfo{cp, cp.Internal.CoverVars})
}
}
}
}
@ -546,7 +561,11 @@ func loadTestFuncs(ptest *Package) (*testFuncs, error) {
// formatTestmain returns the content of the _testmain.go file for t.
func formatTestmain(t *testFuncs) ([]byte, error) {
var buf bytes.Buffer
if err := testmainTmpl.Execute(&buf, t); err != nil {
tmpl := testmainTmpl
if cfg.Experiment.CoverageRedesign {
tmpl = testmainTmplNewCoverage
}
if err := tmpl.Execute(&buf, t); err != nil {
return nil, err
}
return buf.Bytes(), nil
@ -804,3 +823,99 @@ func main() {
}
`)
var testmainTmplNewCoverage = lazytemplate.New("main", `
// Code generated by 'go test'. DO NOT EDIT.
package main
import (
"os"
{{if .Cover}}
_ "unsafe"
{{end}}
{{if .TestMain}}
"reflect"
{{end}}
"testing"
"testing/internal/testdeps"
{{if .ImportTest}}
{{if .NeedTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .ImportXtest}}
{{if .NeedXtest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
)
var tests = []testing.InternalTest{
{{range .Tests}}
{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}
var benchmarks = []testing.InternalBenchmark{
{{range .Benchmarks}}
{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}
var fuzzTargets = []testing.InternalFuzzTarget{
{{range .FuzzTargets}}
{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}
var examples = []testing.InternalExample{
{{range .Examples}}
{"{{.Name}}", {{.Package}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}},
{{end}}
}
func init() {
testdeps.ImportPath = {{.ImportPath | printf "%q"}}
}
{{if .Cover}}
//go:linkname runtime_coverage_processCoverTestDir runtime/coverage.processCoverTestDir
func runtime_coverage_processCoverTestDir(dir string, cfile string, cmode string, cpkgs string) error
//go:linkname testing_registerCover2 testing.registerCover2
func testing_registerCover2(mode string, tearDown func(coverprofile string, gocoverdir string) (string, error))
//go:linkname runtime_coverage_markProfileEmitted runtime/coverage.markProfileEmitted
func runtime_coverage_markProfileEmitted(val bool)
func coverTearDown(coverprofile string, gocoverdir string) (string, error) {
var err error
if gocoverdir == "" {
gocoverdir, err = os.MkdirTemp("", "gocoverdir")
if err != nil {
return "error setting GOCOVERDIR: bad os.MkdirTemp return", err
}
defer os.RemoveAll(gocoverdir)
}
runtime_coverage_markProfileEmitted(true)
cmode := {{printf "%q" .Cover.Mode}}
if err := runtime_coverage_processCoverTestDir(gocoverdir, coverprofile, cmode, {{printf "%q" .Covered}}); err != nil {
return "error generating coverage report", err
}
return "", nil
}
{{end}}
func main() {
{{if .Cover}}
testing_registerCover2({{printf "%q" .Cover.Mode}}, coverTearDown)
{{end}}
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)
{{with .TestMain}}
{{.Package}}.{{.Name}}(m)
os.Exit(int(reflect.ValueOf(m).Elem().FieldByName("exitCode").Int()))
{{else}}
os.Exit(m.Run())
{{end}}
}
`)

View file

@ -69,6 +69,9 @@ func init() {
CmdRun.Run = runRun // break init loop
work.AddBuildFlags(CmdRun, work.DefaultBuildFlags)
if cfg.Experiment != nil && cfg.Experiment.CoverageRedesign {
work.AddCoverFlags(CmdRun, nil)
}
CmdRun.Flag.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
}
@ -146,6 +149,10 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
cmdArgs := args[i:]
load.CheckPackageErrors([]*load.Package{p})
if cfg.Experiment.CoverageRedesign && cfg.BuildCover {
load.PrepareForCoverageBuild([]*load.Package{p})
}
p.Internal.OmitDebug = true
p.Target = "" // must build - not up to date
if p.Internal.CmdlineFiles {

View file

@ -6,6 +6,7 @@ package test
import (
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"fmt"
"io"
"os"
@ -35,7 +36,7 @@ func initCoverProfile() {
if err != nil {
base.Fatalf("%v", err)
}
_, err = fmt.Fprintf(f, "mode: %s\n", testCoverMode)
_, err = fmt.Fprintf(f, "mode: %s\n", cfg.BuildCoverMode)
if err != nil {
base.Fatalf("%v", err)
}
@ -51,7 +52,7 @@ func mergeCoverProfile(ew io.Writer, file string) {
coverMerge.Lock()
defer coverMerge.Unlock()
expect := fmt.Sprintf("mode: %s\n", testCoverMode)
expect := fmt.Sprintf("mode: %s\n", cfg.BuildCoverMode)
buf := make([]byte, len(expect))
r, err := os.Open(file)
if err != nil {
@ -65,7 +66,7 @@ func mergeCoverProfile(ew io.Writer, file string) {
return
}
if err != nil || string(buf) != expect {
fmt.Fprintf(ew, "error: test wrote malformed coverage profile.\n")
fmt.Fprintf(ew, "error: test wrote malformed coverage profile %s.\n", file)
return
}
_, err = io.Copy(coverMerge.f, r)

View file

@ -25,7 +25,8 @@ func TestPassFlagToTestIncludesAllTestFlags(t *testing.T) {
}
name := strings.TrimPrefix(f.Name, "test.")
switch name {
case "testlogfile", "paniconexit0", "fuzzcachedir", "fuzzworker":
case "testlogfile", "paniconexit0", "fuzzcachedir", "fuzzworker",
"gocoverdir":
// These are internal flags.
default:
if !passFlagToTest[name] {

View file

@ -7,7 +7,6 @@ package test
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"go/build"
@ -15,7 +14,6 @@ import (
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
@ -536,9 +534,6 @@ See the documentation of the testing package for more information.
var (
testBench string // -bench flag
testC bool // -c flag
testCover bool // -cover flag
testCoverMode string // -covermode flag
testCoverPaths []string // -coverpkg flag
testCoverPkgs []*load.Package // -coverpkg flag
testCoverProfile string // -coverprofile flag
testFuzz string // -fuzz flag
@ -830,73 +825,16 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
var builds, runs, prints []*work.Action
if testCoverPaths != nil {
match := make([]func(*load.Package) bool, len(testCoverPaths))
matched := make([]bool, len(testCoverPaths))
for i := range testCoverPaths {
match[i] = load.MatchPackage(testCoverPaths[i], base.Cwd())
if cfg.BuildCoverPkg != nil {
match := make([]func(*load.Package) bool, len(cfg.BuildCoverPkg))
for i := range cfg.BuildCoverPkg {
match[i] = load.MatchPackage(cfg.BuildCoverPkg[i], base.Cwd())
}
// Select for coverage all dependencies matching the testCoverPaths patterns.
for _, p := range load.TestPackageList(ctx, pkgOpts, pkgs) {
haveMatch := false
for i := range testCoverPaths {
if match[i](p) {
matched[i] = true
haveMatch = true
}
}
// A package which only has test files can't be imported
// as a dependency, nor can it be instrumented for coverage.
if len(p.GoFiles)+len(p.CgoFiles) == 0 {
continue
}
// Silently ignore attempts to run coverage on
// sync/atomic when using atomic coverage mode.
// Atomic coverage mode uses sync/atomic, so
// we can't also do coverage on it.
if testCoverMode == "atomic" && p.Standard && p.ImportPath == "sync/atomic" {
continue
}
// If using the race detector, silently ignore
// attempts to run coverage on the runtime
// packages. It will cause the race detector
// to be invoked before it has been initialized.
if cfg.BuildRace && p.Standard && (p.ImportPath == "runtime" || strings.HasPrefix(p.ImportPath, "runtime/internal")) {
continue
}
if haveMatch {
testCoverPkgs = append(testCoverPkgs, p)
}
}
// Warn about -coverpkg arguments that are not actually used.
for i := range testCoverPaths {
if !matched[i] {
fmt.Fprintf(os.Stderr, "warning: no packages being tested depend on matches for pattern %s\n", testCoverPaths[i])
}
}
// Mark all the coverage packages for rebuilding with coverage.
for _, p := range testCoverPkgs {
// There is nothing to cover in package unsafe; it comes from the compiler.
if p.ImportPath == "unsafe" {
continue
}
p.Internal.CoverMode = testCoverMode
var coverFiles []string
coverFiles = append(coverFiles, p.GoFiles...)
coverFiles = append(coverFiles, p.CgoFiles...)
coverFiles = append(coverFiles, p.TestGoFiles...)
p.Internal.CoverVars = declareCoverVars(p, coverFiles...)
if testCover && testCoverMode == "atomic" {
ensureImport(p, "sync/atomic")
}
}
// Select for coverage all dependencies matching the -coverpkg
// patterns.
plist := load.TestPackageList(ctx, pkgOpts, pkgs)
testCoverPkgs = load.SelectCoverPackages(plist, match, "test")
}
// Inform the compiler that it should instrument the binary at
@ -937,8 +875,8 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
// Prepare build + run + print actions for all packages being tested.
for _, p := range pkgs {
// sync/atomic import is inserted by the cover tool. See #18486
if testCover && testCoverMode == "atomic" {
ensureImport(p, "sync/atomic")
if cfg.BuildCover && cfg.BuildCoverMode == "atomic" {
load.EnsureImport(p, "sync/atomic")
}
buildTest, runTest, printTest, err := builderTest(b, ctx, pkgOpts, p, allImports[p])
@ -985,22 +923,6 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
b.Do(ctx, root)
}
// ensures that package p imports the named package
func ensureImport(p *load.Package, pkg string) {
for _, d := range p.Internal.Imports {
if d.Name == pkg {
return
}
}
p1 := load.LoadImportWithFlags(pkg, p.Dir, p, &load.ImportStack{}, nil, 0)
if p1.Error != nil {
base.Fatalf("load %s: %v", pkg, p1.Error)
}
p.Internal.Imports = append(p.Internal.Imports, p1)
}
var windowsBadWords = []string{
"install",
"patch",
@ -1022,13 +944,12 @@ func builderTest(b *work.Builder, ctx context.Context, pkgOpts load.PackageOpts,
// ptest - package + test files
// pxtest - package of external test files
var cover *load.TestCover
if testCover {
if cfg.BuildCover {
cover = &load.TestCover{
Mode: testCoverMode,
Local: testCover && testCoverPaths == nil,
Pkgs: testCoverPkgs,
Paths: testCoverPaths,
DeclVars: declareCoverVars,
Mode: cfg.BuildCoverMode,
Local: cfg.BuildCoverPkg == nil,
Pkgs: testCoverPkgs,
Paths: cfg.BuildCoverPkg,
}
}
pmain, ptest, pxtest, err := load.TestPackagesFor(ctx, pkgOpts, p, cover)
@ -1204,50 +1125,6 @@ func addTestVet(b *work.Builder, p *load.Package, runAction, installAction *work
}
}
// isTestFile reports whether the source file is a set of tests and should therefore
// be excluded from coverage analysis.
func isTestFile(file string) bool {
// We don't cover tests, only the code they test.
return strings.HasSuffix(file, "_test.go")
}
// declareCoverVars attaches the required cover variables names
// to the files, to be used when annotating the files.
func declareCoverVars(p *load.Package, files ...string) map[string]*load.CoverVar {
coverVars := make(map[string]*load.CoverVar)
coverIndex := 0
// We create the cover counters as new top-level variables in the package.
// We need to avoid collisions with user variables (GoCover_0 is unlikely but still)
// and more importantly with dot imports of other covered packages,
// so we append 12 hex digits from the SHA-256 of the import path.
// The point is only to avoid accidents, not to defeat users determined to
// break things.
sum := sha256.Sum256([]byte(p.ImportPath))
h := fmt.Sprintf("%x", sum[:6])
for _, file := range files {
if isTestFile(file) {
continue
}
// For a package that is "local" (imported via ./ import or command line, outside GOPATH),
// we record the full path to the file name.
// Otherwise we record the import path, then a forward slash, then the file name.
// This makes profiles within GOPATH file system-independent.
// These names appear in the cmd/cover HTML interface.
var longFile string
if p.Internal.Local {
longFile = filepath.Join(p.Dir, file)
} else {
longFile = path.Join(p.ImportPath, file)
}
coverVars[file] = &load.CoverVar{
File: longFile,
Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h),
}
coverIndex++
}
return coverVars
}
var noTestsToRun = []byte("\ntesting: warning: no tests to run\n")
var noFuzzTestsToFuzz = []byte("\ntesting: warning: no fuzz tests to fuzz\n")
var tooManyFuzzTestsToFuzz = []byte("\ntesting: warning: -fuzz matches more than one fuzz test, won't fuzz\n")
@ -1359,7 +1236,20 @@ func (c *runCache) builderRunTest(b *work.Builder, ctx context.Context, a *work.
fuzzCacheDir := filepath.Join(cache.Default().FuzzDir(), a.Package.ImportPath)
fuzzArg = []string{"-test.fuzzcachedir=" + fuzzCacheDir}
}
args := str.StringList(execCmd, a.Deps[0].BuiltTarget(), testlogArg, panicArg, fuzzArg, testArgs)
coverdirArg := []string{}
if cfg.BuildCover {
gcd := filepath.Join(a.Objdir, "gocoverdir")
if err := b.Mkdir(gcd); err != nil {
// If we can't create a temp dir, terminate immediately
// with an error as opposed to returning an error to the
// caller; failed MkDir most likely indicates that we're
// out of disk space or there is some other systemic error
// that will make forward progress unlikely.
base.Fatalf("failed to create temporary dir: %v", err)
}
coverdirArg = append(coverdirArg, "-test.gocoverdir="+gcd)
}
args := str.StringList(execCmd, a.Deps[0].BuiltTarget(), testlogArg, panicArg, fuzzArg, coverdirArg, testArgs)
if testCoverProfile != "" {
// Write coverage to temporary profile, for merging later.
@ -1858,7 +1748,7 @@ func (c *runCache) saveOutput(a *work.Action) {
// coveragePercentage returns the coverage results (if enabled) for the
// test. It uncovers the data by scanning the output from the test run.
func coveragePercentage(out []byte) string {
if !testCover {
if !cfg.BuildCover {
return ""
}
// The string looks like

View file

@ -33,11 +33,7 @@ func init() {
cf.BoolVar(&testC, "c", false, "")
cf.BoolVar(&cfg.BuildI, "i", false, "")
cf.StringVar(&testO, "o", "", "")
cf.BoolVar(&testCover, "cover", false, "")
cf.Var(coverFlag{(*coverModeFlag)(&testCoverMode)}, "covermode", "")
cf.Var(coverFlag{commaListFlag{&testCoverPaths}}, "coverpkg", "")
work.AddCoverFlags(CmdTest, &testCoverProfile)
cf.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
cf.BoolVar(&testJSON, "json", false, "")
cf.Var(&testVet, "vet", "")
@ -52,7 +48,6 @@ func init() {
cf.StringVar(&testBlockProfile, "blockprofile", "", "")
cf.String("blockprofilerate", "", "")
cf.Int("count", 0, "")
cf.Var(coverFlag{stringFlag{&testCoverProfile}}, "coverprofile", "")
cf.String("cpu", "", "")
cf.StringVar(&testCPUProfile, "cpuprofile", "", "")
cf.Bool("failfast", false, "")
@ -79,55 +74,6 @@ func init() {
}
}
// A coverFlag is a flag.Value that also implies -cover.
type coverFlag struct{ v flag.Value }
func (f coverFlag) String() string { return f.v.String() }
func (f coverFlag) Set(value string) error {
if err := f.v.Set(value); err != nil {
return err
}
testCover = true
return nil
}
type coverModeFlag string
func (f *coverModeFlag) String() string { return string(*f) }
func (f *coverModeFlag) Set(value string) error {
switch value {
case "", "set", "count", "atomic":
*f = coverModeFlag(value)
return nil
default:
return errors.New(`valid modes are "set", "count", or "atomic"`)
}
}
// A commaListFlag is a flag.Value representing a comma-separated list.
type commaListFlag struct{ vals *[]string }
func (f commaListFlag) String() string { return strings.Join(*f.vals, ",") }
func (f commaListFlag) Set(value string) error {
if value == "" {
*f.vals = nil
} else {
*f.vals = strings.Split(value, ",")
}
return nil
}
// A stringFlag is a flag.Value representing a single string.
type stringFlag struct{ val *string }
func (f stringFlag) String() string { return *f.val }
func (f stringFlag) Set(value string) error {
*f.val = value
return nil
}
// outputdirFlag implements the -outputdir flag.
// It interprets an empty value as the working directory of the 'go' command.
type outputdirFlag struct {
@ -458,18 +404,6 @@ helpLoop:
}
}
// Ensure that -race and -covermode are compatible.
if testCoverMode == "" {
testCoverMode = "set"
if cfg.BuildRace {
// Default coverage mode is atomic when -race is set.
testCoverMode = "atomic"
}
}
if cfg.BuildRace && testCoverMode != "atomic" {
base.Fatalf(`-covermode must be "atomic", not %q, when -race is enabled`, testCoverMode)
}
// Forward any unparsed arguments (following --args) to the test binary.
return packageNames, append(injectedFlags, explicitArgs...)
}

View file

@ -7,6 +7,7 @@ package work
import (
"context"
"errors"
"flag"
"fmt"
"go/build"
"os"
@ -81,6 +82,15 @@ and test commands:
Supported only on linux/arm64, linux/amd64.
Supported only on linux/amd64 or linux/arm64 and only with GCC 7 and higher
or Clang/LLVM 9 and higher.
-cover
enable code coverage instrumentation (requires
that GOEXPERIMENT=coverageredesign be set).
-coverpkg pattern1,pattern2,pattern3
For a build that targets package 'main' (e.g. building a Go
executable), apply coverage analysis to each package matching
the patterns. The default is to apply coverage analysis to
packages in the main Go module. See 'go help packages' for a
description of package patterns. Sets -cover.
-v
print the names of packages as they are compiled.
-work
@ -213,6 +223,10 @@ func init() {
AddBuildFlags(CmdBuild, DefaultBuildFlags)
AddBuildFlags(CmdInstall, DefaultBuildFlags)
if cfg.Experiment != nil && cfg.Experiment.CoverageRedesign {
AddCoverFlags(CmdBuild, nil)
AddCoverFlags(CmdInstall, nil)
}
}
// Note that flags consulted by other parts of the code
@ -313,6 +327,31 @@ func AddBuildFlags(cmd *base.Command, mask BuildFlagMask) {
cmd.Flag.StringVar(&cfg.DebugTrace, "debug-trace", "", "")
}
// AddCoverFlags adds coverage-related flags to "cmd". If the
// CoverageRedesign experiment is enabled, we add -cover{mode,pkg} to
// the build command and only -coverprofile to the test command. If
// the CoverageRedesign experiment is disabled, -cover* flags are
// added only to the test command.
func AddCoverFlags(cmd *base.Command, coverProfileFlag *string) {
addCover := false
if cfg.Experiment != nil && cfg.Experiment.CoverageRedesign {
// New coverage enabled: both build and test commands get
// coverage flags.
addCover = true
} else {
// New coverage disabled: only test command gets cover flags.
addCover = coverProfileFlag != nil
}
if addCover {
cmd.Flag.BoolVar(&cfg.BuildCover, "cover", false, "")
cmd.Flag.Var(coverFlag{(*coverModeFlag)(&cfg.BuildCoverMode)}, "covermode", "")
cmd.Flag.Var(coverFlag{commaListFlag{&cfg.BuildCoverPkg}}, "coverpkg", "")
}
if coverProfileFlag != nil {
cmd.Flag.Var(coverFlag{V: stringFlag{coverProfileFlag}}, "coverprofile", "")
}
}
// tagsFlag is the implementation of the -tags flag.
type tagsFlag []string
@ -448,6 +487,10 @@ func runBuild(ctx context.Context, cmd *base.Command, args []string) {
cfg.BuildO = ""
}
if cfg.Experiment.CoverageRedesign && cfg.BuildCover {
load.PrepareForCoverageBuild(pkgs)
}
if cfg.BuildO != "" {
// If the -o name exists and is a directory or
// ends with a slash or backslash, then
@ -677,6 +720,10 @@ func runInstall(ctx context.Context, cmd *base.Command, args []string) {
}
}
if cfg.Experiment.CoverageRedesign && cfg.BuildCover {
load.PrepareForCoverageBuild(pkgs)
}
InstallPackages(ctx, args, pkgs)
}
@ -862,3 +909,53 @@ func FindExecCmd() []string {
}
return ExecCmd
}
// A coverFlag is a flag.Value that also implies -cover.
type coverFlag struct{ V flag.Value }
func (f coverFlag) String() string { return f.V.String() }
func (f coverFlag) Set(value string) error {
if err := f.V.Set(value); err != nil {
return err
}
cfg.BuildCover = true
return nil
}
type coverModeFlag string
func (f *coverModeFlag) String() string { return string(*f) }
func (f *coverModeFlag) Set(value string) error {
switch value {
case "", "set", "count", "atomic":
*f = coverModeFlag(value)
cfg.BuildCoverMode = value
return nil
default:
return errors.New(`valid modes are "set", "count", or "atomic"`)
}
}
// A commaListFlag is a flag.Value representing a comma-separated list.
type commaListFlag struct{ Vals *[]string }
func (f commaListFlag) String() string { return strings.Join(*f.Vals, ",") }
func (f commaListFlag) Set(value string) error {
if value == "" {
*f.Vals = nil
} else {
*f.Vals = strings.Split(value, ",")
}
return nil
}
// A stringFlag is a flag.Value representing a single string.
type stringFlag struct{ val *string }
func (f stringFlag) String() string { return *f.val }
func (f stringFlag) Set(value string) error {
*f.val = value
return nil
}

View file

@ -9,9 +9,11 @@ package work
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"internal/coverage"
"internal/lazyregexp"
"io"
"io/fs"
@ -630,7 +632,13 @@ OverlayLoop:
// If we're doing coverage, preprocess the .go files and put them in the work directory
if p.Internal.CoverMode != "" {
outfiles := []string{}
infiles := []string{}
for i, file := range str.StringList(gofiles, cgofiles) {
if base.IsTestFile(file) {
continue // Not covering this file.
}
var sourceFile string
var coverFile string
var key string
@ -646,13 +654,17 @@ OverlayLoop:
key = file
}
coverFile = strings.TrimSuffix(coverFile, ".go") + ".cover.go"
cover := p.Internal.CoverVars[key]
if cover == nil || base.IsTestFile(file) {
// Not covering this file.
continue
}
if err := b.cover(a, coverFile, sourceFile, cover.Var); err != nil {
return err
if cfg.Experiment.CoverageRedesign {
infiles = append(infiles, sourceFile)
outfiles = append(outfiles, coverFile)
} else {
cover := p.Internal.CoverVars[key]
if cover == nil {
continue // Not covering this file.
}
if err := b.cover(a, coverFile, sourceFile, cover.Var); err != nil {
return err
}
}
if i < len(gofiles) {
gofiles[i] = coverFile
@ -660,6 +672,37 @@ OverlayLoop:
cgofiles[i-len(gofiles)] = coverFile
}
}
if cfg.Experiment.CoverageRedesign {
if len(infiles) != 0 {
// Coverage instrumentation creates new top level
// variables in the target package for things like
// meta-data containers, counter vars, etc. To avoid
// collisions with user variables, suffix the var name
// with 12 hex digits from the SHA-256 hash of the
// import path. Choice of 12 digits is historical/arbitrary,
// we just need enough of the hash to avoid accidents,
// as opposed to precluding determined attempts by
// users to break things.
sum := sha256.Sum256([]byte(a.Package.ImportPath))
coverVar := fmt.Sprintf("goCover_%x_", sum[:6])
mode := a.Package.Internal.CoverMode
if mode == "" {
panic("covermode should be set at this point")
}
pkgcfg := a.Objdir + "pkgcfg.txt"
if err := b.cover2(a, pkgcfg, infiles, outfiles, coverVar, mode); err != nil {
return err
}
} else {
// If there are no input files passed to cmd/cover,
// then we don't want to pass -covercfg when building
// the package with the compiler, so set covermode to
// the empty string so as to signal that we need to do
// that.
p.Internal.CoverMode = ""
}
}
}
// Run cgo.
@ -1897,6 +1940,47 @@ func (b *Builder) cover(a *Action, dst, src string, varName string) error {
src)
}
// cover2 runs, in effect,
//
// go tool cover -pkgcfg=<config file> -mode=b.coverMode -var="varName" -o <outfiles> <infiles>
func (b *Builder) cover2(a *Action, pkgcfg string, infiles, outfiles []string, varName string, mode string) error {
if err := b.writeCoverPkgCfg(a, pkgcfg); err != nil {
return err
}
args := []string{base.Tool("cover"),
"-pkgcfg", pkgcfg,
"-mode", mode,
"-var", varName,
"-o", strings.Join(outfiles, string(os.PathListSeparator)),
}
args = append(args, infiles...)
return b.run(a, a.Objdir, "cover "+a.Package.ImportPath, nil,
cfg.BuildToolexec, args)
}
func (b *Builder) writeCoverPkgCfg(a *Action, file string) error {
p := a.Package
p.Internal.CoverageCfg = a.Objdir + "coveragecfg"
pcfg := coverage.CoverPkgConfig{
PkgPath: p.ImportPath,
PkgName: p.Name,
// Note: coverage granularity is currently hard-wired to
// 'perblock'; there isn't a way using "go build -cover" or "go
// test -cover" to select it. This may change in the future
// depending on user demand.
Granularity: "perblock",
OutConfig: p.Internal.CoverageCfg,
}
if a.Package.Module != nil {
pcfg.ModulePath = a.Package.Module.Path
}
data, err := json.Marshal(pcfg)
if err != nil {
return err
}
return b.writeFile(file, data)
}
var objectMagic = [][]byte{
{'!', '<', 'a', 'r', 'c', 'h', '>', '\n'}, // Package archive
{'<', 'b', 'i', 'g', 'a', 'f', '>', '\n'}, // Package AIX big archive

View file

@ -140,6 +140,9 @@ func (gcToolchain) gc(b *Builder, a *Action, archive string, importcfg, embedcfg
if strings.HasPrefix(RuntimeVersion, "go1") && !strings.Contains(os.Args[0], "go_bootstrap") {
defaultGcFlags = append(defaultGcFlags, "-goversion", RuntimeVersion)
}
if p.Internal.CoverageCfg != "" {
defaultGcFlags = append(defaultGcFlags, "-coveragecfg="+p.Internal.CoverageCfg)
}
if symabis != "" {
defaultGcFlags = append(defaultGcFlags, "-symabis", symabis)
}

View file

@ -71,6 +71,19 @@ func BuildInit() {
base.Fatalf("go: %s environment variable is relative; must be absolute path: %s\n", key, path)
}
}
// Set covermode if not already set.
// Ensure that -race and -covermode are compatible.
if cfg.BuildCoverMode == "" {
cfg.BuildCoverMode = "set"
if cfg.BuildRace {
// Default coverage mode is atomic when -race is set.
cfg.BuildCoverMode = "atomic"
}
}
if cfg.BuildRace && cfg.BuildCoverMode != "atomic" {
base.Fatalf(`-covermode must be "atomic", not %q, when -race is enabled`, cfg.BuildCoverMode)
}
}
// fuzzInstrumentFlags returns compiler flags that enable fuzzing instrumation

View file

@ -174,6 +174,7 @@ func (ts *testScript) setup() {
"GOARCH=" + runtime.GOARCH,
"TESTGO_GOHOSTARCH=" + goHostArch,
"GOCACHE=" + testGOCACHE,
"GOCOVERDIR=" + os.Getenv("GOCOVERDIR"),
"GODEBUG=" + os.Getenv("GODEBUG"),
"GOEXE=" + cfg.ExeSuffix,
"GOEXPERIMENT=" + os.Getenv("GOEXPERIMENT"),

View file

@ -39,6 +39,7 @@ Scripts also have access to these other environment variables:
PATH=<actual PATH>
TMPDIR=$WORK/tmp
GODEBUG=<actual GODEBUG>
GOCOVERDIR=<current setting of GOCOVERDIR>
devnull=<value of os.DevNull>
goversion=<current Go version; for example, 1.12>

View file

@ -0,0 +1,114 @@
# This test checks more of the "go build -cover" functionality,
# specifically which packages get selected when building.
[short] skip
# Skip if new coverage is not enabled.
[!GOEXPERIMENT:coverageredesign] skip
#-------------------------------------------
# Build for coverage.
go build -mod=mod -o $WORK/modex.exe -cover mod.example/main
# Save off old GOCOVERDIR setting
env SAVEGOCOVERDIR=$GOCOVERDIR
# Execute.
mkdir $WORK/covdata
env GOCOVERDIR=$WORK/covdata
exec $WORK/modex.exe
# Restore previous GOCOVERDIR setting
env GOCOVERDIR=$SAVEGOCOVERDIR
# Examine the result.
go tool covdata percent -i=$WORK/covdata
stdout 'coverage: 100.0% of statements'
# By default we want to see packages resident in the module covered,
# but not dependencies.
go tool covdata textfmt -i=$WORK/covdata -o=$WORK/covdata/out.txt
grep 'mode: set' $WORK/covdata/out.txt
grep 'mod.example/main/main.go:' $WORK/covdata/out.txt
grep 'mod.example/sub/sub.go:' $WORK/covdata/out.txt
! grep 'rsc.io' $WORK/covdata/out.txt
rm $WORK/covdata
rm $WORK/modex.exe
#-------------------------------------------
# Repeat the build but with -coverpkg=all
go build -mod=mod -coverpkg=all -o $WORK/modex.exe -cover mod.example/main
# Execute.
mkdir $WORK/covdata
env GOCOVERDIR=$WORK/covdata
exec $WORK/modex.exe
# Restore previous GOCOVERDIR setting
env GOCOVERDIR=$SAVEGOCOVERDIR
# Examine the result.
go tool covdata percent -i=$WORK/covdata
stdout 'coverage:.*[1-9][0-9.]+%'
# The whole enchilada.
go tool covdata textfmt -i=$WORK/covdata -o=$WORK/covdata/out.txt
grep 'mode: set' $WORK/covdata/out.txt
grep 'mod.example/main/main.go:' $WORK/covdata/out.txt
grep 'mod.example/sub/sub.go:' $WORK/covdata/out.txt
grep 'rsc.io' $WORK/covdata/out.txt
grep 'bufio/bufio.go:' $WORK/covdata/out.txt
# Use the covdata tool to select a specific set of module paths
mkdir $WORK/covdata2
go tool covdata merge -pkg=rsc.io/quote -i=$WORK/covdata -o=$WORK/covdata2
# Examine the result.
go tool covdata percent -i=$WORK/covdata2
stdout 'coverage:.*[1-9][0-9.]+%'
# Check for expected packages + check that we don't see things from stdlib.
go tool covdata textfmt -i=$WORK/covdata2 -o=$WORK/covdata2/out.txt
grep 'mode: set' $WORK/covdata2/out.txt
! grep 'mod.example/main/main.go:' $WORK/covdata2/out.txt
! grep 'mod.example/sub/sub.go:' $WORK/covdata2/out.txt
grep 'rsc.io' $WORK/covdata2/out.txt
! grep 'bufio/bufio.go:' $WORK/covdata2/out.txt
#-------------------------------------------
# end of test cmds, start of harness and related files.
-- go.mod --
module mod.example
go 1.20
require rsc.io/quote/v3 v3.0.0
-- main/main.go --
package main
import (
"fmt"
"mod.example/sub"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Go(), sub.F())
}
-- sub/sub.go --
package sub
func F() int {
return 42
}

View file

@ -0,0 +1,149 @@
# This test checks basic "go build -cover" functionality.
[short] skip
# Hard-wire new coverage for this test.
env GOEXPERIMENT=coverageredesign
# Build for coverage.
go build -gcflags=-m -o example.exe -cover example/main &
[race] go build -o examplewithrace.exe -race -cover example/main &
wait
# First execute without GOCOVERDIR set...
env GOCOVERDIR=
exec ./example.exe normal
stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
# ... then with GOCOVERDIR set.
env GOCOVERDIR=data/normal
exec ./example.exe normal
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data/normal
stdout 'coverage:.*[1-9][0-9.]+%'
# Program makes a direct call to os.Exit(0).
env GOCOVERDIR=data/goodexit
exec ./example.exe goodexit
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data/goodexit
stdout 'coverage:.*[1-9][0-9.]+%'
# Program makes a direct call to os.Exit(1).
env GOCOVERDIR=data/badexit
! exec ./example.exe badexit
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data/badexit
stdout 'coverage:.*[1-9][0-9.]+%'
# Program invokes panic.
env GOCOVERDIR=data/panic
! exec ./example.exe panic
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data/panic
stdout 'coverage:.*[0-9.]+%'
# Skip remainder if no race detector support.
[!race] skip
env GOCOVERDIR=data2/normal
exec ./examplewithrace.exe normal
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data2/normal
stdout 'coverage:.*[1-9][0-9.]+%'
# Program makes a direct call to os.Exit(0).
env GOCOVERDIR=data2/goodexit
exec ./examplewithrace.exe goodexit
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data2/goodexit
stdout 'coverage:.*[1-9][0-9.]+%'
# Program makes a direct call to os.Exit(1).
env GOCOVERDIR=data2/badexit
! exec ./examplewithrace.exe badexit
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data2/badexit
stdout 'coverage:.*[1-9][0-9.]+%'
# Program invokes panic.
env GOCOVERDIR=data2/panic
! exec ./examplewithrace.exe panic
! stderr '^warning: GOCOVERDIR not set, no coverage data emitted'
go tool covdata percent -i=data2/panic
stdout 'coverage:.*[0-9.]+%'
# end of test cmds, start of harness and related files.
-- go.mod --
module example
go 1.18
-- main/example.go --
package main
import "example/sub"
func main() {
sub.S()
}
-- sub/sub.go --
package sub
import "os"
func S() {
switch os.Args[1] {
case "normal":
println("hi")
case "goodexit":
os.Exit(0)
case "badexit":
os.Exit(1)
case "panic":
panic("something bad happened")
}
}
-- data/README.txt --
Just a location where we can write coverage profiles.
-- data/normal/f.txt --
X
-- data/goodexit/f.txt --
X
-- data/badexit/f.txt --
X
-- data/panic/f.txt --
X
-- data2/README.txt --
Just a location where we can write coverage profiles.
-- data2/normal/f.txt --
X
-- data2/goodexit/f.txt --
X
-- data2/badexit/f.txt --
X
-- data2/panic/f.txt --
X

View file

@ -0,0 +1,80 @@
[short] skip
# Hard-wire new coverage for this test.
env GOEXPERIMENT=coverageredesign
# Baseline run.
go test -cover example/foo
stdout 'coverage: 50.0% of statements$'
# Coverage percentage output should mention -coverpkg selection.
go test -coverpkg=example/foo example/foo
stdout 'coverage: 50.0% of statements in example/foo'
# Try to ask for coverage of a package that doesn't exist.
go test -coverpkg nonexistent example/bar
stderr 'no packages being tested depend on matches for pattern nonexistent'
stdout 'coverage: \[no statements\]'
# Ask for foo coverage, but test bar.
go test -coverpkg=example/foo example/bar
stdout 'coverage: 50.0% of statements in example/foo'
# end of test cmds, start of harness and related files.
-- go.mod --
module example
go 1.18
-- foo/foo.go --
package foo
func FooFunc() int {
return 42
}
func FooFunc2() int {
return 42
}
-- foo/foo_test.go --
package foo
import "testing"
func TestFoo(t *testing.T) {
if FooFunc() != 42 {
t.Fatalf("bad")
}
}
-- bar/bar.go --
package bar
import "example/foo"
func BarFunc() int {
return foo.FooFunc2()
}
-- bar/bar_test.go --
package bar_test
import (
"example/bar"
"testing"
)
func TestBar(t *testing.T) {
if bar.BarFunc() != 42 {
t.Fatalf("bad")
}
}
-- baz/baz.go --
package baz
func BazFunc() int {
return -42
}

View file

@ -8,6 +8,7 @@ package testing
import (
"fmt"
"internal/goexperiment"
"os"
"sync/atomic"
)
@ -78,6 +79,10 @@ func mustBeNil(err error) {
// coverReport reports the coverage percentage and writes a coverage profile if requested.
func coverReport() {
if goexperiment.CoverageRedesign {
coverReport2()
return
}
var f *os.File
var err error
if *coverProfile != "" {

41
src/testing/newcover.go Normal file
View file

@ -0,0 +1,41 @@
// Copyright 2022 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 with redesigned coverage implementation.
package testing
import (
"fmt"
"internal/goexperiment"
"os"
)
// cover2 variable stores the current coverage mode and a
// tear-down function to be called at the end of the testing run.
var cover2 struct {
mode string
tearDown func(coverprofile string, gocoverdir string) (string, error)
}
// registerCover2 is invoked during "go test -cover" runs by the test harness
// code in _testmain.go; it is used to record a 'tear down' function
// (to be called when the test is complete) and the coverage mode.
func registerCover2(mode string, tearDown func(coverprofile string, gocoverdir string) (string, error)) {
cover2.mode = mode
cover2.tearDown = tearDown
}
// coverReport2 invokes a callback in _testmain.go that will
// emit coverage data at the point where test execution is complete,
// for "go test -cover" runs.
func coverReport2() {
if !goexperiment.CoverageRedesign {
panic("unexpected")
}
if errmsg, err := cover2.tearDown(*coverProfile, *gocoverdir); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", errmsg, err)
os.Exit(2)
}
}

View file

@ -372,6 +372,7 @@ import (
"errors"
"flag"
"fmt"
"internal/goexperiment"
"internal/race"
"io"
"math/rand"
@ -420,6 +421,7 @@ func Init() {
chatty = flag.Bool("test.v", false, "verbose: print additional output")
count = flag.Uint("test.count", 1, "run tests and benchmarks `n` times")
coverProfile = flag.String("test.coverprofile", "", "write a coverage profile to `file`")
gocoverdir = flag.String("test.gocoverdir", "", "write coverage intermediate files to this directory")
matchList = flag.String("test.list", "", "list tests, examples, and benchmarks matching `regexp` then exit")
match = flag.String("test.run", "", "run only tests and examples matching `regexp`")
skip = flag.String("test.skip", "", "do not list or run tests matching `regexp`")
@ -450,6 +452,7 @@ var (
chatty *bool
count *uint
coverProfile *string
gocoverdir *string
matchList *string
match *string
skip *string
@ -578,6 +581,9 @@ func Short() bool {
// values are "set", "count", or "atomic". The return value will be
// empty if test coverage is not enabled.
func CoverMode() string {
if goexperiment.CoverageRedesign {
return cover2.mode
}
return cover.Mode
}
@ -1942,10 +1948,14 @@ func (m *M) before() {
if *mutexProfile != "" && *mutexProfileFraction >= 0 {
runtime.SetMutexProfileFraction(*mutexProfileFraction)
}
if *coverProfile != "" && cover.Mode == "" {
if *coverProfile != "" && CoverMode() == "" {
fmt.Fprintf(os.Stderr, "testing: cannot use -test.coverprofile because test binary was not built with coverage enabled\n")
os.Exit(2)
}
if *gocoverdir != "" && CoverMode() == "" {
fmt.Fprintf(os.Stderr, "testing: cannot use -test.gocoverdir because test binary was not built with coverage enabled\n")
os.Exit(2)
}
if *testlog != "" {
// Note: Not using toOutputDir.
// This file is for use by cmd/go, not users.
@ -2039,7 +2049,7 @@ func (m *M) writeProfiles() {
}
f.Close()
}
if cover.Mode != "" {
if CoverMode() != "" {
coverReport()
}
}