Source file src/internal/coverage/cformat/format.go

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

View as plain text