LocalPrefix string // interpret ./ and ../ imports relative to this prefix
ExeName string // desired name for temporary executable
FuzzInstrument bool // package should be instrumented for fuzzing
- CoverMode string // preprocess Go source files with the coverage tool in this mode
+ Cover CoverSetup // coverage mode and other setup info of -cover is being applied to this package
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 *debug.BuildInfo // add this info to package main
Var string // name of count struct
}
+// CoverSetup holds parameters related to coverage setup for a given package (covermode, etc).
+type CoverSetup struct {
+ Mode string // coverage mode for this package
+ Cfg string // path to config file to pass to "go tool cover"
+ GenMeta bool // ask cover tool to emit a static meta data if set
+}
+
func (p *Package) copyBuild(opts PackageOpts, pp *build.Package) {
p.Internal.Build = pp
}
// Mark package for instrumentation.
- p.Internal.CoverMode = cmode
+ p.Internal.Cover.Mode = cmode
covered = append(covered, p)
// Force import of sync/atomic into package if atomic mode.
// 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"
+ ptest.Internal.Cover.Mode = p.Internal.Cover.Mode
+ pmain.Internal.Cover.Mode = "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
+ ptest.Internal.Cover.Mode = cover.Mode
if !cfg.Experiment.CoverageRedesign {
var coverFiles []string
}
}
- // Prepare build + run + print actions for all packages being tested.
- for _, p := range pkgs {
- // sync/atomic import is inserted by the cover tool if we're
- // using atomic mode (and not compiling sync/atomic package itself).
- // See #18486 and #57445.
- if cfg.BuildCover && cfg.BuildCoverMode == "atomic" &&
- p.ImportPath != "sync/atomic" {
- load.EnsureImport(p, "sync/atomic")
+ if cfg.BuildCover {
+ for _, p := range pkgs {
+ // sync/atomic import is inserted by the cover tool if
+ // we're using atomic mode (and not compiling
+ // sync/atomic package itself). See #18486 and #57445.
+ // Note that this needs to be done prior to any of the
+ // builderTest invocations below, due to the fact that
+ // a given package in the 'pkgs' list may import
+ // package Q which appears later in the list (if this
+ // happens we'll wind up building the Q compile action
+ // before updating its deps to include sync/atomic).
+ if cfg.BuildCoverMode == "atomic" && p.ImportPath != "sync/atomic" {
+ load.EnsureImport(p, "sync/atomic")
+ }
+ // Tag the package for static meta-data generation if no
+ // test files (this works only with the new coverage
+ // design). Do this here (as opposed to in builderTest) so
+ // as to handle the case where we're testing multiple
+ // packages and one of the earlier packages imports a
+ // later package.
+ if len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 &&
+ cfg.Experiment.CoverageRedesign {
+ p.Internal.Cover.GenMeta = true
+ }
}
+ }
+ // Prepare build + run + print actions for all packages being tested.
+ for _, p := range pkgs {
buildTest, runTest, printTest, err := builderTest(b, ctx, pkgOpts, p, allImports[p])
if err != nil {
str := err.Error()
func builderTest(b *work.Builder, ctx context.Context, pkgOpts load.PackageOpts, p *load.Package, imported bool) (buildAction, runAction, printAction *work.Action, err error) {
if len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
+ if cfg.BuildCover && cfg.Experiment.CoverageRedesign {
+ if !p.Internal.Cover.GenMeta {
+ panic("internal error: Cover.GenMeta should already be set")
+ }
+ p.Internal.Cover.Mode = cfg.BuildCoverMode
+ }
build := b.CompileAction(work.ModeBuild, work.ModeBuild, p)
run := &work.Action{
Mode: "test run",
return nil
}
+ coverProfTempFile := func(a *work.Action) string {
+ return a.Objdir + "_cover_.out"
+ }
+
if p := a.Package; len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
- fmt.Fprintf(stdout, "? \t%s\t[no test files]\n", p.ImportPath)
+ reportNoTestFiles := true
+ if cfg.BuildCover && cfg.Experiment.CoverageRedesign {
+ mf, err := work.BuildActionCoverMetaFile(a)
+ if err != nil {
+ return err
+ } else if mf != "" {
+ reportNoTestFiles = false
+ // Write out "percent statements covered".
+ if err := work.WriteCoveragePercent(b, a, mf, stdout); err != nil {
+ return err
+ }
+ // If -coverprofile is in effect, then generate a
+ // coverage profile fragment for this package and
+ // merge it with the final -coverprofile output file.
+ if coverMerge.f != nil {
+ cp := coverProfTempFile(a)
+ if err := work.WriteCoverageProfile(b, a, mf, cp, stdout); err != nil {
+ return err
+ }
+ mergeCoverProfile(stdout, cp)
+ }
+ }
+ }
+ if reportNoTestFiles {
+ fmt.Fprintf(stdout, "? \t%s\t[no test files]\n", p.ImportPath)
+ }
return nil
}
// Write coverage to temporary profile, for merging later.
for i, arg := range args {
if strings.HasPrefix(arg, "-test.coverprofile=") {
- args[i] = "-test.coverprofile=" + a.Objdir + "_cover_.out"
+ args[i] = "-test.coverprofile=" + coverProfTempFile(a)
}
}
}
"debug/elf"
"encoding/json"
"fmt"
+ "internal/coverage/covcmd"
"internal/platform"
"os"
"path/filepath"
return b.CompileAction(mode, depMode, p)
}
+// buildActor implements the Actor interface for package build
+// actions. For most package builds this simply means invoking th
+// *Builder.build method; in the case of "go test -cover" for
+// a package with no test files, we stores some additional state
+// information in the build actor to help with reporting.
+type buildActor struct {
+ // name of static meta-data file fragment emitted by the cover
+ // tool as part of the package build action, for selected
+ // "go test -cover" runs.
+ covMetaFileName string
+}
+
+// newBuildActor returns a new buildActor object, setting up the
+// covMetaFileName field if 'genCoverMeta' flag is set.
+func newBuildActor(p *load.Package, genCoverMeta bool) *buildActor {
+ ba := &buildActor{}
+ if genCoverMeta {
+ ba.covMetaFileName = covcmd.MetaFileForPackage(p.ImportPath)
+ }
+ return ba
+}
+
+func (ba *buildActor) Act(b *Builder, ctx context.Context, a *Action) error {
+ return b.build(ctx, a)
+}
+
// CompileAction returns the action for compiling and possibly installing
// (according to mode) the given package. The resulting action is only
// for building packages (archives), never for linking executables.
a := &Action{
Mode: "build",
Package: p,
- Actor: ActorFunc((*Builder).build),
+ Actor: newBuildActor(p, p.Internal.Cover.GenMeta),
Objdir: b.NewObjdir(),
}
--- /dev/null
+// Copyright 2023 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.
+
+// Action graph execution methods related to coverage.
+
+package work
+
+import (
+ "cmd/go/internal/base"
+ "cmd/go/internal/cfg"
+ "cmd/go/internal/str"
+ "fmt"
+ "internal/coverage/covcmd"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+// CovData invokes "go tool covdata" with the specified arguments
+// as part of the execution of action 'a'.
+func (b *Builder) CovData(a *Action, cmdargs ...any) ([]byte, error) {
+ cmdline := str.StringList(cmdargs...)
+ args := append([]string{}, cfg.BuildToolexec...)
+ args = append(args, base.Tool("covdata"))
+ args = append(args, cmdline...)
+ return b.runOut(a, a.Objdir, nil, args)
+}
+
+// BuildActionCoverMetaFile locates and returns the path of the
+// meta-data file written by the "go tool cover" step as part of the
+// build action for the "go test -cover" run action 'runAct'. Note
+// that if the package has no functions the meta-data file will exist
+// but will be empty; in this case the return is an empty string.
+func BuildActionCoverMetaFile(runAct *Action) (string, error) {
+ p := runAct.Package
+ for i := range runAct.Deps {
+ pred := runAct.Deps[i]
+ if pred.Mode != "build" || pred.Package == nil {
+ continue
+ }
+ if pred.Package.ImportPath == p.ImportPath {
+ metaFile := pred.Objdir + covcmd.MetaFileForPackage(p.ImportPath)
+ f, err := os.Open(metaFile)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+ fi, err2 := f.Stat()
+ if err2 != nil {
+ return "", err2
+ }
+ if fi.Size() == 0 {
+ return "", nil
+ }
+ return metaFile, nil
+ }
+ }
+ return "", fmt.Errorf("internal error: unable to locate build action for package %q run action", p.ImportPath)
+}
+
+// WriteCoveragePercent writes out to the writer 'w' a "percent
+// statements covered" for the package whose test-run action is
+// 'runAct', based on the meta-data file 'mf'. This helper is used in
+// cases where a user runs "go test -cover" on a package that has
+// functions but no tests; in the normal case (package has tests)
+// the percentage is written by the test binary when it runs.
+func WriteCoveragePercent(b *Builder, runAct *Action, mf string, w io.Writer) error {
+ dir := filepath.Dir(mf)
+ output, cerr := b.CovData(runAct, "percent", "-i", dir)
+ if cerr != nil {
+ p := runAct.Package
+ return formatOutput(b.WorkDir, p.Dir, p.ImportPath,
+ p.Desc(), string(output))
+ }
+ _, werr := w.Write(output)
+ return werr
+}
+
+// WriteCoverageProfile writes out a coverage profile fragment for the
+// package whose test-run action is 'runAct'; content is written to
+// the file 'outf' based on the coverage meta-data info found in
+// 'mf'. This helper is used in cases where a user runs "go test
+// -cover" on a package that has functions but no tests.
+func WriteCoverageProfile(b *Builder, runAct *Action, mf, outf string, w io.Writer) error {
+ dir := filepath.Dir(mf)
+ output, err := b.CovData(runAct, "textfmt", "-i", dir, "-o", outf)
+ if err != nil {
+ p := runAct.Package
+ return formatOutput(b.WorkDir, p.Dir, p.ImportPath,
+ p.Desc(), string(output))
+ }
+ _, werr := w.Write(output)
+ return werr
+}
}
// TODO(rsc): Should we include the SWIG version?
}
- if p.Internal.CoverMode != "" {
- fmt.Fprintf(h, "cover %q %q\n", p.Internal.CoverMode, b.toolID("cover"))
+ if p.Internal.Cover.Mode != "" {
+ fmt.Fprintf(h, "cover %q %q\n", p.Internal.Cover.Mode, b.toolID("cover"))
}
if p.Internal.FuzzInstrument {
if fuzzFlags := fuzzInstrumentFlags(); fuzzFlags != nil {
needCgoHdr
needVet
needCompiledGoFiles
+ needCovMetaFile
needStale
)
}
cachedBuild := false
+ needCovMeta := p.Internal.Cover.GenMeta
need := bit(needBuild, !b.IsCmdList && a.needBuild || b.NeedExport) |
bit(needCgoHdr, b.needCgoHdr(a)) |
bit(needVet, a.needVet) |
+ bit(needCovMetaFile, needCovMeta) |
bit(needCompiledGoFiles, b.NeedCompiledGoFiles)
if !p.BinaryOnly {
}
}
+ // Load cached coverage meta-data file fragment, but only if we're
+ // skipping the main build (cachedBuild==true).
+ if cachedBuild && need&needCovMetaFile != 0 {
+ bact := a.Actor.(*buildActor)
+ if err := b.loadCachedObjdirFile(a, cache.Default(), bact.covMetaFileName); err == nil {
+ need &^= needCovMetaFile
+ }
+ }
+
// Load cached vet config, but only if that's all we have left
// (need == needVet, not testing just the one bit).
// If we are going to do a full build anyway,
}
// If we're doing coverage, preprocess the .go files and put them in the work directory
- if p.Internal.CoverMode != "" {
+ if p.Internal.Cover.Mode != "" {
outfiles := []string{}
infiles := []string{}
for i, file := range str.StringList(gofiles, cgofiles) {
// users to break things.
sum := sha256.Sum256([]byte(a.Package.ImportPath))
coverVar := fmt.Sprintf("goCover_%x_", sum[:6])
- mode := a.Package.Internal.CoverMode
+ mode := a.Package.Internal.Cover.Mode
if mode == "" {
panic("covermode should be set at this point")
}
// 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 = ""
+ p.Internal.Cover.Mode = ""
+ }
+ if ba, ok := a.Actor.(*buildActor); ok && ba.covMetaFileName != "" {
+ b.cacheObjdirFile(a, cache.Default(), ba.covMetaFileName)
}
}
}
return b.run(a, a.Objdir, "cover "+a.Package.ImportPath, nil,
cfg.BuildToolexec,
base.Tool("cover"),
- "-mode", a.Package.Internal.CoverMode,
+ "-mode", a.Package.Internal.Cover.Mode,
"-var", varName,
"-o", dst,
src)
func (b *Builder) writeCoverPkgInputs(a *Action, pconfigfile string, covoutputsfile string, outfiles []string) error {
p := a.Package
- p.Internal.CoverageCfg = a.Objdir + "coveragecfg"
+ p.Internal.Cover.Cfg = a.Objdir + "coveragecfg"
pcfg := covcmd.CoverPkgConfig{
PkgPath: p.ImportPath,
PkgName: p.Name,
// test -cover" to select it. This may change in the future
// depending on user demand.
Granularity: "perblock",
- OutConfig: p.Internal.CoverageCfg,
+ OutConfig: p.Internal.Cover.Cfg,
Local: p.Internal.Local,
}
+ if ba, ok := a.Actor.(*buildActor); ok && ba.covMetaFileName != "" {
+ pcfg.EmitMetaFile = a.Objdir + ba.covMetaFileName
+ }
if a.Package.Module != nil {
pcfg.ModulePath = a.Package.Module.Path
}
if err != nil {
return err
}
+ data = append(data, '\n')
if err := b.writeFile(pconfigfile, data); err != nil {
return err
}
if strings.HasPrefix(ToolchainVersion, "go1") && !strings.Contains(os.Args[0], "go_bootstrap") {
defaultGcFlags = append(defaultGcFlags, "-goversion", ToolchainVersion)
}
- if p.Internal.CoverageCfg != "" {
- defaultGcFlags = append(defaultGcFlags, "-coveragecfg="+p.Internal.CoverageCfg)
+ if p.Internal.Cover.Cfg != "" {
+ defaultGcFlags = append(defaultGcFlags, "-coveragecfg="+p.Internal.Cover.Cfg)
}
if p.Internal.PGOProfile != "" {
defaultGcFlags = append(defaultGcFlags, "-pgoprofile="+p.Internal.PGOProfile)
[short] skip
+
+# Initial run with simple coverage.
go test -cover ./pkg1 ./pkg2 ./pkg3 ./pkg4
-stdout 'pkg1 \[no test files\]'
+[!GOEXPERIMENT:coverageredesign] stdout 'pkg1 \[no test files\]'
+[GOEXPERIMENT:coverageredesign] stdout 'pkg1 coverage: 0.0% of statements'
+stdout 'pkg2 \S+ coverage: 0.0% of statements \[no tests to run\]'
+stdout 'pkg3 \S+ coverage: 100.0% of statements'
+stdout 'pkg4 \S+ coverage: \[no statements\]'
+
+# Second run to make sure that caching works properly.
+go test -x -cover ./pkg1 ./pkg2 ./pkg3 ./pkg4
+[!GOEXPERIMENT:coverageredesign] stdout 'pkg1 \[no test files\]'
+[GOEXPERIMENT:coverageredesign] stdout 'pkg1 coverage: 0.0% of statements'
stdout 'pkg2 \S+ coverage: 0.0% of statements \[no tests to run\]'
stdout 'pkg3 \S+ coverage: 100.0% of statements'
stdout 'pkg4 \S+ coverage: \[no statements\]'
+[GOEXPERIMENT:coverageredesign] ! stderr 'link(\.exe"?)? -'
+! stderr 'compile(\.exe"?)? -'
+! stderr 'cover(\.exe"?)? -'
+[GOEXPERIMENT:coverageredesign] stderr 'covdata(\.exe"?)? percent'
+
+# Now add in -coverprofile.
+go test -cover -coverprofile=cov.dat ./pkg1 ./pkg2 ./pkg3 ./pkg4
+[!GOEXPERIMENT:coverageredesign] stdout 'pkg1 \[no test files\]'
+[GOEXPERIMENT:coverageredesign] stdout 'pkg1 coverage: 0.0% of statements'
+stdout 'pkg2 \S+ coverage: 0.0% of statements \[no tests to run\]'
+stdout 'pkg3 \S+ coverage: 100.0% of statements'
+stdout 'pkg4 \S+ coverage: \[no statements\]'
+
+# Validate
+go tool cover -func=cov.dat
+[GOEXPERIMENT:coverageredesign] stdout 'pkg1/a.go:5:\s+F\s+0.0%'
-- go.mod --
module m
internal/coverage/cmerge
< internal/coverage/cformat;
+ internal/coverage, crypto/sha256, FMT
+ < internal/coverage/covcmd;
+
encoding/json,
runtime/debug,
internal/coverage/calloc,
package covcmd
+import (
+ "crypto/sha256"
+ "fmt"
+ "internal/coverage"
+)
+
// CoverPkgConfig is a bundle of information passed from the Go
// command to the cover command during "go build -cover" runs. The
// Go command creates and fills in a struct as below, then passes
// Counter granularity (perblock or perfunc).
CounterGranularity string
}
+
+// MetaFileForPackage returns the expected name of the meta-data file
+// for the package whose import path is 'importPath' in cases where
+// we're using meta-data generated by the cover tool, as opposed to a
+// meta-data file created at runtime.
+func MetaFileForPackage(importPath string) string {
+ var r [32]byte
+ sum := sha256.Sum256([]byte(importPath))
+ copy(r[:], sum[:])
+ return coverage.MetaFilePref + fmt.Sprintf(".%x", r)
+}