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