Created
January 29, 2019 18:27
-
-
Save jeremyschlatter/1bf1f9a23d27ceccc26bd595d6bf9f51 to your computer and use it in GitHub Desktop.
Merge Go coverage profiles from multiple test runs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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