Commit a75eabbc authored by Alberto Bertogli's avatar Alberto Bertogli

test: Generate a prettier coverage report

To make the coverage report a bit more accessible and easier to
navigate, this patch makes the coverage tests generate a new HTML
coverage report (in addition to the classic variant).
parent 809578cb
Pipeline #99730428 passed with stages
in 8 minutes and 4 seconds
......@@ -20,6 +20,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
......
......@@ -44,11 +44,15 @@ go run "${UTILDIR}/gocovcat.go" .coverage/*.out \
# Generate reports based on the merged output.
go tool cover -func="$COVER_DIR/all.out" | sort -k 3 -n > "$COVER_DIR/func.txt"
go tool cover -html="$COVER_DIR/all.out" -o "$COVER_DIR/chasquid.html"
go tool cover -html="$COVER_DIR/all.out" -o "$COVER_DIR/classic.html"
go run "${UTILDIR}/coverhtml.go" \
-input="$COVER_DIR/all.out" -strip=3 \
-output="$COVER_DIR/coverage.html" \
-title="chasquid coverage report" \
-notes="Generated at commit <tt>$(git describe --always --dirty)</tt> ($(git log -1 --format=%ci))"
echo
grep total .coverage/func.txt
echo
echo "Coverage report can be found in:"
echo file://$COVER_DIR/chasquid.html
echo file://$COVER_DIR/coverage.html
// +build ignore
// 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())
}
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
}
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
}
}
}
func (t *Totals) Percent(f string) float32 {
return float32(t.coveredF[f]) / float32(t.totalF[f]) * 100
}
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("&gt;")
case '<':
w.WriteString("&lt;")
case '&':
w.WriteString("&amp;")
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;
}
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>
<tt>Total: {{.Totals.TotalPercent | printf "%.2f"}}%</tt><p>
{{range .Files}}
<tt><a onclick="visible('f::{{.}}')">
{{.}} ({{$.Totals.Percent . | printf "%.1f%%"}})</a></tt><br>
{{- end}}
<div id="source">
{{range .Files}}
<pre class="file" id="f::{{.}}" style="display: none">{{index $.Code .}}</pre>
{{end}}
</div>
</body>
</html>
`
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment