]> Cypherpunks repositories - gostls13.git/commitdiff
runtime/coverage: runtime routines to emit coverage data
authorThan McIntosh <thanm@google.com>
Mon, 11 Oct 2021 11:34:20 +0000 (07:34 -0400)
committerThan McIntosh <thanm@google.com>
Wed, 28 Sep 2022 11:48:10 +0000 (11:48 +0000)
This patch fleshes out the runtime support for emitting coverage data
at the end of a run of an instrumented binary. Data is emitted in the
form of a pair of files, a meta-out-file and counter-data-outfile,
each written to the dir GOCOVERDIR. The meta-out-file is emitted only
if required; no need to emit again if an existing meta-data file with
the same hash and length is present.

Updates #51430.

Change-Id: I59d20a4b8c05910c933ee29527972f8e401b1685
Reviewed-on: https://go-review.googlesource.com/c/go/+/355451
Reviewed-by: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Than McIntosh <thanm@google.com>

src/cmd/go/internal/work/gc.go
src/go/build/deps_test.go
src/internal/coverage/rtcov/rtcov.go
src/runtime/coverage/dummy.s [new file with mode: 0644]
src/runtime/coverage/emit.go [new file with mode: 0644]
src/runtime/coverage/hooks.go [new file with mode: 0644]
src/runtime/coverage/testsupport.go [new file with mode: 0644]
src/runtime/covercounter.go [new file with mode: 0644]
src/runtime/covermeta.go

index 842952911554efeec715b189436e46f44dcc4803..b7fa03205b5c37538b772109ad701246733841b7 100644 (file)
@@ -30,16 +30,16 @@ import (
 const trimPathGoRootFinal string = "$GOROOT"
 
 var runtimePackages = map[string]struct{}{
-       "internal/abi":             struct{}{},
-       "internal/bytealg":         struct{}{},
-       "internal/cpu":             struct{}{},
-       "internal/goarch":          struct{}{},
-       "internal/goos":            struct{}{},
-       "runtime":                  struct{}{},
-       "runtime/internal/atomic":  struct{}{},
-       "runtime/internal/math":    struct{}{},
-       "runtime/internal/sys":     struct{}{},
-       "runtime/internal/syscall": struct{}{},
+       "internal/abi":            struct{}{},
+       "internal/bytealg":        struct{}{},
+       "internal/coverage/rtcov": struct{}{},
+       "internal/cpu":            struct{}{},
+       "internal/goarch":         struct{}{},
+       "internal/goos":           struct{}{},
+       "runtime":                 struct{}{},
+       "runtime/internal/atomic": struct{}{},
+       "runtime/internal/math":   struct{}{},
+       "runtime/internal/sys":    struct{}{},
 }
 
 // The Go toolchain.
index 18f66ae975fe7cc5ae18cc50b092d61a95ca4579..d1aeb00947e375d6b60ff24d6d96ce756afeeb81 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2012 The Go Authors. All rights reserved.
+// 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.
 
@@ -584,6 +584,14 @@ var depsRules = `
     FMT, internal/coverage, os,
     path/filepath, regexp, sort, strconv
     < internal/coverage/pods;
+
+    FMT, bufio, crypto/md5, encoding/binary, runtime/debug,
+    internal/coverage, internal/coverage/cmerge,
+    internal/coverage/cformat, internal/coverage/calloc,
+    internal/coverage/decodecounter, internal/coverage/decodemeta,
+    internal/coverage/encodecounter, internal/coverage/encodemeta,
+    internal/coverage/pods, os, path/filepath, reflect, time, unsafe
+    < runtime/coverage;
 `
 
 // listStdPkgs returns the same list of packages as "go list std".
index 38dbae6c82cff881ffdb56791f4c76b66a1c2d62..bbb93acced7d62e1f641c672534debbc154855b0 100644 (file)
@@ -23,3 +23,12 @@ type CovMetaBlob struct {
        CounterMode        uint8 // coverage.CounterMode
        CounterGranularity uint8 // coverage.CounterGranularity
 }
+
+// CovCounterBlob is a container for encapsulating a counter section
+// (BSS variable) for an instrumented Go module. Here "counters"
+// points to the counter payload and "len" is the number of uint32
+// entries in the section.
+type CovCounterBlob struct {
+       Counters *uint32
+       Len      uint64
+}
diff --git a/src/runtime/coverage/dummy.s b/src/runtime/coverage/dummy.s
new file mode 100644 (file)
index 0000000..7592859
--- /dev/null
@@ -0,0 +1,8 @@
+// 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.
+
+// The runtime package uses //go:linkname to push a few functions into this
+// package but we still need a .s file so the Go tool does not pass -complete
+// to 'go tool compile' so the latter does not complain about Go functions
+// with no bodies.
diff --git a/src/runtime/coverage/emit.go b/src/runtime/coverage/emit.go
new file mode 100644 (file)
index 0000000..99d23de
--- /dev/null
@@ -0,0 +1,611 @@
+// 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 coverage
+
+import (
+       "crypto/md5"
+       "fmt"
+       "internal/coverage"
+       "internal/coverage/encodecounter"
+       "internal/coverage/encodemeta"
+       "internal/coverage/rtcov"
+       "io"
+       "os"
+       "path/filepath"
+       "reflect"
+       "runtime"
+       "time"
+       "unsafe"
+)
+
+// This file contains functions that support the writing of data files
+// emitted at the end of code coverage testing runs, from instrumented
+// executables.
+
+// getCovMetaList returns a list of meta-data blobs registered
+// for the currently executing instrumented program. It is defined in the
+// runtime.
+func getCovMetaList() []rtcov.CovMetaBlob
+
+// getCovCounterList returns a list of counter-data blobs registered
+// for the currently executing instrumented program. It is defined in the
+// runtime.
+func getCovCounterList() []rtcov.CovCounterBlob
+
+// getCovPkgMap returns a map storing the remapped package IDs for
+// hard-coded runtime packages (see internal/coverage/pkgid.go for
+// more on why hard-coded package IDs are needed). This function
+// is defined in the runtime.
+func getCovPkgMap() map[int]int
+
+// emitState holds useful state information during the emit process.
+//
+// When an instrumented program finishes execution and starts the
+// process of writing out coverage data, it's possible that an
+// existing meta-data file already exists in the output directory. In
+// this case openOutputFiles() below will leave the 'mf' field below
+// as nil. If a new meta-data file is needed, field 'mfname' will be
+// the final desired path of the meta file, 'mftmp' will be a
+// temporary file, and 'mf' will be an open os.File pointer for
+// 'mftmp'. The meta-data file payload will be written to 'mf', the
+// temp file will be then closed and renamed (from 'mftmp' to
+// 'mfname'), so as to insure that the meta-data file is created
+// atomically; we want this so that things work smoothly in cases
+// where there are several instances of a given instrumented program
+// all terminating at the same time and trying to create meta-data
+// files simultaneously.
+//
+// For counter data files there is less chance of a collision, hence
+// the openOutputFiles() stores the counter data file in 'cfname' and
+// then places the *io.File into 'cf'.
+type emitState struct {
+       mfname string   // path of final meta-data output file
+       mftmp  string   // path to meta-data temp file (if needed)
+       mf     *os.File // open os.File for meta-data temp file
+       cfname string   // path of final counter data file
+       cftmp  string   // path to counter data temp file
+       cf     *os.File // open os.File for counter data file
+       outdir string   // output directory
+
+       // List of meta-data symbols obtained from the runtime
+       metalist []rtcov.CovMetaBlob
+
+       // List of counter-data symbols obtained from the runtime
+       counterlist []rtcov.CovCounterBlob
+
+       // Table to use for remapping hard-coded pkg ids.
+       pkgmap map[int]int
+
+       // emit debug trace output
+       debug bool
+}
+
+var (
+       // finalHash is computed at init time from the list of meta-data
+       // symbols registered during init. It is used both for writing the
+       // meta-data file and counter-data files.
+       finalHash [16]byte
+       // Set to true when we've computed finalHash + finalMetaLen.
+       finalHashComputed bool
+       // Total meta-data length.
+       finalMetaLen uint64
+       // Records whether we've already attempted to write meta-data.
+       metaDataEmitAttempted bool
+       // Counter mode for this instrumented program run.
+       cmode coverage.CounterMode
+       // Counter granularity for this instrumented program run.
+       cgran coverage.CounterGranularity
+       // Cached value of GOCOVERDIR environment variable.
+       goCoverDir string
+       // Copy of os.Args made at init time, converted into map format.
+       capturedOsArgs map[string]string
+       // Flag used in tests to signal that coverage data already written.
+       covProfileAlreadyEmitted bool
+)
+
+// fileType is used to select between counter-data files and
+// meta-data files.
+type fileType int
+
+const (
+       noFile = 1 << iota
+       metaDataFile
+       counterDataFile
+)
+
+// emitMetaData emits the meta-data output file for this coverage run.
+// This entry point is intended to be invoked by the compiler from
+// an instrumented program's main package init func.
+func emitMetaData() {
+       if covProfileAlreadyEmitted {
+               return
+       }
+       ml, err := prepareForMetaEmit()
+       if err != nil {
+               fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", err)
+               if os.Getenv("GOCOVERDEBUG") != "" {
+                       panic("meta-data write failure")
+               }
+       }
+       if len(ml) == 0 {
+               fmt.Fprintf(os.Stderr, "program not built with -cover\n")
+               return
+       }
+
+       goCoverDir = os.Getenv("GOCOVERDIR")
+       if goCoverDir == "" {
+               fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n")
+               return
+       }
+
+       if err := emitMetaDataToDirectory(goCoverDir, ml); err != nil {
+               fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", err)
+               if os.Getenv("GOCOVERDEBUG") != "" {
+                       panic("meta-data write failure")
+               }
+       }
+}
+
+func modeClash(m coverage.CounterMode) bool {
+       if m == coverage.CtrModeRegOnly || m == coverage.CtrModeTestMain {
+               return false
+       }
+       if cmode == coverage.CtrModeInvalid {
+               cmode = m
+               return false
+       }
+       return cmode != m
+}
+
+func granClash(g coverage.CounterGranularity) bool {
+       if cgran == coverage.CtrGranularityInvalid {
+               cgran = g
+               return false
+       }
+       return cgran != g
+}
+
+// prepareForMetaEmit performs preparatory steps needed prior to
+// emitting a meta-data file, notably computing a final hash of
+// all meta-data blobs and capturing os args.
+func prepareForMetaEmit() ([]rtcov.CovMetaBlob, error) {
+       // Ask the runtime for the list of coverage meta-data symbols.
+       ml := getCovMetaList()
+
+       // In the normal case (go build -o prog.exe ... ; ./prog.exe)
+       // len(ml) will always be non-zero, but we check here since at
+       // some point this function will be reachable via user-callable
+       // APIs (for example, to write out coverage data from a server
+       // program that doesn't ever call os.Exit).
+       if len(ml) == 0 {
+               return nil, nil
+       }
+
+       s := &emitState{
+               metalist: ml,
+               debug:    os.Getenv("GOCOVERDEBUG") != "",
+       }
+
+       // Capture os.Args() now so as to avoid issues if args
+       // are rewritten during program execution.
+       capturedOsArgs = captureOsArgs()
+
+       if s.debug {
+               fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR"))
+               fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n")
+               for k, b := range ml {
+                       fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", k, b.PkgPath)
+                       if b.PkgID != -1 {
+                               fmt.Fprintf(os.Stderr, " hcid: %d", b.PkgID)
+                       }
+                       fmt.Fprintf(os.Stderr, "\n")
+               }
+               pm := getCovPkgMap()
+               fmt.Fprintf(os.Stderr, "=+= remap table:\n")
+               for from, to := range pm {
+                       fmt.Fprintf(os.Stderr, "=+= from %d to %d\n",
+                               uint32(from), uint32(to))
+               }
+       }
+
+       h := md5.New()
+       tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{}))
+       for _, entry := range ml {
+               if _, err := h.Write(entry.Hash[:]); err != nil {
+                       return nil, err
+               }
+               tlen += uint64(entry.Len)
+               ecm := coverage.CounterMode(entry.CounterMode)
+               if modeClash(ecm) {
+                       return nil, fmt.Errorf("coverage counter mode clash: package %s uses mode=%d, but package %s uses mode=%s\n", ml[0].PkgPath, cmode, entry.PkgPath, ecm)
+               }
+               ecg := coverage.CounterGranularity(entry.CounterGranularity)
+               if granClash(ecg) {
+                       return nil, fmt.Errorf("coverage counter granularity clash: package %s uses gran=%d, but package %s uses gran=%s\n", ml[0].PkgPath, cgran, entry.PkgPath, ecg)
+               }
+       }
+
+       // Hash mode and granularity as well.
+       h.Write([]byte(cmode.String()))
+       h.Write([]byte(cgran.String()))
+
+       // Compute final digest.
+       fh := h.Sum(nil)
+       copy(finalHash[:], fh)
+       finalHashComputed = true
+       finalMetaLen = tlen
+
+       return ml, nil
+}
+
+// emitMetaData emits the meta-data output file to the specified
+// directory, returning an error if something went wrong.
+func emitMetaDataToDirectory(outdir string, ml []rtcov.CovMetaBlob) error {
+       ml, err := prepareForMetaEmit()
+       if err != nil {
+               return err
+       }
+       if len(ml) == 0 {
+               return nil
+       }
+
+       metaDataEmitAttempted = true
+
+       s := &emitState{
+               metalist: ml,
+               debug:    os.Getenv("GOCOVERDEBUG") != "",
+               outdir:   outdir,
+       }
+
+       // Open output files.
+       if err := s.openOutputFiles(finalHash, finalMetaLen, metaDataFile); err != nil {
+               return err
+       }
+
+       // Emit meta-data file only if needed (may already be present).
+       if s.needMetaDataFile() {
+               if err := s.emitMetaDataFile(finalHash, finalMetaLen); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+// emitCounterData emits the counter data output file for this coverage run.
+// This entry point is intended to be invoked by the runtime when an
+// instrumented program is terminating or calling os.Exit().
+func emitCounterData() {
+       if goCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted {
+               return
+       }
+       if err := emitCounterDataToDirectory(goCoverDir); err != nil {
+               fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", err)
+               if os.Getenv("GOCOVERDEBUG") != "" {
+                       panic("counter-data write failure")
+               }
+       }
+}
+
+// emitMetaData emits the counter-data output file for this coverage run.
+func emitCounterDataToDirectory(outdir string) error {
+       // Ask the runtime for the list of coverage counter symbols.
+       cl := getCovCounterList()
+       if len(cl) == 0 {
+               // no work to do here.
+               return nil
+       }
+
+       if !finalHashComputed {
+               return fmt.Errorf("error: meta-data not available (binary not built with -cover?)")
+       }
+
+       // Ask the runtime for the list of coverage counter symbols.
+       pm := getCovPkgMap()
+       s := &emitState{
+               counterlist: cl,
+               pkgmap:      pm,
+               outdir:      outdir,
+               debug:       os.Getenv("GOCOVERDEBUG") != "",
+       }
+
+       // Open output file.
+       if err := s.openOutputFiles(finalHash, finalMetaLen, counterDataFile); err != nil {
+               return err
+       }
+       if s.cf == nil {
+               return fmt.Errorf("counter data output file open failed (no additional info")
+       }
+
+       // Emit counter data file.
+       if err := s.emitCounterDataFile(finalHash, s.cf); err != nil {
+               return err
+       }
+       if err := s.cf.Close(); err != nil {
+               return fmt.Errorf("closing counter data file: %v", err)
+       }
+
+       // Counter file has now been closed. Rename the temp to the
+       // final desired path.
+       if err := os.Rename(s.cftmp, s.cfname); err != nil {
+               return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.cfname, s.cftmp, err)
+       }
+
+       return nil
+}
+
+// openMetaFile determines whether we need to emit a meta-data output
+// file, or whether we can reuse the existing file in the coverage out
+// dir. It updates mfname/mftmp/mf fields in 's', returning an error
+// if something went wrong. See the comment on the emitState type
+// definition above for more on how file opening is managed.
+func (s *emitState) openMetaFile(metaHash [16]byte, metaLen uint64) error {
+
+       // Open meta-outfile for reading to see if it exists.
+       fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, metaHash)
+       s.mfname = filepath.Join(s.outdir, fn)
+       fi, err := os.Stat(s.mfname)
+       if err != nil || fi.Size() != int64(metaLen) {
+               // We need a new meta-file.
+               tname := "tmp." + fn + fmt.Sprintf("%d", time.Now().UnixNano())
+               s.mftmp = filepath.Join(s.outdir, tname)
+               s.mf, err = os.Create(s.mftmp)
+               if err != nil {
+                       return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err)
+               }
+       }
+       return nil
+}
+
+// openCounterFile opens an output file for the counter data portion
+// of a test coverage run. If updates the 'cfname' and 'cf' fields in
+// 's', returning an error if something went wrong.
+func (s *emitState) openCounterFile(metaHash [16]byte) error {
+       processID := os.Getpid()
+       fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano())
+       s.cfname = filepath.Join(s.outdir, fn)
+       s.cftmp = filepath.Join(s.outdir, "tmp."+fn)
+       var err error
+       s.cf, err = os.Create(s.cftmp)
+       if err != nil {
+               return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err)
+       }
+       return nil
+}
+
+// openOutputFiles opens output files in preparation for emitting
+// coverage data. In the case of the meta-data file, openOutputFiles
+// may determine that we can reuse an existing meta-data file in the
+// outdir, in which case it will leave the 'mf' field in the state
+// struct as nil. If a new meta-file is needed, the field 'mfname'
+// will be the final desired path of the meta file, 'mftmp' will be a
+// temporary file, and 'mf' will be an open os.File pointer for
+// 'mftmp'. The idea is that the client/caller will write content into
+// 'mf', close it, and then rename 'mftmp' to 'mfname'. This function
+// also opens the counter data output file, setting 'cf' and 'cfname'
+// in the state struct.
+func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error {
+       fi, err := os.Stat(s.outdir)
+       if err != nil {
+               return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err)
+       }
+       if !fi.IsDir() {
+               return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir)
+       }
+
+       if (which & metaDataFile) != 0 {
+               if err := s.openMetaFile(metaHash, metaLen); err != nil {
+                       return err
+               }
+       }
+       if (which & counterDataFile) != 0 {
+               if err := s.openCounterFile(metaHash); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+// emitMetaDataFile emits coverage meta-data to a previously opened
+// temporary file (s.mftmp), then renames the generated file to the
+// final path (s.mfname).
+func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error {
+       if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil {
+               return fmt.Errorf("writing %s: %v\n", s.mftmp, err)
+       }
+       if err := s.mf.Close(); err != nil {
+               return fmt.Errorf("closing meta data temp file: %v", err)
+       }
+
+       // Temp file has now been flushed and closed. Rename the temp to the
+       // final desired path.
+       if err := os.Rename(s.mftmp, s.mfname); err != nil {
+               return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err)
+       }
+
+       return nil
+}
+
+// needMetaDataFile returns TRUE if we need to emit a meta-data file
+// for this program run. It should be used only after
+// openOutputFiles() has been invoked.
+func (s *emitState) needMetaDataFile() bool {
+       return s.mf != nil
+}
+
+func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error {
+       mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w)
+
+       // Note: "sd" is re-initialized on each iteration of the loop
+       // below, and would normally be declared inside the loop, but
+       // placed here escape analysis since we capture it in bufHdr.
+       var sd []byte
+       bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd))
+
+       var blobs [][]byte
+       for _, e := range metalist {
+               bufHdr.Data = uintptr(unsafe.Pointer(e.P))
+               bufHdr.Len = int(e.Len)
+               bufHdr.Cap = int(e.Len)
+               blobs = append(blobs, sd)
+       }
+       return mfw.Write(finalHash, blobs, cmode, gran)
+}
+
+func (s *emitState) NumFuncs() (int, error) {
+       var sd []uint32
+       bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd))
+
+       totalFuncs := 0
+       for _, c := range s.counterlist {
+               bufHdr.Data = uintptr(unsafe.Pointer(c.Counters))
+               bufHdr.Len = int(c.Len)
+               bufHdr.Cap = int(c.Len)
+               for i := 0; i < len(sd); i++ {
+                       // Skip ahead until the next non-zero value.
+                       if sd[i] == 0 {
+                               continue
+                       }
+
+                       // We found a function that was executed.
+                       nCtrs := sd[i]
+                       totalFuncs++
+
+                       // Skip over this function.
+                       i += coverage.FirstCtrOffset + int(nCtrs) - 1
+               }
+       }
+       return totalFuncs, nil
+}
+
+func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error {
+       var sd []uint32
+       bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd))
+
+       dpkg := uint32(0)
+       for _, c := range s.counterlist {
+               bufHdr.Data = uintptr(unsafe.Pointer(c.Counters))
+               bufHdr.Len = int(c.Len)
+               bufHdr.Cap = int(c.Len)
+               for i := 0; i < len(sd); i++ {
+                       // Skip ahead until the next non-zero value.
+                       if sd[i] == 0 {
+                               continue
+                       }
+
+                       // We found a function that was executed.
+                       nCtrs := sd[i+coverage.NumCtrsOffset]
+                       pkgId := sd[i+coverage.PkgIdOffset]
+                       funcId := sd[i+coverage.FuncIdOffset]
+                       cst := i + coverage.FirstCtrOffset
+                       counters := sd[cst : cst+int(nCtrs)]
+
+                       if s.debug {
+                               if pkgId != dpkg {
+                                       dpkg = pkgId
+                                       fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn",
+                                               i, pkgId)
+                               }
+                               fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", i, funcId, nCtrs)
+                       }
+
+                       // Vet and/or fix up package ID. A package ID of zero
+                       // indicates that there is some new package X that is a
+                       // runtime dependency, and this package has code that
+                       // executes before its corresponding init package runs.
+                       // This is a fatal error that we should only see during
+                       // Go development (e.g. tip).
+                       ipk := int32(pkgId)
+                       if ipk == 0 {
+                               fmt.Fprintf(os.Stderr, "\n")
+                               reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
+                       } else if ipk < 0 {
+                               if newId, ok := s.pkgmap[int(ipk)]; ok {
+                                       pkgId = uint32(newId)
+                               } else {
+                                       fmt.Fprintf(os.Stderr, "\n")
+                                       reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
+                               }
+                       } else {
+                               // The package ID value stored in the counter array
+                               // has 1 added to it (so as to preclude the
+                               // possibility of a zero value ; see
+                               // runtime.addCovMeta), so subtract off 1 here to form
+                               // the real package ID.
+                               pkgId--
+                       }
+
+                       if err := f(pkgId, funcId, counters); err != nil {
+                               return err
+                       }
+
+                       // Skip over this function.
+                       i += coverage.FirstCtrOffset + int(nCtrs) - 1
+               }
+               if s.debug {
+                       fmt.Fprintf(os.Stderr, "\n")
+               }
+       }
+       return nil
+}
+
+// captureOsArgs converts os.Args() into the format we use to store
+// this info in the counter data file (counter data file "args"
+// section is a generic key-value collection). See the 'args' section
+// in internal/coverage/defs.go for more info. The args map
+// is also used to capture GOOS + GOARCH values as well.
+func captureOsArgs() map[string]string {
+       m := make(map[string]string)
+       m["argc"] = fmt.Sprintf("%d", len(os.Args))
+       for k, a := range os.Args {
+               m[fmt.Sprintf("argv%d", k)] = a
+       }
+       m["GOOS"] = runtime.GOOS
+       m["GOARCH"] = runtime.GOARCH
+       return m
+}
+
+// emitCounterDataFile emits the counter data portion of a
+// coverage output file (to the file 's.cf').
+func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error {
+       cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128)
+       if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil {
+               return err
+       }
+       return nil
+}
+
+// markProfileEmitted signals the runtime/coverage machinery that
+// coverate data output files have already been written out, and there
+// is no need to take any additional action at exit time. This
+// function is called (via linknamed reference) from the
+// coverage-related boilerplate code in _testmain.go emitted for go
+// unit tests.
+func markProfileEmitted(val bool) {
+       covProfileAlreadyEmitted = val
+}
+
+func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) {
+       metaList := getCovMetaList()
+       pkgMap := getCovPkgMap()
+
+       println("internal error in coverage meta-data tracking:")
+       println("encountered bad pkgID:", pkgID, " at slot:", slot,
+               " fnID:", fnID, " numCtrs:", nCtrs)
+       println("list of hard-coded runtime package IDs needs revising.")
+       println("[see the comment on the 'rtPkgs' var in ")
+       println(" <goroot>/src/internal/coverage/pkid.go]")
+       println("registered list:")
+       for k, b := range metaList {
+               print("slot: ", k, " path='", b.PkgPath, "' ")
+               if b.PkgID != -1 {
+                       print(" hard-coded id: ", b.PkgID)
+               }
+               println("")
+       }
+       println("remap table:")
+       for from, to := range pkgMap {
+               println("from ", from, " to ", to)
+       }
+}
diff --git a/src/runtime/coverage/hooks.go b/src/runtime/coverage/hooks.go
new file mode 100644 (file)
index 0000000..a9fbf9d
--- /dev/null
@@ -0,0 +1,42 @@
+// 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 coverage
+
+import _ "unsafe"
+
+// initHook is invoked from the main package "init" routine in
+// programs built with "-cover". This function is intended to be
+// called only by the compiler.
+//
+// If 'istest' is false, it indicates we're building a regular program
+// ("go build -cover ..."), in which case we immediately try to write
+// out the meta-data file, and register emitCounterData as an exit
+// hook.
+//
+// If 'istest' is true (indicating that the program in question is a
+// Go test binary), then we tentatively queue up both emitMetaData and
+// emitCounterData as exit hooks. In the normal case (e.g. regular "go
+// test -cover" run) the testmain.go boilerplate will run at the end
+// of the test, write out the coverage percentage, and then invoke
+// markProfileEmitted() to indicate that no more work needs to be
+// done. If however that call is never made, this is a sign that the
+// test binary is being used as a replacement binary for the tool
+// being tested, hence we do want to run exit hooks when the program
+// terminates.
+func initHook(istest bool) {
+       // Note: hooks are run in reverse registration order, so
+       // register the counter data hook before the meta-data hook
+       // (in the case where two hooks are needed).
+       runOnNonZeroExit := true
+       runtime_addExitHook(emitCounterData, runOnNonZeroExit)
+       if istest {
+               runtime_addExitHook(emitMetaData, runOnNonZeroExit)
+       } else {
+               emitMetaData()
+       }
+}
+
+//go:linkname runtime_addExitHook runtime.addExitHook
+func runtime_addExitHook(f func(), runOnNonZeroExit bool)
diff --git a/src/runtime/coverage/testsupport.go b/src/runtime/coverage/testsupport.go
new file mode 100644 (file)
index 0000000..0d0605c
--- /dev/null
@@ -0,0 +1,207 @@
+// 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 coverage
+
+import (
+       "fmt"
+       "internal/coverage"
+       "internal/coverage/calloc"
+       "internal/coverage/cformat"
+       "internal/coverage/cmerge"
+       "internal/coverage/decodecounter"
+       "internal/coverage/decodemeta"
+       "internal/coverage/pods"
+       "os"
+)
+
+// processCoverTestDir is called (via a linknamed reference) from
+// testmain code when "go test -cover" is in effect. It is not
+// intended to be used other than internally by the Go command's
+// generated code.
+func processCoverTestDir(dir string, cfile string, cm string, cpkg string) error {
+       cmode := coverage.ParseCounterMode(cm)
+       if cmode == coverage.CtrModeInvalid {
+               return fmt.Errorf("invalid counter mode %q", cm)
+       }
+
+       // Emit meta-data and counter data.
+       ml := getCovMetaList()
+       if len(ml) == 0 {
+               // This corresponds to the case where we have a package that
+               // contains test code but no functions (which is fine). In this
+               // case there is no need to emit anything.
+       } else {
+               if err := emitMetaDataToDirectory(dir, ml); err != nil {
+                       return err
+               }
+               if err := emitCounterDataToDirectory(dir); err != nil {
+                       return err
+               }
+       }
+
+       // Collect pods from test run. For the majority of cases we would
+       // expect to see a single pod here, but allow for multiple pods in
+       // case the test harness is doing extra work to collect data files
+       // from builds that it kicks off as part of the testing.
+       podlist, err := pods.CollectPods([]string{dir}, false)
+       if err != nil {
+               return fmt.Errorf("reading from %s: %v", dir, err)
+       }
+
+       // Open text output file if appropriate.
+       var tf *os.File
+       var tfClosed bool
+       if cfile != "" {
+               var err error
+               tf, err = os.Create(cfile)
+               if err != nil {
+                       return fmt.Errorf("internal error: opening coverage data output file %q: %v", cfile, err)
+               }
+               defer func() {
+                       if !tfClosed {
+                               tfClosed = true
+                               tf.Close()
+                       }
+               }()
+       }
+
+       // Read/process the pods.
+       ts := &tstate{
+               cm:    &cmerge.Merger{},
+               cf:    cformat.NewFormatter(cmode),
+               cmode: cmode,
+       }
+       for _, p := range podlist {
+               if err := ts.processPod(p); err != nil {
+                       return err
+               }
+       }
+
+       // Emit percent.
+       if err := ts.cf.EmitPercent(os.Stdout, cpkg, true); err != nil {
+               return err
+       }
+
+       // Emit text output.
+       if tf != nil {
+               if err := ts.cf.EmitTextual(tf); err != nil {
+                       return err
+               }
+               tfClosed = true
+               if err := tf.Close(); err != nil {
+                       return fmt.Errorf("closing %s: %v", cfile, err)
+               }
+       }
+
+       return nil
+}
+
+type tstate struct {
+       calloc.BatchCounterAlloc
+       cm    *cmerge.Merger
+       cf    *cformat.Formatter
+       cmode coverage.CounterMode
+}
+
+// processPod reads coverage counter data for a specific pod.
+func (ts *tstate) processPod(p pods.Pod) error {
+       // Open meta-data file
+       f, err := os.Open(p.MetaFile)
+       if err != nil {
+               return fmt.Errorf("unable to open meta-data file %s: %v", p.MetaFile, err)
+       }
+       defer func() {
+               f.Close()
+       }()
+       var mfr *decodemeta.CoverageMetaFileReader
+       mfr, err = decodemeta.NewCoverageMetaFileReader(f, nil)
+       if err != nil {
+               return fmt.Errorf("error reading meta-data file %s: %v", p.MetaFile, err)
+       }
+       newmode := mfr.CounterMode()
+       if newmode != ts.cmode {
+               return fmt.Errorf("internal error: counter mode clash: %q from test harness, %q from data file %s", ts.cmode.String(), newmode.String(), p.MetaFile)
+       }
+       newgran := mfr.CounterGranularity()
+       if err := ts.cm.SetModeAndGranularity(p.MetaFile, cmode, newgran); err != nil {
+               return err
+       }
+
+       // Read counter data files.
+       pmm := make(map[pkfunc][]uint32)
+       for _, cdf := range p.CounterDataFiles {
+               cf, err := os.Open(cdf)
+               if err != nil {
+                       return fmt.Errorf("opening counter data file %s: %s", cdf, err)
+               }
+               var cdr *decodecounter.CounterDataReader
+               cdr, err = decodecounter.NewCounterDataReader(cdf, cf)
+               if err != nil {
+                       return fmt.Errorf("reading counter data file %s: %s", cdf, err)
+               }
+               var data decodecounter.FuncPayload
+               for {
+                       ok, err := cdr.NextFunc(&data)
+                       if err != nil {
+                               return fmt.Errorf("reading counter data file %s: %v", cdf, err)
+                       }
+                       if !ok {
+                               break
+                       }
+
+                       // NB: sanity check on pkg and func IDs?
+                       key := pkfunc{pk: data.PkgIdx, fcn: data.FuncIdx}
+                       if prev, found := pmm[key]; found {
+                               // Note: no overflow reporting here.
+                               if err, _ := ts.cm.MergeCounters(data.Counters, prev); err != nil {
+                                       return fmt.Errorf("processing counter data file %s: %v", cdf, err)
+                               }
+                       }
+                       c := ts.AllocateCounters(len(data.Counters))
+                       copy(c, data.Counters)
+                       pmm[key] = c
+               }
+       }
+
+       // Visit meta-data file.
+       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 fmt.Errorf("reading pkg %d from meta-file %s: %s", pkIdx, p.MetaFile, err)
+               }
+               ts.cf.SetPackage(pd.PackagePath())
+               var fd coverage.FuncDesc
+               nf := pd.NumFuncs()
+               for fnIdx := uint32(0); fnIdx < nf; fnIdx++ {
+                       if err := pd.ReadFunc(fnIdx, &fd); err != nil {
+                               return fmt.Errorf("reading meta-data file %s: %v",
+                                       p.MetaFile, err)
+                       }
+                       key := pkfunc{pk: pkIdx, fcn: fnIdx}
+                       counters, haveCounters := pmm[key]
+                       for i := 0; i < len(fd.Units); i++ {
+                               u := fd.Units[i]
+                               // Skip units with non-zero parent (no way to represent
+                               // these in the existing format).
+                               if u.Parent != 0 {
+                                       continue
+                               }
+                               count := uint32(0)
+                               if haveCounters {
+                                       count = counters[i]
+                               }
+                               ts.cf.AddUnit(fd.Srcfile, fd.Funcname, fd.Lit, u, count)
+                       }
+               }
+       }
+       return nil
+}
+
+type pkfunc struct {
+       pk, fcn uint32
+}
diff --git a/src/runtime/covercounter.go b/src/runtime/covercounter.go
new file mode 100644 (file)
index 0000000..72842bd
--- /dev/null
@@ -0,0 +1,26 @@
+// 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 runtime
+
+import (
+       "internal/coverage/rtcov"
+       "unsafe"
+)
+
+//go:linkname runtime_coverage_getCovCounterList runtime/coverage.getCovCounterList
+func runtime_coverage_getCovCounterList() []rtcov.CovCounterBlob {
+       res := []rtcov.CovCounterBlob{}
+       u32sz := unsafe.Sizeof(uint32(0))
+       for datap := &firstmoduledata; datap != nil; datap = datap.next {
+               if datap.covctrs == datap.ecovctrs {
+                       continue
+               }
+               res = append(res, rtcov.CovCounterBlob{
+                       Counters: (*uint32)(unsafe.Pointer(datap.covctrs)),
+                       Len:      uint64((datap.ecovctrs - datap.covctrs) / u32sz),
+               })
+       }
+       return res
+}
index 90bc20f45b04f7dac62632ffbe4d032d767c225b..54ef42ae1ff440bdcde58bd3700290383f074351 100644 (file)
@@ -24,26 +24,6 @@ var covMeta struct {
        hardCodedListNeedsUpdating bool
 }
 
