From 87db4ffadac291bc878cb892e05601610ca68ef5 Mon Sep 17 00:00:00 2001 From: Than McIntosh Date: Mon, 11 Oct 2021 07:34:20 -0400 Subject: [PATCH] runtime/coverage: runtime routines to emit coverage data 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 TryBot-Result: Gopher Robot Run-TryBot: Than McIntosh --- src/cmd/go/internal/work/gc.go | 20 +- src/go/build/deps_test.go | 10 +- src/internal/coverage/rtcov/rtcov.go | 9 + src/runtime/coverage/dummy.s | 8 + src/runtime/coverage/emit.go | 611 +++++++++++++++++++++++++++ src/runtime/coverage/hooks.go | 42 ++ src/runtime/coverage/testsupport.go | 207 +++++++++ src/runtime/covercounter.go | 26 ++ src/runtime/covermeta.go | 30 +- 9 files changed, 932 insertions(+), 31 deletions(-) create mode 100644 src/runtime/coverage/dummy.s create mode 100644 src/runtime/coverage/emit.go create mode 100644 src/runtime/coverage/hooks.go create mode 100644 src/runtime/coverage/testsupport.go create mode 100644 src/runtime/covercounter.go diff --git a/src/cmd/go/internal/work/gc.go b/src/cmd/go/internal/work/gc.go index 8429529115..b7fa03205b 100644 --- a/src/cmd/go/internal/work/gc.go +++ b/src/cmd/go/internal/work/gc.go @@ -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. diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index 18f66ae975..d1aeb00947 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -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". diff --git a/src/internal/coverage/rtcov/rtcov.go b/src/internal/coverage/rtcov/rtcov.go index 38dbae6c82..bbb93acced 100644 --- a/src/internal/coverage/rtcov/rtcov.go +++ b/src/internal/coverage/rtcov/rtcov.go @@ -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 index 0000000000..75928593a0 --- /dev/null +++ b/src/runtime/coverage/dummy.s @@ -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 index 0000000000..99d23dec10 --- /dev/null +++ b/src/runtime/coverage/emit.go @@ -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("", 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(" /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 index 0000000000..a9fbf9d7dd --- /dev/null +++ b/src/runtime/coverage/hooks.go @@ -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 index 0000000000..0d0605c0f2 --- /dev/null +++ b/src/runtime/coverage/testsupport.go @@ -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 index 0000000000..72842bdd94 --- /dev/null +++ b/src/runtime/covercounter.go @@ -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 +} diff --git a/src/runtime/covermeta.go b/src/runtime/covermeta.go index 90bc20f45b..54ef42ae1f 100644 --- a/src/runtime/covermeta.go +++ b/src/runtime/covermeta.go @@ -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(" /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 +} -- 2.48.1