diff options
| author | Than McIntosh <thanm@google.com> | 2021-10-21 10:57:23 -0400 |
|---|---|---|
| committer | Than McIntosh <thanm@google.com> | 2022-09-28 11:47:16 +0000 |
| commit | 7a74829858bbc2fdf8b2d8902f7df46935cf6be9 (patch) | |
| tree | e890c58e098c341d3c588d511cd73f5dac7df7fc /src/internal/coverage | |
| parent | dbe56ff6c7a0162f318ed2dd7331c9de82e11d6d (diff) | |
| download | go-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.go | 29 | ||||
| -rw-r--r-- | src/internal/coverage/cformat/fmt_test.go | 92 | ||||
| -rw-r--r-- | src/internal/coverage/cformat/format.go | 340 | ||||
| -rw-r--r-- | src/internal/coverage/cmerge/merge.go | 104 | ||||
| -rw-r--r-- | src/internal/coverage/cmerge/merge_test.go | 105 | ||||
| -rw-r--r-- | src/internal/coverage/pods/pods.go | 194 | ||||
| -rw-r--r-- | src/internal/coverage/pods/pods_test.go | 143 |
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") + } + } +} |
