Skip to content

Instantly share code, notes, and snippets.

@jeremyschlatter
Created January 29, 2019 18:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremyschlatter/1bf1f9a23d27ceccc26bd595d6bf9f51 to your computer and use it in GitHub Desktop.
Save jeremyschlatter/1bf1f9a23d27ceccc26bd595d6bf9f51 to your computer and use it in GitHub Desktop.
Merge Go coverage profiles from multiple test runs
package main
import (
"flag"
"fmt"
"os"
"golang.org/x/tools/cover"
)
var output = flag.String("output", "", "path to store aggregated coverage profile")
func main() {
flag.Parse()
if flag.NArg() == 0 || *output == "" {
fmt.Println("usage: merge -output <output> [profiles...]")
os.Exit(1)
}
aggregateProfiles(flag.Args(), *output)
}
// aggregateProfiles aggregates coverage profiles.
// The provided profiles are all assumed to have been generated with the
// same parameters. In particular, they should include the same files,
// with the same source code contents, and use the same coverage mode
// (set vs count vs atomic).
func aggregateProfiles(fileNames []string, aggregateOutput string) error {
if len(fileNames) == 0 {
return fmt.Errorf("must include at least one profile")
}
// Parse profiles.
var profiles [][]*cover.Profile
var mode string
for i, fileName := range fileNames {
current, err := cover.ParseProfiles(fileName)
if err != nil {
return err
}
// Error checking.
if i == 0 {
mode = current[0].Mode
}
for _, profile := range current {
if profile.Mode != mode {
fmt.Errorf(
"unexpected coverage mode: the first coverage mode we saw in the first file was %q, but %v has mode %q for file %q",
mode, fileName, profile.Mode, profile.FileName,
)
}
}
if i > 0 {
prev := profiles[len(profiles)-1]
if len(current) != len(prev) {
return fmt.Errorf(
"mismatched profiles: %v lists %v files, but %v lists %v files",
fileNames[i-1], len(prev), fileNames[i], len(current),
)
}
for j := range current {
if current[j].FileName != prev[j].FileName {
return fmt.Errorf(
"mismatched profiles: %v lists %q as the %v'th file, but %v lists %q there instead",
fileNames[i-1], prev[j].FileName, j, fileNames[i], current[j].FileName,
)
}
}
}
profiles = append(profiles, current)
}
// Aggregate.
aggregate := profiles[0]
for _, profiledFiles := range profiles[1:] {
for j, profile := range profiledFiles {
for k, block := range profile.Blocks {
if profile.Mode == "set" {
aggregate[j].Blocks[k].Count |= block.Count
} else {
aggregate[j].Blocks[k].Count += block.Count
}
}
}
}
// Print.
{
f, err := os.Create(aggregateOutput)
if err != nil {
return err
}
defer f.Close()
// First line is "mode: foo", where foo is "set", "count", or "atomic".
// Rest of file is in the format
// encoding/base64/base64.go:34.44,37.40 3 1
// where the fields are: name.go:line.column,line.column numberOfStatements count
//
// - https://github.com/golang/go/blob/66065c3115861c73b8804037a6d9d5986ffa9913/src/cmd/cover/profile.go#L53-L56
fmt.Fprintf(f, "mode: %v\n", mode)
for _, profile := range aggregate {
for _, block := range profile.Blocks {
fmt.Fprintf(
f,
"%s:%d.%d,%d.%d %d %d\n",
profile.FileName,
block.StartLine,
block.StartCol,
block.EndLine,
block.EndCol,
block.NumStmt,
block.Count,
)
}
}
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment