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