runtime/coverage: remove uses of //go:linkname

Move code to internal/coverage/cfile, making it possible to
access directly from testing/internal/testdeps, so that we can
avoid needing //go:linkname hacks.

For #67401.

Change-Id: I10b23a9970164afd2165e718ef3b2d9e86783883
Reviewed-on: https://go-review.googlesource.com/c/go/+/585820
Auto-Submit: Russ Cox <rsc@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Russ Cox 2024-05-15 16:06:23 -04:00 committed by Gopher Robot
parent 647870becc
commit 180ea45566
20 changed files with 160 additions and 132 deletions

View file

@ -0,0 +1,153 @@
// 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 cfile
import (
"fmt"
"internal/coverage"
"io"
"sync/atomic"
"unsafe"
)
// WriteMetaDir implements [runtime/coverage.WriteMetaDir].
func WriteMetaDir(dir string) error {
if !finalHashComputed {
return fmt.Errorf("error: no meta-data available (binary not built with -cover?)")
}
return emitMetaDataToDirectory(dir, getCovMetaList())
}
// WriteMeta implements [runtime/coverage.WriteMeta].
func WriteMeta(w io.Writer) error {
if w == nil {
return fmt.Errorf("error: nil writer in WriteMeta")
}
if !finalHashComputed {
return fmt.Errorf("error: no meta-data available (binary not built with -cover?)")
}
ml := getCovMetaList()
return writeMetaData(w, ml, cmode, cgran, finalHash)
}
// WriteCountersDir implements [runtime/coverage.WriteCountersDir].
func WriteCountersDir(dir string) error {
if cmode != coverage.CtrModeAtomic {
return fmt.Errorf("WriteCountersDir invoked for program built with -covermode=%s (please use -covermode=atomic)", cmode.String())
}
return emitCounterDataToDirectory(dir)
}
// WriteCounters implements [runtime/coverage.WriteCounters].
func WriteCounters(w io.Writer) error {
if w == nil {
return fmt.Errorf("error: nil writer in WriteCounters")
}
if cmode != coverage.CtrModeAtomic {
return fmt.Errorf("WriteCounters invoked for program built with -covermode=%s (please use -covermode=atomic)", cmode.String())
}
// Ask the runtime for the list of coverage counter symbols.
cl := getCovCounterList()
if len(cl) == 0 {
return fmt.Errorf("program not built with -cover")
}
if !finalHashComputed {
return fmt.Errorf("meta-data not written yet, unable to write counter data")
}
pm := getCovPkgMap()
s := &emitState{
counterlist: cl,
pkgmap: pm,
}
return s.emitCounterDataToWriter(w)
}
// ClearCounters implements [runtime/coverage.ClearCounters].
func ClearCounters() error {
cl := getCovCounterList()
if len(cl) == 0 {
return fmt.Errorf("program not built with -cover")
}
if cmode != coverage.CtrModeAtomic {
return fmt.Errorf("ClearCounters invoked for program built with -covermode=%s (please use -covermode=atomic)", cmode.String())
}
// Implementation note: this function would be faster and simpler
// if we could just zero out the entire counter array, but for the
// moment we go through and zero out just the slots in the array
// corresponding to the counter values. We do this to avoid the
// following bad scenario: suppose that a user builds their Go
// program with "-cover", and that program has a function (call it
// main.XYZ) that invokes ClearCounters:
//
// func XYZ() {
// ... do some stuff ...
// coverage.ClearCounters()
// if someCondition { <<--- HERE
// ...
// }
// }
//
// At the point where ClearCounters executes, main.XYZ has not yet
// finished running, thus as soon as the call returns the line
// marked "HERE" above will trigger the writing of a non-zero
// value into main.XYZ's counter slab. However since we've just
// finished clearing the entire counter segment, we will have lost
// the values in the prolog portion of main.XYZ's counter slab
// (nctrs, pkgid, funcid). This means that later on at the end of
// program execution as we walk through the entire counter array
// for the program looking for executed functions, we'll zoom past
// main.XYZ's prolog (which was zero'd) and hit the non-zero
// counter value corresponding to the "HERE" block, which will
// then be interpreted as the start of another live function.
// Things will go downhill from there.
//
// This same scenario is also a potential risk if the program is
// running on an architecture that permits reordering of
// writes/stores, since the inconsistency described above could
// arise here. Example scenario:
//
// func ABC() {
// ... // prolog
// if alwaysTrue() {
// XYZ() // counter update here
// }
// }
//
// In the instrumented version of ABC, the prolog of the function
// will contain a series of stores to the initial portion of the
// counter array to write number-of-counters, pkgid, funcid. Later
// in the function there is also a store to increment a counter
// for the block containing the call to XYZ(). If the CPU is
// allowed to reorder stores and decides to issue the XYZ store
// before the prolog stores, this could be observable as an
// inconsistency similar to the one above. Hence the requirement
// for atomic counter mode: according to package atomic docs,
// "...operations that happen in a specific order on one thread,
// will always be observed to happen in exactly that order by
// another thread". Thus we can be sure that there will be no
// inconsistency when reading the counter array from the thread
// running ClearCounters.
for _, c := range cl {
sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), int(c.Len))
for i := 0; i < len(sd); i++ {
// Skip ahead until the next non-zero value.
sdi := sd[i].Load()
if sdi == 0 {
continue
}
// We found a function that was executed; clear its counters.
nCtrs := sdi
for j := 0; j < int(nCtrs); j++ {
sd[i+coverage.FirstCtrOffset+j].Store(0)
}
// Move to next function.
i += coverage.FirstCtrOffset + int(nCtrs) - 1
}
}
return nil
}

View file

@ -0,0 +1,615 @@
// 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 cfile implements management of coverage files.
// It provides functionality exported in runtime/coverage as well as
// additional functionality used directly by package testing
// through testing/internal/testdeps.
package cfile
import (
"crypto/md5"
"fmt"
"internal/coverage"
"internal/coverage/encodecounter"
"internal/coverage/encodemeta"
"internal/coverage/rtcov"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"sync/atomic"
"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.
//go:linkname getCovMetaList
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.
//go:linkname getCovCounterList
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.
//go:linkname getCovPkgMap
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
}
// emitMetaDataToDirectory 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")
}
}
}
// emitCounterDataToDirectory 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
}
// emitCounterDataToWriter emits counter data for this coverage run to an io.Writer.
func (s *emitState) emitCounterDataToWriter(w io.Writer) error {
if err := s.emitCounterDataFile(finalHash, w); err != nil {
return 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 + strconv.FormatInt(time.Now().UnixNano(), 10)
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)
var blobs [][]byte
for _, e := range metalist {
sd := unsafe.Slice(e.P, int(e.Len))
blobs = append(blobs, sd)
}
return mfw.Write(finalHash, blobs, cmode, gran)
}
func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error {
var tcounters []uint32
rdCounters := func(actrs []atomic.Uint32, ctrs []uint32) []uint32 {
ctrs = ctrs[:0]
for i := range actrs {
ctrs = append(ctrs, actrs[i].Load())
}
return ctrs
}
dpkg := uint32(0)
for _, c := range s.counterlist {
sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), int(c.Len))
for i := 0; i < len(sd); i++ {
// Skip ahead until the next non-zero value.
sdi := sd[i].Load()
if sdi == 0 {
continue
}
// We found a function that was executed.
nCtrs := sd[i+coverage.NumCtrsOffset].Load()
pkgId := sd[i+coverage.PkgIdOffset].Load()
funcId := sd[i+coverage.FuncIdOffset].Load()
cst := i + coverage.FirstCtrOffset
counters := sd[cst : cst+int(nCtrs)]
// Check to make sure that we have at least one live
// counter. See the implementation note in ClearCoverageCounters
// for a description of why this is needed.
isLive := false
for i := 0; i < len(counters); i++ {
if counters[i].Load() != 0 {
isLive = true
break
}
}
if !isLive {
// Skip this function.
i += coverage.FirstCtrOffset + int(nCtrs) - 1
continue
}
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--
}
tcounters = rdCounters(counters, tcounters)
if err := f(pkgId, funcId, tcounters); 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"] = strconv.Itoa(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 coverage machinery that
// coverage 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 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)
}
}

