From ccd992355df7192993c666236047820244914598 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 21:19:13 +0200 Subject: Adding upstream version 1.21.8. Signed-off-by: Daniel Baumann --- src/internal/coverage/cformat/fmt_test.go | 155 +++++++++++++ src/internal/coverage/cformat/format.go | 352 ++++++++++++++++++++++++++++++ 2 files changed, 507 insertions(+) create mode 100644 src/internal/coverage/cformat/fmt_test.go create mode 100644 src/internal/coverage/cformat/format.go (limited to 'src/internal/coverage/cformat') 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=". +// +// 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 +} -- cgit v1.2.3