diff options
Diffstat (limited to 'src/runtime/coverage')
-rw-r--r-- | src/runtime/coverage/apis.go | 178 | ||||
-rw-r--r-- | src/runtime/coverage/dummy.s | 8 | ||||
-rw-r--r-- | src/runtime/coverage/emit.go | 667 | ||||
-rw-r--r-- | src/runtime/coverage/emitdata_test.go | 451 | ||||
-rw-r--r-- | src/runtime/coverage/hooks.go | 42 | ||||
-rw-r--r-- | src/runtime/coverage/testdata/harness.go | 259 | ||||
-rw-r--r-- | src/runtime/coverage/testdata/issue56006/repro.go | 26 | ||||
-rw-r--r-- | src/runtime/coverage/testdata/issue56006/repro_test.go | 8 | ||||
-rw-r--r-- | src/runtime/coverage/testsupport.go | 234 | ||||
-rw-r--r-- | src/runtime/coverage/ts_test.go | 58 |
10 files changed, 1931 insertions, 0 deletions
diff --git a/src/runtime/coverage/apis.go b/src/runtime/coverage/apis.go new file mode 100644 index 0000000..7d851f9 --- /dev/null +++ b/src/runtime/coverage/apis.go @@ -0,0 +1,178 @@ +// 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" + "io" + "reflect" + "sync/atomic" + "unsafe" +) + +// WriteMetaDir writes a coverage meta-data file for the currently +// running program to the directory specified in 'dir'. An error will +// be returned if the operation can't be completed successfully (for +// example, if the currently running program was not built with +// "-cover", or if the directory does not exist). +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 writes the meta-data content (the payload that would +// normally be emitted to a meta-data file) for the currently running +// program to the the writer 'w'. An error will be returned if the +// operation can't be completed successfully (for example, if the +// currently running program was not built with "-cover", or if a +// write fails). +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 writes a coverage counter-data file for the +// currently running program to the directory specified in 'dir'. An +// error will be returned if the operation can't be completed +// successfully (for example, if the currently running program was not +// built with "-cover", or if the directory does not exist). The +// counter data written will be a snapshot taken at the point of the +// call. +func WriteCountersDir(dir string) error { + return emitCounterDataToDirectory(dir) +} + +// WriteCounters writes coverage counter-data content for +// the currently running program to the writer 'w'. An error will be +// returned if the operation can't be completed successfully (for +// example, if the currently running program was not built with +// "-cover", or if a write fails). The counter data written will be a +// snapshot taken at the point of the invocation. +func WriteCounters(w io.Writer) error { + if w == nil { + return fmt.Errorf("error: nil writer in WriteCounters") + } + // 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 clears/resets all coverage counter variables in the +// currently running program. It returns an error if the program in +// question was not built with the "-cover" flag. Clearing of coverage +// counters is also not supported for programs not using atomic +// counter mode (see more detailed comments below for the rationale +// here). +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 build 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. + + var sd []atomic.Uint32 + + bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) + for _, c := range cl { + 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. + 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 +} diff --git a/src/runtime/coverage/dummy.s b/src/runtime/coverage/dummy.s new file mode 100644 index 0000000..7592859 --- /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 0000000..2aed99c --- /dev/null +++ b/src/runtime/coverage/emit.go @@ -0,0 +1,667 @@ +// 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" + "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. +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 +} + +// emitMetaData 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 + fmt.Sprintf("%d", time.Now().UnixNano()) + s.mftmp = filepath.Join(s.outdir, tname) + s.mf, err = os.Create(s.mftmp) + if err != nil { + return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err) + } + } + return nil +} + +// openCounterFile opens an output file for the counter data portion +// of a test coverage run. If updates the 'cfname' and 'cf' fields in +// 's', returning an error if something went wrong. +func (s *emitState) openCounterFile(metaHash [16]byte) error { + processID := os.Getpid() + fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano()) + s.cfname = filepath.Join(s.outdir, fn) + s.cftmp = filepath.Join(s.outdir, "tmp."+fn) + var err error + s.cf, err = os.Create(s.cftmp) + if err != nil { + return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err) + } + return nil +} + +// openOutputFiles opens output files in preparation for emitting +// coverage data. In the case of the meta-data file, openOutputFiles +// may determine that we can reuse an existing meta-data file in the +// outdir, in which case it will leave the 'mf' field in the state +// struct as nil. If a new meta-file is needed, the field 'mfname' +// will be the final desired path of the meta file, 'mftmp' will be a +// temporary file, and 'mf' will be an open os.File pointer for +// 'mftmp'. The idea is that the client/caller will write content into +// 'mf', close it, and then rename 'mftmp' to 'mfname'. This function +// also opens the counter data output file, setting 'cf' and 'cfname' +// in the state struct. +func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error { + fi, err := os.Stat(s.outdir) + if err != nil { + return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err) + } + if !fi.IsDir() { + return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir) + } + + if (which & metaDataFile) != 0 { + if err := s.openMetaFile(metaHash, metaLen); err != nil { + return err + } + } + if (which & counterDataFile) != 0 { + if err := s.openCounterFile(metaHash); err != nil { + return err + } + } + return nil +} + +// emitMetaDataFile emits coverage meta-data to a previously opened +// temporary file (s.mftmp), then renames the generated file to the +// final path (s.mfname). +func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error { + if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil { + return fmt.Errorf("writing %s: %v\n", s.mftmp, err) + } + if err := s.mf.Close(); err != nil { + return fmt.Errorf("closing meta data temp file: %v", err) + } + + // Temp file has now been flushed and closed. Rename the temp to the + // final desired path. + if err := os.Rename(s.mftmp, s.mfname); err != nil { + return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err) + } + + return nil +} + +// needMetaDataFile returns TRUE if we need to emit a meta-data file +// for this program run. It should be used only after +// openOutputFiles() has been invoked. +func (s *emitState) needMetaDataFile() bool { + return s.mf != nil +} + +func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error { + mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w) + + // Note: "sd" is re-initialized on each iteration of the loop + // below, and would normally be declared inside the loop, but + // placed here escape analysis since we capture it in bufHdr. + var sd []byte + bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) + + var blobs [][]byte + for _, e := range metalist { + bufHdr.Data = uintptr(unsafe.Pointer(e.P)) + bufHdr.Len = int(e.Len) + bufHdr.Cap = int(e.Len) + blobs = append(blobs, sd) + } + return mfw.Write(finalHash, blobs, cmode, gran) +} + +func (s *emitState) NumFuncs() (int, error) { + var sd []atomic.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. + sdi := sd[i].Load() + if sdi == 0 { + continue + } + + // We found a function that was executed. + nCtrs := sdi + + // 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 + st := i + coverage.FirstCtrOffset + counters := sd[st : st+int(nCtrs)] + 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 + } + + totalFuncs++ + + // Move to the next function. + i += coverage.FirstCtrOffset + int(nCtrs) - 1 + } + } + return totalFuncs, nil +} + +func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error { + var sd []atomic.Uint32 + var tcounters []uint32 + bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) + + 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 { + 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. + 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"] = fmt.Sprintf("%d", len(os.Args)) + for k, a := range os.Args { + m[fmt.Sprintf("argv%d", k)] = a + } + m["GOOS"] = runtime.GOOS + m["GOARCH"] = runtime.GOARCH + return m +} + +// emitCounterDataFile emits the counter data portion of a +// coverage output file (to the file 's.cf'). +func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error { + cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128) + if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil { + return err + } + return nil +} + +// markProfileEmitted signals the runtime/coverage machinery that +// coverate data output files have already been written out, and there +// is no need to take any additional action at exit time. This +// function is called (via linknamed reference) from the +// coverage-related boilerplate code in _testmain.go emitted for go +// unit tests. +func markProfileEmitted(val bool) { + covProfileAlreadyEmitted = val +} + +func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) { + metaList := getCovMetaList() + pkgMap := getCovPkgMap() + + println("internal error in coverage meta-data tracking:") + println("encountered bad pkgID:", pkgID, " at slot:", slot, + " fnID:", fnID, " numCtrs:", nCtrs) + println("list of hard-coded runtime package IDs needs revising.") + println("[see the comment on the 'rtPkgs' var in ") + println(" <goroot>/src/internal/coverage/pkid.go]") + println("registered list:") + for k, b := range metaList { + print("slot: ", k, " path='", b.PkgPath, "' ") + if b.PkgID != -1 { + print(" hard-coded id: ", b.PkgID) + } + println("") + } + println("remap table:") + for from, to := range pkgMap { + println("from ", from, " to ", to) + } +} diff --git a/src/runtime/coverage/emitdata_test.go b/src/runtime/coverage/emitdata_test.go new file mode 100644 index 0000000..3839e44 --- /dev/null +++ b/src/runtime/coverage/emitdata_test.go @@ -0,0 +1,451 @@ +// 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/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. + bdir := mkdir(t, filepath.Join(dir, "build")) + hargs := []string{"-cover", "-coverpkg=all"} + if testing.CoverMode() != "" { + hargs = append(hargs, "-covermode="+testing.CoverMode()) + } + harnessPath := buildHarness(t, bdir, hargs) + + t.Logf("harness path is %s", harnessPath) + + // 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, harnessPath, dir) + }) + t.Run("emitToWriter", func(t *testing.T) { + t.Parallel() + testEmitToWriter(t, harnessPath, dir) + }) + t.Run("emitToNonexistentDir", func(t *testing.T) { + t.Parallel() + testEmitToNonexistentDir(t, harnessPath, dir) + }) + t.Run("emitToNilWriter", func(t *testing.T) { + t.Parallel() + testEmitToNilWriter(t, harnessPath, dir) + }) + t.Run("emitToFailingWriter", func(t *testing.T) { + t.Parallel() + testEmitToFailingWriter(t, harnessPath, dir) + }) + t.Run("emitWithCounterClear", func(t *testing.T) { + t.Parallel() + testEmitWithCounterClear(t, harnessPath, 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) { + if testing.CoverMode() == "" { + 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) + upmergeCoverData(t, rdir) + }) +} + +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) + upmergeCoverData(t, rdir) + }) +} + +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) + upmergeCoverData(t, rdir) + }) +} + +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) + upmergeCoverData(t, rdir) + }) +} + +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) + upmergeCoverData(t, rdir) + }) +} + +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) + upmergeCoverData(t, rdir) + }) +} + +func testEmitWithCounterClear(t *testing.T, harnessPath string, dir string) { + // Ensure that we have two versions of the harness: one built with + // -covermode=atomic and one built with -covermode=set (we need + // both modes to test all of the functionality). + var nonatomicHarnessPath, atomicHarnessPath string + if testing.CoverMode() != "atomic" { + nonatomicHarnessPath = harnessPath + bdir2 := mkdir(t, filepath.Join(dir, "build2")) + hargs := []string{"-covermode=atomic", "-coverpkg=all"} + atomicHarnessPath = buildHarness(t, bdir2, hargs) + } else { + atomicHarnessPath = harnessPath + mode := "set" + if testing.CoverMode() != "" && testing.CoverMode() != "atomic" { + mode = testing.CoverMode() + } + // Build a special nonatomic covermode version of the harness + // (we need both modes to test all of the functionality). + bdir2 := mkdir(t, filepath.Join(dir, "build2")) + hargs := []string{"-covermode=" + mode, "-coverpkg=all"} + nonatomicHarnessPath = buildHarness(t, bdir2, hargs) + } + + withAndWithoutRunner(func(setGoCoverDir bool, tag string) { + // First a run with the nonatomic harness path, which we + // expect to fail. + tp := "emitWithCounterClear" + rdir1, edir1 := mktestdirs(t, tag, tp+"1", dir) + output, err := runHarness(t, nonatomicHarnessPath, tp, + setGoCoverDir, rdir1, edir1) + if err == nil { + t.Logf("%s", output) + t.Fatalf("running '%s -tp %s': unexpected success", + nonatomicHarnessPath, tp) + } + + // Next a run with the atomic harness path, which we + // expect to succeed. + rdir2, edir2 := mktestdirs(t, tag, tp+"2", dir) + output, err = runHarness(t, atomicHarnessPath, tp, + setGoCoverDir, rdir2, edir2) + 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, edir2, want, avoid); msg != "" { + t.Logf("%s", output) + t.Errorf("coverage data from %q output match failed: %s", tp, msg) + } + + if testing.CoverMode() == "atomic" { + upmergeCoverData(t, edir2) + upmergeCoverData(t, rdir2) + } else { + upmergeCoverData(t, edir1) + upmergeCoverData(t, rdir1) + } + }) +} + +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", err) + } + + // 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) + } + } +} diff --git a/src/runtime/coverage/hooks.go b/src/runtime/coverage/hooks.go new file mode 100644 index 0000000..a9fbf9d --- /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/testdata/harness.go b/src/runtime/coverage/testdata/harness.go new file mode 100644 index 0000000..5c87e4c --- /dev/null +++ b/src/runtime/coverage/testdata/harness.go @@ -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" + "io/ioutil" + "log" + "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 := ioutil.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 := ioutil.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() +} diff --git a/src/runtime/coverage/testdata/issue56006/repro.go b/src/runtime/coverage/testdata/issue56006/repro.go new file mode 100644 index 0000000..60a4925 --- /dev/null +++ b/src/runtime/coverage/testdata/issue56006/repro.go @@ -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 + } + } +} diff --git a/src/runtime/coverage/testdata/issue56006/repro_test.go b/src/runtime/coverage/testdata/issue56006/repro_test.go new file mode 100644 index 0000000..674d819 --- /dev/null +++ b/src/runtime/coverage/testdata/issue56006/repro_test.go @@ -0,0 +1,8 @@ +package main + +import "testing" + +func TestSomething(t *testing.T) { + go infloop() + println(blah(1) + blah(0)) +} diff --git a/src/runtime/coverage/testsupport.go b/src/runtime/coverage/testsupport.go new file mode 100644 index 0000000..a481bbb --- /dev/null +++ b/src/runtime/coverage/testsupport.go @@ -0,0 +1,234 @@ +// 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" + "io" + "os" + "strings" +) + +// 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 { + return processCoverTestDirInternal(dir, cfile, cm, cpkg, os.Stdout) +} + +// processCoverTestDirInternal is an io.Writer version of processCoverTestDir, +// exposed for unit testing. +func processCoverTestDirInternal(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) + for _, p := range podlist { + if !strings.Contains(p.MetaFile, hashstring) { + continue + } + if err := ts.processPod(p); err != nil { + return err + } + } + + // Emit percent. + if err := ts.cf.EmitPercent(w, 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 + } + + // 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()) + 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/coverage/ts_test.go b/src/runtime/coverage/ts_test.go new file mode 100644 index 0000000..b826058 --- /dev/null +++ b/src/runtime/coverage/ts_test.go @@ -0,0 +1,58 @@ +// 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 ( + "internal/goexperiment" + "os" + "path/filepath" + "strings" + "testing" + _ "unsafe" +) + +//go:linkname testing_testGoCoverDir testing.testGoCoverDir +func testing_testGoCoverDir() string + +// TestTestSupport does a basic verification of the functionality in +// runtime/coverage.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 + } + t.Logf("testing.testGoCoverDir() returns %s mode=%s\n", + testing_testGoCoverDir(), testing.CoverMode()) + + textfile := filepath.Join(t.TempDir(), "file.txt") + var sb strings.Builder + err := processCoverTestDirInternal(testing_testGoCoverDir(), 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() + want1 := "runtime/coverage" + want2 := "of statements" + if !strings.Contains(strout, want1) || + !strings.Contains(strout, want2) { + t.Logf("output from run: %s\n", strout) + t.Fatalf("percent output missing key tokens: %q and %q", + want1, want2) + } +} |