1
2
3
4
5
6
7 package main
8
9 import (
10 "bufio"
11 "bytes"
12 "encoding/json"
13 "errors"
14 "fmt"
15 "go/ast"
16 "go/parser"
17 "go/token"
18 "io"
19 "os"
20 "os/exec"
21 "path"
22 "path/filepath"
23 "runtime"
24 "strings"
25 "text/tabwriter"
26
27 "golang.org/x/tools/cover"
28 )
29
30
31
32
33
34
35
36
37
38
39
40
41
42 func funcOutput(profile, outputFile string) error {
43 profiles, err := cover.ParseProfiles(profile)
44 if err != nil {
45 return err
46 }
47
48 dirs, err := findPkgs(profiles)
49 if err != nil {
50 return err
51 }
52
53 var out *bufio.Writer
54 if outputFile == "" {
55 out = bufio.NewWriter(os.Stdout)
56 } else {
57 fd, err := os.Create(outputFile)
58 if err != nil {
59 return err
60 }
61 defer fd.Close()
62 out = bufio.NewWriter(fd)
63 }
64 defer out.Flush()
65
66 tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0)
67 defer tabber.Flush()
68
69 var total, covered int64
70 for _, profile := range profiles {
71 fn := profile.FileName
72 file, err := findFile(dirs, fn)
73 if err != nil {
74 return err
75 }
76 funcs, err := findFuncs(file)
77 if err != nil {
78 return err
79 }
80
81 for _, f := range funcs {
82 c, t := f.coverage(profile)
83 fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, percent(c, t))
84 total += t
85 covered += c
86 }
87 }
88 fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", percent(covered, total))
89
90 return nil
91 }
92
93
94 func findFuncs(name string) ([]*FuncExtent, error) {
95 fset := token.NewFileSet()
96 parsedFile, err := parser.ParseFile(fset, name, nil, 0)
97 if err != nil {
98 return nil, err
99 }
100 visitor := &FuncVisitor{
101 fset: fset,
102 name: name,
103 astFile: parsedFile,
104 }
105 ast.Walk(visitor, visitor.astFile)
106 return visitor.funcs, nil
107 }
108
109
110 type FuncExtent struct {
111 name string
112 startLine int
113 startCol int
114 endLine int
115 endCol int
116 }
117
118
119 type FuncVisitor struct {
120 fset *token.FileSet
121 name string
122 astFile *ast.File
123 funcs []*FuncExtent
124 }
125
126
127 func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor {
128 switch n := node.(type) {
129 case *ast.FuncDecl:
130 if n.Body == nil {
131
132 break
133 }
134 start := v.fset.Position(n.Pos())
135 end := v.fset.Position(n.End())
136 fe := &FuncExtent{
137 name: n.Name.Name,
138 startLine: start.Line,
139 startCol: start.Column,
140 endLine: end.Line,
141 endCol: end.Column,
142 }
143 v.funcs = append(v.funcs, fe)
144 }
145 return v
146 }
147
148
149 func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) {
150
151
152 var covered, total int64
153
154 for _, b := range profile.Blocks {
155 if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) {
156
157 break
158 }
159 if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) {
160
161 continue
162 }
163 total += int64(b.NumStmt)
164 if b.Count > 0 {
165 covered += int64(b.NumStmt)
166 }
167 }
168 return covered, total
169 }
170
171
172 type Pkg struct {
173 ImportPath string
174 Dir string
175 Error *struct {
176 Err string
177 }
178 }
179
180 func findPkgs(profiles []*cover.Profile) (map[string]*Pkg, error) {
181
182 pkgs := make(map[string]*Pkg)
183 var list []string
184 for _, profile := range profiles {
185 if strings.HasPrefix(profile.FileName, ".") || filepath.IsAbs(profile.FileName) {
186
187 continue
188 }
189 pkg := path.Dir(profile.FileName)
190 if _, ok := pkgs[pkg]; !ok {
191 pkgs[pkg] = nil
192 list = append(list, pkg)
193 }
194 }
195
196 if len(list) == 0 {
197 return pkgs, nil
198 }
199
200
201
202 goTool := filepath.Join(runtime.GOROOT(), "bin/go")
203 cmd := exec.Command(goTool, append([]string{"list", "-e", "-json"}, list...)...)
204 var stderr bytes.Buffer
205 cmd.Stderr = &stderr
206 stdout, err := cmd.Output()
207 if err != nil {
208 return nil, fmt.Errorf("cannot run go list: %v\n%s", err, stderr.Bytes())
209 }
210 dec := json.NewDecoder(bytes.NewReader(stdout))
211 for {
212 var pkg Pkg
213 err := dec.Decode(&pkg)
214 if err == io.EOF {
215 break
216 }
217 if err != nil {
218 return nil, fmt.Errorf("decoding go list json: %v", err)
219 }
220 pkgs[pkg.ImportPath] = &pkg
221 }
222 return pkgs, nil
223 }
224
225
226 func findFile(pkgs map[string]*Pkg, file string) (string, error) {
227 if strings.HasPrefix(file, ".") || filepath.IsAbs(file) {
228
229 return file, nil
230 }
231 pkg := pkgs[path.Dir(file)]
232 if pkg != nil {
233 if pkg.Dir != "" {
234 return filepath.Join(pkg.Dir, path.Base(file)), nil
235 }
236 if pkg.Error != nil {
237 return "", errors.New(pkg.Error.Err)
238 }
239 }
240 return "", fmt.Errorf("did not find package for %s in go list output", file)
241 }
242
243 func percent(covered, total int64) float64 {
244 if total == 0 {
245 total = 1
246 }
247 return 100.0 * float64(covered) / float64(total)
248 }
249
View as plain text