From: Than McIntosh Date: Mon, 11 Oct 2021 15:26:19 +0000 (-0400) Subject: cmd/go: support new hybrid coverage instrumentation X-Git-Tag: go1.20rc1~868 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=53773a5d0892be4489b4d5e91bbc8ae61000ada7;p=gostls13.git 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 Run-TryBot: Than McIntosh Reviewed-by: Bryan Mills --- diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index f8cc52343a..79410f0bad 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -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 diff --git a/src/cmd/go/go_test.go b/src/cmd/go/go_test.go index 556ba9cde5..ee1cbc15eb 100644 --- a/src/cmd/go/go_test.go +++ b/src/cmd/go/go_test.go @@ -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", diff --git a/src/cmd/go/internal/cfg/cfg.go b/src/cmd/go/internal/cfg/cfg.go index 2a1475ef2e..fbf91be604 100644 --- a/src/cmd/go/internal/cfg/cfg.go +++ b/src/cmd/go/internal/cfg/cfg.go @@ -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 diff --git a/src/cmd/go/internal/help/helpdoc.go b/src/cmd/go/internal/help/helpdoc.go index 2398260536..72abccd16b 100644 --- a/src/cmd/go/internal/help/helpdoc.go +++ b/src/cmd/go/internal/help/helpdoc.go @@ -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 diff --git a/src/cmd/go/internal/load/pkg.go b/src/cmd/go/internal/load/pkg.go index 522c372f10..3e110dcd7c 100644 --- a/src/cmd/go/internal/load/pkg.go +++ b/src/cmd/go/internal/load/pkg.go @@ -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 +} diff --git a/src/cmd/go/internal/load/test.go b/src/cmd/go/internal/load/test.go index 1abefd8ad1..0c20a23b00 100644 --- a/src/cmd/go/internal/load/test.go +++ b/src/cmd/go/internal/load/test.go @@ -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}} +} + +`) diff --git a/src/cmd/go/internal/run/run.go b/src/cmd/go/internal/run/run.go index 2804db2296..8221b0395b 100644 --- a/src/cmd/go/internal/run/run.go +++ b/src/cmd/go/internal/run/run.go @@ -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 { diff --git a/src/cmd/go/internal/test/cover.go b/src/cmd/go/internal/test/cover.go index 657d22a6b4..f614458dc4 100644 --- a/src/cmd/go/internal/test/cover.go +++ b/src/cmd/go/internal/test/cover.go @@ -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) diff --git a/src/cmd/go/internal/test/flagdefs_test.go b/src/cmd/go/internal/test/flagdefs_test.go index 64317fd04e..337f136d06 100644 --- a/src/cmd/go/internal/test/flagdefs_test.go +++ b/src/cmd/go/internal/test/flagdefs_test.go @@ -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] { diff --git a/src/cmd/go/internal/test/test.go b/src/cmd/go/internal/test/test.go index 7248445796..c262362d4d 100644 --- a/src/cmd/go/internal/test/test.go +++ b/src/cmd/go/internal/test/test.go @@ -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 diff --git a/src/cmd/go/internal/test/testflag.go b/src/cmd/go/internal/test/testflag.go index 8f5ab38d9d..2b2bd87732 100644 --- a/src/cmd/go/internal/test/testflag.go +++ b/src/cmd/go/internal/test/testflag.go @@ -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...) } diff --git a/src/cmd/go/internal/work/build.go b/src/cmd/go/internal/work/build.go index bce923a459..2acc153c3c 100644 --- a/src/cmd/go/internal/work/build.go +++ b/src/cmd/go/internal/work/build.go @@ -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 +} diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go index 1dd23ebdda..198d6081bb 100644 --- a/src/cmd/go/internal/work/exec.go +++ b/src/cmd/go/internal/work/exec.go @@ -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= -mode=b.coverMode -var="varName" -o +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 diff --git a/src/cmd/go/internal/work/gc.go b/src/cmd/go/internal/work/gc.go index b7fa03205b..9c50975f99 100644 --- a/src/cmd/go/internal/work/gc.go +++ b/src/cmd/go/internal/work/gc.go @@ -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) } diff --git a/src/cmd/go/internal/work/init.go b/src/cmd/go/internal/work/init.go index 67bd6a4c67..d5f7c9c4b3 100644 --- a/src/cmd/go/internal/work/init.go +++ b/src/cmd/go/internal/work/init.go @@ -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 diff --git a/src/cmd/go/script_test.go b/src/cmd/go/script_test.go index 4f519aa0ee..5914efe6d8 100644 --- a/src/cmd/go/script_test.go +++ b/src/cmd/go/script_test.go @@ -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"), diff --git a/src/cmd/go/testdata/script/README b/src/cmd/go/testdata/script/README index 6acef31018..93c0867d19 100644 --- a/src/cmd/go/testdata/script/README +++ b/src/cmd/go/testdata/script/README @@ -39,6 +39,7 @@ Scripts also have access to these other environment variables: PATH= TMPDIR=$WORK/tmp GODEBUG= + GOCOVERDIR= devnull= goversion= 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 index 0000000000..447ca7788c --- /dev/null +++ b/src/cmd/go/testdata/script/cover_build_pkg_select.txt @@ -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 index 0000000000..b61e631abd --- /dev/null +++ b/src/cmd/go/testdata/script/cover_build_simple.txt @@ -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 index 0000000000..97a1d2cbbb --- /dev/null +++ b/src/cmd/go/testdata/script/cover_test_pkgselect.txt @@ -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 +} diff --git a/src/testing/cover.go b/src/testing/cover.go index 62ee5ac9c0..b52e53a926 100644 --- a/src/testing/cover.go +++ b/src/testing/cover.go @@ -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 index 0000000000..e90b9c9805 --- /dev/null +++ b/src/testing/newcover.go @@ -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) + } +} diff --git a/src/testing/testing.go b/src/testing/testing.go index 81268ec61f..7e86faf950 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -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() } }