| // 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) |
| } |