View file

@ -0,0 +1,550 @@
// 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 cfile
import (
"fmt"
"internal/coverage"
"internal/goexperiment"
"internal/platform"
"internal/testenv"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// Set to true for debugging (linux only).
const fixedTestDir = false
func TestCoverageApis(t *testing.T) {
if testing.Short() {
t.Skipf("skipping test: too long for short mode")
}
if !goexperiment.CoverageRedesign {
t.Skipf("skipping new coverage tests (experiment not enabled)")
}
testenv.MustHaveGoBuild(t)
dir := t.TempDir()
if fixedTestDir {
dir = "/tmp/qqqzzz"
os.RemoveAll(dir)
mkdir(t, dir)
}
// Build harness. We need two copies of the harness, one built
// with -covermode=atomic and one built non-atomic.
bdir1 := mkdir(t, filepath.Join(dir, "build1"))
hargs1 := []string{"-covermode=atomic", "-coverpkg=all"}
atomicHarnessPath := buildHarness(t, bdir1, hargs1)
nonAtomicMode := testing.CoverMode()
if testing.CoverMode() == "atomic" {
nonAtomicMode = "set"
}
bdir2 := mkdir(t, filepath.Join(dir, "build2"))
hargs2 := []string{"-coverpkg=all", "-covermode=" + nonAtomicMode}
nonAtomicHarnessPath := buildHarness(t, bdir2, hargs2)
t.Logf("atomic harness path is %s", atomicHarnessPath)
t.Logf("non-atomic harness path is %s", nonAtomicHarnessPath)
// Sub-tests for each API we want to inspect, plus
// extras for error testing.
t.Run("emitToDir", func(t *testing.T) {
t.Parallel()
testEmitToDir(t, atomicHarnessPath, dir)
})
t.Run("emitToWriter", func(t *testing.T) {
t.Parallel()
testEmitToWriter(t, atomicHarnessPath, dir)
})
t.Run("emitToNonexistentDir", func(t *testing.T) {
t.Parallel()
testEmitToNonexistentDir(t, atomicHarnessPath, dir)
})
t.Run("emitToNilWriter", func(t *testing.T) {
t.Parallel()
testEmitToNilWriter(t, atomicHarnessPath, dir)
})
t.Run("emitToFailingWriter", func(t *testing.T) {
t.Parallel()
testEmitToFailingWriter(t, atomicHarnessPath, dir)
})
t.Run("emitWithCounterClear", func(t *testing.T) {
t.Parallel()
testEmitWithCounterClear(t, atomicHarnessPath, dir)
})
t.Run("emitToDirNonAtomic", func(t *testing.T) {
t.Parallel()
testEmitToDirNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
})
t.Run("emitToWriterNonAtomic", func(t *testing.T) {
t.Parallel()
testEmitToWriterNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
})
t.Run("emitWithCounterClearNonAtomic", func(t *testing.T) {
t.Parallel()
testEmitWithCounterClearNonAtomic(t, nonAtomicHarnessPath, nonAtomicMode, dir)
})
}
// upmergeCoverData helps improve coverage data for this package
// itself. If this test itself is being invoked with "-cover", then
// what we'd like is for package coverage data (that is, coverage for
// routines in "runtime/coverage") to be incorporated into the test
// run from the "harness.exe" runs we've just done. We can accomplish
// this by doing a merge from the harness gocoverdir's to the test
// gocoverdir.
func upmergeCoverData(t *testing.T, gocoverdir string, mode string) {
if testing.CoverMode() != mode {
return
}
testGoCoverDir := os.Getenv("GOCOVERDIR")
if testGoCoverDir == "" {
return
}
args := []string{"tool", "covdata", "merge", "-pkg=runtime/coverage",
"-o", testGoCoverDir, "-i", gocoverdir}
t.Logf("up-merge of covdata from %s to %s", gocoverdir, testGoCoverDir)
t.Logf("executing: go %+v", args)
cmd := exec.Command(testenv.GoToolPath(t), args...)
if b, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("covdata merge failed (%v): %s", err, b)
}
}
// buildHarness builds the helper program "harness.exe".
func buildHarness(t *testing.T, dir string, opts []string) string {
harnessPath := filepath.Join(dir, "harness.exe")
harnessSrc := filepath.Join("testdata", "harness.go")
args := []string{"build", "-o", harnessPath}
args = append(args, opts...)
args = append(args, harnessSrc)
//t.Logf("harness build: go %+v\n", args)
cmd := exec.Command(testenv.GoToolPath(t), args...)
if b, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("build failed (%v): %s", err, b)
}
return harnessPath
}
func mkdir(t *testing.T, d string) string {
t.Helper()
if err := os.Mkdir(d, 0777); err != nil {
t.Fatalf("mkdir failed: %v", err)
}
return d
}
// updateGoCoverDir updates the specified environment 'env' to set
// GOCOVERDIR to 'gcd' (if setGoCoverDir is TRUE) or removes
// GOCOVERDIR from the environment (if setGoCoverDir is false).
func updateGoCoverDir(env []string, gcd string, setGoCoverDir bool) []string {
rv := []string{}
found := false
for _, v := range env {
if strings.HasPrefix(v, "GOCOVERDIR=") {
if !setGoCoverDir {
continue
}
v = "GOCOVERDIR=" + gcd
found = true
}
rv = append(rv, v)
}
if !found && setGoCoverDir {
rv = append(rv, "GOCOVERDIR="+gcd)
}
return rv
}
func runHarness(t *testing.T, harnessPath string, tp string, setGoCoverDir bool, rdir, edir string) (string, error) {
t.Logf("running: %s -tp %s -o %s with rdir=%s and GOCOVERDIR=%v", harnessPath, tp, edir, rdir, setGoCoverDir)
cmd := exec.Command(harnessPath, "-tp", tp, "-o", edir)
cmd.Dir = rdir
cmd.Env = updateGoCoverDir(os.Environ(), rdir, setGoCoverDir)
b, err := cmd.CombinedOutput()
//t.Logf("harness run output: %s\n", string(b))
return string(b), err
}
func testForSpecificFunctions(t *testing.T, dir string, want []string, avoid []string) string {
args := []string{"tool", "covdata", "debugdump",
"-live", "-pkg=command-line-arguments", "-i=" + dir}
t.Logf("running: go %v\n", args)
cmd := exec.Command(testenv.GoToolPath(t), args...)
b, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("'go tool covdata failed (%v): %s", err, b)
}
output := string(b)
rval := ""
for _, f := range want {
wf := "Func: " + f + "\n"
if strings.Contains(output, wf) {
continue
}
rval += fmt.Sprintf("error: output should contain %q but does not\n", wf)
}
for _, f := range avoid {
wf := "Func: " + f + "\n"
if strings.Contains(output, wf) {
rval += fmt.Sprintf("error: output should not contain %q but does\n", wf)
}
}
if rval != "" {
t.Logf("=-= begin output:\n" + output + "\n=-= end output\n")
}
return rval
}
func withAndWithoutRunner(f func(setit bool, tag string)) {
// Run 'f' with and without GOCOVERDIR set.
for i := 0; i < 2; i++ {
tag := "x"
setGoCoverDir := true
if i == 0 {
setGoCoverDir = false
tag = "y"
}
f(setGoCoverDir, tag)
}
}
func mktestdirs(t *testing.T, tag, tp, dir string) (string, string) {
t.Helper()
rdir := mkdir(t, filepath.Join(dir, tp+"-rdir-"+tag))
edir := mkdir(t, filepath.Join(dir, tp+"-edir-"+tag))
return rdir, edir
}
func testEmitToDir(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitToDir"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp,
setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp emitDir': %v", err)
}
// Just check to make sure meta-data file and counter data file were
// written. Another alternative would be to run "go tool covdata"
// or equivalent, but for now, this is what we've got.
dents, err := os.ReadDir(edir)
if err != nil {
t.Fatalf("os.ReadDir(%s) failed: %v", edir, err)
}
mfc := 0
cdc := 0
for _, e := range dents {
if e.IsDir() {
continue
}
if strings.HasPrefix(e.Name(), coverage.MetaFilePref) {
mfc++
} else if strings.HasPrefix(e.Name(), coverage.CounterFilePref) {
cdc++
}
}
wantmf := 1
wantcf := 1
if mfc != wantmf {
t.Errorf("EmitToDir: want %d meta-data files, got %d\n", wantmf, mfc)
}
if cdc != wantcf {
t.Errorf("EmitToDir: want %d counter-data files, got %d\n", wantcf, cdc)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitToWriter(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitToWriter"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': %v", tp, err)
}
want := []string{"main", tp}
avoid := []string{"final"}
if msg := testForSpecificFunctions(t, edir, want, avoid); msg != "" {
t.Errorf("coverage data from %q output match failed: %s", tp, msg)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitToNonexistentDir(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitToNonexistentDir"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': %v", tp, err)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitToUnwritableDir(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitToUnwritableDir"
rdir, edir := mktestdirs(t, tag, tp, dir)
// Make edir unwritable.
if err := os.Chmod(edir, 0555); err != nil {
t.Fatalf("chmod failed: %v", err)
}
defer os.Chmod(edir, 0777)
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': %v", tp, err)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitToNilWriter(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitToNilWriter"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': %v", tp, err)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitToFailingWriter(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitToFailingWriter"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp, setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': %v", tp, err)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitWithCounterClear(t *testing.T, harnessPath string, dir string) {
withAndWithoutRunner(func(setGoCoverDir bool, tag string) {
tp := "emitWithCounterClear"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp,
setGoCoverDir, rdir, edir)
if err != nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': %v", tp, err)
}
want := []string{tp, "postClear"}
avoid := []string{"preClear", "main", "final"}
if msg := testForSpecificFunctions(t, edir, want, avoid); msg != "" {
t.Logf("%s", output)
t.Errorf("coverage data from %q output match failed: %s", tp, msg)
}
upmergeCoverData(t, edir, "atomic")
upmergeCoverData(t, rdir, "atomic")
})
}
func testEmitToDirNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
tp := "emitToDir"
tag := "nonatomdir"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp,
true, rdir, edir)
// We expect an error here.
if err == nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': did not get expected error", tp)
}
got := strings.TrimSpace(string(output))
want := "WriteCountersDir invoked for program built"
if !strings.Contains(got, want) {
t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
tp, got, want)
}
upmergeCoverData(t, edir, naMode)
upmergeCoverData(t, rdir, naMode)
}
func testEmitToWriterNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
tp := "emitToWriter"
tag := "nonatomw"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp,
true, rdir, edir)
// We expect an error here.
if err == nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s': did not get expected error", tp)
}
got := strings.TrimSpace(string(output))
want := "WriteCounters invoked for program built"
if !strings.Contains(got, want) {
t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
tp, got, want)
}
upmergeCoverData(t, edir, naMode)
upmergeCoverData(t, rdir, naMode)
}
func testEmitWithCounterClearNonAtomic(t *testing.T, harnessPath string, naMode string, dir string) {
tp := "emitWithCounterClear"
tag := "cclear"
rdir, edir := mktestdirs(t, tag, tp, dir)
output, err := runHarness(t, harnessPath, tp,
true, rdir, edir)
// We expect an error here.
if err == nil {
t.Logf("%s", output)
t.Fatalf("running 'harness -tp %s' nonatomic: did not get expected error", tp)
}
got := strings.TrimSpace(string(output))
want := "ClearCounters invoked for program built"
if !strings.Contains(got, want) {
t.Errorf("running 'harness -tp %s': got:\n%s\nwant: %s",
tp, got, want)
}
upmergeCoverData(t, edir, naMode)
upmergeCoverData(t, rdir, naMode)
}
func TestApisOnNocoverBinary(t *testing.T) {
if testing.Short() {
t.Skipf("skipping test: too long for short mode")
}
testenv.MustHaveGoBuild(t)
dir := t.TempDir()
// Build harness with no -cover.
bdir := mkdir(t, filepath.Join(dir, "nocover"))
edir := mkdir(t, filepath.Join(dir, "emitDirNo"))
harnessPath := buildHarness(t, bdir, nil)
output, err := runHarness(t, harnessPath, "emitToDir", false, edir, edir)
if err == nil {
t.Fatalf("expected error on TestApisOnNocoverBinary harness run")
}
const want = "not built with -cover"
if !strings.Contains(output, want) {
t.Errorf("error output does not contain %q: %s", want, output)
}
}
func TestIssue56006EmitDataRaceCoverRunningGoroutine(t *testing.T) {
if testing.Short() {
t.Skipf("skipping test: too long for short mode")
}
if !goexperiment.CoverageRedesign {
t.Skipf("skipping new coverage tests (experiment not enabled)")
}
// This test requires "go test -race -cover", meaning that we need
// go build, go run, and "-race" support.
testenv.MustHaveGoRun(t)
if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) ||
!testenv.HasCGO() {
t.Skip("skipped due to lack of race detector support / CGO")
}
// This will run a program with -cover and -race where we have a
// goroutine still running (and updating counters) at the point where
// the test runtime is trying to write out counter data.
cmd := exec.Command(testenv.GoToolPath(t), "test", "-cover", "-race")
cmd.Dir = filepath.Join("testdata", "issue56006")
b, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go test -cover -race failed: %v\n%s", err, b)
}
// Don't want to see any data races in output.
avoid := []string{"DATA RACE"}
for _, no := range avoid {
if strings.Contains(string(b), no) {
t.Logf("%s\n", string(b))
t.Fatalf("found %s in test output, not permitted", no)
}
}
}
func TestIssue59563TruncatedCoverPkgAll(t *testing.T) {
if testing.Short() {
t.Skipf("skipping test: too long for short mode")
}
testenv.MustHaveGoRun(t)
tmpdir := t.TempDir()
ppath := filepath.Join(tmpdir, "foo.cov")
cmd := exec.Command(testenv.GoToolPath(t), "test", "-coverpkg=all", "-coverprofile="+ppath)
cmd.Dir = filepath.Join("testdata", "issue59563")
b, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go test -cover failed: %v\n%s", err, b)
}
cmd = exec.Command(testenv.GoToolPath(t), "tool", "cover", "-func="+ppath)
b, err = cmd.CombinedOutput()
if err != nil {
t.Fatalf("go tool cover -func failed: %v", err)
}
lines := strings.Split(string(b), "\n")
nfound := 0
bad := false
for _, line := range lines {
f := strings.Fields(line)
if len(f) == 0 {
continue
}
// We're only interested in the specific function "large" for
// the testcase being built. See the #59563 for details on why
// size matters.
if !(strings.HasPrefix(f[0], "internal/coverage/cfile/testdata/issue59563/repro.go") && strings.Contains(line, "large")) {
continue
}
nfound++
want := "100.0%"
if f[len(f)-1] != want {
t.Errorf("wanted %s got: %q\n", want, line)
bad = true
}
}
if nfound != 1 {
t.Errorf("wanted 1 found, got %d\n", nfound)
bad = true
}
if bad {
t.Logf("func output:\n%s\n", string(b))
}
}

