diff options
Diffstat (limited to 'src/cmd/internal/cov')
-rw-r--r-- | src/cmd/internal/cov/covcmd/cmddefs.go | 97 | ||||
-rw-r--r-- | src/cmd/internal/cov/mreader.go | 85 | ||||
-rw-r--r-- | src/cmd/internal/cov/read_test.go | 102 | ||||
-rw-r--r-- | src/cmd/internal/cov/readcovdata.go | 277 | ||||
-rw-r--r-- | src/cmd/internal/cov/testdata/small.go | 7 |
5 files changed, 568 insertions, 0 deletions
diff --git a/src/cmd/internal/cov/covcmd/cmddefs.go b/src/cmd/internal/cov/covcmd/cmddefs.go new file mode 100644 index 0000000..cb848d3 --- /dev/null +++ b/src/cmd/internal/cov/covcmd/cmddefs.go @@ -0,0 +1,97 @@ +// 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. + +package covcmd + +import ( + "crypto/sha256" + "fmt" + "internal/coverage" +) + +// CoverPkgConfig is a bundle of information passed from the Go +// command to the cover command during "go build -cover" runs. The +// Go command creates and fills in a struct as below, then passes +// file containing the encoded JSON for the struct to the "cover" +// tool when instrumenting the source files in a Go package. +type CoverPkgConfig struct { + // File into which cmd/cover should emit summary info + // when instrumentation is complete. + OutConfig string + + // Import path for the package being instrumented. + PkgPath string + + // Package name. + PkgName string + + // Instrumentation granularity: one of "perfunc" or "perblock" (default) + Granularity string + + // Module path for this package (empty if no go.mod in use) + ModulePath string + + // Local mode indicates we're doing a coverage build or test of a + // package selected via local import path, e.g. "./..." or + // "./foo/bar" as opposed to a non-relative import path. See the + // corresponding field in cmd/go's PackageInternal struct for more + // info. + Local bool + + // EmitMetaFile if non-empty is the path to which the cover tool should + // directly emit a coverage meta-data file for the package, if the + // package has any functions in it. The go command will pass in a value + // here if we've been asked to run "go test -cover" on a package that + // doesn't have any *_test.go files. + EmitMetaFile string +} + +// CoverFixupConfig contains annotations/notes generated by the +// cmd/cover tool (during instrumentation) to be passed on to the +// compiler when the instrumented code is compiled. The cmd/cover tool +// creates a struct of this type, JSON-encodes it, and emits the +// result to a file, which the Go command then passes to the compiler +// when the instrumented package is built. +type CoverFixupConfig struct { + // Name of the variable (created by cmd/cover) containing the + // encoded meta-data for the package. + MetaVar string + + // Length of the meta-data. + MetaLen int + + // Hash computed by cmd/cover of the meta-data. + MetaHash string + + // Instrumentation strategy. For now this is always set to + // "normal", but in the future we may add new values (for example, + // if panic paths are instrumented, or if the instrumenter + // eliminates redundant counters). + Strategy string + + // Prefix assigned to the names of counter variables generated + // during instrumentation by cmd/cover. + CounterPrefix string + + // Name chosen for the package ID variable generated during + // instrumentation. + PkgIdVar string + + // Counter mode (e.g. set/count/atomic) + CounterMode string + + // Counter granularity (perblock or perfunc). + CounterGranularity string +} + +// MetaFileForPackage returns the expected name of the meta-data file +// for the package whose import path is 'importPath' in cases where +// we're using meta-data generated by the cover tool, as opposed to a +// meta-data file created at runtime. +func MetaFileForPackage(importPath string) string { + var r [32]byte + sum := sha256.Sum256([]byte(importPath)) + copy(r[:], sum[:]) + return coverage.MetaFilePref + fmt.Sprintf(".%x", r) +} diff --git a/src/cmd/internal/cov/mreader.go b/src/cmd/internal/cov/mreader.go new file mode 100644 index 0000000..30f53d6 --- /dev/null +++ b/src/cmd/internal/cov/mreader.go @@ -0,0 +1,85 @@ +// 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. + +package cov + +import ( + "cmd/internal/bio" + "io" + "os" +) + +// This file contains the helper "MReader", a wrapper around bio plus +// an "mmap'd read-only" view of the file obtained from bio.SliceRO(). +// MReader is designed to implement the io.ReaderSeeker interface. +// Since bio.SliceOS() is not guaranteed to succeed, MReader falls back +// on explicit reads + seeks provided by bio.Reader if needed. + +type MReader struct { + f *os.File + rdr *bio.Reader + fileView []byte + off int64 +} + +func NewMreader(f *os.File) (*MReader, error) { + rdr := bio.NewReader(f) + fi, err := f.Stat() + if err != nil { + return nil, err + } + r := MReader{ + f: f, + rdr: rdr, + fileView: rdr.SliceRO(uint64(fi.Size())), + } + return &r, nil +} + +func (r *MReader) Read(p []byte) (int, error) { + if r.fileView != nil { + amt := len(p) + toread := r.fileView[r.off:] + if len(toread) < 1 { + return 0, io.EOF + } + if len(toread) < amt { + amt = len(toread) + } + copy(p, toread) + r.off += int64(amt) + return amt, nil + } + return io.ReadFull(r.rdr, p) +} + +func (r *MReader) ReadByte() (byte, error) { + if r.fileView != nil { + toread := r.fileView[r.off:] + if len(toread) < 1 { + return 0, io.EOF + } + rv := toread[0] + r.off++ + return rv, nil + } + return r.rdr.ReadByte() +} + +func (r *MReader) Seek(offset int64, whence int) (int64, error) { + if r.fileView == nil { + return r.rdr.MustSeek(offset, whence), nil + } + switch whence { + case io.SeekStart: + r.off = offset + return offset, nil + case io.SeekCurrent: + return r.off, nil + case io.SeekEnd: + r.off = int64(len(r.fileView)) + offset + return r.off, nil + } + panic("other modes not implemented") +} diff --git a/src/cmd/internal/cov/read_test.go b/src/cmd/internal/cov/read_test.go new file mode 100644 index 0000000..fa2151a --- /dev/null +++ b/src/cmd/internal/cov/read_test.go @@ -0,0 +1,102 @@ +// 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. + +package cov_test + +import ( + "cmd/internal/cov" + "fmt" + "internal/coverage" + "internal/coverage/decodecounter" + "internal/coverage/decodemeta" + "internal/coverage/pods" + "internal/goexperiment" + "internal/testenv" + "os" + "path/filepath" + "testing" +) + +// visitor implements the CovDataVisitor interface in a very stripped +// down way, just keeps track of interesting events. +type visitor struct { + metaFileCount int + counterFileCount int + funcCounterData int + metaFuncCount int +} + +func (v *visitor) BeginPod(p pods.Pod) {} +func (v *visitor) EndPod(p pods.Pod) {} +func (v *visitor) VisitMetaDataFile(mdf string, mfr *decodemeta.CoverageMetaFileReader) { + v.metaFileCount++ +} +func (v *visitor) BeginCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) { + v.counterFileCount++ +} +func (v *visitor) EndCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) {} +func (v *visitor) VisitFuncCounterData(payload decodecounter.FuncPayload) { v.funcCounterData++ } +func (v *visitor) EndCounters() {} +func (v *visitor) BeginPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) {} +func (v *visitor) EndPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) {} +func (v *visitor) VisitFunc(pkgIdx uint32, fnIdx uint32, fd *coverage.FuncDesc) { v.metaFuncCount++ } +func (v *visitor) Finish() {} + +func TestIssue58411(t *testing.T) { + testenv.MustHaveGoBuild(t) + if !goexperiment.CoverageRedesign { + t.Skipf("skipping since this test requires 'go build -cover'") + } + + // Build a tiny test program with -cover. Smallness is important; + // it is one of the factors that triggers issue 58411. + d := t.TempDir() + exepath := filepath.Join(d, "small.exe") + path := filepath.Join("testdata", "small.go") + cmd := testenv.Command(t, testenv.GoToolPath(t), "build", + "-o", exepath, "-cover", path) + b, err := cmd.CombinedOutput() + if len(b) != 0 { + t.Logf("## build output:\n%s", b) + } + if err != nil { + t.Fatalf("build error: %v", err) + } + + // Run to produce coverage data. Note the large argument; we need a large + // argument (more than 4k) to trigger the bug, but the overall file + // has to remain small (since large files will be read with mmap). + covdir := filepath.Join(d, "covdata") + if err = os.Mkdir(covdir, 0777); err != nil { + t.Fatalf("creating covdir: %v", err) + } + large := fmt.Sprintf("%07999d", 0) + cmd = testenv.Command(t, exepath, "1", "2", "3", large) + cmd.Dir = covdir + cmd.Env = append(os.Environ(), "GOCOVERDIR="+covdir) + b, err = cmd.CombinedOutput() + if err != nil { + t.Logf("## run output:\n%s", b) + t.Fatalf("build error: %v", err) + } + + vis := &visitor{} + + // Read resulting coverage data. Without the fix, this would + // yield a "short read" error. + const verbosityLevel = 0 + const flags = 0 + cdr := cov.MakeCovDataReader(vis, []string{covdir}, verbosityLevel, flags, nil) + err = cdr.Visit() + if err != nil { + t.Fatalf("visit failed: %v", err) + } + + // make sure we saw a few things just for grins + const want = "{metaFileCount:1 counterFileCount:1 funcCounterData:1 metaFuncCount:1}" + got := fmt.Sprintf("%+v", *vis) + if want != got { + t.Errorf("visitor contents: want %v got %v\n", want, got) + } +} diff --git a/src/cmd/internal/cov/readcovdata.go b/src/cmd/internal/cov/readcovdata.go new file mode 100644 index 0000000..086be40 --- /dev/null +++ b/src/cmd/internal/cov/readcovdata.go @@ -0,0 +1,277 @@ +// 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. + +package cov + +import ( + "cmd/internal/bio" + "fmt" + "internal/coverage" + "internal/coverage/decodecounter" + "internal/coverage/decodemeta" + "internal/coverage/pods" + "io" + "os" +) + +// CovDataReader is a general-purpose helper/visitor object for +// reading coverage data files in a structured way. Clients create a +// CovDataReader to process a given collection of coverage data file +// directories, then pass in a visitor object with methods that get +// invoked at various important points. CovDataReader is intended +// to facilitate common coverage data file operations such as +// merging or intersecting data files, analyzing data files, or +// dumping data files. +type CovDataReader struct { + vis CovDataVisitor + indirs []string + matchpkg func(name string) bool + flags CovDataReaderFlags + err error + verbosityLevel int +} + +// MakeCovDataReader creates a CovDataReader object to process the +// given set of input directories. Here 'vis' is a visitor object +// providing methods to be invoked as we walk through the data, +// 'indirs' is the set of coverage data directories to examine, +// 'verbosityLevel' controls the level of debugging trace messages +// (zero for off, higher for more output), 'flags' stores flags that +// indicate what to do if errors are detected, and 'matchpkg' is a +// caller-provided function that can be used to select specific +// packages by name (if nil, then all packages are included). +func MakeCovDataReader(vis CovDataVisitor, indirs []string, verbosityLevel int, flags CovDataReaderFlags, matchpkg func(name string) bool) *CovDataReader { + return &CovDataReader{ + vis: vis, + indirs: indirs, + matchpkg: matchpkg, + verbosityLevel: verbosityLevel, + flags: flags, + } +} + +// CovDataVisitor defines hooks for clients of CovDataReader. When the +// coverage data reader makes its way through a coverage meta-data +// file and counter data files, it will invoke the methods below to +// hand off info to the client. The normal sequence of expected +// visitor method invocations is: +// +// for each pod P { +// BeginPod(p) +// let MF be the meta-data file for P +// VisitMetaDataFile(MF) +// for each counter data file D in P { +// BeginCounterDataFile(D) +// for each live function F in D { +// VisitFuncCounterData(F) +// } +// EndCounterDataFile(D) +// } +// EndCounters(MF) +// for each package PK in MF { +// BeginPackage(PK) +// if <PK matched according to package pattern and/or modpath> { +// for each function PF in PK { +// VisitFunc(PF) +// } +// } +// EndPackage(PK) +// } +// EndPod(p) +// } +// Finish() + +type CovDataVisitor interface { + // Invoked at the start and end of a given pod (a pod here is a + // specific coverage meta-data files with the counter data files + // that correspond to it). + BeginPod(p pods.Pod) + EndPod(p pods.Pod) + + // Invoked when the reader is starting to examine the meta-data + // file for a pod. Here 'mdf' is the path of the file, and 'mfr' + // is an open meta-data reader. + VisitMetaDataFile(mdf string, mfr *decodemeta.CoverageMetaFileReader) + + // Invoked when the reader processes a counter data file, first + // the 'begin' method at the start, then the 'end' method when + // we're done with the file. + BeginCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) + EndCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) + + // Invoked once for each live function in the counter data file. + VisitFuncCounterData(payload decodecounter.FuncPayload) + + // Invoked when we've finished processing the counter files in a + // POD (e.g. no more calls to VisitFuncCounterData). + EndCounters() + + // Invoked for each package in the meta-data file for the pod, + // first the 'begin' method when processing of the package starts, + // then the 'end' method when we're done + BeginPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) + EndPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) + + // Invoked for each function the package being visited. + VisitFunc(pkgIdx uint32, fnIdx uint32, fd *coverage.FuncDesc) + + // Invoked when all counter + meta-data file processing is complete. + Finish() +} + +type CovDataReaderFlags uint32 + +const ( + CovDataReaderNoFlags CovDataReaderFlags = 0 + PanicOnError = 1 << iota + PanicOnWarning +) + +func (r *CovDataReader) Visit() error { + podlist, err := pods.CollectPods(r.indirs, false) + if err != nil { + return fmt.Errorf("reading inputs: %v", err) + } + if len(podlist) == 0 { + r.warn("no applicable files found in input directories") + } + for _, p := range podlist { + if err := r.visitPod(p); err != nil { + return err + } + } + r.vis.Finish() + return nil +} + +func (r *CovDataReader) verb(vlevel int, s string, a ...interface{}) { + if r.verbosityLevel >= vlevel { + fmt.Fprintf(os.Stderr, s, a...) + fmt.Fprintf(os.Stderr, "\n") + } +} + +func (r *CovDataReader) warn(s string, a ...interface{}) { + fmt.Fprintf(os.Stderr, "warning: ") + fmt.Fprintf(os.Stderr, s, a...) + fmt.Fprintf(os.Stderr, "\n") + if (r.flags & PanicOnWarning) != 0 { + panic("unexpected warning") + } +} + +func (r *CovDataReader) fatal(s string, a ...interface{}) error { + if r.err != nil { + return nil + } + errstr := "error: " + fmt.Sprintf(s, a...) + "\n" + if (r.flags & PanicOnError) != 0 { + fmt.Fprintf(os.Stderr, "%s", errstr) + panic("fatal error") + } + r.err = fmt.Errorf("%s", errstr) + return r.err +} + +// visitPod examines a coverage data 'pod', that is, a meta-data file and +// zero or more counter data files that refer to that meta-data file. +func (r *CovDataReader) visitPod(p pods.Pod) error { + r.verb(1, "visiting pod: metafile %s with %d counter files", + p.MetaFile, len(p.CounterDataFiles)) + r.vis.BeginPod(p) + + // Open meta-file + f, err := os.Open(p.MetaFile) + if err != nil { + return r.fatal("unable to open meta-file %s", p.MetaFile) + } + defer f.Close() + br := bio.NewReader(f) + fi, err := f.Stat() + if err != nil { + return r.fatal("unable to stat metafile %s: %v", p.MetaFile, err) + } + fileView := br.SliceRO(uint64(fi.Size())) + br.MustSeek(0, io.SeekStart) + + r.verb(1, "fileView for pod is length %d", len(fileView)) + + var mfr *decodemeta.CoverageMetaFileReader + mfr, err = decodemeta.NewCoverageMetaFileReader(f, fileView) + if err != nil { + return r.fatal("decoding meta-file %s: %s", p.MetaFile, err) + } + r.vis.VisitMetaDataFile(p.MetaFile, mfr) + + // Read counter data files. + for k, cdf := range p.CounterDataFiles { + cf, err := os.Open(cdf) + if err != nil { + return r.fatal("opening counter data file %s: %s", cdf, err) + } + defer func(f *os.File) { + f.Close() + }(cf) + var mr *MReader + mr, err = NewMreader(cf) + if err != nil { + return r.fatal("creating reader for counter data file %s: %s", cdf, err) + } + var cdr *decodecounter.CounterDataReader + cdr, err = decodecounter.NewCounterDataReader(cdf, mr) + if err != nil { + return r.fatal("reading counter data file %s: %s", cdf, err) + } + r.vis.BeginCounterDataFile(cdf, cdr, p.Origins[k]) + var data decodecounter.FuncPayload + for { + ok, err := cdr.NextFunc(&data) + if err != nil { + return r.fatal("reading counter data file %s: %v", cdf, err) + } + if !ok { + break + } + r.vis.VisitFuncCounterData(data) + } + r.vis.EndCounterDataFile(cdf, cdr, p.Origins[k]) + } + r.vis.EndCounters() + + // NB: packages in the meta-file will be in dependency order (basically + // the order in which init files execute). Do we want an additional sort + // pass here, say by packagepath? + np := uint32(mfr.NumPackages()) + payload := []byte{} + for pkIdx := uint32(0); pkIdx < np; pkIdx++ { + var pd *decodemeta.CoverageMetaDataDecoder + pd, payload, err = mfr.GetPackageDecoder(pkIdx, payload) + if err != nil { + return r.fatal("reading pkg %d from meta-file %s: %s", pkIdx, p.MetaFile, err) + } + r.processPackage(p.MetaFile, pd, pkIdx) + } + r.vis.EndPod(p) + + return nil +} + +func (r *CovDataReader) processPackage(mfname string, pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) error { + if r.matchpkg != nil { + if !r.matchpkg(pd.PackagePath()) { + return nil + } + } + r.vis.BeginPackage(pd, pkgIdx) + nf := pd.NumFuncs() + var fd coverage.FuncDesc + for fidx := uint32(0); fidx < nf; fidx++ { + if err := pd.ReadFunc(fidx, &fd); err != nil { + return r.fatal("reading meta-data file %s: %v", mfname, err) + } + r.vis.VisitFunc(pkgIdx, fidx, &fd) + } + r.vis.EndPackage(pd, pkgIdx) + return nil +} diff --git a/src/cmd/internal/cov/testdata/small.go b/src/cmd/internal/cov/testdata/small.go new file mode 100644 index 0000000..d81cb70 --- /dev/null +++ b/src/cmd/internal/cov/testdata/small.go @@ -0,0 +1,7 @@ +package main + +import "os" + +func main() { + println(len(os.Args)) +} |