diff options
Diffstat (limited to '')
-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 | 609 | ||||
-rw-r--r-- | src/runtime/coverage/emitdata_test.go | 550 | ||||
-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/testdata/issue59563/repro.go | 823 | ||||
-rw-r--r-- | src/runtime/coverage/testdata/issue59563/repro_test.go | 14 | ||||
-rw-r--r-- | src/runtime/coverage/testsupport.go | 323 | ||||
-rw-r--r-- | src/runtime/coverage/ts_test.go | 207 |
12 files changed, 3047 insertions, 0 deletions
diff --git a/src/runtime/coverage/apis.go b/src/runtime/coverage/apis.go new file mode 100644 index 0000000..15ba04a --- /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" + "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 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 { + 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 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") + } + 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 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 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 +} 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..6fe04da --- /dev/null +++ b/src/runtime/coverage/emit.go @@ -0,0 +1,609 @@ +// 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" + "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. +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 +} + +// 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 runtime/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 (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..3558dd2 --- /dev/null +++ b/src/runtime/coverage/emitdata_test.go @@ -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 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. 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", 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) + } + } +} + +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", err) + } + + 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], "runtime/coverage/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)) + } +} 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/testdata/issue59563/repro.go b/src/runtime/coverage/testdata/issue59563/repro.go new file mode 100644 index 0000000..d054567 --- /dev/null +++ b/src/runtime/coverage/testdata/issue59563/repro.go @@ -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 +} diff --git a/src/runtime/coverage/testdata/issue59563/repro_test.go b/src/runtime/coverage/testdata/issue59563/repro_test.go new file mode 100644 index 0000000..15c8e01 --- /dev/null +++ b/src/runtime/coverage/testdata/issue59563/repro_test.go @@ -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) + } +} diff --git a/src/runtime/coverage/testsupport.go b/src/runtime/coverage/testsupport.go new file mode 100644 index 0000000..f169580 --- /dev/null +++ b/src/runtime/coverage/testsupport.go @@ -0,0 +1,323 @@ +// 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 ( + "encoding/json" + "fmt" + "internal/coverage" + "internal/coverage/calloc" + "internal/coverage/cformat" + "internal/coverage/cmerge" + "internal/coverage/decodecounter" + "internal/coverage/decodemeta" + "internal/coverage/pods" + "io" + "os" + "path/filepath" + "runtime/internal/atomic" + "strings" + "unsafe" +) + +// 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) + 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 { + // Unmarshall 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) +} diff --git a/src/runtime/coverage/ts_test.go b/src/runtime/coverage/ts_test.go new file mode 100644 index 0000000..b4c6e97 --- /dev/null +++ b/src/runtime/coverage/ts_test.go @@ -0,0 +1,207 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package coverage + +import ( + "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 +// 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 + } + 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 := processCoverTestDirInternal(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 = processCoverTestDirInternal(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) + } +} |