aboutsummaryrefslogtreecommitdiff
path: root/src/internal/coverage
diff options
context:
space:
mode:
authorThan McIntosh <thanm@google.com>2021-10-21 10:57:23 -0400
committerThan McIntosh <thanm@google.com>2022-09-28 11:47:16 +0000
commit7a74829858bbc2fdf8b2d8902f7df46935cf6be9 (patch)
treee890c58e098c341d3c588d511cd73f5dac7df7fc /src/internal/coverage
parentdbe56ff6c7a0162f318ed2dd7331c9de82e11d6d (diff)
downloadgo-7a74829858bbc2fdf8b2d8902f7df46935cf6be9.tar.xz
cmd/covdata: add tools to read/manipulate coverage data files
Add a set of helper packages for reading collections of related meta-data and counter-data files ("pods") produced by runs of coverage-instrumented binaries, and a new tool program (cmd/covdata) for dumping and/or manipulating coverage data files. Currently "go tool covdata" subcommands include 'merge', 'intersect', 'percent', 'pkglist', 'subtract', 'debugdump', and 'textfmt' (conversion to the legacy "go tool cover" format). Updates #51430. Change-Id: I44167c578f574b4636ab8726e726388531fd3258 Reviewed-on: https://go-review.googlesource.com/c/go/+/357609 Run-TryBot: Than McIntosh <thanm@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: David Chase <drchase@google.com>
Diffstat (limited to 'src/internal/coverage')
-rw-r--r--src/internal/coverage/calloc/batchcounteralloc.go29
-rw-r--r--src/internal/coverage/cformat/fmt_test.go92
-rw-r--r--src/internal/coverage/cformat/format.go340
-rw-r--r--src/internal/coverage/cmerge/merge.go104
-rw-r--r--src/internal/coverage/cmerge/merge_test.go105
-rw-r--r--src/internal/coverage/pods/pods.go194
-rw-r--r--src/internal/coverage/pods/pods_test.go143
7 files changed, 1007 insertions, 0 deletions
diff --git a/src/internal/coverage/calloc/batchcounteralloc.go b/src/internal/coverage/calloc/batchcounteralloc.go
new file mode 100644
index 0000000000..2b6495d7a2
--- /dev/null
+++ b/src/internal/coverage/calloc/batchcounteralloc.go
@@ -0,0 +1,29 @@
+// 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 calloc
+
+// This package contains a simple "batch" allocator for allocating
+// coverage counters (slices of uint32 basically), for working with
+// coverage data files. Collections of counter arrays tend to all be
+// live/dead over the same time period, so a good fit for batch
+// allocation.
+
+type BatchCounterAlloc struct {
+ pool []uint32
+}
+
+func (ca *BatchCounterAlloc) AllocateCounters(n int) []uint32 {
+ const chunk = 8192
+ if n > cap(ca.pool) {
+ siz := chunk
+ if n > chunk {
+ siz = n
+ }
+ ca.pool = make([]uint32, siz)
+ }
+ rv := ca.pool[:n]
+ ca.pool = ca.pool[n:]
+ return rv
+}
diff --git a/src/internal/coverage/cformat/fmt_test.go b/src/internal/coverage/cformat/fmt_test.go
new file mode 100644
index 0000000000..4d6da44286
--- /dev/null
+++ b/src/internal/coverage/cformat/fmt_test.go
@@ -0,0 +1,92 @@
+// 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 cformat_test
+
+import (
+ "internal/coverage"
+ "internal/coverage/cformat"
+ "strings"
+ "testing"
+)
+
+func TestBasics(t *testing.T) {
+ fm := cformat.NewFormatter(coverage.CtrModeAtomic)
+ fm.SetPackage("my/pack")
+
+ mku := func(stl, enl, nx uint32) coverage.CoverableUnit {
+ return coverage.CoverableUnit{
+ StLine: stl,
+ EnLine: enl,
+ NxStmts: nx,
+ }
+ }
+ fn1units := []coverage.CoverableUnit{
+ mku(10, 11, 2),
+ mku(15, 11, 1),
+ }
+ fn2units := []coverage.CoverableUnit{
+ mku(20, 25, 3),
+ mku(30, 31, 2),
+ mku(33, 40, 7),
+ }
+ fn3units := []coverage.CoverableUnit{
+ mku(99, 100, 1),
+ }
+ for k, u := range fn1units {
+ fm.AddUnit("p.go", "f1", false, u, uint32(k))
+ }
+ for k, u := range fn2units {
+ fm.AddUnit("q.go", "f2", false, u, 0)
+ fm.AddUnit("q.go", "f2", false, u, uint32(k))
+ }
+ for _, u := range fn3units {
+ fm.AddUnit("lit.go", "f3", true, u, 0)
+ }
+
+ var b1, b2, b3 strings.Builder
+ if err := fm.EmitTextual(&b1); err != nil {
+ t.Fatalf("EmitTextual returned %v", err)
+ }
+ wantText := strings.TrimSpace(`
+mode: atomic
+lit.go:99.0,100.0 1 0
+p.go:10.0,11.0 2 0
+p.go:15.0,11.0 1 1
+q.go:20.0,25.0 3 0
+q.go:30.0,31.0 2 1
+q.go:33.0,40.0 7 2`)
+ gotText := strings.TrimSpace(b1.String())
+ if wantText != gotText {
+ t.Errorf("emit text: got:\n%s\nwant:\n%s\n", gotText, wantText)
+ }
+
+ if err := fm.EmitPercent(&b2, "", false); err != nil {
+ t.Fatalf("EmitPercent returned %v", err)
+ }
+ wantPercent := strings.TrimSpace(`
+my/pack coverage: 62.5% of statements
+`)
+ gotPercent := strings.TrimSpace(b2.String())
+ if wantPercent != gotPercent {
+ t.Errorf("emit percent: got:\n%s\nwant:\n%s\n", gotPercent, wantPercent)
+ }
+
+ if err := fm.EmitFuncs(&b3); err != nil {
+ t.Fatalf("EmitFuncs returned %v", err)
+ }
+ wantFuncs := strings.TrimSpace(`
+p.go:10: f1 33.3%
+q.go:20: f2 75.0%
+total (statements) 62.5%`)
+ gotFuncs := strings.TrimSpace(b3.String())
+ if wantFuncs != gotFuncs {
+ t.Errorf("emit funcs: got:\n%s\nwant:\n%s\n", gotFuncs, wantFuncs)
+ }
+ if false {
+ t.Logf("text is %s\n", b1.String())
+ t.Logf("perc is %s\n", b2.String())
+ t.Logf("funcs is %s\n", b3.String())
+ }
+}
diff --git a/src/internal/coverage/cformat/format.go b/src/internal/coverage/cformat/format.go
new file mode 100644
index 0000000000..a8276ff124
--- /dev/null
+++ b/src/internal/coverage/cformat/format.go
@@ -0,0 +1,340 @@
+// 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 cformat
+
+// This package provides apis for producing human-readable summaries
+// of coverage data (e.g. a coverage percentage for a given package or
+// set of packages) and for writing data in the legacy test format
+// emitted by "go test -coverprofile=<outfile>".
+//
+// The model for using these apis is to create a Formatter object,
+// then make a series of calls to SetPackage and AddUnit passing in
+// data read from coverage meta-data and counter-data files. E.g.
+//
+// myformatter := cformat.NewFormatter()
+// ...
+// for each package P in meta-data file: {
+// myformatter.SetPackage(P)
+// for each function F in P: {
+// for each coverable unit U in F: {
+// myformatter.AddUnit(U)
+// }
+// }
+// }
+// myformatter.EmitPercent(os.Stdout, "")
+// myformatter.EmitTextual(somefile)
+//
+// These apis are linked into tests that are built with "-cover", and
+// called at the end of test execution to produce text output or
+// emit coverage percentages.
+
+import (
+ "fmt"
+ "internal/coverage"
+ "internal/coverage/cmerge"
+ "io"
+ "sort"
+ "text/tabwriter"
+)
+
+type Formatter struct {
+ // Maps import path to package state.
+ pm map[string]*pstate
+ // Records current package being visited.
+ pkg string
+ // Pointer to current package state.
+ p *pstate
+ // Counter mode.
+ cm coverage.CounterMode
+}
+
+// pstate records package-level coverage data state:
+// - a table of functions (file/fname/literal)
+// - a map recording the index/ID of each func encountered so far
+// - a table storing execution count for the coverable units in each func
+type pstate struct {
+ // slice of unique functions
+ funcs []fnfile
+ // maps function to index in slice above (index acts as function ID)
+ funcTable map[fnfile]uint32
+
+ // A table storing coverage counts for each coverable unit.
+ unitTable map[extcu]uint32
+}
+
+// extcu encapsulates a coverable unit within some function.
+type extcu struct {
+ fnfid uint32 // index into p.funcs slice
+ coverage.CoverableUnit
+}
+
+// fnfile is a function-name/file-name tuple.
+type fnfile struct {
+ file string
+ fname string
+ lit bool
+}
+
+func NewFormatter(cm coverage.CounterMode) *Formatter {
+ return &Formatter{
+ pm: make(map[string]*pstate),
+ cm: cm,
+ }
+}
+
+// SetPackage tells the formatter that we're about to visit the
+// coverage data for the package with the specified import path.
+// Note that it's OK to call SetPackage more than once with the
+// same import path; counter data values will be accumulated.
+func (fm *Formatter) SetPackage(importpath string) {
+ if importpath == fm.pkg {
+ return
+ }
+ fm.pkg = importpath
+ ps, ok := fm.pm[importpath]
+ if !ok {
+ ps = new(pstate)
+ fm.pm[importpath] = ps
+ ps.unitTable = make(map[extcu]uint32)
+ ps.funcTable = make(map[fnfile]uint32)
+ }
+ fm.p = ps
+}
+
+// AddUnit passes info on a single coverable unit (file, funcname,
+// literal flag, range of lines, and counter value) to the formatter.
+// Counter values will be accumulated where appropriate.
+func (fm *Formatter) AddUnit(file string, fname string, isfnlit bool, unit coverage.CoverableUnit, count uint32) {
+ if fm.p == nil {
+ panic("AddUnit invoked before SetPackage")
+ }
+ fkey := fnfile{file: file, fname: fname, lit: isfnlit}
+ idx, ok := fm.p.funcTable[fkey]
+ if !ok {
+ idx = uint32(len(fm.p.funcs))
+ fm.p.funcs = append(fm.p.funcs, fkey)
+ fm.p.funcTable[fkey] = idx
+ }
+ ukey := extcu{fnfid: idx, CoverableUnit: unit}
+ pcount := fm.p.unitTable[ukey]
+ var result uint32
+ if fm.cm == coverage.CtrModeSet {
+ if count != 0 || pcount != 0 {
+ result = 1
+ }
+ } else {
+ // Use saturating arithmetic.
+ result, _ = cmerge.SaturatingAdd(pcount, count)
+ }
+ fm.p.unitTable[ukey] = result
+}
+
+// sortUnits sorts a slice of extcu objects in a package according to
+// source position information (e.g. file and line). Note that we don't
+// include function name as part of the sorting criteria, the thinking
+// being that is better to provide things in the original source order.
+func (p *pstate) sortUnits(units []extcu) {
+ sort.Slice(units, func(i, j int) bool {
+ ui := units[i]
+ uj := units[j]
+ ifile := p.funcs[ui.fnfid].file
+ jfile := p.funcs[uj.fnfid].file
+ if ifile != jfile {
+ return ifile < jfile
+ }
+ // NB: not taking function literal flag into account here (no
+ // need, since other fields are guaranteed to be distinct).
+ if units[i].StLine != units[j].StLine {
+ return units[i].StLine < units[j].StLine
+ }
+ if units[i].EnLine != units[j].EnLine {
+ return units[i].EnLine < units[j].EnLine
+ }
+ if units[i].StCol != units[j].StCol {
+ return units[i].StCol < units[j].StCol
+ }
+ if units[i].EnCol != units[j].EnCol {
+ return units[i].EnCol < units[j].EnCol
+ }
+ return units[i].NxStmts < units[j].NxStmts
+ })
+}
+
+// EmitTextual writes the accumulated coverage data in the legacy
+// cmd/cover text format to the writer 'w'. We sort the data items by
+// importpath, source file, and line number before emitting (this sorting
+// is not explicitly mandated by the format, but seems like a good idea
+// for repeatable/deterministic dumps).
+func (fm *Formatter) EmitTextual(w io.Writer) error {
+ if fm.cm == coverage.CtrModeInvalid {
+ panic("internal error, counter mode unset")
+ }
+ if _, err := fmt.Fprintf(w, "mode: %s\n", fm.cm.String()); err != nil {
+ return err
+ }
+ pkgs := make([]string, 0, len(fm.pm))
+ for importpath := range fm.pm {
+ pkgs = append(pkgs, importpath)
+ }
+ sort.Strings(pkgs)
+ for _, importpath := range pkgs {
+ p := fm.pm[importpath]
+ units := make([]extcu, 0, len(p.unitTable))
+ for u := range p.unitTable {
+ units = append(units, u)
+ }
+ p.sortUnits(units)
+ for _, u := range units {
+ count := p.unitTable[u]
+ file := p.funcs[u.fnfid].file
+ if _, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n",
+ file, u.StLine, u.StCol,
+ u.EnLine, u.EnCol, u.NxStmts, count); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// EmitPercent writes out a "percentage covered" string to the writer 'w'.
+func (fm *Formatter) EmitPercent(w io.Writer, covpkgs string, noteEmpty bool) error {
+ pkgs := make([]string, 0, len(fm.pm))
+ for importpath := range fm.pm {
+ pkgs = append(pkgs, importpath)
+ }
+ sort.Strings(pkgs)
+ seenPkg := false
+ for _, importpath := range pkgs {
+ seenPkg = true
+ p := fm.pm[importpath]
+ var totalStmts, coveredStmts uint64
+ for unit, count := range p.unitTable {
+ nx := uint64(unit.NxStmts)
+ totalStmts += nx
+ if count != 0 {
+ coveredStmts += nx
+ }
+ }
+ if _, err := fmt.Fprintf(w, "\t%s\t", importpath); err != nil {
+ return err
+ }
+ if totalStmts == 0 {
+ if _, err := fmt.Fprintf(w, "coverage: [no statements]\n"); err != nil {
+ return err
+ }
+ } else {
+ if _, err := fmt.Fprintf(w, "coverage: %.1f%% of statements%s\n", 100*float64(coveredStmts)/float64(totalStmts), covpkgs); err != nil {
+ return err
+ }
+ }
+ }
+ if noteEmpty && !seenPkg {
+ if _, err := fmt.Fprintf(w, "coverage: [no statements]\n"); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// EmitFuncs writes out a function-level summary to the writer 'w'. A
+// note on handling function literals: although we collect coverage
+// data for unnamed literals, it probably does not make sense to
+// include them in the function summary since there isn't any good way
+// to name them (this is also consistent with the legacy cmd/cover
+// implementation). We do want to include their counts in the overall
+// summary however.
+func (fm *Formatter) EmitFuncs(w io.Writer) error {
+ if fm.cm == coverage.CtrModeInvalid {
+ panic("internal error, counter mode unset")
+ }
+ perc := func(covered, total uint64) float64 {
+ if total == 0 {
+ total = 1
+ }
+ return 100.0 * float64(covered) / float64(total)
+ }
+ tabber := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
+ defer tabber.Flush()
+ allStmts := uint64(0)
+ covStmts := uint64(0)
+
+ pkgs := make([]string, 0, len(fm.pm))
+ for importpath := range fm.pm {
+ pkgs = append(pkgs, importpath)
+ }
+ sort.Strings(pkgs)
+
+ // Emit functions for each package, sorted by import path.
+ for _, importpath := range pkgs {
+ p := fm.pm[importpath]
+ if len(p.unitTable) == 0 {
+ continue
+ }
+ units := make([]extcu, 0, len(p.unitTable))
+ for u := range p.unitTable {
+ units = append(units, u)
+ }
+
+ // Within a package, sort the units, then walk through the
+ // sorted array. Each time we hit a new function, emit the
+ // summary entry for the previous function, then make one last
+ // emit call at the end of the loop.
+ p.sortUnits(units)
+ fname := ""
+ ffile := ""
+ flit := false
+ var fline uint32
+ var cstmts, tstmts uint64
+ captureFuncStart := func(u extcu) {
+ fname = p.funcs[u.fnfid].fname
+ ffile = p.funcs[u.fnfid].file
+ flit = p.funcs[u.fnfid].lit
+ fline = u.StLine
+ }
+ emitFunc := func(u extcu) error {
+ // Don't emit entries for function literals (see discussion
+ // in function header comment above).
+ if !flit {
+ if _, err := fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n",
+ ffile, fline, fname, perc(cstmts, tstmts)); err != nil {
+ return err
+ }
+ }
+ captureFuncStart(u)
+ allStmts += tstmts
+ covStmts += cstmts
+ tstmts = 0
+ cstmts = 0
+ return nil
+ }
+ for k, u := range units {
+ if k == 0 {
+ captureFuncStart(u)
+ } else {
+ if fname != p.funcs[u.fnfid].fname {
+ // New function; emit entry for previous one.
+ if err := emitFunc(u); err != nil {
+ return err
+ }
+ }
+ }
+ tstmts += uint64(u.NxStmts)
+ count := p.unitTable[u]
+ if count != 0 {
+ cstmts += uint64(u.NxStmts)
+ }
+ }
+ if err := emitFunc(extcu{}); err != nil {
+ return err
+ }
+ }
+ if _, err := fmt.Fprintf(tabber, "%s\t%s\t%.1f%%\n",
+ "total", "(statements)", perc(covStmts, allStmts)); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/src/internal/coverage/cmerge/merge.go b/src/internal/coverage/cmerge/merge.go
new file mode 100644
index 0000000000..c482b8bfa8
--- /dev/null
+++ b/src/internal/coverage/cmerge/merge.go
@@ -0,0 +1,104 @@
+// 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 cmerge
+
+// package cmerge provides a few small utility APIs for helping
+// with merging of counter data for a given function.
+
+import (
+ "fmt"
+ "internal/coverage"
+ "math"
+)
+
+// Merger provides state and methods to help manage the process of
+// merging together coverage counter data for a given function, for
+// tools that need to implicitly merge counter as they read multiple
+// coverage counter data files.
+type Merger struct {
+ cmode coverage.CounterMode
+ cgran coverage.CounterGranularity
+ overflow bool
+}
+
+// MergeCounters takes the counter values in 'src' and merges them
+// into 'dst' according to the correct counter mode.
+func (m *Merger) MergeCounters(dst, src []uint32) (error, bool) {
+ if len(src) != len(dst) {
+ return fmt.Errorf("merging counters: len(dst)=%d len(src)=%d", len(dst), len(src)), false
+ }
+ if m.cmode == coverage.CtrModeSet {
+ for i := 0; i < len(src); i++ {
+ if src[i] != 0 {
+ dst[i] = 1
+ }
+ }
+ } else {
+ for i := 0; i < len(src); i++ {
+ dst[i] = m.SaturatingAdd(dst[i], src[i])
+ }
+ }
+ ovf := m.overflow
+ m.overflow = false
+ return nil, ovf
+}
+
+// Saturating add does a saturating addition of 'dst' and 'src',
+// returning added value or math.MaxUint32 if there is an overflow.
+// Overflows are recorded in case the client needs to track them.
+func (m *Merger) SaturatingAdd(dst, src uint32) uint32 {
+ result, overflow := SaturatingAdd(dst, src)
+ if overflow {
+ m.overflow = true
+ }
+ return result
+}
+
+// Saturating add does a saturing addition of 'dst' and 'src',
+// returning added value or math.MaxUint32 plus an overflow flag.
+func SaturatingAdd(dst, src uint32) (uint32, bool) {
+ d, s := uint64(dst), uint64(src)
+ sum := d + s
+ overflow := false
+ if uint64(uint32(sum)) != sum {
+ overflow = true
+ sum = math.MaxUint32
+ }
+ return uint32(sum), overflow
+}
+
+// SetModeAndGranularity records the counter mode and granularity for
+// the current merge. In the specific case of merging across coverage
+// data files from different binaries, where we're combining data from
+// more than one meta-data file, we need to check for mode/granularity
+// clashes.
+func (cm *Merger) SetModeAndGranularity(mdf string, cmode coverage.CounterMode, cgran coverage.CounterGranularity) error {
+ // Collect counter mode and granularity so as to detect clashes.
+ if cm.cmode != coverage.CtrModeInvalid {
+ if cm.cmode != cmode {
+ return fmt.Errorf("counter mode clash while reading meta-data file %s: previous file had %s, new file has %s", mdf, cm.cmode.String(), cmode.String())
+ }
+ if cm.cgran != cgran {
+ return fmt.Errorf("counter granularity clash while reading meta-data file %s: previous file had %s, new file has %s", mdf, cm.cgran.String(), cgran.String())
+ }
+ }
+ cm.cmode = cmode
+ cm.cgran = cgran
+ return nil
+}
+
+func (cm *Merger) ResetModeAndGranularity() {
+ cm.cmode = coverage.CtrModeInvalid
+ cm.cgran = coverage.CtrGranularityInvalid
+ cm.overflow = false
+}
+
+func (cm *Merger) Mode() coverage.CounterMode {
+ return cm.cmode
+}
+
+func (cm *Merger) Granularity() coverage.CounterGranularity {
+ return cm.cgran
+}
diff --git a/src/internal/coverage/cmerge/merge_test.go b/src/internal/coverage/cmerge/merge_test.go
new file mode 100644
index 0000000000..e45589f6b8
--- /dev/null
+++ b/src/internal/coverage/cmerge/merge_test.go
@@ -0,0 +1,105 @@
+// 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 cmerge_test
+
+import (
+ "fmt"
+ "internal/coverage"
+ "internal/coverage/cmerge"
+ "testing"
+)
+
+func TestClash(t *testing.T) {
+ m := &cmerge.Merger{}
+ err := m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerBlock)
+ if err != nil {
+ t.Fatalf("unexpected clash")
+ }
+ err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerBlock)
+ if err != nil {
+ t.Fatalf("unexpected clash")
+ }
+ err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeCount, coverage.CtrGranularityPerBlock)
+ if err == nil {
+ t.Fatalf("expected mode clash, not found")
+ }
+ err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeSet, coverage.CtrGranularityPerFunc)
+ if err == nil {
+ t.Fatalf("expected granularity clash, not found")
+ }
+ m.ResetModeAndGranularity()
+ err = m.SetModeAndGranularity("mdf1.data", coverage.CtrModeCount, coverage.CtrGranularityPerFunc)
+ if err != nil {
+ t.Fatalf("unexpected clash after reset")
+ }
+}
+
+func TestBasic(t *testing.T) {
+ scenarios := []struct {
+ cmode coverage.CounterMode
+ cgran coverage.CounterGranularity
+ src, dst, res []uint32
+ iters int
+ merr bool
+ overflow bool
+ }{
+ {
+ cmode: coverage.CtrModeSet,
+ cgran: coverage.CtrGranularityPerBlock,
+ src: []uint32{1, 0, 1},
+ dst: []uint32{1, 1, 0},
+ res: []uint32{1, 1, 1},
+ iters: 2,
+ overflow: false,
+ },
+ {
+ cmode: coverage.CtrModeCount,
+ cgran: coverage.CtrGranularityPerBlock,
+ src: []uint32{1, 0, 3},
+ dst: []uint32{5, 7, 0},
+ res: []uint32{6, 7, 3},
+ iters: 1,
+ overflow: false,
+ },
+ {
+ cmode: coverage.CtrModeCount,
+ cgran: coverage.CtrGranularityPerBlock,
+ src: []uint32{4294967200, 0, 3},
+ dst: []uint32{4294967001, 7, 0},
+ res: []uint32{4294967295, 7, 3},
+ iters: 1,
+ overflow: true,
+ },
+ }
+
+ for k, scenario := range scenarios {
+ var err error
+ var ovf bool
+ m := &cmerge.Merger{}
+ mdf := fmt.Sprintf("file%d", k)
+ err = m.SetModeAndGranularity(mdf, scenario.cmode, scenario.cgran)
+ if err != nil {
+ t.Fatalf("case %d SetModeAndGranularity failed: %v", k, err)
+ }
+ for i := 0; i < scenario.iters; i++ {
+ err, ovf = m.MergeCounters(scenario.dst, scenario.src)
+ if ovf != scenario.overflow {
+ t.Fatalf("case %d overflow mismatch: got %v want %v", k, ovf, scenario.overflow)
+ }
+ if !scenario.merr && err != nil {
+ t.Fatalf("case %d unexpected err %v", k, err)
+ }
+ if scenario.merr && err == nil {
+ t.Fatalf("case %d expected err, not received", k)
+ }
+ for i := range scenario.dst {
+ if scenario.dst[i] != scenario.res[i] {
+ t.Fatalf("case %d: bad merge at %d got %d want %d",
+ k, i, scenario.dst[i], scenario.res[i])
+ }
+ }
+ }
+ }
+}
diff --git a/src/internal/coverage/pods/pods.go b/src/internal/coverage/pods/pods.go
new file mode 100644
index 0000000000..432c7b6bd6
--- /dev/null
+++ b/src/internal/coverage/pods/pods.go
@@ -0,0 +1,194 @@
+// 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 pods
+
+import (
+ "fmt"
+ "internal/coverage"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strconv"
+)
+
+// Pod encapsulates a set of files emitted during the executions of a
+// coverage-instrumented binary. Each pod contains a single meta-data
+// file, and then 0 or more counter data files that refer to that
+// meta-data file. Pods are intended to simplify processing of
+// coverage output files in the case where we have several coverage
+// output directories containing output files derived from more
+// than one instrumented executable. In the case where the files that
+// make up a pod are spread out across multiple directories, each
+// element of the "Origins" field below will be populated with the
+// index of the originating directory for the corresponding counter
+// data file (within the slice of input dirs handed to CollectPods).
+// The ProcessIDs field will be populated with the process ID of each
+// data file in the CounterDataFiles slice.
+type Pod struct {
+ MetaFile string
+ CounterDataFiles []string
+ Origins []int
+ ProcessIDs []int
+}
+
+// CollectPods visits the files contained within the directories in
+// the list 'dirs', collects any coverage-related files, partitions
+// them into pods, and returns a list of the pods to the caller, along
+// with an error if something went wrong during directory/file
+// reading.
+//
+// CollectPods skips over any file that is not related to coverage
+// (e.g. avoids looking at things that are not meta-data files or
+// counter-data files). CollectPods also skips over 'orphaned' counter
+// data files (e.g. counter data files for which we can't find the
+// corresponding meta-data file). If "warn" is true, CollectPods will
+// issue warnings to stderr when it encounters non-fatal problems (for
+// orphans or a directory with no meta-data files).
+func CollectPods(dirs []string, warn bool) ([]Pod, error) {
+ files := []string{}
+ dirIndices := []int{}
+ for k, dir := range dirs {
+ dents, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ for _, e := range dents {
+ if e.IsDir() {
+ continue
+ }
+ files = append(files, filepath.Join(dir, e.Name()))
+ dirIndices = append(dirIndices, k)
+ }
+ }
+ return collectPodsImpl(files, dirIndices, warn), nil
+}
+
+// CollectPodsFromFiles functions the same as "CollectPods" but
+// operates on an explicit list of files instead of a directory.
+func CollectPodsFromFiles(files []string, warn bool) []Pod {
+ return collectPodsImpl(files, nil, warn)
+}
+
+type fileWithAnnotations struct {
+ file string
+ origin int
+ pid int
+}
+
+type protoPod struct {
+ mf string
+ elements []fileWithAnnotations
+}
+
+// collectPodsImpl examines the specified list of files and picks out
+// subsets that correspond to coverage pods. The first stage in this
+// process is collecting a set { M1, M2, ... MN } where each M_k is a
+// distinct coverage meta-data file. We then create a single pod for
+// each meta-data file M_k, then find all of the counter data files
+// that refer to that meta-data file (recall that the counter data
+// file name incorporates the meta-data hash), and add the counter
+// data file to the appropriate pod.
+//
+// This process is complicated by the fact that we need to keep track
+// of directory indices for counter data files. Here is an example to
+// motivate:
+//
+// directory 1:
+//
+// M1 covmeta.9bbf1777f47b3fcacb05c38b035512d6
+// C1 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677673.1662138360208416486
+// C2 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677637.1662138359974441782
+//
+// directory 2:
+//
+// M2 covmeta.9bbf1777f47b3fcacb05c38b035512d6
+// C3 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677445.1662138360208416480
+// C4 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677677.1662138359974441781
+// M3 covmeta.a723844208cea2ae80c63482c78b2245
+// C5 covcounters.a723844208cea2ae80c63482c78b2245.3677445.1662138360208416480
+// C6 covcounters.a723844208cea2ae80c63482c78b2245.1877677.1662138359974441781
+//
+// In these two directories we have three meta-data files, but only
+// two are distinct, meaning that we'll wind up with two pods. The
+// first pod (with meta-file M1) will have four counter data files
+// (C1, C2, C3, C4) and the second pod will have two counter data files
+// (C5, C6).
+func collectPodsImpl(files []string, dirIndices []int, warn bool) []Pod {
+ metaRE := regexp.MustCompile(fmt.Sprintf(`^%s\.(\S+)$`, coverage.MetaFilePref))
+ mm := make(map[string]protoPod)
+ for _, f := range files {
+ base := filepath.Base(f)
+ if m := metaRE.FindStringSubmatch(base); m != nil {
+ tag := m[1]
+ // We need to allow for the possibility of duplicate
+ // meta-data files. If we hit this case, use the
+ // first encountered as the canonical version.
+ if _, ok := mm[tag]; !ok {
+ mm[tag] = protoPod{mf: f}
+ }
+ // FIXME: should probably check file length and hash here for
+ // the duplicate.
+ }
+ }
+ counterRE := regexp.MustCompile(fmt.Sprintf(coverage.CounterFileRegexp, coverage.CounterFilePref))
+ for k, f := range files {
+ base := filepath.Base(f)
+ if m := counterRE.FindStringSubmatch(base); m != nil {
+ tag := m[1] // meta hash
+ pid, err := strconv.Atoi(m[2])
+ if err != nil {
+ continue
+ }
+ if v, ok := mm[tag]; ok {
+ idx := -1
+ if dirIndices != nil {
+ idx = dirIndices[k]
+ }
+ fo := fileWithAnnotations{file: f, origin: idx, pid: pid}
+ v.elements = append(v.elements, fo)
+ mm[tag] = v
+ } else {
+ if warn {
+ warning("skipping orphaned counter file: %s", f)
+ }
+ }
+ }
+ }
+ if len(mm) == 0 {
+ if warn {
+ warning("no coverage data files found")
+ }
+ return nil
+ }
+ pods := make([]Pod, 0, len(mm))
+ for _, p := range mm {
+ sort.Slice(p.elements, func(i, j int) bool {
+ return p.elements[i].file < p.elements[j].file
+ })
+ pod := Pod{
+ MetaFile: p.mf,
+ CounterDataFiles: make([]string, 0, len(p.elements)),
+ Origins: make([]int, 0, len(p.elements)),
+ ProcessIDs: make([]int, 0, len(p.elements)),
+ }
+ for _, e := range p.elements {
+ pod.CounterDataFiles = append(pod.CounterDataFiles, e.file)
+ pod.Origins = append(pod.Origins, e.origin)
+ pod.ProcessIDs = append(pod.ProcessIDs, e.pid)
+ }
+ pods = append(pods, pod)
+ }
+ sort.Slice(pods, func(i, j int) bool {
+ return pods[i].MetaFile < pods[j].MetaFile
+ })
+ return pods
+}
+
+func warning(s string, a ...interface{}) {
+ fmt.Fprintf(os.Stderr, "warning: ")
+ fmt.Fprintf(os.Stderr, s, a...)
+ fmt.Fprintf(os.Stderr, "\n")
+}
diff --git a/src/internal/coverage/pods/pods_test.go b/src/internal/coverage/pods/pods_test.go
new file mode 100644
index 0000000000..5ce9edf6e2
--- /dev/null
+++ b/src/internal/coverage/pods/pods_test.go
@@ -0,0 +1,143 @@
+// 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 pods_test
+
+import (
+ "crypto/md5"
+ "fmt"
+ "internal/coverage"
+ "internal/coverage/pods"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+)
+
+func TestPodCollection(t *testing.T) {
+ //testenv.MustHaveGoBuild(t)
+
+ mkdir := func(d string, perm os.FileMode) string {
+ dp := filepath.Join(t.TempDir(), d)
+ if err := os.Mkdir(dp, perm); err != nil {
+ t.Fatal(err)
+ }
+ return dp
+ }
+
+ mkfile := func(d string, fn string) string {
+ fp := filepath.Join(d, fn)
+ if err := ioutil.WriteFile(fp, []byte("foo"), 0666); err != nil {
+ t.Fatal(err)
+ }
+ return fp
+ }
+
+ mkmeta := func(dir string, tag string) string {
+ hash := md5.Sum([]byte(tag))
+ fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, hash)
+ return mkfile(dir, fn)
+ }
+
+ mkcounter := func(dir string, tag string, nt int) string {
+ hash := md5.Sum([]byte(tag))
+ dummyPid := int(42)
+ fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, hash, dummyPid, nt)
+ return mkfile(dir, fn)
+ }
+
+ trim := func(path string) string {
+ b := filepath.Base(path)
+ d := filepath.Dir(path)
+ db := filepath.Base(d)
+ return db + "/" + b
+ }
+
+ podToString := func(p pods.Pod) string {
+ rv := trim(p.MetaFile) + " [\n"
+ for k, df := range p.CounterDataFiles {
+ rv += trim(df)
+ if p.Origins != nil {
+ rv += fmt.Sprintf(" o:%d", p.Origins[k])
+ }
+ rv += "\n"
+ }
+ return rv + "]"
+ }
+
+ // Create a couple of directories.
+ o1 := mkdir("o1", 0777)
+ o2 := mkdir("o2", 0777)
+
+ // Add some random files (not coverage related)
+ mkfile(o1, "blah.txt")
+ mkfile(o1, "something.exe")
+
+ // Add a meta-data file with two counter files to first dir.
+ mkmeta(o1, "m1")
+ mkcounter(o1, "m1", 1)
+ mkcounter(o1, "m1", 2)
+ mkcounter(o1, "m1", 2)
+
+ // Add a counter file with no associated meta file.
+ mkcounter(o1, "orphan", 9)
+
+ // Add a meta-data file with three counter files to second dir.
+ mkmeta(o2, "m2")
+ mkcounter(o2, "m2", 1)
+ mkcounter(o2, "m2", 2)
+ mkcounter(o2, "m2", 3)
+
+ // Add a duplicate of the first meta-file and a corresponding
+ // counter file to the second dir. This is intended to capture
+ // the scenario where we have two different runs of the same
+ // coverage-instrumented binary, but with the output files
+ // sent to separate directories.
+ mkmeta(o2, "m1")
+ mkcounter(o2, "m1", 11)
+
+ // Collect pods.
+ podlist, err := pods.CollectPods([]string{o1, o2}, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Verify pods
+ if len(podlist) != 2 {
+ t.Fatalf("expected 2 pods got %d pods", len(podlist))
+ }
+
+ for k, p := range podlist {
+ t.Logf("%d: mf=%s\n", k, p.MetaFile)
+ }
+
+ expected := []string{
+ `o1/covmeta.ae7be26cdaa742ca148068d5ac90eaca [
+o1/covcounters.ae7be26cdaa742ca148068d5ac90eaca.42.1 o:0
+o1/covcounters.ae7be26cdaa742ca148068d5ac90eaca.42.2 o:0
+o2/covcounters.ae7be26cdaa742ca148068d5ac90eaca.42.11 o:1
+]`,
+ `o2/covmeta.aaf2f89992379705dac844c0a2a1d45f [
+o2/covcounters.aaf2f89992379705dac844c0a2a1d45f.42.1 o:1
+o2/covcounters.aaf2f89992379705dac844c0a2a1d45f.42.2 o:1
+o2/covcounters.aaf2f89992379705dac844c0a2a1d45f.42.3 o:1
+]`,
+ }
+ for k, exp := range expected {
+ got := podToString(podlist[k])
+ if exp != got {
+ t.Errorf("pod %d: expected:\n%s\ngot:\n%s", k, exp, got)
+ }
+ }
+
+ // Check handling of bad/unreadable dir.
+ if runtime.GOOS == "linux" {
+ dbad := "/dev/null"
+ _, err = pods.CollectPods([]string{dbad}, true)
+ if err == nil {
+ t.Errorf("exected error due to unreadable dir")
+ }
+ }
+}