mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
We've accumulated a few linter issues around comments and a couple of variable names. While none of them is major, this patch cleans them up so it's easier to go through the linter output, and we can start being more strict about it.
293 lines
6.2 KiB
Go
293 lines
6.2 KiB
Go
//go:build !coverage
|
|
// +build !coverage
|
|
|
|
// Generate an HTML visualization of a Go coverage profile.
|
|
// Serves a similar purpose to "go tool cover -html", but has a different
|
|
// visual style.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/cover"
|
|
)
|
|
|
|
var (
|
|
input = flag.String("input", "", "input file")
|
|
output = flag.String("output", "", "output file")
|
|
strip = flag.Int("strip", 0, "how many path entries to strip")
|
|
title = flag.String("title", "Coverage report", "page title")
|
|
notes = flag.String("notes", "", "notes to add at the beginning (HTML)")
|
|
)
|
|
|
|
func errorf(f string, a ...interface{}) {
|
|
fmt.Printf(f, a...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
profiles, err := cover.ParseProfiles(*input)
|
|
if err != nil {
|
|
errorf("Error parsing input %q: %v\n", *input, err)
|
|
}
|
|
|
|
totals := &Totals{
|
|
totalF: map[string]int{},
|
|
coveredF: map[string]int{},
|
|
}
|
|
files := []string{}
|
|
code := map[string]template.HTML{}
|
|
for _, p := range profiles {
|
|
files = append(files, p.FileName)
|
|
totals.Add(p)
|
|
|
|
fname := strings.Join(strings.Split(p.FileName, "/")[*strip:], "/")
|
|
src, err := ioutil.ReadFile(fname)
|
|
if err != nil {
|
|
errorf("Failed to read %q: %v", fname, err)
|
|
}
|
|
|
|
code[p.FileName] = genHTML(src, p.Boundaries(src))
|
|
}
|
|
|
|
out, err := os.Create(*output)
|
|
if err != nil {
|
|
errorf("Failed to open output file %q: %v", *output, err)
|
|
}
|
|
|
|
data := struct {
|
|
Title string
|
|
Notes template.HTML
|
|
Files []string
|
|
Code map[string]template.HTML
|
|
Totals *Totals
|
|
}{
|
|
Title: *title,
|
|
Notes: template.HTML(*notes),
|
|
Files: files,
|
|
Code: code,
|
|
Totals: totals,
|
|
}
|
|
|
|
tmpl := template.Must(template.New("html").Parse(htmlTmpl))
|
|
err = tmpl.Execute(out, data)
|
|
if err != nil {
|
|
errorf("Failed to execute template: %v", err)
|
|
}
|
|
|
|
for _, f := range files {
|
|
fmt.Printf("%5.1f%% %v\n", totals.Percent(f), f)
|
|
}
|
|
fmt.Printf("\n")
|
|
fmt.Printf("Total: %.1f\n", totals.TotalPercent())
|
|
}
|
|
|
|
// Totals is used to keep track of total counters.
|
|
type Totals struct {
|
|
// Total statements.
|
|
total int
|
|
|
|
// Covered statements.
|
|
covered int
|
|
|
|
// Total statements per file.
|
|
totalF map[string]int
|
|
|
|
// Covered statements per file.
|
|
coveredF map[string]int
|
|
}
|
|
|
|
// Add the given profile to the total counters.
|
|
func (t *Totals) Add(p *cover.Profile) {
|
|
for _, b := range p.Blocks {
|
|
t.total += b.NumStmt
|
|
t.totalF[p.FileName] += b.NumStmt
|
|
if b.Count > 0 {
|
|
t.covered += b.NumStmt
|
|
t.coveredF[p.FileName] += b.NumStmt
|
|
}
|
|
}
|
|
}
|
|
|
|
// Percent covered for the given file.
|
|
func (t *Totals) Percent(f string) float32 {
|
|
return float32(t.coveredF[f]) / float32(t.totalF[f]) * 100
|
|
}
|
|
|
|
// TotalPercent covered, across all files.
|
|
func (t *Totals) TotalPercent() float32 {
|
|
return float32(t.covered) / float32(t.total) * 100
|
|
}
|
|
|
|
func genHTML(src []byte, boundaries []cover.Boundary) template.HTML {
|
|
// Position -> []Boundary
|
|
// The order matters, we expect to receive start-end pairs in order, so
|
|
// they are properly added.
|
|
bs := map[int][]cover.Boundary{}
|
|
for _, b := range boundaries {
|
|
bs[b.Offset] = append(bs[b.Offset], b)
|
|
}
|
|
|
|
w := &strings.Builder{}
|
|
for i := range src {
|
|
// Emit boundary markers.
|
|
for _, b := range bs[i] {
|
|
if b.Start {
|
|
n := 0
|
|
if b.Count > 0 {
|
|
n = int(math.Floor(b.Norm*4)) + 1
|
|
}
|
|
fmt.Fprintf(w, `<span class="cov%v" title="%v">`, n, b.Count)
|
|
} else {
|
|
w.WriteString("</span>")
|
|
}
|
|
}
|
|
|
|
switch b := src[i]; b {
|
|
case '>':
|
|
w.WriteString(">")
|
|
case '<':
|
|
w.WriteString("<")
|
|
case '&':
|
|
w.WriteString("&")
|
|
case '\t':
|
|
w.WriteString(" ")
|
|
default:
|
|
w.WriteByte(b)
|
|
}
|
|
}
|
|
return template.HTML(w.String())
|
|
}
|
|
|
|
const htmlTmpl = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{.Title}}</title>
|
|
|
|
<style>
|
|
body {
|
|
font: 100%/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans",
|
|
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
|
"Segoe UI Emoji", "Segoe UI Symbol";
|
|
color: #333;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0 0 0.5em;
|
|
}
|
|
|
|
a {
|
|
color: #1c3986;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
a:hover {
|
|
color: #069;
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
tr:nth-child(odd) {
|
|
background: #f5f5f7;
|
|
}
|
|
|
|
tr.total {
|
|
border-top: 1px solid;
|
|
font-weight: bold;
|
|
}
|
|
|
|
td {
|
|
padding: 0.2em 1em;
|
|
}
|
|
|
|
td.pcnt {
|
|
text-align: right;
|
|
}
|
|
|
|
code, pre, tt {
|
|
font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console,
|
|
Terminal, Consolas, Liberation Mono, DejaVu Sans Mono,
|
|
Courier New, monospace;
|
|
color:#333;
|
|
}
|
|
|
|
pre {
|
|
padding: 0.5em 0.8em;
|
|
background: #f8f8f8;
|
|
border-radius: 1em;
|
|
border:1px solid #e5e5e5;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
// Color palette from graphiq.
|
|
.cov0 { color: red; }
|
|
.cov1 { color: #0B7BAB; }
|
|
.cov2 { color: #09639B; }
|
|
.cov3 { color: #034A8B; }
|
|
.cov4 { color: #00337C; }
|
|
.cov5 { color: #032663; }
|
|
</style>
|
|
|
|
<script>
|
|
function visible(id) {
|
|
history.replaceState(undefined, undefined, "#" + id);
|
|
var all = document.getElementsByClassName("file");
|
|
for (var i = 0; i < all.length; i++) {
|
|
var elem = all.item(i);
|
|
elem.style.display = "none";
|
|
}
|
|
var chosen = document.getElementById(id);
|
|
chosen.style.display = "block";
|
|
}
|
|
|
|
window.onload = function() {
|
|
var id = window.location.hash.replace("#", "");
|
|
if (id != "") {
|
|
visible(id);
|
|
}
|
|
};
|
|
</script>
|
|
</head>
|
|
|
|
<body>
|
|
<h1>{{.Title}}</h1>
|
|
|
|
{{.Notes}}<p>
|
|
|
|
<table>
|
|
{{range .Files}}
|
|
<tr>
|
|
<td><a onclick="visible('f::{{.}}')" tabindex="0"> {{.}} </a></td>
|
|
<td class="pcnt">{{$.Totals.Percent . | printf "%.1f%%"}}</td>
|
|
</tr>
|
|
{{- end}}
|
|
|
|
<tr class="total">
|
|
<td>Total</td>
|
|
<td class="pcnt">{{.Totals.TotalPercent | printf "%.1f"}}%</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div id="source">
|
|
{{range .Files}}
|
|
<pre class="file" id="f::{{.}}" style="display: none">{{index $.Code .}}</pre>
|
|
{{end}}
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|
|
`
|