View file

@ -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 cfile
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 (via runtime/coverage.initHook).
//
// 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)

View file

@ -0,0 +1,259 @@
// 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 main
import (
"flag"
"fmt"
"internal/coverage/slicewriter"
"io"
"log"
"os"
"path/filepath"
"runtime/coverage"
"strings"
)
var verbflag = flag.Int("v", 0, "Verbose trace output level")
var testpointflag = flag.String("tp", "", "Testpoint to run")
var outdirflag = flag.String("o", "", "Output dir into which to emit")
func emitToWriter() {
log.SetPrefix("emitToWriter: ")
var slwm slicewriter.WriteSeeker
if err := coverage.WriteMeta(&slwm); err != nil {
log.Fatalf("error: WriteMeta returns %v", err)
}
mf := filepath.Join(*outdirflag, "covmeta.0abcdef")
if err := os.WriteFile(mf, slwm.BytesWritten(), 0666); err != nil {
log.Fatalf("error: writing %s: %v", mf, err)
}
var slwc slicewriter.WriteSeeker
if err := coverage.WriteCounters(&slwc); err != nil {
log.Fatalf("error: WriteCounters returns %v", err)
}
cf := filepath.Join(*outdirflag, "covcounters.0abcdef.99.77")
if err := os.WriteFile(cf, slwc.BytesWritten(), 0666); err != nil {
log.Fatalf("error: writing %s: %v", cf, err)
}
}
func emitToDir() {
log.SetPrefix("emitToDir: ")
if err := coverage.WriteMetaDir(*outdirflag); err != nil {
log.Fatalf("error: WriteMetaDir returns %v", err)
}
if err := coverage.WriteCountersDir(*outdirflag); err != nil {
log.Fatalf("error: WriteCountersDir returns %v", err)
}
}
func emitToNonexistentDir() {
log.SetPrefix("emitToNonexistentDir: ")
want := []string{
"no such file or directory", // linux-ish
"system cannot find the file specified", // windows
"does not exist", // plan9
}
checkWant := func(which string, got string) {
found := false
for _, w := range want {
if strings.Contains(got, w) {
found = true
break
}
}
if !found {
log.Fatalf("%s emit to bad dir: got error:\n %v\nwanted error with one of:\n %+v", which, got, want)
}
}
// Mangle the output directory to produce something nonexistent.
mangled := *outdirflag + "_MANGLED"
if err := coverage.WriteMetaDir(mangled); err == nil {
log.Fatal("expected error from WriteMetaDir to nonexistent dir")
} else {
got := fmt.Sprintf("%v", err)
checkWant("meta data", got)
}
// Now try to emit counter data file to a bad dir.
if err := coverage.WriteCountersDir(mangled); err == nil {
log.Fatal("expected error emitting counter data to bad dir")
} else {
got := fmt.Sprintf("%v", err)
checkWant("counter data", got)
}
}
func emitToUnwritableDir() {
log.SetPrefix("emitToUnwritableDir: ")
want := "permission denied"
if err := coverage.WriteMetaDir(*outdirflag); err == nil {
log.Fatal("expected error from WriteMetaDir to unwritable dir")
} else {
got := fmt.Sprintf("%v", err)
if !strings.Contains(got, want) {
log.Fatalf("meta-data emit to unwritable dir: wanted error containing %q got %q", want, got)
}
}
// Similarly with writing counter data.
if err := coverage.WriteCountersDir(*outdirflag); err == nil {
log.Fatal("expected error emitting counter data to unwritable dir")
} else {
got := fmt.Sprintf("%v", err)
if !strings.Contains(got, want) {
log.Fatalf("emitting counter data to unwritable dir: wanted error containing %q got %q", want, got)
}
}
}
func emitToNilWriter() {
log.SetPrefix("emitToWriter: ")
want := "nil writer"
var bad io.WriteSeeker
if err := coverage.WriteMeta(bad); err == nil {
log.Fatal("expected error passing nil writer for meta emit")
} else {
got := fmt.Sprintf("%v", err)
if !strings.Contains(got, want) {
log.Fatalf("emitting meta-data passing nil writer: wanted error containing %q got %q", want, got)
}
}
if err := coverage.WriteCounters(bad); err == nil {
log.Fatal("expected error passing nil writer for counter emit")
} else {
got := fmt.Sprintf("%v", err)
if !strings.Contains(got, want) {
log.Fatalf("emitting counter data passing nil writer: wanted error containing %q got %q", want, got)
}
}
}
type failingWriter struct {
writeCount int
writeLimit int
slws slicewriter.WriteSeeker
}
func (f *failingWriter) Write(p []byte) (n int, err error) {
c := f.writeCount
f.writeCount++
if f.writeLimit < 0 || c < f.writeLimit {
return f.slws.Write(p)
}
return 0, fmt.Errorf("manufactured write error")
}
func (f *failingWriter) Seek(offset int64, whence int) (int64, error) {
return f.slws.Seek(offset, whence)
}
func (f *failingWriter) reset(lim int) {
f.writeCount = 0
f.writeLimit = lim
f.slws = slicewriter.WriteSeeker{}
}
func writeStressTest(tag string, testf func(testf *failingWriter) error) {
// Invoke the function initially without the write limit
// set, to capture the number of writes performed.
fw := &failingWriter{writeLimit: -1}
testf(fw)
// Now that we know how many writes are going to happen, run the
// function repeatedly, each time with a Write operation set to
// fail at a new spot. The goal here is to make sure that:
// A) an error is reported, and B) nothing crashes.
tot := fw.writeCount
for i := 0; i < tot; i++ {
fw.reset(i)
err := testf(fw)
if err == nil {
log.Fatalf("no error from write %d tag %s", i, tag)
}
}
}
func postClear() int {
return 42
}
func preClear() int {
return 42
}
// This test is designed to ensure that write errors are properly
// handled by the code that writes out coverage data. It repeatedly
// invokes the 'emit to writer' apis using a specially crafted writer
// that captures the total number of expected writes, then replays the
// execution N times with a manufactured write error at the
// appropriate spot.
func emitToFailingWriter() {
log.SetPrefix("emitToFailingWriter: ")
writeStressTest("emit-meta", func(f *failingWriter) error {
return coverage.WriteMeta(f)
})
writeStressTest("emit-counter", func(f *failingWriter) error {
return coverage.WriteCounters(f)
})
}
func emitWithCounterClear() {
log.SetPrefix("emitWitCounterClear: ")
preClear()
if err := coverage.ClearCounters(); err != nil {
log.Fatalf("clear failed: %v", err)
}
postClear()
if err := coverage.WriteMetaDir(*outdirflag); err != nil {
log.Fatalf("error: WriteMetaDir returns %v", err)
}
if err := coverage.WriteCountersDir(*outdirflag); err != nil {
log.Fatalf("error: WriteCountersDir returns %v", err)
}
}
func final() int {
println("I run last.")
return 43
}
func main() {
log.SetFlags(0)
flag.Parse()
if *testpointflag == "" {
log.Fatalf("error: no testpoint (use -tp flag)")
}
if *outdirflag == "" {
log.Fatalf("error: no output dir specified (use -o flag)")
}
switch *testpointflag {
case "emitToDir":
emitToDir()
case "emitToWriter":
emitToWriter()
case "emitToNonexistentDir":
emitToNonexistentDir()
case "emitToUnwritableDir":
emitToUnwritableDir()
case "emitToNilWriter":
emitToNilWriter()
case "emitToFailingWriter":
emitToFailingWriter()
case "emitWithCounterClear":
emitWithCounterClear()
default:
log.Fatalf("error: unknown testpoint %q", *testpointflag)
}
final()
}

