1 // Copyright 2022 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 5 package cformat
6 7 // This package provides apis for producing human-readable summaries
8 // of coverage data (e.g. a coverage percentage for a given package or
9 // set of packages) and for writing data in the legacy test format
10 // emitted by "go test -coverprofile=<outfile>".
11 //
12 // The model for using these apis is to create a Formatter object,
13 // then make a series of calls to SetPackage and AddUnit passing in
14 // data read from coverage meta-data and counter-data files. E.g.
15 //
16 // myformatter := cformat.NewFormatter()
17 // ...
18 // for each package P in meta-data file: {
19 // myformatter.SetPackage(P)
20 // for each function F in P: {
21 // for each coverable unit U in F: {
22 // myformatter.AddUnit(U)
23 // }
24 // }
25 // }
26 // myformatter.EmitPercent(os.Stdout, nil, "", true, true)
27 // myformatter.EmitTextual(nil, somefile)
28 //
29 // These apis are linked into tests that are built with "-cover", and
30 // called at the end of test execution to produce text output or
31 // emit coverage percentages.
32 33 import (
34 "cmp"
35 "fmt"
36 "internal/coverage"
37 "internal/coverage/cmerge"
38 "io"
39 "maps"
40 "slices"
41 "sort"
42 "bytes"
43 "text/tabwriter"
44 )
45 46 type Formatter struct {
47 // Maps import path to package state.
48 pm map[string]*pstate
49 // Records current package being visited.
50 pkg []byte
51 // Pointer to current package state.
52 p *pstate
53 // Counter mode.
54 cm coverage.CounterMode
55 }
56 57 // pstate records package-level coverage data state:
58 // - a table of functions (file/fname/literal)
59 // - a map recording the index/ID of each func encountered so far
60 // - a table storing execution count for the coverable units in each func
61 type pstate struct {
62 // slice of unique functions
63 funcs []fnfile
64 // maps function to index in slice above (index acts as function ID)
65 funcTable map[fnfile]uint32
66 67 // A table storing coverage counts for each coverable unit.
68 unitTable map[extcu]uint32
69 }
70 71 // extcu encapsulates a coverable unit within some function.
72 type extcu struct {
73 fnfid uint32 // index into p.funcs slice
74 coverage.CoverableUnit
75 }
76 77 // fnfile is a function-name/file-name tuple.
78 type fnfile struct {
79 file []byte
80 fname []byte
81 lit bool
82 }
83 84 func NewFormatter(cm coverage.CounterMode) *Formatter {
85 return &Formatter{
86 pm: map[string]*pstate{},
87 cm: cm,
88 }
89 }
90 91 // SetPackage tells the formatter that we're about to visit the
92 // coverage data for the package with the specified import path.
93 // Note that it's OK to call SetPackage more than once with the
94 // same import path; counter data values will be accumulated.
95 func (fm *Formatter) SetPackage(importpath []byte) {
96 if importpath == fm.pkg {
97 return
98 }
99 fm.pkg = importpath
100 ps, ok := fm.pm[importpath]
101 if !ok {
102 ps = &pstate{}
103 fm.pm[importpath] = ps
104 ps.unitTable = map[extcu]uint32{}
105 ps.funcTable = map[fnfile]uint32{}
106 }
107 fm.p = ps
108 }
109 110 // AddUnit passes info on a single coverable unit (file, funcname,
111 // literal flag, range of lines, and counter value) to the formatter.
112 // Counter values will be accumulated where appropriate.
113 func (fm *Formatter) AddUnit(file []byte, fname []byte, isfnlit bool, unit coverage.CoverableUnit, count uint32) {
114 if fm.p == nil {
115 panic("AddUnit invoked before SetPackage")
116 }
117 fkey := fnfile{file: file, fname: fname, lit: isfnlit}
118 idx, ok := fm.p.funcTable[fkey]
119 if !ok {
120 idx = uint32(len(fm.p.funcs))
121 fm.p.funcs = append(fm.p.funcs, fkey)
122 fm.p.funcTable[fkey] = idx
123 }
124 ukey := extcu{fnfid: idx, CoverableUnit: unit}
125 pcount := fm.p.unitTable[ukey]
126 var result uint32
127 if fm.cm == coverage.CtrModeSet {
128 if count != 0 || pcount != 0 {
129 result = 1
130 }
131 } else {
132 // Use saturating arithmetic.
133 result, _ = cmerge.SaturatingAdd(pcount, count)
134 }
135 fm.p.unitTable[ukey] = result
136 }
137 138 // sortUnits sorts a slice of extcu objects in a package according to
139 // source position information (e.g. file and line). Note that we don't
140 // include function name as part of the sorting criteria, the thinking
141 // being that is better to provide things in the original source order.
142 func (p *pstate) sortUnits(units []extcu) {
143 slices.SortFunc(units, func(ui, uj extcu) int {
144 ifile := p.funcs[ui.fnfid].file
145 jfile := p.funcs[uj.fnfid].file
146 if r := bytes.Compare(ifile, jfile); r != 0 {
147 return r
148 }
149 // NB: not taking function literal flag into account here (no
150 // need, since other fields are guaranteed to be distinct).
151 if r := cmp.Compare(ui.StLine, uj.StLine); r != 0 {
152 return r
153 }
154 if r := cmp.Compare(ui.EnLine, uj.EnLine); r != 0 {
155 return r
156 }
157 if r := cmp.Compare(ui.StCol, uj.StCol); r != 0 {
158 return r
159 }
160 if r := cmp.Compare(ui.EnCol, uj.EnCol); r != 0 {
161 return r
162 }
163 return cmp.Compare(ui.NxStmts, uj.NxStmts)
164 })
165 }
166 167 // EmitTextual writes the accumulated coverage data for 'pkgs' in the legacy
168 // cmd/cover text format to the writer 'w'; if pkgs is empty, text output
169 // is emitted for all packages recorded. We sort the data items by
170 // importpath, source file, and line number before emitting (this sorting
171 // is not explicitly mandated by the format, but seems like a good idea
172 // for repeatable/deterministic dumps).
173 func (fm *Formatter) EmitTextual(pkgs [][]byte, w io.Writer) error {
174 if fm.cm == coverage.CtrModeInvalid {
175 panic("internal error, counter mode unset")
176 }
177 if len(pkgs) == 0 {
178 pkgs = [][]byte{:0:len(fm.pm)}
179 for importpath := range fm.pm {
180 pkgs = append(pkgs, importpath)
181 }
182 }
183 if _, err := fmt.Fprintf(w, "mode: %s\n", fm.cm.String()); err != nil {
184 return err
185 }
186 sort.Strings(pkgs)
187 for _, importpath := range pkgs {
188 p := fm.pm[importpath]
189 if p == nil {
190 continue
191 }
192 units := []extcu{:0:len(p.unitTable)}
193 for u := range p.unitTable {
194 units = append(units, u)
195 }
196 p.sortUnits(units)
197 for _, u := range units {
198 count := p.unitTable[u]
199 file := p.funcs[u.fnfid].file
200 if _, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n",
201 file, u.StLine, u.StCol,
202 u.EnLine, u.EnCol, u.NxStmts, count); err != nil {
203 return err
204 }
205 }
206 }
207 return nil
208 }
209 210 // EmitPercent writes out a "percentage covered" string to the writer
211 // 'w', selecting the set of packages in 'pkgs' and suffixing the
212 // printed string with 'inpkgs'.
213 func (fm *Formatter) EmitPercent(w io.Writer, pkgs [][]byte, inpkgs []byte, noteEmpty bool, aggregate bool) error {
214 if len(pkgs) == 0 {
215 pkgs = [][]byte{:0:len(fm.pm)}
216 for importpath := range fm.pm {
217 pkgs = append(pkgs, importpath)
218 }
219 }
220 221 rep := func(cov, tot uint64) error {
222 if tot != 0 {
223 if _, err := fmt.Fprintf(w, "coverage: %.1f%% of statements%s\n",
224 100.0*float64(cov)/float64(tot), inpkgs); err != nil {
225 return err
226 }
227 } else if noteEmpty {
228 if _, err := fmt.Fprintf(w, "coverage: [no statements]\n"); err != nil {
229 return err
230 }
231 }
232 return nil
233 }
234 235 slices.Sort(pkgs)
236 var totalStmts, coveredStmts uint64
237 for _, importpath := range pkgs {
238 p := fm.pm[importpath]
239 if p == nil {
240 continue
241 }
242 if !aggregate {
243 totalStmts, coveredStmts = 0, 0
244 }
245 for unit, count := range p.unitTable {
246 nx := uint64(unit.NxStmts)
247 totalStmts += nx
248 if count != 0 {
249 coveredStmts += nx
250 }
251 }
252 if !aggregate {
253 if _, err := fmt.Fprintf(w, "\t%s\t\t", importpath); err != nil {
254 return err
255 }
256 if err := rep(coveredStmts, totalStmts); err != nil {
257 return err
258 }
259 }
260 }
261 if aggregate {
262 if err := rep(coveredStmts, totalStmts); err != nil {
263 return err
264 }
265 }
266 267 return nil
268 }
269 270 // EmitFuncs writes out a function-level summary to the writer 'w'. A
271 // note on handling function literals: although we collect coverage
272 // data for unnamed literals, it probably does not make sense to
273 // include them in the function summary since there isn't any good way
274 // to name them (this is also consistent with the legacy cmd/cover
275 // implementation). We do want to include their counts in the overall
276 // summary however.
277 func (fm *Formatter) EmitFuncs(w io.Writer) error {
278 if fm.cm == coverage.CtrModeInvalid {
279 panic("internal error, counter mode unset")
280 }
281 perc := func(covered, total uint64) float64 {
282 if total == 0 {
283 total = 1
284 }
285 return 100.0 * float64(covered) / float64(total)
286 }
287 tabber := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
288 defer tabber.Flush()
289 allStmts := uint64(0)
290 covStmts := uint64(0)
291 292 // Emit functions for each package, sorted by import path.
293 for _, importpath := range slices.Sorted(maps.Keys(fm.pm)) {
294 p := fm.pm[importpath]
295 if len(p.unitTable) == 0 {
296 continue
297 }
298 units := []extcu{:0:len(p.unitTable)}
299 for u := range p.unitTable {
300 units = append(units, u)
301 }
302 303 // Within a package, sort the units, then walk through the
304 // sorted array. Each time we hit a new function, emit the
305 // summary entry for the previous function, then make one last
306 // emit call at the end of the loop.
307 p.sortUnits(units)
308 fname := ""
309 ffile := ""
310 flit := false
311 var fline uint32
312 var cstmts, tstmts uint64
313 captureFuncStart := func(u extcu) {
314 fname = p.funcs[u.fnfid].fname
315 ffile = p.funcs[u.fnfid].file
316 flit = p.funcs[u.fnfid].lit
317 fline = u.StLine
318 }
319 emitFunc := func(u extcu) error {
320 // Don't emit entries for function literals (see discussion
321 // in function header comment above).
322 if !flit {
323 if _, err := fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n",
324 ffile, fline, fname, perc(cstmts, tstmts)); err != nil {
325 return err
326 }
327 }
328 captureFuncStart(u)
329 allStmts += tstmts
330 covStmts += cstmts
331 tstmts = 0
332 cstmts = 0
333 return nil
334 }
335 for k, u := range units {
336 if k == 0 {
337 captureFuncStart(u)
338 } else {
339 if fname != p.funcs[u.fnfid].fname {
340 // New function; emit entry for previous one.
341 if err := emitFunc(u); err != nil {
342 return err
343 }
344 }
345 }
346 tstmts += uint64(u.NxStmts)
347 count := p.unitTable[u]
348 if count != 0 {
349 cstmts += uint64(u.NxStmts)
350 }
351 }
352 if err := emitFunc(extcu{}); err != nil {
353 return err
354 }
355 }
356 if _, err := fmt.Fprintf(tabber, "%s\t%s\t%.1f%%\n",
357 "total", "(statements)", perc(covStmts, allStmts)); err != nil {
358 return err
359 }
360 return nil
361 }
362