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