// gocovmerge takes the results from multiple `go test -coverprofile` runs and // merges them into one profile package main import ( "flag" "fmt" "io" "log" "os" "sort" "golang.org/x/tools/cover" ) func mergeProfiles(p *cover.Profile, merge *cover.Profile) { if p.Mode != merge.Mode { log.Fatalf("cannot merge profiles with different modes") } // Since the blocks are sorted, we can keep track of where the last block // was inserted and only look at the blocks after that as targets for merge startIndex := 0 for _, b := range merge.Blocks { startIndex = mergeProfileBlock(p, b, startIndex) } } func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) int { sortFunc := func(i int) bool { pi := p.Blocks[i+startIndex] return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol) } i := 0 if !sortFunc(i) { i = sort.Search(len(p.Blocks)-startIndex, sortFunc) } i += startIndex if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol { if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol { log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb) } switch p.Mode { case "set": p.Blocks[i].Count |= pb.Count case "count", "atomic": p.Blocks[i].Count += pb.Count default: log.Fatalf("unsupported covermode: '%s'", p.Mode) } } else { if i > 0 { pa := p.Blocks[i-1] if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) { log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb) } } if i < len(p.Blocks)-1 { pa := p.Blocks[i+1] if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) { log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb) } } p.Blocks = append(p.Blocks, cover.ProfileBlock{}) copy(p.Blocks[i+1:], p.Blocks[i:]) p.Blocks[i] = pb } return i + 1 } func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile { i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName }) if i < len(profiles) && profiles[i].FileName == p.FileName { mergeProfiles(profiles[i], p) } else { profiles = append(profiles, nil) copy(profiles[i+1:], profiles[i:]) profiles[i] = p } return profiles } func dumpProfiles(profiles []*cover.Profile, out io.Writer) { if len(profiles) == 0 { return } fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode) for _, p := range profiles { for _, b := range p.Blocks { fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine, b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count) } } } func main() { flag.Parse() var merged []*cover.Profile for _, file := range flag.Args() { profiles, err := cover.ParseProfiles(file) if err != nil { log.Fatalf("failed to parse profiles: %v", err) } for _, p := range profiles { merged = addProfile(merged, p) } } dumpProfiles(merged, os.Stdout) }