...

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

Documentation: internal/coverage/cformat

     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  	"slices"
    40  	"strings"
    41  	"text/tabwriter"
    42  )
    43  
    44  type 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
    59  type 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.
    70  type extcu struct {
    71  	fnfid uint32 // index into p.funcs slice
    72  	coverage.CoverableUnit
    73  }
    74  
    75  // fnfile is a function-name/file-name tuple.
    76  type fnfile struct {
    77  	file  string
    78  	fname string
    79  	lit   bool
    80  }
    81  
    82  func 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.
    93  func (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.
   111  func (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.
   140  func (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).
   170  func (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'.
   205  func (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.
   269  func (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  

View as plain text