View file

@ -0,0 +1,26 @@
package main
//go:noinline
func blah(x int) int {
if x != 0 {
return x + 42
}
return x - 42
}
func main() {
go infloop()
println(blah(1) + blah(0))
}
var G int
func infloop() {
for {
G += blah(1)
G += blah(0)
if G > 10000 {
G = 0
}
}
}

View file

@ -0,0 +1,8 @@
package main
import "testing"
func TestSomething(t *testing.T) {
go infloop()
println(blah(1) + blah(0))
}

View file

@ -0,0 +1,823 @@
// Copyright 2023 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 repro
import (
"fmt"
"net/http"
)
func small() {
go func() {
fmt.Println(http.ListenAndServe("localhost:7070", nil))
}()
}
func large(x int) int {
if x == 0 {
x += 0
} else if x == 1 {
x += 1
} else if x == 2 {
x += 2
} else if x == 3 {
x += 3
} else if x == 4 {
x += 4
} else if x == 5 {
x += 5
} else if x == 6 {
x += 6
} else if x == 7 {
x += 7
} else if x == 8 {
x += 8
} else if x == 9 {
x += 9
} else if x == 10 {
x += 10
} else if x == 11 {
x += 11
} else if x == 12 {
x += 12
} else if x == 13 {
x += 13
} else if x == 14 {
x += 14
} else if x == 15 {
x += 15
} else if x == 16 {
x += 16
} else if x == 17 {
x += 17
} else if x == 18 {
x += 18
} else if x == 19 {
x += 19
} else if x == 20 {
x += 20
} else if x == 21 {
x += 21
} else if x == 22 {
x += 22
} else if x == 23 {
x += 23
} else if x == 24 {
x += 24
} else if x == 25 {
x += 25
} else if x == 26 {
x += 26
} else if x == 27 {
x += 27
} else if x == 28 {
x += 28
} else if x == 29 {
x += 29
} else if x == 30 {
x += 30
} else if x == 31 {
x += 31
} else if x == 32 {
x += 32
} else if x == 33 {
x += 33
} else if x == 34 {
x += 34
} else if x == 35 {
x += 35
} else if x == 36 {
x += 36
} else if x == 37 {
x += 37
} else if x == 38 {
x += 38
} else if x == 39 {
x += 39
} else if x == 40 {
x += 40
} else if x == 41 {
x += 41
} else if x == 42 {
x += 42
} else if x == 43 {
x += 43
} else if x == 44 {
x += 44
} else if x == 45 {
x += 45
} else if x == 46 {
x += 46
} else if x == 47 {
x += 47
} else if x == 48 {
x += 48
} else if x == 49 {
x += 49
} else if x == 50 {
x += 50
} else if x == 51 {
x += 51
} else if x == 52 {
x += 52
} else if x == 53 {
x += 53
} else if x == 54 {
x += 54
} else if x == 55 {
x += 55
} else if x == 56 {
x += 56
} else if x == 57 {
x += 57
} else if x == 58 {
x += 58
} else if x == 59 {
x += 59
} else if x == 60 {
x += 60
} else if x == 61 {
x += 61
} else if x == 62 {
x += 62
} else if x == 63 {
x += 63
} else if x == 64 {
x += 64
} else if x == 65 {
x += 65
} else if x == 66 {
x += 66
} else if x == 67 {
x += 67
} else if x == 68 {
x += 68
} else if x == 69 {
x += 69
} else if x == 70 {
x += 70
} else if x == 71 {
x += 71
} else if x == 72 {
x += 72
} else if x == 73 {
x += 73
} else if x == 74 {
x += 74
} else if x == 75 {
x += 75
} else if x == 76 {
x += 76
} else if x == 77 {
x += 77
} else if x == 78 {
x += 78
} else if x == 79 {
x += 79
} else if x == 80 {
x += 80
} else if x == 81 {
x += 81
} else if x == 82 {
x += 82
} else if x == 83 {
x += 83
} else if x == 84 {
x += 84
} else if x == 85 {
x += 85
} else if x == 86 {
x += 86
} else if x == 87 {
x += 87
} else if x == 88 {
x += 88
} else if x == 89 {
x += 89
} else if x == 90 {
x += 90
} else if x == 91 {
x += 91
} else if x == 92 {
x += 92
} else if x == 93 {
x += 93
} else if x == 94 {
x += 94
} else if x == 95 {
x += 95
} else if x == 96 {
x += 96
} else if x == 97 {
x += 97
} else if x == 98 {
x += 98
} else if x == 99 {
x += 99
} else if x == 100 {
x += 100
} else if x == 101 {
x += 101
} else if x == 102 {
x += 102
} else if x == 103 {
x += 103
} else if x == 104 {
x += 104
} else if x == 105 {
x += 105
} else if x == 106 {
x += 106
} else if x == 107 {
x += 107
} else if x == 108 {
x += 108
} else if x == 109 {
x += 109
} else if x == 110 {
x += 110
} else if x == 111 {
x += 111
} else if x == 112 {
x += 112
} else if x == 113 {
x += 113
} else if x == 114 {
x += 114
} else if x == 115 {
x += 115
} else if x == 116 {
x += 116
} else if x == 117 {
x += 117
} else if x == 118 {
x += 118
} else if x == 119 {
x += 119
} else if x == 120 {
x += 120
} else if x == 121 {
x += 121
} else if x == 122 {
x += 122
} else if x == 123 {
x += 123
} else if x == 124 {
x += 124
} else if x == 125 {
x += 125
} else if x == 126 {
x += 126
} else if x == 127 {
x += 127
} else if x == 128 {
x += 128
} else if x == 129 {
x += 129
} else if x == 130 {
x += 130
} else if x == 131 {
x += 131
} else if x == 132 {
x += 132
} else if x == 133 {
x += 133
} else if x == 134 {
x += 134
} else if x == 135 {
x += 135
} else if x == 136 {
x += 136
} else if x == 137 {
x += 137
} else if x == 138 {
x += 138
} else if x == 139 {
x += 139
} else if x == 140 {
x += 140
} else if x == 141 {
x += 141
} else if x == 142 {
x += 142
} else if x == 143 {
x += 143
} else if x == 144 {
x += 144
} else if x == 145 {
x += 145
} else if x == 146 {
x += 146
} else if x == 147 {
x += 147
} else if x == 148 {
x += 148
} else if x == 149 {
x += 149
} else if x == 150 {
x += 150
} else if x == 151 {
x += 151
} else if x == 152 {
x += 152
} else if x == 153 {
x += 153
} else if x == 154 {
x += 154
} else if x == 155 {
x += 155
} else if x == 156 {
x += 156
} else if x == 157 {
x += 157
} else if x == 158 {
x += 158
} else if x == 159 {
x += 159
} else if x == 160 {
x += 160
} else if x == 161 {
x += 161
} else if x == 162 {
x += 162
} else if x == 163 {
x += 163
} else if x == 164 {
x += 164
} else if x == 165 {
x += 165
} else if x == 166 {
x += 166
} else if x == 167 {
x += 167
} else if x == 168 {
x += 168
} else if x == 169 {
x += 169
} else if x == 170 {
x += 170
} else if x == 171 {
x += 171
} else if x == 172 {
x += 172
} else if x == 173 {
x += 173
} else if x == 174 {
x += 174
} else if x == 175 {
x += 175
} else if x == 176 {
x += 176
} else if x == 177 {
x += 177
} else if x == 178 {
x += 178
} else if x == 179 {
x += 179
} else if x == 180 {
x += 180
} else if x == 181 {
x += 181
} else if x == 182 {
x += 182
} else if x == 183 {
x += 183
} else if x == 184 {
x += 184
} else if x == 185 {
x += 185
} else if x == 186 {
x += 186
} else if x == 187 {
x += 187
} else if x == 188 {
x += 188
} else if x == 189 {
x += 189
} else if x == 190 {
x += 190
} else if x == 191 {
x += 191
} else if x == 192 {
x += 192
} else if x == 193 {
x += 193
} else if x == 194 {
x += 194
} else if x == 195 {
x += 195
} else if x == 196 {
x += 196
} else if x == 197 {
x += 197
} else if x == 198 {
x += 198
} else if x == 199 {
x += 199
} else if x == 200 {
x += 200
} else if x == 201 {
x += 201
} else if x == 202 {
x += 202
} else if x == 203 {
x += 203
} else if x == 204 {
x += 204
} else if x == 205 {
x += 205
} else if x == 206 {
x += 206
} else if x == 207 {
x += 207
} else if x == 208 {
x += 208
} else if x == 209 {
x += 209
} else if x == 210 {
x += 210
} else if x == 211 {
x += 211
} else if x == 212 {
x += 212
} else if x == 213 {
x += 213
} else if x == 214 {
x += 214
} else if x == 215 {
x += 215
} else if x == 216 {
x += 216
} else if x == 217 {
x += 217
} else if x == 218 {
x += 218
} else if x == 219 {
x += 219
} else if x == 220 {
x += 220
} else if x == 221 {
x += 221
} else if x == 222 {
x += 222
} else if x == 223 {
x += 223
} else if x == 224 {
x += 224
} else if x == 225 {
x += 225
} else if x == 226 {
x += 226
} else if x == 227 {
x += 227
} else if x == 228 {
x += 228
} else if x == 229 {
x += 229
} else if x == 230 {
x += 230
} else if x == 231 {
x += 231
} else if x == 232 {
x += 232
} else if x == 233 {
x += 233
} else if x == 234 {
x += 234
} else if x == 235 {
x += 235
} else if x == 236 {
x += 236
} else if x == 237 {
x += 237
} else if x == 238 {
x += 238
} else if x == 239 {
x += 239
} else if x == 240 {
x += 240
} else if x == 241 {
x += 241
} else if x == 242 {
x += 242
} else if x == 243 {
x += 243
} else if x == 244 {
x += 244
} else if x == 245 {
x += 245
} else if x == 246 {
x += 246
} else if x == 247 {
x += 247
} else if x == 248 {
x += 248
} else if x == 249 {
x += 249
} else if x == 250 {
x += 250
} else if x == 251 {
x += 251
} else if x == 252 {
x += 252
} else if x == 253 {
x += 253
} else if x == 254 {
x += 254
} else if x == 255 {
x += 255
} else if x == 256 {
x += 256
} else if x == 257 {
x += 257
} else if x == 258 {
x += 258
} else if x == 259 {
x += 259
} else if x == 260 {
x += 260
} else if x == 261 {
x += 261
} else if x == 262 {
x += 262
} else if x == 263 {
x += 263
} else if x == 264 {
x += 264
} else if x == 265 {
x += 265
} else if x == 266 {
x += 266
} else if x == 267 {
x += 267
} else if x == 268 {
x += 268
} else if x == 269 {
x += 269
} else if x == 270 {
x += 270
} else if x == 271 {
x += 271
} else if x == 272 {
x += 272
} else if x == 273 {
x += 273
} else if x == 274 {
x += 274
} else if x == 275 {
x += 275
} else if x == 276 {
x += 276
} else if x == 277 {
x += 277
} else if x == 278 {
x += 278
} else if x == 279 {
x += 279
} else if x == 280 {
x += 280
} else if x == 281 {
x += 281
} else if x == 282 {
x += 282
} else if x == 283 {
x += 283
} else if x == 284 {
x += 284
} else if x == 285 {
x += 285
} else if x == 286 {
x += 286
} else if x == 287 {
x += 287
} else if x == 288 {
x += 288
} else if x == 289 {
x += 289
} else if x == 290 {
x += 290
} else if x == 291 {
x += 291
} else if x == 292 {
x += 292
} else if x == 293 {
x += 293
} else if x == 294 {
x += 294
} else if x == 295 {
x += 295
} else if x == 296 {
x += 296
} else if x == 297 {
x += 297
} else if x == 298 {
x += 298
} else if x == 299 {
x += 299
} else if x == 300 {
x += 300
} else if x == 301 {
x += 301
} else if x == 302 {
x += 302
} else if x == 303 {
x += 303
} else if x == 304 {
x += 304
} else if x == 305 {
x += 305
} else if x == 306 {
x += 306
} else if x == 307 {
x += 307
} else if x == 308 {
x += 308
} else if x == 309 {
x += 309
} else if x == 310 {
x += 310
} else if x == 311 {
x += 311
} else if x == 312 {
x += 312
} else if x == 313 {
x += 313
} else if x == 314 {
x += 314
} else if x == 315 {
x += 315
} else if x == 316 {
x += 316
} else if x == 317 {
x += 317
} else if x == 318 {
x += 318
} else if x == 319 {
x += 319
} else if x == 320 {
x += 320
} else if x == 321 {
x += 321
} else if x == 322 {
x += 322
} else if x == 323 {
x += 323
} else if x == 324 {
x += 324
} else if x == 325 {
x += 325
} else if x == 326 {
x += 326
} else if x == 327 {
x += 327
} else if x == 328 {
x += 328
} else if x == 329 {
x += 329
} else if x == 330 {
x += 330
} else if x == 331 {
x += 331
} else if x == 332 {
x += 332
} else if x == 333 {
x += 333
} else if x == 334 {
x += 334
} else if x == 335 {
x += 335
} else if x == 336 {
x += 336
} else if x == 337 {
x += 337
} else if x == 338 {
x += 338
} else if x == 339 {
x += 339
} else if x == 340 {
x += 340
} else if x == 341 {
x += 341
} else if x == 342 {
x += 342
} else if x == 343 {
x += 343
} else if x == 344 {
x += 344
} else if x == 345 {
x += 345
} else if x == 346 {
x += 346
} else if x == 347 {
x += 347
} else if x == 348 {
x += 348
} else if x == 349 {
x += 349
} else if x == 350 {
x += 350
} else if x == 351 {
x += 351
} else if x == 352 {
x += 352
} else if x == 353 {
x += 353
} else if x == 354 {
x += 354
} else if x == 355 {
x += 355
} else if x == 356 {
x += 356
} else if x == 357 {
x += 357
} else if x == 358 {
x += 358
} else if x == 359 {
x += 359
} else if x == 360 {
x += 360
} else if x == 361 {
x += 361
} else if x == 362 {
x += 362
} else if x == 363 {
x += 363
} else if x == 364 {
x += 364
} else if x == 365 {
x += 365
} else if x == 366 {
x += 366
} else if x == 367 {
x += 367
} else if x == 368 {
x += 368
} else if x == 369 {
x += 369
} else if x == 370 {
x += 370
} else if x == 371 {
x += 371
} else if x == 372 {
x += 372
} else if x == 373 {
x += 373
} else if x == 374 {
x += 374
} else if x == 375 {
x += 375
} else if x == 376 {
x += 376
} else if x == 377 {
x += 377
} else if x == 378 {
x += 378
} else if x == 379 {
x += 379
} else if x == 380 {
x += 380
} else if x == 381 {
x += 381
} else if x == 382 {
x += 382
} else if x == 383 {
x += 383
} else if x == 384 {
x += 384
} else if x == 385 {
x += 385
} else if x == 386 {
x += 386
} else if x == 387 {
x += 387
} else if x == 388 {
x += 388
} else if x == 389 {
x += 389
} else if x == 390 {
x += 390
} else if x == 391 {
x += 391
} else if x == 392 {
x += 392
} else if x == 393 {
x += 393
} else if x == 394 {
x += 394
} else if x == 395 {
x += 395
} else if x == 396 {
x += 396
} else if x == 397 {
x += 397
} else if x == 398 {
x += 398
} else if x == 399 {
x += 399
} else if x == 400 {
x += 400
}
return x * x
}

