summaryrefslogtreecommitdiffstats
path: root/src/internal/coverage/cformat
diff options
context:
space:
mode:
Diffstat (limited to 'src/internal/coverage/cformat')
-rw-r--r--src/internal/coverage/cformat/fmt_test.go155
-rw-r--r--src/internal/coverage/cformat/format.go352
2 files changed, 507 insertions, 0 deletions
diff --git a/src/internal/coverage/cformat/fmt_test.go b/src/internal/coverage/cformat/fmt_test.go
new file mode 100644
index 0000000..f5ed01b
--- /dev/null
+++ b/src/internal/coverage/cformat/fmt_test.go
@@ -0,0 +1,155 @@
+// 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"
+ "slices"
+ "strings"
+ "testing"
+)
+
+func TestBasics(t *testing.T) {
+ fm := cformat.NewFormatter(coverage.CtrModeAtomic)
+
+ 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),
+ }
+ fm.SetPackage("my/pack1")
+ 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))
+ }
+ fm.SetPackage("my/pack2")
+ for _, u := range fn3units {
+ fm.AddUnit("lit.go", "f3", true, u, 0)
+ }
+
+ var b1, b2, b3, b4 strings.Builder
+ if err := fm.EmitTextual(&b1); err != nil {
+ t.Fatalf("EmitTextual returned %v", err)
+ }
+ wantText := strings.TrimSpace(`
+mode: atomic
+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
+lit.go:99.0,100.0 1 0`)
+ gotText := strings.TrimSpace(b1.String())
+ if wantText != gotText {
+ t.Errorf("emit text: got:\n%s\nwant:\n%s\n", gotText, wantText)
+ }
+
+ // Percent output with no aggregation.
+ noCoverPkg := ""
+ if err := fm.EmitPercent(&b2, noCoverPkg, false, false); err != nil {
+ t.Fatalf("EmitPercent returned %v", err)
+ }
+ wantPercent := strings.Fields(`
+ my/pack1 coverage: 66.7% of statements
+ my/pack2 coverage: 0.0% of statements
+`)
+ gotPercent := strings.Fields(b2.String())
+ if !slices.Equal(wantPercent, gotPercent) {
+ t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n",
+ gotPercent, wantPercent)
+ }
+
+ // Percent mode with aggregation.
+ withCoverPkg := " in ./..."
+ if err := fm.EmitPercent(&b3, withCoverPkg, false, true); err != nil {
+ t.Fatalf("EmitPercent returned %v", err)
+ }
+ wantPercent = strings.Fields(`
+ coverage: 62.5% of statements in ./...
+`)
+ gotPercent = strings.Fields(b3.String())
+ if !slices.Equal(wantPercent, gotPercent) {
+ t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n",
+ gotPercent, wantPercent)
+ }
+
+ if err := fm.EmitFuncs(&b4); 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(b4.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("perc2 is %s\n", b3.String())
+ t.Logf("funcs is %s\n", b4.String())
+ }
+}
+
+func TestEmptyPackages(t *testing.T) {
+
+ fm := cformat.NewFormatter(coverage.CtrModeAtomic)
+ fm.SetPackage("my/pack1")
+ fm.SetPackage("my/pack2")
+
+ // No aggregation.
+ {
+ var b strings.Builder
+ noCoverPkg := ""
+ if err := fm.EmitPercent(&b, noCoverPkg, true, false); err != nil {
+ t.Fatalf("EmitPercent returned %v", err)
+ }
+ wantPercent := strings.Fields(`
+ my/pack1 coverage: [no statements]
+ my/pack2 coverage: [no statements]
+`)
+ gotPercent := strings.Fields(b.String())
+ if !slices.Equal(wantPercent, gotPercent) {
+ t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n",
+ gotPercent, wantPercent)
+ }
+ }
+
+ // With aggregation.
+ {
+ var b strings.Builder
+ noCoverPkg := ""
+ if err := fm.EmitPercent(&b, noCoverPkg, true, true); err != nil {
+ t.Fatalf("EmitPercent returned %v", err)
+ }
+ wantPercent := strings.Fields(`
+ coverage: [no statements]
+`)
+ gotPercent := strings.Fields(b.String())
+ if !slices.Equal(wantPercent, gotPercent) {
+ t.Errorf("emit percent: got:\n%+v\nwant:\n%+v\n",
+ gotPercent, wantPercent)
+ }
+ }
+}
diff --git a/src/internal/coverage/cformat/format.go b/src/internal/coverage/cformat/format.go
new file mode 100644
index 0000000..7e7a277
--- /dev/null
+++ b/src/internal/coverage/cformat/format.go
@@ -0,0 +1,352 @@
+// 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, "", true, true)
+// 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, aggregate bool) error {
+ pkgs := make([]string, 0, len(fm.pm))
+ for importpath := range fm.pm {
+ pkgs = append(pkgs, importpath)
+ }
+
+ rep := func(cov, tot uint64) error {
+ if tot != 0 {
+ if _, err := fmt.Fprintf(w, "coverage: %.1f%% of statements%s\n",
+ 100.0*float64(cov)/float64(tot), covpkgs); err != nil {
+ return err
+ }
+ } else if noteEmpty {
+ if _, err := fmt.Fprintf(w, "coverage: [no statements]\n"); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+
+ sort.Strings(pkgs)
+ var totalStmts, coveredStmts uint64
+ for _, importpath := range pkgs {
+ p := fm.pm[importpath]
+ if !aggregate {
+ totalStmts, coveredStmts = 0, 0
+ }
+ for unit, count := range p.unitTable {
+ nx := uint64(unit.NxStmts)
+ totalStmts += nx
+ if count != 0 {
+ coveredStmts += nx
+ }
+ }
+ if !aggregate {
+ if _, err := fmt.Fprintf(w, "\t%s\t\t", importpath); err != nil {
+ return err
+ }
+ if err := rep(coveredStmts, totalStmts); err != nil {
+ return err
+ }
+ }
+ }
+ if aggregate {
+ if err := rep(coveredStmts, totalStmts); 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
+}