-func reportErrorInHardcodedList(slot int32, pkgId int32) {
-       println("internal error in coverage meta-data tracking:")
-       println("encountered bad pkg ID ", pkgId, " at slot ", slot)
-       println("list of hard-coded runtime package IDs needs revising.")
-       println("[see the comment on the 'rtPkgs' var in ")
-       println(" <goroot>/src/internal/coverage/pkid.go]")
-       println("registered list:")
-       for k, b := range covMeta.metaList {
-               print("slot: ", k, " path='", b.PkgPath, "' ")
-               if b.PkgID != -1 {
-                       print(" hard-coded id: ", b.PkgID)
-               }
-               println("")
-       }
-       println("remap table:")
-       for from, to := range covMeta.pkgMap {
-               println("from ", from, " to ", to)
-       }
-}
-
 // addCovMeta is invoked during package "init" functions by the
 // compiler when compiling for coverage instrumentation; here 'p' is a
 // meta-data blob of length 'dlen' for the package in question, 'hash'
@@ -80,3 +60,13 @@ func addCovMeta(p unsafe.Pointer, dlen uint32, hash [16]byte, pkpath string, pki
        // ID zero is reserved as invalid.
        return uint32(slot + 1)
 }
+
+//go:linkname runtime_coverage_getCovMetaList runtime/coverage.getCovMetaList
+func runtime_coverage_getCovMetaList() []rtcov.CovMetaBlob {
+       return covMeta.metaList
+}
+
+//go:linkname runtime_coverage_getCovPkgMap runtime/coverage.getCovPkgMap
+func runtime_coverage_getCovPkgMap() map[int]int {
+       return covMeta.pkgMap
+}