]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: support new hybrid coverage instrumentation
authorThan McIntosh <thanm@google.com>
Mon, 11 Oct 2021 15:26:19 +0000 (11:26 -0400)
committerThan McIntosh <thanm@google.com>
Wed, 28 Sep 2022 11:50:58 +0000 (11:50 +0000)
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>
23 files changed:
src/cmd/go/alldocs.go
src/cmd/go/go_test.go
src/cmd/go/internal/cfg/cfg.go
src/cmd/go/internal/help/helpdoc.go
src/cmd/go/internal/load/pkg.go
src/cmd/go/internal/load/test.go
src/cmd/go/internal/run/run.go
src/cmd/go/internal/test/cover.go
src/cmd/go/internal/test/flagdefs_test.go
src/cmd/go/internal/test/test.go
src/cmd/go/internal/test/testflag.go
src/cmd/go/internal/work/build.go
src/cmd/go/internal/work/exec.go
src/cmd/go/internal/work/gc.go
src/cmd/go/internal/work/init.go
src/cmd/go/script_test.go
src/cmd/go/testdata/script/README
src/cmd/go/testdata/script/cover_build_pkg_select.txt [new file with mode: 0644]
src/cmd/go/testdata/script/cover_build_simple.txt [new file with mode: 0644]
src/cmd/go/testdata/script/cover_test_pkgselect.txt [new file with mode: 0644]
src/testing/cover.go
src/testing/newcover.go [new file with mode: 0644]
src/testing/testing.go

index f8cc52343a41a236c8b0a6ede30be2ae34a6a992..79410f0bad40655333ff7bd73ba775dfce270092 100644 (file)
 //             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
 //             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
index 556ba9cde5b095bc1c4e091a18de4572befa16b3..ee1cbc15eba4fe62d472c551fba21d2052841046 100644 (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",
index 2a1475ef2e7aea0b527b17632215cbdaf241e9e4..fbf91be60482726b611e132798932c72f341eedb 100644 (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
index 2398260536c0731fb2f10e81548eee88577dc8b6..72abccd16b8e6abb91cbe04e4f71af7bcda4c386 100644 (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
index 522c372f105026f6ba879fbfbd067ed4da071dc2..3e110dcd7c6a12463b097567791db90443526441 100644 (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
+}
index 1abefd8ad1cd705236357fde1f4535fe84230f4e..0c20a23b00718c4a8ea16dec56215066fb06c165 100644 (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
+
+                       if !cfg.Experiment.CoverageRedesign {
+                               var coverFiles []string
+                               coverFiles = append(coverFiles, ptest.GoFiles...)
+                               coverFiles = append(coverFiles, ptest.CgoFiles...)
+                               ptest.Internal.CoverVars = DeclareCoverVars(ptest, coverFiles...)
+                       }
+               }
 
-       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 {
+                       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}}
+}
+
+`)
index 2804db2296419dfdafbf08bac25fc5632a4deb6f..8221b0395b24ce9af6929549af7bb5520b8b894a 100644 (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 {
index 657d22a6b4d2ff8a7a1d69c6e2ed76e6c869c3e6..f614458dc46a15baead3d1efa7839e0593894eee 100644 (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)
index 64317fd04e9da7fada991e1e1ccaeee69709ac02..337f136d06177aecddb6637aa365e5c409ea5bc8 100644 (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] {
index 7248445796e522a35e5c80b4c53582dcde31ae63..c262362d4de3f431f263e8f7620c6a4f1a853c00 100644 (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
index 8f5ab38d9de7785b11ee9d7c02c5a02d13ef55ec..2b2bd877327de50553c77e26d87e63bd5d7a4aa6 100644 (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...)
 }
index bce923a459551136a68b2d9356f613cef58b57b6..2acc153c3c0d4c8d85d6a289aecd3b5bc2873e99 100644 (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
+}
index 1dd23ebdda1cff5817f64739fc54e1606fbb80e1..198d6081bb6babf7684178b1754a96c011ce5fb0 100644 (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
index b7fa03205b5c37538b772109ad701246733841b7..9c50975f996f384194362d554f950926ee9957cb 100644 (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)
        }
index 67bd6a4c6705704075f3a05776ce10bf71151f34..d5f7c9c4b3c713218d8f5d3c43ae63b7140b0e66 100644 (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
index 4f519aa0ee6dc3a053b914b894548946071852f4..5914efe6d8f44c2515a8060b726bc84f2d66c858 100644 (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"),
index 6acef31018767be4af71cebce8a7d09791fd8eca..93c0867d19cd71a7978a57fe7f36496adf0a0b6a 100644 (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>
 
diff --git a/src/cmd/go/testdata/script/cover_build_pkg_select.txt b/src/cmd/go/testdata/script/cover_build_pkg_select.txt
new file mode 100644 (file)
index 0000000..447ca77
--- /dev/null
@@ -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
+}
+
+
diff --git a/src/cmd/go/testdata/script/cover_build_simple.txt b/src/cmd/go/testdata/script/cover_build_simple.txt
new file mode 100644 (file)
index 0000000..b61e631
--- /dev/null
@@ -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
diff --git a/src/cmd/go/testdata/script/cover_test_pkgselect.txt b/src/cmd/go/testdata/script/cover_test_pkgselect.txt
new file mode 100644 (file)
index 0000000..97a1d2c
--- /dev/null
@@ -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
+}
index 62ee5ac9c0fb05394968f0589fa8054dcd2c0853..b52e53a9268dd7d9a9619579d4a3689f1cca4f85 100644 (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 != "" {
diff --git a/src/testing/newcover.go b/src/testing/newcover.go
new file mode 100644 (file)
index 0000000..e90b9c9
--- /dev/null
@@ -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)
+       }
+}
index 81268ec61f66683eb1cd808b09f3c9e08fe7176e..7e86faf9508cee2c310b31fce6503022c26d305f 100644 (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()
        }
 }