cover.go raw

   1  package cover
   2  
   3  import (
   4  	"encoding/json"
   5  	"fmt"
   6  	"os"
   7  	"path/filepath"
   8  	"sort"
   9  	"strconv"
  10  	"strings"
  11  )
  12  
  13  // coverSiteEntry mirrors one entry of the <output>.cover_sites.json table the
  14  // compiler writes for a -cover build.
  15  type coverSiteEntry struct {
  16  	ID   int32  `json:"id"`
  17  	Name string `json:"name"`
  18  }
  19  
  20  type coverRec struct {
  21  	fn   string
  22  	file string
  23  	line int32
  24  	hits uint64
  25  }
  26  
  27  // Report joins the compiler's site table with the counters the
  28  // instrumented program dumped, producing the same text stage4's pkg/mxcover
  29  // prints: totals, per-file table, then every site that never ran. The test
  30  // harness prints it for the package it just ran, so -cover needs no external
  31  // tool on either compiler.
  32  func Report(sitesPath, countsPath, prefix string) (string, error) {
  33  	sitesData, err := os.ReadFile(sitesPath)
  34  	if err != nil {
  35  		return "", fmt.Errorf("cannot read %s", sitesPath)
  36  	}
  37  	countsData, err := os.ReadFile(countsPath)
  38  	if err != nil {
  39  		return "", fmt.Errorf("cannot read %s", countsPath)
  40  	}
  41  	var sites []coverSiteEntry
  42  	if err := json.Unmarshal(sitesData, &sites); err != nil {
  43  		return "", fmt.Errorf("cannot parse %s", sitesPath)
  44  	}
  45  	hits := parseCounts(string(countsData))
  46  	var recs []coverRec
  47  	for _, s := range sites {
  48  		at := strings.LastIndexByte(s.Name, '@')
  49  		if at < 0 {
  50  			continue
  51  		}
  52  		fn := s.Name[:at]
  53  		loc := s.Name[at+1:]
  54  		colon := strings.LastIndexByte(loc, ':')
  55  		if colon < 0 {
  56  			continue
  57  		}
  58  		file := loc[:colon]
  59  		if file == "" {
  60  			// A synthetic block (a package init) has no source file.
  61  			continue
  62  		}
  63  		if prefix != "" && !strings.HasPrefix(file, prefix) {
  64  			continue
  65  		}
  66  		line, _ := strconv.Atoi(loc[colon+1:])
  67  		recs = append(recs, coverRec{fn: fn, file: file, line: int32(line), hits: hits[strconv.Itoa(int(s.ID))]})
  68  	}
  69  	if len(recs) == 0 {
  70  		note := ""
  71  		if prefix != "" {
  72  			note = " under " + prefix
  73  		}
  74  		return "", fmt.Errorf("no sites%s", note)
  75  	}
  76  	sort.Slice(recs, func(i, j int) bool {
  77  		if recs[i].file != recs[j].file {
  78  			return recs[i].file < recs[j].file
  79  		}
  80  		if recs[i].line != recs[j].line {
  81  			return recs[i].line < recs[j].line
  82  		}
  83  		return recs[i].fn < recs[j].fn
  84  	})
  85  
  86  	var b strings.Builder
  87  	covered := int32(0)
  88  	for _, r := range recs {
  89  		if r.hits > 0 {
  90  			covered++
  91  		}
  92  	}
  93  	fmt.Fprintf(&b, "sites: %d covered: %d (%s)\n", len(recs), covered, Pct(covered, int32(len(recs))))
  94  
  95  	// Per-file table, in path order (the sort above ordered by file, then line).
  96  	var curFile string
  97  	var fileTotal, fileCov int32
  98  	flush := func() {
  99  		if curFile != "" {
 100  			fmt.Fprintf(&b, "%s  %d/%d  %s\n", Pct(fileCov, fileTotal), fileCov, fileTotal, curFile)
 101  		}
 102  	}
 103  	for _, r := range recs {
 104  		if r.file != curFile {
 105  			flush()
 106  			curFile = r.file
 107  			fileTotal = 0
 108  			fileCov = 0
 109  		}
 110  		fileTotal++
 111  		if r.hits > 0 {
 112  			fileCov++
 113  		}
 114  	}
 115  	flush()
 116  
 117  	uncovered := int32(0)
 118  	for _, r := range recs {
 119  		if r.hits == 0 {
 120  			uncovered++
 121  		}
 122  	}
 123  	fmt.Fprintf(&b, "\nuncovered sites: %d\n", uncovered)
 124  	for _, r := range recs {
 125  		if r.hits == 0 {
 126  			fmt.Fprintf(&b, "  %s:%d  %s\n", r.file, r.line, r.fn)
 127  		}
 128  	}
 129  	return b.String(), nil
 130  }
 131  
 132  // Pct renders a percentage with one decimal, matching pkg/mxcover (a
 133  // zero total keeps its two leading spaces so the columns stay aligned).
 134  func Pct(n, total int32) string {
 135  	if total == 0 {
 136  		return "  0.0%"
 137  	}
 138  	tenths := (int64(n) * 1000) / int64(total)
 139  	return strconv.FormatInt(tenths/10, 10) + "." + strconv.FormatInt(tenths%10, 10) + "%"
 140  }
 141  
 142  // parseCounts reads the runtime's dump: one "coversite <id> count=<n>" line
 143  // per site that ran, keyed by the id as text.
 144  func parseCounts(data string) map[string]uint64 {
 145  	m := map[string]uint64{}
 146  	for _, line := range strings.Split(data, "\n") {
 147  		line = strings.TrimSpace(line)
 148  		if !strings.HasPrefix(line, "coversite ") {
 149  			continue
 150  		}
 151  		rest := line[len("coversite "):]
 152  		sp := strings.IndexByte(rest, ' ')
 153  		if sp <= 0 {
 154  			continue
 155  		}
 156  		id := rest[:sp]
 157  		rest = rest[sp+1:]
 158  		if !strings.HasPrefix(rest, "count=") {
 159  			continue
 160  		}
 161  		n, _ := strconv.ParseUint(strings.TrimSpace(rest[len("count="):]), 10, 64)
 162  		m[id] = n
 163  	}
 164  	return m
 165  }
 166  
 167  // ModulePrefix returns the module path plus a trailing slash, which is the
 168  // prefix that selects the module's own files out of the link set (the runtime
 169  // and stdlib are compiled in too). It mirrors the compiler's own module-root
 170  // search, with MOXIEROOT as the fallback for a target staged outside the tree.
 171  func ModulePrefix(startDir string) string {
 172  	dir := startDir
 173  	if dir == "" {
 174  		dir = "."
 175  	}
 176  	if abs, err := filepath.Abs(dir); err == nil {
 177  		dir = abs
 178  	}
 179  	for {
 180  		for _, name := range []string{"moxie.mod", "go.mod"} {
 181  			p := filepath.Join(dir, name)
 182  			if data, err := os.ReadFile(p); err == nil {
 183  				if mp := parseModuleName(string(data)); mp != "" {
 184  					return mp + "/"
 185  				}
 186  				return ""
 187  			}
 188  		}
 189  		parent := filepath.Dir(dir)
 190  		if parent == dir {
 191  			break
 192  		}
 193  		dir = parent
 194  	}
 195  	if root := os.Getenv("MOXIEROOT"); root != "" {
 196  		for _, name := range []string{"moxie.mod", "go.mod"} {
 197  			if data, err := os.ReadFile(filepath.Join(root, name)); err == nil {
 198  				if mp := parseModuleName(string(data)); mp != "" {
 199  					return mp + "/"
 200  				}
 201  			}
 202  		}
 203  	}
 204  	return ""
 205  }
 206  
 207  func parseModuleName(data string) string {
 208  	for _, line := range strings.Split(data, "\n") {
 209  		line = strings.TrimSpace(line)
 210  		if strings.HasPrefix(line, "module ") {
 211  			return strings.TrimSpace(line[len("module "):])
 212  		}
 213  	}
 214  	return ""
 215  }
 216