author | Alberto Bertogli
<albertito@blitiri.com.ar> 2019-10-26 00:24:07 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2019-10-26 00:56:33 UTC |
parent | 809578cb57b3907dfa0b2e8cbbfd7775c16a4929 |
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(">") + 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; + } + + 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> +`