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 "subtract" and
8// "intersect" subcommands of "go tool covdata".
9
10import (
11	"flag"
12	"fmt"
13	"internal/coverage"
14	"internal/coverage/decodecounter"
15	"internal/coverage/decodemeta"
16	"internal/coverage/pods"
17	"os"
18	"strings"
19)
20
21// makeSubtractIntersectOp creates a subtract or intersect operation.
22// 'mode' here must be either "subtract" or "intersect".
23func makeSubtractIntersectOp(mode string) covOperation {
24	outdirflag = flag.String("o", "", "Output directory to write")
25	s := &sstate{
26		mode:  mode,
27		mm:    newMetaMerge(),
28		inidx: -1,
29	}
30	return s
31}
32
33// sstate holds state needed to implement subtraction and intersection
34// operations on code coverage data files. This type provides methods
35// to implement the CovDataVisitor interface, and is designed to be
36// used in concert with the CovDataReader utility, which abstracts
37// away most of the grubby details of reading coverage data files.
38type sstate struct {
39	mm    *metaMerge
40	inidx int
41	mode  string
42	// Used only for intersection; keyed by pkg/fn ID, it keeps track of
43	// just the set of functions for which we have data in the current
44	// input directory.
45	imm map[pkfunc]struct{}
46}
47
48func (s *sstate) Usage(msg string) {
49	if len(msg) > 0 {
50		fmt.Fprintf(os.Stderr, "error: %s\n", msg)
51	}
52	fmt.Fprintf(os.Stderr, "usage: go tool covdata %s -i=dir1,dir2 -o=<dir>\n\n", s.mode)
53	flag.PrintDefaults()
54	fmt.Fprintf(os.Stderr, "\nExamples:\n\n")
55	op := "from"
56	if s.mode == intersectMode {
57		op = "with"
58	}
59	fmt.Fprintf(os.Stderr, "  go tool covdata %s -i=dir1,dir2 -o=outdir\n\n", s.mode)
60	fmt.Fprintf(os.Stderr, "  \t%ss dir2 %s dir1, writing result\n", s.mode, op)
61	fmt.Fprintf(os.Stderr, "  \tinto output dir outdir.\n")
62	os.Exit(2)
63}
64
65func (s *sstate) Setup() {
66	if *indirsflag == "" {
67		usage("select input directories with '-i' option")
68	}
69	indirs := strings.Split(*indirsflag, ",")
70	if s.mode == subtractMode && len(indirs) != 2 {
71		usage("supply exactly two input dirs for subtract operation")
72	}
73	if *outdirflag == "" {
74		usage("select output directory with '-o' option")
75	}
76}
77
78func (s *sstate) BeginPod(p pods.Pod) {
79	s.mm.beginPod()
80}
81
82func (s *sstate) EndPod(p pods.Pod) {
83	const pcombine = false
84	s.mm.endPod(pcombine)
85}
86
87func (s *sstate) EndCounters() {
88	if s.imm != nil {
89		s.pruneCounters()
90	}
91}
92
93// pruneCounters performs a function-level partial intersection using the
94// current POD counter data (s.mm.pod.pmm) and the intersected data from
95// PODs in previous dirs (s.imm).
96func (s *sstate) pruneCounters() {
97	pkeys := make([]pkfunc, 0, len(s.mm.pod.pmm))
98	for k := range s.mm.pod.pmm {
99		pkeys = append(pkeys, k)
100	}
101	// Remove anything from pmm not found in imm. We don't need to
102	// go the other way (removing things from imm not found in pmm)
103	// since we don't add anything to imm if there is no pmm entry.
104	for _, k := range pkeys {
105		if _, found := s.imm[k]; !found {
106			delete(s.mm.pod.pmm, k)
107		}
108	}
109	s.imm = nil
110}
111
112func (s *sstate) BeginCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) {
113	dbgtrace(2, "visiting counter data file %s diridx %d", cdf, dirIdx)
114	if s.inidx != dirIdx {
115		if s.inidx > dirIdx {
116			// We're relying on having data files presented in
117			// the order they appear in the inputs (e.g. first all
118			// data files from input dir 0, then dir 1, etc).
119			panic("decreasing dir index, internal error")
120		}
121		if dirIdx == 0 {
122			// No need to keep track of the functions in the first
123			// directory, since that info will be replicated in
124			// s.mm.pod.pmm.
125			s.imm = nil
126		} else {
127			// We're now starting to visit the Nth directory, N != 0.
128			if s.mode == intersectMode {
129				if s.imm != nil {
130					s.pruneCounters()
131				}
132				s.imm = make(map[pkfunc]struct{})
133			}
134		}
135		s.inidx = dirIdx
136	}
137}
138
139func (s *sstate) EndCounterDataFile(cdf string, cdr *decodecounter.CounterDataReader, dirIdx int) {
140}
141
142func (s *sstate) VisitFuncCounterData(data decodecounter.FuncPayload) {
143	key := pkfunc{pk: data.PkgIdx, fcn: data.FuncIdx}
144
145	if *verbflag >= 5 {
146		fmt.Printf("ctr visit fid=%d pk=%d inidx=%d data.Counters=%+v\n", data.FuncIdx, data.PkgIdx, s.inidx, data.Counters)
147	}
148
149	// If we're processing counter data from the initial (first) input
150	// directory, then just install it into the counter data map
151	// as usual.
152	if s.inidx == 0 {
153		s.mm.visitFuncCounterData(data)
154		return
155	}
156
157	// If we're looking at counter data from a dir other than
158	// the first, then perform the intersect/subtract.
159	if val, ok := s.mm.pod.pmm[key]; ok {
160		if s.mode == subtractMode {
161			for i := 0; i < len(data.Counters); i++ {
162				if data.Counters[i] != 0 {
163					val.Counters[i] = 0
164				}
165			}
166		} else if s.mode == intersectMode {
167			s.imm[key] = struct{}{}
168			for i := 0; i < len(data.Counters); i++ {
169				if data.Counters[i] == 0 {
170					val.Counters[i] = 0
171				}
172			}
173		}
174	}
175}
176
177func (s *sstate) VisitMetaDataFile(mdf string, mfr *decodemeta.CoverageMetaFileReader) {
178	if s.mode == intersectMode {
179		s.imm = make(map[pkfunc]struct{})
180	}
181	s.mm.visitMetaDataFile(mdf, mfr)
182}
183
184func (s *sstate) BeginPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) {
185	s.mm.visitPackage(pd, pkgIdx, false)
186}
187
188func (s *sstate) EndPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32) {
189}
190
191func (s *sstate) VisitFunc(pkgIdx uint32, fnIdx uint32, fd *coverage.FuncDesc) {
192	s.mm.visitFunc(pkgIdx, fnIdx, fd, s.mode, false)
193}
194
195func (s *sstate) Finish() {
196}
197