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 main
6
7// This file contains functions and apis to support the "go tool
8// covdata" sub-commands that relate to dumping text format summaries
9// and reports: "pkglist", "func",  "debugdump", "percent", and
10// "textfmt".
11
12import (
13	"flag"
14	"fmt"
15	"internal/coverage"
16	"internal/coverage/calloc"
17	"internal/coverage/cformat"
18	"internal/coverage/cmerge"
19	"internal/coverage/decodecounter"
20	"internal/coverage/decodemeta"
21	"internal/coverage/pods"
22	"os"
23	"sort"
24	"strings"
25)
26
27var textfmtoutflag *string
28var liveflag *bool
29
30func makeDumpOp(cmd string) covOperation {
31	if cmd == textfmtMode || cmd == percentMode {
32		textfmtoutflag = flag.String("o", "", "Output text format to file")
33	}
34	if cmd == debugDumpMode {
35		liveflag = flag.Bool("live", false, "Select only live (executed) functions for dump output.")
36	}
37	d := &dstate{
38		cmd: cmd,
39		cm:  &cmerge.Merger{},
40	}
41	// For these modes (percent, pkglist, func, etc), use a relaxed
42	// policy when it comes to counter mode clashes. For a percent
43	// report, for example, we only care whether a given line is
44	// executed at least once, so it's ok to (effectively) merge
45	// together runs derived from different counter modes.
46	if d.cmd == percentMode || d.cmd == funcMode || d.cmd == pkglistMode {
47		d.cm.SetModeMergePolicy(cmerge.ModeMergeRelaxed)
48	}
49	if d.cmd == pkglistMode {
50		d.pkgpaths = make(map[string]struct{})
51	}
52	return d
53}
54
55// dstate encapsulates state and provides methods for implementing
56// various dump operations. Specifically, dstate implements the
57// CovDataVisitor interface, and is designed to be used in
58// concert with the CovDataReader utility, which abstracts away most
59// of the grubby details of reading coverage data files.
60type dstate struct {
61	// for batch allocation of counter arrays
62	calloc.BatchCounterAlloc
63
64	// counter merging state + methods
65	cm *cmerge.Merger
66
67	// counter data formatting helper
68	format *cformat.Formatter
69
70	// 'mm' stores values read from a counter data file; the pkfunc key
71	// is a pkgid/funcid pair that uniquely identifies a function in
72	// instrumented application.
73	mm map[pkfunc]decodecounter.FuncPayload
74
75	// pkm maps package ID to the number of functions in the package
76	// with that ID. It is used to report inconsistencies in counter
77	// data (for example, a counter data entry with pkgid=N funcid=10
78	// where package N only has 3 functions).
79	pkm map[uint32]uint32
80
81	// pkgpaths records all package import paths encountered while
82	// visiting coverage data files (used to implement the "pkglist"
83	// subcommand).
84	pkgpaths map[string]struct{}
85
86	// Current package name and import path.
87	pkgName       string
88	pkgImportPath string
89
90	// Module path for current package (may be empty).
91	modulePath string
92
93	// Dump subcommand (ex: "textfmt", "debugdump", etc).
94	cmd string
95
96	// File to which we will write text format output, if enabled.
97	textfmtoutf *os.File
98
99	// Total and covered statements (used by "debugdump" subcommand).
100	totalStmts, coveredStmts int
101
102	// Records whether preamble has been emitted for current pkg
103	// (used when in "debugdump" mode)
104	preambleEmitted bool
105}
106
107func (d *dstate) Usage(msg string) {
108	if len(msg) > 0 {
109		fmt.Fprintf(os.Stderr, "error: %s\n", msg)
110	}
111	fmt.Fprintf(os.Stderr, "usage: go tool covdata %s -i=<directories>\n\n", d.cmd)
112	flag.PrintDefaults()
113	fmt.Fprintf(os.Stderr, "\nExamples:\n\n")
114	switch d.cmd {
115	case pkglistMode:
116		fmt.Fprintf(os.Stderr, "  go tool covdata pkglist -i=dir1,dir2\n\n")
117		fmt.Fprintf(os.Stderr, "  \treads coverage data files from dir1+dirs2\n")
118		fmt.Fprintf(os.Stderr, "  \tand writes out a list of the import paths\n")
119		fmt.Fprintf(os.Stderr, "  \tof all compiled packages.\n")
120	case textfmtMode:
121		fmt.Fprintf(os.Stderr, "  go tool covdata textfmt -i=dir1,dir2 -o=out.txt\n\n")
122		fmt.Fprintf(os.Stderr, "  \tmerges data from input directories dir1+dir2\n")
123		fmt.Fprintf(os.Stderr, "  \tand emits text format into file 'out.txt'\n")
124	case percentMode:
125		fmt.Fprintf(os.Stderr, "  go tool covdata percent -i=dir1,dir2\n\n")
126		fmt.Fprintf(os.Stderr, "  \tmerges data from input directories dir1+dir2\n")
127		fmt.Fprintf(os.Stderr, "  \tand emits percentage of statements covered\n\n")
128	case funcMode:
129		fmt.Fprintf(os.Stderr, "  go tool covdata func -i=dir1,dir2\n\n")
130		fmt.Fprintf(os.Stderr, "  \treads coverage data files from dir1+dirs2\n")
131		fmt.Fprintf(os.Stderr, "  \tand writes out coverage profile data for\n")
132		fmt.Fprintf(os.Stderr, "  \teach function.\n")
133	case debugDumpMode:
134		fmt.Fprintf(os.Stderr, "  go tool covdata debugdump [flags] -i=dir1,dir2\n\n")
135		fmt.Fprintf(os.Stderr, "  \treads coverage data from dir1+dir2 and dumps\n")
136		fmt.Fprintf(os.Stderr, "  \tcontents in human-readable form to stdout, for\n")
137		fmt.Fprintf(os.Stderr, "  \tdebugging purposes.\n")
138	default:
139		panic("unexpected")
140	}
141	Exit(2)
142}
143
144// Setup is called once at program startup time to vet flag values
145// and do any necessary setup operations.
146func (d *dstate) Setup() {
147	if *indirsflag == "" {
148		d.Usage("select input directories with '-i' option")
149	}
150	if d.cmd == textfmtMode || (d.cmd == percentMode && *textfmtoutflag != "") {
151		if *textfmtoutflag == "" {
152			d.Usage("select output file name with '-o' option")
153		}
154		var err error
155		d.textfmtoutf, err = os.Create(*textfmtoutflag)
156		if err != nil {
157			d.Usage(fmt.Sprintf("unable to open textfmt output file %q: %v", *textfmtoutflag, err))
158		}
159	}
160	if d.cmd == debugDumpMode {
161		fmt.Printf("/* WARNING: the format of this dump is not stable and is\n")
162		fmt.Printf(" * expected to change from one Go release to the next.\n")
163		fmt.Printf(" *\n")
164		fmt.Printf(" * produced by:\n")
165		args := append([]string{os.Args[0]}, debugDumpMode)
166		args = append(args, os.Args[1:]...)
167		fmt.Printf(" *\t%s\n", strings.Join(args, " "))
168		fmt.Printf(" */\n")
169	}
170}
171
172func (d *dstate) BeginPod(p pods.Pod) {
173	d.mm = make(map[pkfunc]decodecounter.FuncPayload)
174}
175
176func (d *dstate) EndPod(p pods.Pod) {
177	if d.cmd == debugDumpMode {
178		d.cm.ResetModeAndGranularity()
179	}
180}
181
182func (d *dstate) BeginCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) {
183	dbgtrace(2, "visit counter data file %s dirIdx %d", cdf, dirIdx)
184	if d.cmd == debugDumpMode {
185		fmt.Printf("data file %s", cdf)
186		if cdr.Goos() != "" {
187			fmt.Printf(" GOOS=%s", cdr.Goos())
188		}
189		if cdr.Goarch() != "" {
190			fmt.Printf(" GOARCH=%s", cdr.Goarch())
191		}
192		if len(cdr.OsArgs()) != 0 {
193			fmt.Printf("  program args: %+v\n", cdr.OsArgs())
194		}
195		fmt.Printf("\n")
196	}
197}
198
199func (d *dstate) EndCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) {
200}
201
202func (d *dstate) VisitFuncCounterData(data decodecounter.FuncPayload) {
203	if nf, ok := d.pkm[data.PkgIdx]; !ok || data.FuncIdx > nf {
204		warn("func payload inconsistency: id [p=%d,f=%d] nf=%d len(ctrs)=%d in VisitFuncCounterData, ignored", data.PkgIdx, data.FuncIdx, nf, len(data.Counters))
205		return
206	}
207	key := pkfunc{pk: data.PkgIdx, fcn: data.FuncIdx}
208	val, found := d.mm[key]
209
210	dbgtrace(5, "ctr visit pk=%d fid=%d found=%v len(val.ctrs)=%d len(data.ctrs)=%d", data.PkgIdx, data.FuncIdx, found, len(val.Counters), len(data.Counters))
211
212	if len(val.Counters) < len(data.Counters) {
213		t := val.Counters
214		val.Counters = d.AllocateCounters(len(data.Counters))
215		copy(val.Counters, t)
216	}
217	err, overflow := d.cm.MergeCounters(val.Counters, data.Counters)
218	if err != nil {
219		fatal("%v", err)
220	}
221	if overflow {
222		warn("uint32 overflow during counter merge")
223	}
224	d.mm[key] = val
225}
226
227func (d *dstate) EndCounters() {
228}
229
230func (d *dstate) VisitMetaDataFile(mdf string, mfr *decodemeta.CoverageMetaFileReader) {
231	newgran := mfr.CounterGranularity()
232	newmode := mfr.CounterMode()
233	if err := d.cm.SetModeAndGranularity(mdf, newmode, newgran); err != nil {
234		fatal("%v", err)
235	}
236	if d.cmd == debugDumpMode {
237		fmt.Printf("Cover mode: %s\n", newmode.String())
238		fmt.Printf("Cover granularity: %s\n", newgran.String())
239	}
240	if d.format == nil {
241		d.format = cformat.NewFormatter(mfr.CounterMode())
242	}
243
244	// To provide an additional layer of checking when reading counter
245	// data, walk the meta-data file to determine the set of legal
246	// package/function combinations. This will help catch bugs in the
247	// counter file reader.
248	d.pkm = make(map[uint32]uint32)
249	np := uint32(mfr.NumPackages())
250	payload := []byte{}
251	for pkIdx := uint32(0); pkIdx < np; pkIdx++ {
252		var pd *decodemeta.CoverageMetaDataDecoder
253		var err error
254		pd, payload, err = mfr.GetPackageDecoder(pkIdx, payload)
255		if err != nil {
256			fatal("reading pkg %d from meta-file %s: %s", pkIdx, mdf, err)
257		}
258		d.pkm[pkIdx] = pd.NumFuncs()
259	}
260}
261
262func (d *dstate) BeginPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) {
263	d.preambleEmitted = false
264	d.pkgImportPath = pd.PackagePath()
265	d.pkgName = pd.PackageName()
266	d.modulePath = pd.ModulePath()
267	if d.cmd == pkglistMode {
268		d.pkgpaths[d.pkgImportPath] = struct{}{}
269	}
270	d.format.SetPackage(pd.PackagePath())
271}
272
273func (d *dstate) EndPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) {
274}
275
276func (d *dstate) VisitFunc(pkgIdx uint32, fnIdx uint32, fd *coverage.FuncDesc) {
277	var counters []uint32
278	key := pkfunc{pk: pkgIdx, fcn: fnIdx}
279	v, haveCounters := d.mm[key]
280
281	dbgtrace(5, "meta visit pk=%d fid=%d fname=%s file=%s found=%v len(val.ctrs)=%d", pkgIdx, fnIdx, fd.Funcname, fd.Srcfile, haveCounters, len(v.Counters))
282
283	suppressOutput := false
284	if haveCounters {
285		counters = v.Counters
286	} else if d.cmd == debugDumpMode && *liveflag {
287		suppressOutput = true
288	}
289
290	if d.cmd == debugDumpMode && !suppressOutput {
291		if !d.preambleEmitted {
292			fmt.Printf("\nPackage path: %s\n", d.pkgImportPath)
293			fmt.Printf("Package name: %s\n", d.pkgName)
294			fmt.Printf("Module path: %s\n", d.modulePath)
295			d.preambleEmitted = true
296		}
297		fmt.Printf("\nFunc: %s\n", fd.Funcname)
298		fmt.Printf("Srcfile: %s\n", fd.Srcfile)
299		fmt.Printf("Literal: %v\n", fd.Lit)
300	}
301	for i := 0; i < len(fd.Units); i++ {
302		u := fd.Units[i]
303		var count uint32
304		if counters != nil {
305			count = counters[i]
306		}
307		d.format.AddUnit(fd.Srcfile, fd.Funcname, fd.Lit, u, count)
308		if d.cmd == debugDumpMode && !suppressOutput {
309			fmt.Printf("%d: L%d:C%d -- L%d:C%d ",
310				i, u.StLine, u.StCol, u.EnLine, u.EnCol)
311			if u.Parent != 0 {
312				fmt.Printf("Parent:%d = %d\n", u.Parent, count)
313			} else {
314				fmt.Printf("NS=%d = %d\n", u.NxStmts, count)
315			}
316		}
317		d.totalStmts += int(u.NxStmts)
318		if count != 0 {
319			d.coveredStmts += int(u.NxStmts)
320		}
321	}
322}
323
324func (d *dstate) Finish() {
325	// d.format maybe nil here if the specified input dir was empty.
326	if d.format != nil {
327		if d.cmd == percentMode {
328			d.format.EmitPercent(os.Stdout, nil, "", false, false)
329		}
330		if d.cmd == funcMode {
331			d.format.EmitFuncs(os.Stdout)
332		}
333		if d.textfmtoutf != nil {
334			if err := d.format.EmitTextual(d.textfmtoutf); err != nil {
335				fatal("writing to %s: %v", *textfmtoutflag, err)
336			}
337		}
338	}
339	if d.textfmtoutf != nil {
340		if err := d.textfmtoutf.Close(); err != nil {
341			fatal("closing textfmt output file %s: %v", *textfmtoutflag, err)
342		}
343	}
344	if d.cmd == debugDumpMode {
345		fmt.Printf("totalStmts: %d coveredStmts: %d\n", d.totalStmts, d.coveredStmts)
346	}
347	if d.cmd == pkglistMode {
348		pkgs := make([]string, 0, len(d.pkgpaths))
349		for p := range d.pkgpaths {
350			pkgs = append(pkgs, p)
351		}
352		sort.Strings(pkgs)
353		for _, p := range pkgs {
354			fmt.Printf("%s\n", p)
355		}
356	}
357}
358