1
2
3
4
5 package main
6
7 import (
8 "bufio"
9 "bytes"
10 "cmd/internal/browser"
11 "fmt"
12 "html/template"
13 "io"
14 "math"
15 "os"
16 "path/filepath"
17 "strings"
18
19 "golang.org/x/tools/cover"
20 )
21
22
23
24
25 func htmlOutput(profile, outfile string) error {
26 profiles, err := cover.ParseProfiles(profile)
27 if err != nil {
28 return err
29 }
30
31 var d templateData
32
33 dirs, err := findPkgs(profiles)
34 if err != nil {
35 return err
36 }
37
38 for _, profile := range profiles {
39 fn := profile.FileName
40 if profile.Mode == "set" {
41 d.Set = true
42 }
43 file, err := findFile(dirs, fn)
44 if err != nil {
45 return err
46 }
47 src, err := os.ReadFile(file)
48 if err != nil {
49 return fmt.Errorf("can't read %q: %v", fn, err)
50 }
51 var buf strings.Builder
52 err = htmlGen(&buf, src, profile.Boundaries(src))
53 if err != nil {
54 return err
55 }
56 d.Files = append(d.Files, &templateFile{
57 Name: fn,
58 Body: template.HTML(buf.String()),
59 Coverage: percentCovered(profile),
60 })
61 }
62
63 var out *os.File
64 if outfile == "" {
65 var dir string
66 dir, err = os.MkdirTemp("", "cover")
67 if err != nil {
68 return err
69 }
70 out, err = os.Create(filepath.Join(dir, "coverage.html"))
71 } else {
72 out, err = os.Create(outfile)
73 }
74 if err != nil {
75 return err
76 }
77 err = htmlTemplate.Execute(out, d)
78 if err2 := out.Close(); err == nil {
79 err = err2
80 }
81 if err != nil {
82 return err
83 }
84
85 if outfile == "" {
86 if !browser.Open("file://" + out.Name()) {
87 fmt.Fprintf(os.Stderr, "HTML output written to %s\n", out.Name())
88 }
89 }
90
91 return nil
92 }
93
94
95
96
97 func percentCovered(p *cover.Profile) float64 {
98 var total, covered int64
99 for _, b := range p.Blocks {
100 total += int64(b.NumStmt)
101 if b.Count > 0 {
102 covered += int64(b.NumStmt)
103 }
104 }
105 if total == 0 {
106 return 0
107 }
108 return float64(covered) / float64(total) * 100
109 }
110
111
112
113 func htmlGen(w io.Writer, src []byte, boundaries []cover.Boundary) error {
114 dst := bufio.NewWriter(w)
115 for i := range src {
116 for len(boundaries) > 0 && boundaries[0].Offset == i {
117 b := boundaries[0]
118 if b.Start {
119 n := 0
120 if b.Count > 0 {
121 n = int(math.Floor(b.Norm*9)) + 1
122 }
123 fmt.Fprintf(dst, `<span class="cov%v" title="%v">`, n, b.Count)
124 } else {
125 dst.WriteString("</span>")
126 }
127 boundaries = boundaries[1:]
128 }
129 switch b := src[i]; b {
130 case '>':
131 dst.WriteString(">")
132 case '<':
133 dst.WriteString("<")
134 case '&':
135 dst.WriteString("&")
136 case '\t':
137 dst.WriteString(" ")
138 default:
139 dst.WriteByte(b)
140 }
141 }
142 return dst.Flush()
143 }
144
145
146
147 func rgb(n int) string {
148 if n == 0 {
149 return "rgb(192, 0, 0)"
150 }
151
152 r := 128 - 12*(n-1)
153 g := 128 + 12*(n-1)
154 b := 128 + 3*(n-1)
155 return fmt.Sprintf("rgb(%v, %v, %v)", r, g, b)
156 }
157
158
159 func colors() template.CSS {
160 var buf bytes.Buffer
161 for i := 0; i < 11; i++ {
162 fmt.Fprintf(&buf, ".cov%v { color: %v }\n", i, rgb(i))
163 }
164 return template.CSS(buf.String())
165 }
166
167 var htmlTemplate = template.Must(template.New("html").Funcs(template.FuncMap{
168 "colors": colors,
169 }).Parse(tmplHTML))
170
171 type templateData struct {
172 Files []*templateFile
173 Set bool
174 }
175
176
177
178
179
180
181
182 func (td templateData) PackageName() string {
183 if len(td.Files) == 0 {
184 return ""
185 }
186 fileName := td.Files[0].Name
187 elems := strings.Split(fileName, "/")
188
189 for i := len(elems) - 2; i >= 0; i-- {
190 if elems[i] != "" {
191 return elems[i]
192 }
193 }
194 return ""
195 }
196
197 type templateFile struct {
198 Name string
199 Body template.HTML
200 Coverage float64
201 }
202
203 const tmplHTML = `
204 <!DOCTYPE html>
205 <html>
206 <head>
207 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
208 <title>{{$pkg := .PackageName}}{{if $pkg}}{{$pkg}}: {{end}}Go Coverage Report</title>
209 <style>
210 body {
211 background: black;
212 color: rgb(80, 80, 80);
213 }
214 body, pre, #legend span {
215 font-family: Menlo, monospace;
216 font-weight: bold;
217 }
218 #topbar {
219 background: black;
220 position: fixed;
221 top: 0; left: 0; right: 0;
222 height: 42px;
223 border-bottom: 1px solid rgb(80, 80, 80);
224 }
225 #content {
226 margin-top: 50px;
227 }
228 #nav, #legend {
229 float: left;
230 margin-left: 10px;
231 }
232 #legend {
233 margin-top: 12px;
234 }
235 #nav {
236 margin-top: 10px;
237 }
238 #legend span {
239 margin: 0 5px;
240 }
241 {{colors}}
242 </style>
243 </head>
244 <body>
245 <div id="topbar">
246 <div id="nav">
247 <select id="files">
248 {{range $i, $f := .Files}}
249 <option value="file{{$i}}">{{$f.Name}} ({{printf "%.1f" $f.Coverage}}%)</option>
250 {{end}}
251 </select>
252 </div>
253 <div id="legend">
254 <span>not tracked</span>
255 {{if .Set}}
256 <span class="cov0">not covered</span>
257 <span class="cov8">covered</span>
258 {{else}}
259 <span class="cov0">no coverage</span>
260 <span class="cov1">low coverage</span>
261 <span class="cov2">*</span>
262 <span class="cov3">*</span>
263 <span class="cov4">*</span>
264 <span class="cov5">*</span>
265 <span class="cov6">*</span>
266 <span class="cov7">*</span>
267 <span class="cov8">*</span>
268 <span class="cov9">*</span>
269 <span class="cov10">high coverage</span>
270 {{end}}
271 </div>
272 </div>
273 <div id="content">
274 {{range $i, $f := .Files}}
275 <pre class="file" id="file{{$i}}" style="display: none">{{$f.Body}}</pre>
276 {{end}}
277 </div>
278 </body>
279 <script>
280 (function() {
281 var files = document.getElementById('files');
282 var visible;
283 files.addEventListener('change', onChange, false);
284 function select(part) {
285 if (visible)
286 visible.style.display = 'none';
287 visible = document.getElementById(part);
288 if (!visible)
289 return;
290 files.value = part;
291 visible.style.display = 'block';
292 location.hash = part;
293 }
294 function onChange() {
295 select(files.value);
296 window.scrollTo(0, 0);
297 }
298 if (location.hash != "") {
299 select(location.hash.substr(1));
300 }
301 if (!visible) {
302 select("file0");
303 }
304 })();
305 </script>
306 </html>
307 `
308
View as plain text