├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── coverage-04.dtd ├── coverage-with-data.xml ├── go.mod ├── go.sum └── gocov-xml.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | coverage.xml 3 | 4 | gocov-xml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Alexey Palazhchenko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: fvb 2 | 3 | prepare: 4 | go get -u github.com/axw/gocov/... 5 | go get -u github.com/gorilla/mux/... 6 | gocov test -v github.com/gorilla/mux > mux.json 7 | 8 | fvb: 9 | gofmt -e -s -w . 10 | go vet . 11 | go run ./gocov-xml.go < mux.json > coverage.xml 12 | xmllint --valid --noout coverage.xml 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocov XML 2 | 3 | A tool to generate Go coverage in XML report for using with tools/plugins like Jenkins/Cobertura. 4 | 5 | > Table of Contents 6 | 7 | - [gocov XML](#gocov-xml) 8 | - [Installation](#installation) 9 | - [Usage](#usage) 10 | - [Examples](#examples) 11 | - [Generate coverage by passing `gocov` output as input to `gocov-xml`](#generate-coverage-by-passing-gocov-output-as-input-to-gocov-xml) 12 | - [Specifying optional source](#specifying-optional-source) 13 | - [Authors](#authors) 14 | 15 | This is a simple helper tool for generating XML output in [Cobertura](http://cobertura.sourceforge.net/) format 16 | for CIs like [Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin), [vsts](https://www.visualstudio.com/team-services) and others 17 | from [github.com/axw/gocov](https://github.com/axw/gocov) output. 18 | The generated XML output is in the latest [coverage-04.dtd](http://cobertura.sourceforge.net/xml/coverage-04.dtd) schema 19 | 20 | ## Installation 21 | 22 | Just type the following to install the program and its dependencies: 23 | 24 | For Go 1.17 and above: 25 | 26 | ```bash 27 | go install github.com/axw/gocov/gocov@latest 28 | go install github.com/AlekSi/gocov-xml@latest 29 | ``` 30 | 31 | For previous Go versions: 32 | 33 | ```bash 34 | go get github.com/axw/gocov/... 35 | go get github.com/AlekSi/gocov-xml 36 | ``` 37 | 38 | ## Usage 39 | 40 | > **NOTE**: `gocov-xml` reads data from the standard input. 41 | 42 | ```bash 43 | gocov [-source ] 44 | ``` 45 | 46 | Where, 47 | 48 | - **`source`**: Absolute path to source. Defaults to the current working directory. 49 | 50 | ### Examples 51 | 52 | #### Generate coverage by passing `gocov` output as input to `gocov-xml` 53 | 54 | ```bash 55 | gocov test github.com/gorilla/mux | gocov-xml > coverage.xml 56 | ``` 57 | 58 | #### Specifying optional source 59 | 60 | ```bash 61 | gocov test github.com/gorilla/mux | gocov-xml -source /abs/path/to/source > coverage.xml 62 | ``` 63 | 64 | ## Authors 65 | 66 | - [Alexey Palazhchenko (AlekSi)](https://github.com/AlekSi) 67 | - [Yukinari Toyota (t-yuki)](https://github.com/t-yuki) 68 | - [Marin Bek (marinbek)](https://github.com/marinbek) 69 | - [Alex Castle (acastle)](https://github.com/acastle) 70 | - [Billy Yao (yaoyaozong)](https://github.com/yaoyaozong) 71 | - [Abhijith DA (abhijithda)](https://github.com/abhijithda) 72 | -------------------------------------------------------------------------------- /coverage-04.dtd: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /coverage-with-data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | C:/local/mvn-coverage-example/src/main/java 7 | --source 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AlekSi/gocov-xml 2 | 3 | go 1.13 4 | 5 | require github.com/axw/gocov v1.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/axw/gocov v1.1.0 h1:y5U1krExoJDlb/kNtzxyZQmNRprFOFCutWbNjcQvmVM= 2 | github.com/axw/gocov v1.1.0/go.mod h1:H9G4tivgdN3pYSSVrTFBr6kGDCmAkgbJhtxFzAvgcdw= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 9 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 12 | golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /gocov-xml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "flag" 7 | "fmt" 8 | "go/token" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | "time" 15 | 16 | "github.com/axw/gocov" 17 | ) 18 | 19 | // Coverage information 20 | type Coverage struct { 21 | XMLName xml.Name `xml:"coverage"` 22 | LineRate float32 `xml:"line-rate,attr"` 23 | BranchRate float32 `xml:"branch-rate,attr"` 24 | LinesCovered float32 `xml:"lines-covered,attr"` 25 | LinesValid int64 `xml:"lines-valid,attr"` 26 | BranchesCovered int64 `xml:"branches-covered,attr"` 27 | BranchesValid int64 `xml:"branches-valid,attr"` 28 | Complexity float32 `xml:"complexity,attr"` 29 | Version string `xml:"version,attr"` 30 | Timestamp int64 `xml:"timestamp,attr"` 31 | Packages []Package `xml:"packages>package"` 32 | Sources []string `xml:"sources>source"` 33 | } 34 | 35 | // Package information 36 | type Package struct { 37 | Name string `xml:"name,attr"` 38 | LineRate float32 `xml:"line-rate,attr"` 39 | BranchRate float32 `xml:"branch-rate,attr"` 40 | Complexity float32 `xml:"complexity,attr"` 41 | Classes []Class `xml:"classes>class"` 42 | LineCount int64 `xml:"line-count,attr"` 43 | LineHits int64 `xml:"line-hits,attr"` 44 | } 45 | 46 | // Class information 47 | type Class struct { 48 | Name string `xml:"name,attr"` 49 | Filename string `xml:"filename,attr"` 50 | LineRate float32 `xml:"line-rate,attr"` 51 | BranchRate float32 `xml:"branch-rate,attr"` 52 | Complexity float32 `xml:"complexity,attr"` 53 | Methods []Method `xml:"methods>method"` 54 | Lines []Line `xml:"lines>line"` 55 | LineCount int64 `xml:"line-count,attr"` 56 | LineHits int64 `xml:"line-hits,attr"` 57 | } 58 | 59 | // Method information 60 | type Method struct { 61 | Name string `xml:"name,attr"` 62 | Signature string `xml:"signature,attr"` 63 | LineRate float32 `xml:"line-rate,attr"` 64 | BranchRate float32 `xml:"branch-rate,attr"` 65 | Complexity float32 `xml:"complexity,attr"` 66 | Lines []Line `xml:"lines>line"` 67 | LineCount int64 `xml:"line-count,attr"` 68 | LineHits int64 `xml:"line-hits,attr"` 69 | } 70 | 71 | // Line information 72 | type Line struct { 73 | Number int `xml:"number,attr"` 74 | Hits int64 `xml:"hits,attr"` 75 | } 76 | 77 | func main() { 78 | sourcePathPtr := flag.String( 79 | "source", 80 | "", 81 | "Absolute path to source. Defaults to current working directory.", 82 | ) 83 | 84 | flag.Parse() 85 | 86 | // Parse the commandline arguments. 87 | var sourcePath string 88 | var err error 89 | if *sourcePathPtr != "" { 90 | sourcePath = *sourcePathPtr 91 | if !filepath.IsAbs(sourcePath) { 92 | panic(fmt.Sprintf("Source path is a relative path: %s", sourcePath)) 93 | } 94 | } else { 95 | sourcePath, err = os.Getwd() 96 | if err != nil { 97 | panic(err) 98 | } 99 | } 100 | 101 | sources := make([]string, 1) 102 | sources[0] = sourcePath 103 | var r struct{ Packages []gocov.Package } 104 | var totalLines, totalHits int64 105 | err = json.NewDecoder(os.Stdin).Decode(&r) 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | fset := token.NewFileSet() 111 | tokenFiles := make(map[string]*token.File) 112 | 113 | // convert packages 114 | packages := make([]Package, len(r.Packages)) 115 | for i, gPackage := range r.Packages { 116 | // group functions by filename and "class" (type) 117 | files := make(map[string]map[string]*Class) 118 | for _, gFunction := range gPackage.Functions { 119 | // get the releative path by base path. 120 | fpath, err := filepath.Rel(sourcePath, gFunction.File) 121 | if err != nil { 122 | panic(err) 123 | } 124 | classes := files[fpath] 125 | if classes == nil { 126 | // group functions by "class" (type) in a File 127 | classes = make(map[string]*Class) 128 | files[fpath] = classes 129 | } 130 | 131 | s := strings.Split("-."+gFunction.Name, ".") // className is "-" for package-level functions 132 | className, methodName := s[len(s)-2], s[len(s)-1] 133 | class := classes[className] 134 | if class == nil { 135 | class = &Class{Name: className, Filename: fpath, Methods: []Method{}, Lines: []Line{}} 136 | classes[className] = class 137 | } 138 | 139 | // from github.com/axw/gocov /gocov/annotate.go#printFunctionSource 140 | // Load the file for line information. Probably overkill, maybe 141 | // just compute the lines from offsets in here. 142 | setContent := false 143 | tokenFile := tokenFiles[gFunction.File] 144 | if tokenFile == nil { 145 | info, err := os.Stat(gFunction.File) 146 | if err != nil { 147 | panic(err) 148 | } 149 | tokenFile = fset.AddFile(gFunction.File, fset.Base(), int(info.Size())) 150 | setContent = true 151 | } 152 | 153 | tokenData, err := ioutil.ReadFile(gFunction.File) 154 | if err != nil { 155 | panic(err) 156 | } 157 | if setContent { 158 | // This processes the content and records line number info. 159 | tokenFile.SetLinesForContent(tokenData) 160 | } 161 | 162 | // convert statements to lines 163 | lines := make([]Line, len(gFunction.Statements)) 164 | var funcHits int 165 | for i, s := range gFunction.Statements { 166 | lineno := tokenFile.Line(tokenFile.Pos(s.Start)) 167 | line := Line{Number: lineno, Hits: s.Reached} 168 | if int(s.Reached) > 0 { 169 | funcHits++ 170 | } 171 | lines[i] = line 172 | class.Lines = append(class.Lines, line) 173 | } 174 | lineRate := float32(funcHits) / float32(len(gFunction.Statements)) 175 | 176 | class.Methods = append(class.Methods, Method{Name: methodName, Lines: lines, LineRate: lineRate}) 177 | class.LineCount += int64(len(gFunction.Statements)) 178 | class.LineHits += int64(funcHits) 179 | } 180 | 181 | // fill package with "classes" 182 | p := Package{Name: gPackage.Name, Classes: []Class{}} 183 | for _, classes := range files { 184 | for _, class := range classes { 185 | p.LineCount += class.LineCount 186 | p.LineHits += class.LineHits 187 | class.LineRate = float32(class.LineHits) / float32(class.LineCount) 188 | p.Classes = append(p.Classes, *class) 189 | sort.Slice(class.Methods, func(i, j int) bool { 190 | return class.Methods[i].Name < class.Methods[j].Name 191 | }) 192 | sort.Slice(class.Lines, func(i, j int) bool { 193 | return class.Lines[i].Number < class.Lines[j].Number 194 | }) 195 | } 196 | p.LineRate = float32(p.LineHits) / float32(p.LineCount) 197 | } 198 | sort.Slice(p.Classes, func(i, j int) bool { 199 | if p.Classes[i].Filename != p.Classes[j].Filename { 200 | return p.Classes[i].Filename < p.Classes[j].Filename 201 | } 202 | return p.Classes[i].Name < p.Classes[j].Name 203 | }) 204 | packages[i] = p 205 | totalLines += p.LineCount 206 | totalHits += p.LineHits 207 | } 208 | 209 | coverage := Coverage{Sources: sources, Packages: packages, Timestamp: time.Now().UnixNano() / int64(time.Millisecond), LinesCovered: float32(totalHits), LinesValid: int64(totalLines), LineRate: float32(totalHits) / float32(totalLines)} 210 | 211 | fmt.Printf(xml.Header) 212 | fmt.Printf("\n") 213 | 214 | encoder := xml.NewEncoder(os.Stdout) 215 | encoder.Indent("", "\t") 216 | err = encoder.Encode(coverage) 217 | if err != nil { 218 | panic(err) 219 | } 220 | 221 | fmt.Println() 222 | } 223 | --------------------------------------------------------------------------------