//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, ``, n, b.Count) } else { w.WriteString("") } } 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 = ` {{.Title}}

{{.Title}}

{{.Notes}}

{{range .Files}} {{- end}}
{{.}} {{$.Totals.Percent . | printf "%.1f%%"}}
Total {{.Totals.TotalPercent | printf "%.1f"}}%

{{range .Files}} {{end}}
`