├── .gitignore ├── CHANGELOG.md ├── README.md ├── go.mod ├── go.sum ├── go_coverage.go ├── go_coverage_test.go └── preview.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage.out 3 | go-coverage 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.0.1] - 2021-07-08 10 | ### Changed 11 | - Rename to go-coverage 12 | 13 | ### Added 14 | - Support to filter out files 15 | - Get functions sorted by most uncovered lines 16 | - Ability to preview via fzf 17 | 18 | [Unreleased]: https://github.com/gojek/go-coverage/compare/v0.0.1...main 19 | [0.0.1]: https://github.com/gojek/go-coverage/releases/tag/v0.0.1 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

go-coverage

3 | 4 |

5 | Increase code coverage of Go projects 6 |

7 |

8 | 9 | 10 |
11 |

Table of Contents

12 |
    13 |
  1. 14 | About The Project 15 |
  2. 16 |
  3. 17 | Getting Started 18 | 22 |
  4. 23 |
  5. Usage
  6. 24 |
  7. Roadmap
  8. 25 |
26 |
27 | 28 | 29 | ## About The Project 30 | 31 | The key challenge with large code bases with low test coverage is to prioritize which sections of code to test first. 32 | 33 | The standard coverage tools tell about the code coverage percentage and what is covered and uncovered however it doesn't give an input on which functions to cover first and what will be the impact of covering them. 34 | 35 | This tool addresses the challenge by providing the sorted list of functions to cover and the impact associated with covering it. 36 | 37 | 38 | ## Getting Started 39 | 40 | To get a local copy up and running follow these simple steps. 41 | 42 | ### Prerequisites 43 | 44 | You'll need Go installed to use this tool. [Here](https://golang.org/doc/install) is the installation instructions for Go. 45 | 46 | ### Installation 47 | 48 | Via go get 49 | ```shell 50 | go get -u github.com/gojek/go-coverage 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Prerequisites 56 | 57 | Generate the coverage profile for your Go codebase, usually done via 58 | ```shell 59 | go test ./... -coverprofile=coverage.out 60 | ``` 61 | 62 | ### Get lines uncovered greater than 10 63 | 64 | ```shell 65 | go-coverage -f coverage.out --line-filter 10 66 | ``` 67 | 68 | ### Get trimmed file names 69 | 70 | ```shell 71 | go-coverage -f coverage.out --line-filter 10 --trim 72 | ``` 73 | 74 | ```shell 75 | +-------------------------+-------------------------------------+-----------------+--------+ 76 | | FILE | FUNCTION | UNCOVERED LINES | IMPACT | 77 | +-------------------------+-------------------------------------+-----------------+--------+ 78 | | ...ice/config/config.go | RadiusForClosestDriverByServicetype | 26 | 1.9 | 79 | | ...ice/config/config.go | RadiusForServicetype | 26 | 1.9 | 80 | | ...ice/config/config.go | AliceDriverLimit | 26 | 1.9 | 81 | | ...ice/config/config.go | ConsumerDriverLimitByServicetype | 26 | 1.9 | 82 | | .../service/handlers.go | findDriver | 19 | 1.4 | 83 | | ...ice/extern/driver.go | driverAllocationStatusFromAPI | 19 | 1.4 | 84 | | .../service/handlers.go | updateDriverVehicleTags | 18 | 1.3 | 85 | | ...ice/config/config.go | ConsumerDriverLimit | 14 | 1.0 | 86 | | ...vice/service/cron.go | startCrons | 14 | 1.0 | 87 | | ...ice/config/config.go | RadiusForVehicleType | 13 | 0.9 | 88 | | ...ice/config/config.go | matchVehicleType | 12 | 0.9 | 89 | | ...rvice/service/api.go | startServer | 11 | 0.8 | 90 | +-------------------------+-------------------------------------+-----------------+--------+ 91 | ``` 92 | 93 | ### Exclude file name pattern 94 | 95 | ```shell 96 | go-coverage -f coverage.out --exclude ".*config.*" --line-filter 10 --trim 97 | ``` 98 | 99 | ```shell 100 | +-------------------------+-------------------------------+-----------------+--------+ 101 | | FILE | FUNCTION | UNCOVERED LINES | IMPACT | 102 | +-------------------------+-------------------------------+-----------------+--------+ 103 | | .../service/handlers.go | findDriver | 19 | 1.4 | 104 | | ...ice/extern/driver.go | driverAllocationStatusFromAPI | 19 | 1.4 | 105 | | .../service/handlers.go | updateDriverVehicleTags | 18 | 1.3 | 106 | | ...vice/service/cron.go | startCrons | 14 | 1.0 | 107 | | ...rvice/service/api.go | startServer | 11 | 0.8 | 108 | +-------------------------+-------------------------------+-----------------+--------+ 109 | ``` 110 | 111 | ## Roadmap 112 | 113 | - [ ] Support generation of HTML 114 | - [ ] Integrate with gitlab 115 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gojek/go-coverage 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect 7 | github.com/mattn/go-runewidth v0.0.13 // indirect 8 | github.com/olekukonko/tablewriter v0.0.5 9 | github.com/thoas/go-funk v0.9.0 10 | github.com/urfave/cli/v2 v2.3.0 11 | golang.org/x/tools v0.1.5 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 8 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 9 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 10 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 11 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 15 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 16 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 17 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 18 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 19 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 22 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 23 | github.com/thoas/go-funk v0.9.0 h1:Yzu8aTjTb1sqHZzSZLBt4qaZrFfjNizhA7IfnefjEzo= 24 | github.com/thoas/go-funk v0.9.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 25 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 26 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 27 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 28 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 29 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 30 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 31 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 32 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 46 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 47 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 48 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 49 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 53 | gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= 54 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | -------------------------------------------------------------------------------- /go_coverage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/olekukonko/tablewriter" 6 | "github.com/thoas/go-funk" 7 | "github.com/urfave/cli/v2" 8 | "go/ast" 9 | "go/build" 10 | "go/parser" 11 | "go/token" 12 | "golang.org/x/tools/cover" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "sort" 18 | "strconv" 19 | "strings" 20 | ) 21 | 22 | func main() { 23 | var trim bool 24 | 25 | app := &cli.App{ 26 | Name: "go-coverage", 27 | Usage: "identify complex untested functions", 28 | Flags: []cli.Flag{ 29 | &cli.Int64Flag{ 30 | Name: "line-filter", 31 | Value: 0, 32 | Usage: "functions with untested lines lower than this will be filtered out", 33 | }, 34 | &cli.BoolFlag{ 35 | Name: "trim", 36 | Aliases: []string{"t"}, 37 | Value: false, 38 | Usage: "trim file name", 39 | Destination: &trim, 40 | }, 41 | &cli.StringFlag{ 42 | Name: "format", 43 | Value: "table", 44 | Usage: "display format", 45 | }, 46 | &cli.StringFlag{ 47 | Name: "exclude", 48 | Value: "", 49 | Usage: "regex of the file to exclude", 50 | }, 51 | &cli.StringFlag{ 52 | Name: "file", 53 | Aliases: []string{"f"}, 54 | Usage: "coverage file", 55 | Required: true, 56 | }, 57 | }, 58 | Action: func(c *cli.Context) error { 59 | profiles, err := cover.ParseProfiles(c.String("file")) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | funcInfos, total, covered, err := getFunctionInfos(profiles) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | sort.Slice(funcInfos, func(i, j int) bool { 70 | return funcInfos[i].uncoveredLines > funcInfos[j].uncoveredLines 71 | }) 72 | 73 | f := funk.Filter(funcInfos, func(x *funcInfo) bool { 74 | return x.uncoveredLines > c.Int64("line-filter") 75 | }).([]*funcInfo) 76 | 77 | exc := c.String("exclude") 78 | 79 | if exc != "" { 80 | r, regexErr := regexp.Compile(exc) 81 | if regexErr != nil { 82 | return regexErr 83 | } 84 | 85 | f = funk.Filter(f, func(x *funcInfo) bool { 86 | return !r.Match([]byte(x.fileName)) 87 | }).([]*funcInfo) 88 | } 89 | 90 | if c.String("format") == "table" { 91 | printTable(f, trim, covered, total) 92 | } else { 93 | printBat(f, trim, covered, total) 94 | } 95 | return nil 96 | }, 97 | } 98 | 99 | err := app.Run(os.Args) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | } 104 | 105 | func getTrimmedFileName(fn string, trim bool) string { 106 | if trim { 107 | fn = trimString(fn, 20) 108 | } 109 | return fn 110 | } 111 | 112 | func fmtFuncInfo(x *funcInfo, covered int64, total int64, trim bool) []string { 113 | fn := getTrimmedFileName(x.fileName, trim) 114 | tc := calculateCoverage(covered, total) 115 | return []string{ 116 | fn, 117 | x.functionName, 118 | strconv.Itoa(x.functionStartLine), 119 | strconv.Itoa(x.functionEndLine), 120 | strconv.FormatInt(x.uncoveredLines, 10), 121 | fmt.Sprintf("%.1f", calculateCoverage(covered+x.uncoveredLines, total)-tc)} 122 | } 123 | 124 | func printBat(f []*funcInfo, trim bool, covered int64, total int64) { 125 | var fStr [][]string 126 | 127 | 128 | for _, fInfo := range f { 129 | fStr = append(fStr, fmtFuncInfo(fInfo,covered, total, trim)) 130 | } 131 | 132 | for _, v := range fStr { 133 | fmt.Println(strings.Join(v, " ")) 134 | } 135 | } 136 | 137 | func calculateCoverage(covered int64, total int64) float64 { 138 | return float64(covered) / float64(total) * 100 139 | } 140 | 141 | func printTable(f []*funcInfo, trim bool, covered int64, total int64) { 142 | var fStr [][]string 143 | tc := calculateCoverage(covered, total) 144 | fStr = funk.Map(f, func(x *funcInfo) []string { 145 | fn := getTrimmedFileName(x.fileName, trim) 146 | return []string{ 147 | fn, 148 | x.functionName, 149 | strconv.FormatInt(x.uncoveredLines, 10), 150 | fmt.Sprintf("%.1f", calculateCoverage(covered+x.uncoveredLines, total) - tc)} 151 | }).([][]string) 152 | table := tablewriter.NewWriter(os.Stdout) 153 | table.SetHeader([]string{"File", "Function", "Uncovered Lines", "Impact"}) 154 | table.AppendBulk(fStr) 155 | table.Render() 156 | } 157 | 158 | func getFunctionInfos(profiles []*cover.Profile) ([]*funcInfo, int64, int64, error) { 159 | var total, covered int64 160 | var funcInfos []*funcInfo 161 | for _, profile := range profiles { 162 | fn := profile.FileName 163 | file, err := findFile(fn) 164 | if err != nil { 165 | return nil, 0, 0, err 166 | } 167 | funcs, err := findFuncs(file) 168 | if err != nil { 169 | return nil, 0, 0, err 170 | } 171 | // Now match up functions and profile blocks. 172 | for _, f := range funcs { 173 | c, t := f.coverage(profile) 174 | funcInfos = append(funcInfos, 175 | &funcInfo{fileName: file, 176 | functionName: f.name, 177 | functionStartLine: f.startLine, 178 | functionEndLine: f.endLine, 179 | uncoveredLines: t - c}) 180 | total += t 181 | covered += c 182 | } 183 | } 184 | return funcInfos, total, covered, nil 185 | } 186 | 187 | func trimString(s string, i int) string { 188 | if len(s) > i { 189 | return "..." + s[len(s)-i:] 190 | } 191 | return s 192 | } 193 | 194 | type funcInfo struct { 195 | fileName string 196 | functionName string 197 | functionStartLine int 198 | functionEndLine int 199 | uncoveredLines int64 200 | } 201 | 202 | // findFuncs parses the file and returns a slice of FuncExtent descriptors. 203 | func findFuncs(name string) ([]*FuncExtent, error) { 204 | fset := token.NewFileSet() 205 | parsedFile, err := parser.ParseFile(fset, name, nil, 0) 206 | if err != nil { 207 | return nil, err 208 | } 209 | visitor := &FuncVisitor{ 210 | fset: fset, 211 | name: name, 212 | astFile: parsedFile, 213 | } 214 | ast.Walk(visitor, visitor.astFile) 215 | return visitor.funcs, nil 216 | } 217 | 218 | // FuncExtent describes a function's extent in the source by file and position. 219 | type FuncExtent struct { 220 | name string 221 | startLine int 222 | startCol int 223 | endLine int 224 | endCol int 225 | } 226 | 227 | // FuncVisitor implements the visitor that builds the function position list for a file. 228 | type FuncVisitor struct { 229 | fset *token.FileSet 230 | name string // Name of file. 231 | astFile *ast.File 232 | funcs []*FuncExtent 233 | } 234 | 235 | // Visit implements the ast.Visitor interface. 236 | func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { 237 | switch n := node.(type) { 238 | case *ast.FuncDecl: 239 | start := v.fset.Position(n.Pos()) 240 | end := v.fset.Position(n.End()) 241 | fe := &FuncExtent{ 242 | name: n.Name.Name, 243 | startLine: start.Line, 244 | startCol: start.Column, 245 | endLine: end.Line, 246 | endCol: end.Column, 247 | } 248 | v.funcs = append(v.funcs, fe) 249 | } 250 | return v 251 | } 252 | 253 | // coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. 254 | func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { 255 | // We could avoid making this n^2 overall by doing a single scan and annotating the functions, 256 | // but the sizes of the data structures is never very large and the scan is almost instantaneous. 257 | var covered, total int64 258 | // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. 259 | for _, b := range profile.Blocks { 260 | if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { 261 | // Past the end of the function. 262 | break 263 | } 264 | if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { 265 | // Before the beginning of the function 266 | continue 267 | } 268 | total += int64(b.NumStmt) 269 | if b.Count > 0 { 270 | covered += int64(b.NumStmt) 271 | } 272 | } 273 | if total == 0 { 274 | total = 1 // Avoid zero denominator. 275 | } 276 | return covered, total 277 | } 278 | 279 | // findFile finds the location of the named file in GOROOT, GOPATH etc. 280 | func findFile(file string) (string, error) { 281 | dir, file := filepath.Split(file) 282 | pkg, err := build.Import(dir, ".", build.FindOnly) 283 | if err != nil { 284 | return "", fmt.Errorf("can't find %q: %v", file, err) 285 | } 286 | return filepath.Join(pkg.Dir, file), nil 287 | } 288 | -------------------------------------------------------------------------------- /go_coverage_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func Test_trimString(t *testing.T) { 10 | 11 | tests := []struct { 12 | in string 13 | trim int 14 | want string 15 | }{ 16 | {"test", 20, "test"}, 17 | {"test", 3, "...est"}, 18 | {"testtesttesttesttesttest", 20, "...testtesttesttesttest"}, 19 | {"testtesttesttesttesttesttesttesttesttesttestbest", 20, "...testtesttesttestbest"}, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.in, func(t *testing.T) { 23 | if got := trimString(tt.in, tt.trim); got != tt.want { 24 | t.Errorf("trimString() = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func Test_getTrimmedFileName(t *testing.T) { 31 | type args struct { 32 | fn string 33 | trim bool 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want string 39 | }{ 40 | {"trim enabled", args{fn: "test_file_name_test_file_name", trim: true}, "..._name_test_file_name"}, 41 | {"trim enabled", args{fn: "test_file_name_test_file_name", trim: false}, "test_file_name_test_file_name"}, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | if got := getTrimmedFileName(tt.args.fn, tt.args.trim); got != tt.want { 46 | t.Errorf("getTrimmedFileName() = %v, want %v", got, tt.want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func Test_fmtFuncInfo(t *testing.T) { 53 | type args struct { 54 | x *funcInfo 55 | covered int64 56 | total int64 57 | trim bool 58 | } 59 | tests := []struct { 60 | name string 61 | args args 62 | want []string 63 | }{ 64 | {"returns function details without trim when trim is false", 65 | args{ 66 | x: &funcInfo{ 67 | fileName: "test", 68 | functionName: "test_func_name_test_func_name_test_func_name", 69 | functionStartLine: 10, 70 | functionEndLine: 20, 71 | uncoveredLines: 0}, 72 | covered: 50, 73 | total: 50, 74 | trim: false}, 75 | []string{"test", "test_func_name_test_func_name_test_func_name", "10", "20", "0", "0.0"}, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | if got := fmtFuncInfo(tt.args.x, tt.args.covered, tt.args.total, tt.args.trim); !reflect.DeepEqual(got, tt.want) { 81 | t.Errorf("fmtFuncInfo() = %v, want %v", got, tt.want) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func Test_calculateCoverage(t *testing.T) { 88 | type args struct { 89 | covered int64 90 | total int64 91 | } 92 | tests := []struct { 93 | name string 94 | args args 95 | want float64 96 | }{ 97 | {"fully covered", args{100, 100}, 100.0}, 98 | {"partially covered", args{50, 100}, 50.0}, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | if got := calculateCoverage(tt.args.covered, tt.args.total); got != tt.want { 103 | t.Errorf("calculateCoverage() = %v, want %v", got, tt.want) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func Test_findFile(t *testing.T) { 110 | path, _ := os.Getwd() 111 | 112 | type args struct { 113 | file string 114 | } 115 | tests := []struct { 116 | name string 117 | args args 118 | want string 119 | wantErr bool 120 | }{ 121 | {"file from this package", args{file: "github.com/gojek/go-coverage/go_coverage.go"}, path + "/go_coverage.go", false}, 122 | {"invalid file", args{file: "go_coverage_unknown.go"}, "", true}, 123 | 124 | } 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | got, err := findFile(tt.args.file) 128 | if (err != nil) != tt.wantErr { 129 | t.Errorf("findFile() error = %v, wantErr %v", err, tt.wantErr) 130 | return 131 | } 132 | if got != tt.want { 133 | t.Errorf("findFile() got = %v, want %v", got, tt.want) 134 | } 135 | }) 136 | } 137 | } -------------------------------------------------------------------------------- /preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FILENAME=$(cut -d" " -f1 <<< $1); 4 | START_LINE=$(cut -d" " -f3 <<< $1); 5 | END_LINE=$(cut -d" " -f4 <<< $1); 6 | bat --style=numbers --color=always -r $START_LINE:$END_LINE $FILENAME 7 | --------------------------------------------------------------------------------