├── LICENSE ├── README.md └── gocovmerge.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Wade Simmons 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gocovmerge 2 | ========== 3 | 4 | gocovmerge takes the results from multiple `go test -coverprofile` runs and 5 | merges them into one profile. 6 | 7 | usage 8 | ----- 9 | 10 | gocovmerge [coverprofiles...] 11 | 12 | gocovmerge takes the source coverprofiles as the arguments (output from 13 | `go test -coverprofile coverage.out`) and outputs a merged version of the 14 | files to standard out. You can only merge profiles that were generated from the 15 | same source code. If there are source lines that overlap or do not merge, the 16 | process will exit with an error code. 17 | -------------------------------------------------------------------------------- /gocovmerge.go: -------------------------------------------------------------------------------- 1 | // gocovmerge takes the results from multiple `go test -coverprofile` runs and 2 | // merges them into one profile 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "sort" 12 | 13 | "golang.org/x/tools/cover" 14 | ) 15 | 16 | func mergeProfiles(p *cover.Profile, merge *cover.Profile) { 17 | if p.Mode != merge.Mode { 18 | log.Fatalf("cannot merge profiles with different modes") 19 | } 20 | // Since the blocks are sorted, we can keep track of where the last block 21 | // was inserted and only look at the blocks after that as targets for merge 22 | startIndex := 0 23 | for _, b := range merge.Blocks { 24 | startIndex = mergeProfileBlock(p, b, startIndex) 25 | } 26 | } 27 | 28 | func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) int { 29 | sortFunc := func(i int) bool { 30 | pi := p.Blocks[i+startIndex] 31 | return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol) 32 | } 33 | 34 | i := 0 35 | if sortFunc(i) != true { 36 | i = sort.Search(len(p.Blocks)-startIndex, sortFunc) 37 | } 38 | i += startIndex 39 | if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol { 40 | if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol { 41 | log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb) 42 | } 43 | switch p.Mode { 44 | case "set": 45 | p.Blocks[i].Count |= pb.Count 46 | case "count", "atomic": 47 | p.Blocks[i].Count += pb.Count 48 | default: 49 | log.Fatalf("unsupported covermode: '%s'", p.Mode) 50 | } 51 | } else { 52 | if i > 0 { 53 | pa := p.Blocks[i-1] 54 | if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) { 55 | log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb) 56 | } 57 | } 58 | if i < len(p.Blocks)-1 { 59 | pa := p.Blocks[i+1] 60 | if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) { 61 | log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb) 62 | } 63 | } 64 | p.Blocks = append(p.Blocks, cover.ProfileBlock{}) 65 | copy(p.Blocks[i+1:], p.Blocks[i:]) 66 | p.Blocks[i] = pb 67 | } 68 | return i + 1 69 | } 70 | 71 | func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile { 72 | i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName }) 73 | if i < len(profiles) && profiles[i].FileName == p.FileName { 74 | mergeProfiles(profiles[i], p) 75 | } else { 76 | profiles = append(profiles, nil) 77 | copy(profiles[i+1:], profiles[i:]) 78 | profiles[i] = p 79 | } 80 | return profiles 81 | } 82 | 83 | func dumpProfiles(profiles []*cover.Profile, out io.Writer) { 84 | if len(profiles) == 0 { 85 | return 86 | } 87 | fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode) 88 | for _, p := range profiles { 89 | for _, b := range p.Blocks { 90 | 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) 91 | } 92 | } 93 | } 94 | 95 | func main() { 96 | flag.Parse() 97 | 98 | var merged []*cover.Profile 99 | 100 | for _, file := range flag.Args() { 101 | profiles, err := cover.ParseProfiles(file) 102 | if err != nil { 103 | log.Fatalf("failed to parse profiles: %v", err) 104 | } 105 | for _, p := range profiles { 106 | merged = addProfile(merged, p) 107 | } 108 | } 109 | 110 | dumpProfiles(merged, os.Stdout) 111 | } 112 | --------------------------------------------------------------------------------