| // 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, nil, "", true, true) | |
| // myformatter.EmitTextual(nil, 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 ( | |
| "cmp" | |
| "fmt" | |
| "internal/coverage" | |
| "internal/coverage/cmerge" | |
| "io" | |
| "maps" | |
| "slices" | |
| "sort" | |
| "strings" | |
| "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) { | |
| slices.SortFunc(units, func(ui, uj extcu) int { | |
| ifile := p.funcs[ui.fnfid].file | |
| jfile := p.funcs[uj.fnfid].file | |
| if r := strings.Compare(ifile, jfile); r != 0 { | |
| return r | |
| } | |
| // NB: not taking function literal flag into account here (no | |
| // need, since other fields are guaranteed to be distinct). | |
| if r := cmp.Compare(ui.StLine, uj.StLine); r != 0 { | |
| return r | |
| } | |
| if r := cmp.Compare(ui.EnLine, uj.EnLine); r != 0 { | |
| return r | |
| } | |
| if r := cmp.Compare(ui.StCol, uj.StCol); r != 0 { | |
| return r | |
| } | |
| if r := cmp.Compare(ui.EnCol, uj.EnCol); r != 0 { | |
| return r | |
| } | |
| return cmp.Compare(ui.NxStmts, uj.NxStmts) | |
| }) | |
| } | |
| // EmitTextual writes the accumulated coverage data for 'pkgs' in the legacy | |
| // cmd/cover text format to the writer 'w'; if pkgs is empty, text output | |
| // is emitted for all packages recorded. 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(pkgs []string, w io.Writer) error { | |
| if fm.cm == coverage.CtrModeInvalid { | |
| panic("internal error, counter mode unset") | |
| } | |
| if len(pkgs) == 0 { | |
| pkgs = make([]string, 0, len(fm.pm)) | |
| for importpath := range fm.pm { | |
| pkgs = append(pkgs, importpath) | |
| } | |
| } | |
| if _, err := fmt.Fprintf(w, "mode: %s\n", fm.cm.String()); err != nil { | |
| return err | |
| } | |
| sort.Strings(pkgs) | |
| for _, importpath := range pkgs { | |
| p := fm.pm[importpath] | |
| if p == nil { | |
| continue | |
| } | |
| 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', selecting the set of packages in 'pkgs' and suffixing the | |
| // printed string with 'inpkgs'. | |
| func (fm *Formatter) EmitPercent(w io.Writer, pkgs []string, inpkgs string, noteEmpty bool, aggregate bool) error { | |
| if len(pkgs) == 0 { | |
| 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), inpkgs); err != nil { | |
| return err | |
| } | |
| } else if noteEmpty { | |
| if _, err := fmt.Fprintf(w, "coverage: [no statements]\n"); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| slices.Sort(pkgs) | |
| var totalStmts, coveredStmts uint64 | |
| for _, importpath := range pkgs { | |
| p := fm.pm[importpath] | |
| if p == nil { | |
| continue | |
| } | |
| 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) | |
| // Emit functions for each package, sorted by import path. | |
| for _, importpath := range slices.Sorted(maps.Keys(fm.pm)) { | |
| 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 | |
| } |