Skip to content

Instantly share code, notes, and snippets.

@zachmu
Created April 17, 2026 23:00
Show Gist options
  • Select an option

  • Save zachmu/23975276bd89a1cfd338e2c81c05e2cd to your computer and use it in GitHub Desktop.

Select an option

Save zachmu/23975276bd89a1cfd338e2c81c05e2cd to your computer and use it in GitHub Desktop.
// generate.go transforms a Go coverage HTML report (produced by `go tool cover -html`)
// into a directory of per-file HTML pages with a shared stylesheet and an index page
// showing a table of all files with their coverage percentages.
//
// Usage: go run generate.go <coverage.html>
//
// Output is written to the current directory:
// index.html — table of all source files with coverage stats
// style.css — shared stylesheet
// files/<path>.html — per-file coverage pages mirroring source paths
package main
import (
"bufio"
"fmt"
"html"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
type fileEntry struct {
id string // "file0", "file1", ...
path string // "github.com/dolthub/dolt/go/cmd/..."
coverage float64 // 0.0 to 100.0
content string // raw HTML content between <pre> tags
}
// htmlPath returns the output path for a file's coverage page.
// e.g. "github.com/dolthub/dolt/go/cmd/dolt/cli/help.go" -> "files/github.com/dolthub/dolt/go/cmd/dolt/cli/help.html"
func (e fileEntry) htmlPath() string {
p := e.path
if strings.HasSuffix(p, ".go") {
p = p[:len(p)-3] + ".html"
} else {
p = p + ".html"
}
return filepath.Join("files", p)
}
// cssRelPath returns the relative path from a file's coverage page back to style.css at the root.
func (e fileEntry) cssRelPath() string {
depth := strings.Count(e.htmlPath(), string(os.PathSeparator))
return strings.Repeat("../", depth) + "style.css"
}
// indexRelPath returns the relative path from a file's coverage page back to index.html at the root.
func (e fileEntry) indexRelPath() string {
depth := strings.Count(e.htmlPath(), string(os.PathSeparator))
return strings.Repeat("../", depth) + "index.html"
}
var optionRe = regexp.MustCompile(`<option value="(file\d+)">(.+?) \(([0-9.]+)%\)</option>`)
var preOpenRe = regexp.MustCompile(`<pre class="file" id="(file\d+)"[^>]*>`)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: %s <coverage.html>\n", os.Args[0])
os.Exit(1)
}
inputPath := os.Args[1]
entries, err := parse(inputPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
writeCSS("style.css")
writeIndex("index.html", entries)
for _, e := range entries {
outPath := e.htmlPath()
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
fmt.Fprintf(os.Stderr, "error creating dir for %s: %v\n", outPath, err)
os.Exit(1)
}
writeFilePage(outPath, e)
}
fmt.Printf("Generated index.html + %d file pages\n", len(entries))
}
func parse(path string) ([]fileEntry, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
entryMap := make(map[string]*fileEntry)
var entryOrder []string
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)
type state int
const (
stateOptions state = iota
stateContent
)
currentState := stateOptions
var currentID string
var contentBuf strings.Builder
for scanner.Scan() {
line := scanner.Text()
switch currentState {
case stateOptions:
if m := optionRe.FindStringSubmatch(line); m != nil {
id := m[1]
filePath := m[2]
cov, _ := strconv.ParseFloat(m[3], 64)
entryMap[id] = &fileEntry{
id: id,
path: filePath,
coverage: cov,
}
entryOrder = append(entryOrder, id)
}
if m := preOpenRe.FindStringSubmatch(line); m != nil {
currentState = stateContent
currentID = m[1]
idx := preOpenRe.FindStringIndex(line)
contentBuf.Reset()
contentBuf.WriteString(line[idx[1]:])
}
case stateContent:
if strings.Contains(line, "</pre>") {
idx := strings.Index(line, "</pre>")
contentBuf.WriteString("\n")
contentBuf.WriteString(line[:idx])
if e, ok := entryMap[currentID]; ok {
e.content = contentBuf.String()
}
contentBuf.Reset()
currentID = ""
if m := preOpenRe.FindStringSubmatch(line[idx:]); m != nil {
currentID = m[1]
pidx := preOpenRe.FindStringIndex(line[idx:])
contentBuf.WriteString(line[idx+pidx[1]:])
} else {
currentState = stateOptions
}
} else {
contentBuf.WriteString("\n")
contentBuf.WriteString(line)
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
entries := make([]fileEntry, 0, len(entryOrder))
for _, id := range entryOrder {
if e, ok := entryMap[id]; ok {
entries = append(entries, *e)
}
}
return entries, nil
}
func writeCSS(path string) {
css := `/* Shared coverage stylesheet */
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
background: #1a1a2e;
color: #e0e0e0;
}
/* ---- Index page ---- */
.index-header {
padding: 20px 30px;
border-bottom: 1px solid #333;
background: #16213e;
}
.index-header h1 {
margin: 0 0 4px 0;
font-size: 22px;
color: #eee;
}
.index-header .summary {
font-size: 14px;
color: #999;
}
.search-box {
padding: 10px 30px;
background: #16213e;
border-bottom: 1px solid #333;
position: sticky;
top: 0;
z-index: 10;
}
.search-box input {
width: 100%;
max-width: 600px;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #444;
background: #1a1a2e;
color: #e0e0e0;
font-size: 14px;
font-family: Menlo, monospace;
}
.search-box input::placeholder { color: #666; }
table.coverage-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
table.coverage-table th {
text-align: left;
padding: 8px 12px;
border-bottom: 2px solid #444;
background: #16213e;
position: sticky;
top: 52px;
z-index: 5;
cursor: pointer;
user-select: none;
color: #aaa;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
table.coverage-table th:hover { color: #fff; }
table.coverage-table td {
padding: 6px 12px;
border-bottom: 1px solid #2a2a3e;
}
table.coverage-table tr:hover { background: #222244; }
table.coverage-table a {
color: #7eb8da;
text-decoration: none;
font-family: Menlo, monospace;
font-size: 12px;
}
table.coverage-table a:hover { text-decoration: underline; }
.file-cell {
max-width: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-cell a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.cov-pct {
font-family: Menlo, monospace;
font-size: 12px;
text-align: right;
white-space: nowrap;
}
.cov-high { color: #20ec9b; }
.cov-med { color: #e8c54a; }
.cov-low { color: #e05555; }
.cov-zero { color: #777; }
.bar-cell {
width: 180px;
min-width: 180px;
}
.bar-bg {
background: #2a2a2a;
border-radius: 3px;
height: 14px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
min-width: 1px;
}
/* ---- File page ---- */
.file-header {
padding: 16px 24px;
border-bottom: 1px solid #333;
background: #16213e;
}
.file-header h1 {
margin: 0;
font-size: 16px;
font-family: Menlo, monospace;
color: #eee;
word-break: break-all;
}
.file-header .back {
display: inline-block;
margin-bottom: 8px;
color: #7eb8da;
text-decoration: none;
font-size: 13px;
}
.file-header .back:hover { text-decoration: underline; }
.file-header .file-cov {
margin-top: 6px;
font-size: 13px;
}
.source-container {
padding: 0;
overflow-x: auto;
}
table.source-code {
border-collapse: collapse;
font-family: Menlo, monospace;
font-size: 13px;
line-height: 1.5;
width: 100%;
}
table.source-code td {
padding: 0 12px;
white-space: pre;
vertical-align: top;
}
table.source-code .line-no {
text-align: right;
color: #555;
user-select: none;
width: 1%;
padding-right: 16px;
border-right: 1px solid #333;
}
table.source-code .line-content {
padding-left: 16px;
}
/* Coverage coloring — matches Go's coverage tool classes */
.cov0 { color: rgb(192, 0, 0); }
.cov1 { color: rgb(128, 128, 128); }
.cov2 { color: rgb(116, 140, 131); }
.cov3 { color: rgb(104, 152, 134); }
.cov4 { color: rgb(92, 164, 137); }
.cov5 { color: rgb(80, 176, 140); }
.cov6 { color: rgb(68, 188, 143); }
.cov7 { color: rgb(56, 200, 146); }
.cov8 { color: rgb(44, 212, 149); }
.cov9 { color: rgb(32, 224, 152); }
.cov10 { color: rgb(20, 236, 155); }
/* Not-tracked text (default color) */
.source-code .line-content { color: rgb(80, 80, 80); }
`
os.WriteFile(path, []byte(css), 0644)
}
func writeIndex(path string, entries []fileEntry) {
f, _ := os.Create(path)
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
var totalCov float64
var nonZero int
for _, e := range entries {
totalCov += e.coverage
if e.coverage > 0 {
nonZero++
}
}
avgCov := 0.0
if len(entries) > 0 {
avgCov = totalCov / float64(len(entries))
}
fmt.Fprintf(w, `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go Coverage Report</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="index-header">
<h1>Go Coverage Report</h1>
<div class="summary">%d files &middot; %d with coverage &middot; %.1f%% average</div>
</div>
<div class="search-box">
<input type="text" id="filter" placeholder="Filter files..." autofocus>
</div>
<table class="coverage-table">
<thead>
<tr>
<th onclick="sortTable(0)">File</th>
<th onclick="sortTable(1)" style="text-align:right">Coverage</th>
<th>Bar</th>
</tr>
</thead>
<tbody id="tbody">
`, len(entries), nonZero, avgCov)
for _, e := range entries {
covClass := coverageClass(e.coverage)
barColor := barColorForCoverage(e.coverage)
fmt.Fprintf(w, `<tr data-path="%s">
<td class="file-cell"><a href="%s">%s</a></td>
<td class="cov-pct %s">%.1f%%</td>
<td class="bar-cell"><div class="bar-bg"><div class="bar-fill" style="width:%.1f%%;background:%s"></div></div></td>
</tr>
`, html.EscapeString(e.path), html.EscapeString(e.htmlPath()), html.EscapeString(e.path), covClass, e.coverage, e.coverage, barColor)
}
fmt.Fprintf(w, `</tbody>
</table>
<script>
document.getElementById('filter').addEventListener('input', function() {
var val = this.value.toLowerCase();
var rows = document.querySelectorAll('#tbody tr');
for (var i = 0; i < rows.length; i++) {
var path = rows[i].getAttribute('data-path').toLowerCase();
rows[i].style.display = path.indexOf(val) >= 0 ? '' : 'none';
}
});
var sortDir = [1, 1, 1];
function sortTable(col) {
var tbody = document.getElementById('tbody');
var rows = Array.from(tbody.querySelectorAll('tr'));
sortDir[col] *= -1;
rows.sort(function(a, b) {
var av, bv;
if (col === 1) {
av = parseFloat(a.children[col].textContent);
bv = parseFloat(b.children[col].textContent);
} else {
av = a.children[col].textContent.toLowerCase();
bv = b.children[col].textContent.toLowerCase();
}
if (av < bv) return -sortDir[col];
if (av > bv) return sortDir[col];
return 0;
});
for (var i = 0; i < rows.length; i++) tbody.appendChild(rows[i]);
}
</script>
</body>
</html>
`)
}
func writeFilePage(path string, e fileEntry) {
f, _ := os.Create(path)
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
_, shortName := splitPkgFile(e.path)
covClass := coverageClass(e.coverage)
fmt.Fprintf(w, `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>%s — Coverage</title>
<link rel="stylesheet" href="%s">
</head>
<body>
<div class="file-header">
<a class="back" href="%s">&larr; Back to index</a>
<h1>%s</h1>
<div class="file-cov">Coverage: <span class="%s">%.1f%%</span></div>
</div>
<div class="source-container">
<table class="source-code">
`, html.EscapeString(shortName), e.cssRelPath(), e.indexRelPath(), html.EscapeString(e.path), covClass, e.coverage)
lines := strings.Split(e.content, "\n")
lineNum := 1
// Track the currently open <span> tag so we can re-open it on continuation lines.
// Coverage spans in the original HTML can cross line boundaries.
var openSpan string // e.g. `<span class="cov0" title="0">` or ""
for i, line := range lines {
if i == 0 && strings.TrimSpace(line) == "" {
continue
}
// Build the line content with proper span wrapping
var rendered string
if openSpan != "" {
rendered = openSpan + line
} else {
rendered = line
}
// Update openSpan state by scanning for span open/close tags in this line.
// After processing, openSpan reflects whatever span is still open at end of line.
openSpan = trackOpenSpan(openSpan, line)
// If a span is still open at end of line, close it so the <td> is well-formed.
if openSpan != "" {
rendered += "</span>"
}
fmt.Fprintf(w, "<tr><td class=\"line-no\">%d</td><td class=\"line-content\">%s</td></tr>\n", lineNum, rendered)
lineNum++
}
fmt.Fprintf(w, `</table>
</div>
</body>
</html>
`)
}
var spanOpenRe = regexp.MustCompile(`<span class="cov\d+" title="\d+">`)
// trackOpenSpan processes a line of HTML and returns the span tag that is still open
// at the end of the line (if any). It scans left-to-right for <span ...> and </span> tags.
func trackOpenSpan(currentOpen string, line string) string {
open := currentOpen
s := line
for len(s) > 0 {
openIdx := strings.Index(s, "<span ")
closeIdx := strings.Index(s, "</span>")
// Neither found — done
if openIdx < 0 && closeIdx < 0 {
break
}
// Only close found, or close comes first
if openIdx < 0 || (closeIdx >= 0 && closeIdx < openIdx) {
open = ""
s = s[closeIdx+len("</span>"):]
continue
}
// Open found (and comes before any close)
m := spanOpenRe.FindString(s[openIdx:])
if m == "" {
break
}
open = m
s = s[openIdx+len(m):]
}
return open
}
func splitPkgFile(path string) (pkg, file string) {
idx := strings.LastIndex(path, "/")
if idx < 0 {
return "", path
}
return path[:idx], path[idx+1:]
}
func coverageClass(pct float64) string {
if pct == 0 {
return "cov-zero"
}
if pct < 30 {
return "cov-low"
}
if pct < 70 {
return "cov-med"
}
return "cov-high"
}
func barColorForCoverage(pct float64) string {
if pct == 0 {
return "#444"
}
if pct < 30 {
return "#e05555"
}
if pct < 70 {
return "#e8c54a"
}
return "#20ec9b"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment