format.mx raw

   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