├── .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 | -
14 | About The Project
15 |
16 | -
17 | Getting Started
18 |
22 |
23 | - Usage
24 | - Roadmap
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 |
--------------------------------------------------------------------------------