View file

@ -0,0 +1,14 @@
// Copyright 2023 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 repro
import "testing"
func TestSomething(t *testing.T) {
small()
for i := 0; i < 1001; i++ {
large(i)
}
}

View file

@ -0,0 +1,317 @@
// 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 cfile
import (
"encoding/json"
"fmt"
"internal/coverage"
"internal/coverage/calloc"
"internal/coverage/cformat"
"internal/coverage/cmerge"
"internal/coverage/decodecounter"
"internal/coverage/decodemeta"
"internal/coverage/pods"
"internal/runtime/atomic"
"io"
"os"
"path/filepath"
"strings"
"unsafe"
)
// ProcessCoverTestDir is called 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, w io.Writer) 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,
}
// Generate the expected hash string based on the final meta-data
// hash for this test, then look only for pods that refer to that
// hash (just in case there are multiple instrumented executables
// in play). See issue #57924 for more on this.
hashstring := fmt.Sprintf("%x", finalHash)
importpaths := make(map[string]struct{})
for _, p := range podlist {
if !strings.Contains(p.MetaFile, hashstring) {
continue
}
if err := ts.processPod(p, importpaths); err != nil {
return err
}
}
metafilespath := filepath.Join(dir, coverage.MetaFilesFileName)
if _, err := os.Stat(metafilespath); err == nil {
if err := ts.readAuxMetaFiles(metafilespath, importpaths); err != nil {
return err
}
}
// Emit percent.
if err := ts.cf.EmitPercent(w, cpkg, true, 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, importpaths map[string]struct{}) 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
}
// A map to store counter data, indexed by pkgid/fnid tuple.
pmm := make(map[pkfunc][]uint32)
// Helper to read a single counter data file.
readcdf := func(cdf string) error {
cf, err := os.Open(cdf)
if err != nil {
return fmt.Errorf("opening counter data file %s: %s", cdf, err)
}
defer cf.Close()
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
}
return nil
}
// Read counter data files.
for _, cdf := range p.CounterDataFiles {
if err := readcdf(cdf); err != nil {
return err
}
}
// 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())
importpaths[pd.PackagePath()] = struct{}{}
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
}
func (ts *tstate) readAuxMetaFiles(metafiles string, importpaths map[string]struct{}) error {
// Unmarshal the information on available aux metafiles into
// a MetaFileCollection struct.
var mfc coverage.MetaFileCollection
data, err := os.ReadFile(metafiles)
if err != nil {
return fmt.Errorf("error reading auxmetafiles file %q: %v", metafiles, err)
}
if err := json.Unmarshal(data, &mfc); err != nil {
return fmt.Errorf("error reading auxmetafiles file %q: %v", metafiles, err)
}
// Walk through each available aux meta-file. If we've already
// seen the package path in question during the walk of the
// "regular" meta-data file, then we can skip the package,
// otherwise construct a dummy pod with the single meta-data file
// (no counters) and invoke processPod on it.
for i := range mfc.ImportPaths {
p := mfc.ImportPaths[i]
if _, ok := importpaths[p]; ok {
continue
}
var pod pods.Pod
pod.MetaFile = mfc.MetaFileFragments[i]
if err := ts.processPod(pod, importpaths); err != nil {
return err
}
}
return nil
}
// Snapshot returns a snapshot of coverage percentage at a moment of
// time within a running test, so as to support the testing.Coverage()
// function. This version doesn't examine coverage meta-data, so the
// result it returns will be less accurate (more "slop") due to the
// fact that we don't look at the meta data to see how many statements
// are associated with each counter.
func Snapshot() float64 {
cl := getCovCounterList()
if len(cl) == 0 {
// no work to do here.
return 0.0
}
tot := uint64(0)
totExec := uint64(0)
for _, c := range cl {
sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), c.Len)
tot += uint64(len(sd))
for i := 0; i < len(sd); i++ {
// Skip ahead until the next non-zero value.
if sd[i].Load() == 0 {
continue
}
// We found a function that was executed.
nCtrs := sd[i+coverage.NumCtrsOffset].Load()
cst := i + coverage.FirstCtrOffset
if cst+int(nCtrs) > len(sd) {
break
}
counters := sd[cst : cst+int(nCtrs)]
for i := range counters {
if counters[i].Load() != 0 {
totExec++
}
}
i += coverage.FirstCtrOffset + int(nCtrs) - 1
}
}
if tot == 0 {
return 0.0
}
return float64(totExec) / float64(tot)
}

