git » chasquid » commit a75eabb

test: Generate a prettier coverage report

author Alberto Bertogli
2019-10-26 00:24:07 UTC
committer Alberto Bertogli
2019-10-26 00:56:33 UTC
parent 809578cb57b3907dfa0b2e8cbbfd7775c16a4929

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).

go.sum +1 -0
test/cover.sh +7 -3
test/util/coverhtml.go +259 -0

diff --git a/go.sum b/go.sum
index 76af045..c300955 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/test/cover.sh b/test/cover.sh
index 3368507..822c98e 100755
--- a/test/cover.sh
+++ b/test/cover.sh
@@ -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
 
diff --git a/test/util/coverhtml.go b/test/util/coverhtml.go
new file mode 100644
index 0000000..85f91b0
--- /dev/null
+++ b/test/util/coverhtml.go
@@ -0,0 +1,259 @@
+// +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>
+`