diff options
Diffstat (limited to 'src/runtime/coverage/testsupport.go')
-rw-r--r-- | src/runtime/coverage/testsupport.go | 323 |
1 files changed, 323 insertions, 0 deletions
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) +} |