View file

@ -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 cfile
import (
"encoding/json"
"internal/coverage"
"internal/goexperiment"
"internal/testenv"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
_ "unsafe"
)
//go:linkname testing_testGoCoverDir testing.testGoCoverDir
func testing_testGoCoverDir() string
func testGoCoverDir(t *testing.T) string {
tgcd := testing_testGoCoverDir()
if tgcd != "" {
return tgcd
}
return t.TempDir()
}
// TestTestSupport does a basic verification of the functionality in
// ProcessCoverTestDir (doing this here as opposed to
// relying on other test paths will provide a better signal when
// running "go test -cover" for this package).
func TestTestSupport(t *testing.T) {
if !goexperiment.CoverageRedesign {
return
}
if testing.CoverMode() == "" {
return
}
tgcd := testGoCoverDir(t)
t.Logf("testing.testGoCoverDir() returns %s mode=%s\n",
tgcd, testing.CoverMode())
textfile := filepath.Join(t.TempDir(), "file.txt")
var sb strings.Builder
err := ProcessCoverTestDir(tgcd, textfile,
testing.CoverMode(), "", &sb)
if err != nil {
t.Fatalf("bad: %v", err)
}
// Check for existence of text file.
if inf, err := os.Open(textfile); err != nil {
t.Fatalf("problems opening text file %s: %v", textfile, err)
} else {
inf.Close()
}
// Check for percent output with expected tokens.
strout := sb.String()
want := "of statements"
if !strings.Contains(strout, want) {
t.Logf("output from run: %s\n", strout)
t.Fatalf("percent output missing token: %q", want)
}
}
var funcInvoked bool
//go:noinline
func thisFunctionOnlyCalledFromSnapshotTest(n int) int {
if funcInvoked {
panic("bad")
}
funcInvoked = true
// Contents here not especially important, just so long as we
// have some statements.
t := 0
for i := 0; i < n; i++ {
for j := 0; j < i; j++ {
t += i ^ j
}
}
return t
}
// Tests runtime/coverage.snapshot() directly. Note that if
// coverage is not enabled, the hook is designed to just return
// zero.
func TestCoverageSnapshot(t *testing.T) {
C1 := Snapshot()
thisFunctionOnlyCalledFromSnapshotTest(15)
C2 := Snapshot()
cond := "C1 > C2"
val := C1 > C2
if testing.CoverMode() != "" {
cond = "C1 >= C2"
val = C1 >= C2
}
t.Logf("%f %f\n", C1, C2)
if val {
t.Errorf("erroneous snapshots, %s = true C1=%f C2=%f",
cond, C1, C2)
}
}
const hellogo = `
package main
func main() {
println("hello")
}
`
// Returns a pair F,T where F is a meta-data file generated from
// "hello.go" above, and T is a token to look for that should be
// present in the coverage report from F.
func genAuxMeta(t *testing.T, dstdir string) (string, string) {
// Do a GOCOVERDIR=<tmp> go run hello.go
src := filepath.Join(dstdir, "hello.go")
if err := os.WriteFile(src, []byte(hellogo), 0777); err != nil {
t.Fatalf("write failed: %v", err)
}
args := []string{"run", "-covermode=" + testing.CoverMode(), src}
cmd := exec.Command(testenv.GoToolPath(t), args...)
cmd.Env = updateGoCoverDir(os.Environ(), dstdir, true)
if b, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("go run failed (%v): %s", err, b)
}
// Pick out the generated meta-data file.
files, err := os.ReadDir(dstdir)
if err != nil {
t.Fatalf("reading %s: %v", dstdir, err)
}
for _, f := range files {
if strings.HasPrefix(f.Name(), "covmeta") {
return filepath.Join(dstdir, f.Name()), "hello.go:"
}
}
t.Fatalf("could not locate generated meta-data file")
return "", ""
}
func TestAuxMetaDataFiles(t *testing.T) {
if !goexperiment.CoverageRedesign {
return
}
if testing.CoverMode() == "" {
return
}
testenv.MustHaveGoRun(t)
tgcd := testGoCoverDir(t)
t.Logf("testing.testGoCoverDir() returns %s mode=%s\n",
tgcd, testing.CoverMode())
td := t.TempDir()
// Manufacture a new, separate meta-data file not related to this
// test. Contents are not important, just so long as the
// packages/paths are different.
othermetadir := filepath.Join(td, "othermeta")
if err := os.Mkdir(othermetadir, 0777); err != nil {
t.Fatalf("mkdir failed: %v", err)
}
mfile, token := genAuxMeta(t, othermetadir)
// Write a metafiles file.
metafiles := filepath.Join(tgcd, coverage.MetaFilesFileName)
mfc := coverage.MetaFileCollection{
ImportPaths: []string{"command-line-arguments"},
MetaFileFragments: []string{mfile},
}
jdata, err := json.Marshal(mfc)
if err != nil {
t.Fatalf("marshal MetaFileCollection: %v", err)
}
if err := os.WriteFile(metafiles, jdata, 0666); err != nil {
t.Fatalf("write failed: %v", err)
}
// Kick off guts of test.
var sb strings.Builder
textfile := filepath.Join(td, "file2.txt")
err = ProcessCoverTestDir(tgcd, textfile,
testing.CoverMode(), "", &sb)
if err != nil {
t.Fatalf("bad: %v", err)
}
if err = os.Remove(metafiles); err != nil {
t.Fatalf("removing metafiles file: %v", err)
}
// Look for the expected things in the coverage profile.
contents, err := os.ReadFile(textfile)
strc := string(contents)
if err != nil {
t.Fatalf("problems reading text file %s: %v", textfile, err)
}
if !strings.Contains(strc, token) {
t.Logf("content: %s\n", string(contents))
t.Fatalf("cov profile does not contain aux meta content %q", token)
}
}