1 | // Copyright 2013 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | // This file implements the visitor that computes the (line, column)-(line-column) range for each function. |
6 | |
7 | package main |
8 | |
9 | import ( |
10 | "bufio" |
11 | "fmt" |
12 | "go/ast" |
13 | "go/build" |
14 | "go/parser" |
15 | "go/token" |
16 | "os" |
17 | "path/filepath" |
18 | "text/tabwriter" |
19 | |
20 | "golang.org/x/tools/cover" |
21 | ) |
22 | |
23 | // funcOutput takes two file names as arguments, a coverage profile to read as input and an output |
24 | // file to write ("" means to write to standard output). The function reads the profile and produces |
25 | // as output the coverage data broken down by function, like this: |
26 | // |
27 | // fmt/format.go:30: init 100.0% |
28 | // fmt/format.go:57: clearflags 100.0% |
29 | // ... |
30 | // fmt/scan.go:1046: doScan 100.0% |
31 | // fmt/scan.go:1075: advance 96.2% |
32 | // fmt/scan.go:1119: doScanf 96.8% |
33 | // total: (statements) 91.9% |
34 | |
35 | func funcOutput(profile, outputFile string) error { |
36 | profiles, err := cover.ParseProfiles(profile) |
37 | if err != nil { |
38 | return err |
39 | } |
40 | |
41 | var out *bufio.Writer |
42 | if outputFile == "" { |
43 | out = bufio.NewWriter(os.Stdout) |
44 | } else { |
45 | fd, err := os.Create(outputFile) |
46 | if err != nil { |
47 | return err |
48 | } |
49 | defer fd.Close() |
50 | out = bufio.NewWriter(fd) |
51 | } |
52 | defer out.Flush() |
53 | |
54 | tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0) |
55 | defer tabber.Flush() |
56 | |
57 | var total, covered int64 |
58 | for _, profile := range profiles { |
59 | fn := profile.FileName |
60 | file, err := findFile(fn) |
61 | if err != nil { |
62 | return err |
63 | } |
64 | funcs, err := findFuncs(file) |
65 | if err != nil { |
66 | return err |
67 | } |
68 | // Now match up functions and profile blocks. |
69 | for _, f := range funcs { |
70 | c, t := f.coverage(profile) |
71 | fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, 100.0*float64(c)/float64(t)) |
72 | total += t |
73 | covered += c |
74 | } |
75 | } |
76 | fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", 100.0*float64(covered)/float64(total)) |
77 | |
78 | return nil |
79 | } |
80 | |
81 | // findFuncs parses the file and returns a slice of FuncExtent descriptors. |
82 | func findFuncs(name string) ([]*FuncExtent, error) { |
83 | fset := token.NewFileSet() |
84 | parsedFile, err := parser.ParseFile(fset, name, nil, 0) |
85 | if err != nil { |
86 | return nil, err |
87 | } |
88 | visitor := &FuncVisitor{ |
89 | fset: fset, |
90 | name: name, |
91 | astFile: parsedFile, |
92 | } |
93 | ast.Walk(visitor, visitor.astFile) |
94 | return visitor.funcs, nil |
95 | } |
96 | |
97 | // FuncExtent describes a function's extent in the source by file and position. |
98 | type FuncExtent struct { |
99 | name string |
100 | startLine int |
101 | startCol int |
102 | endLine int |
103 | endCol int |
104 | } |
105 | |
106 | // FuncVisitor implements the visitor that builds the function position list for a file. |
107 | type FuncVisitor struct { |
108 | fset *token.FileSet |
109 | name string // Name of file. |
110 | astFile *ast.File |
111 | funcs []*FuncExtent |
112 | } |
113 | |
114 | // Visit implements the ast.Visitor interface. |
115 | func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { |
116 | switch n := node.(type) { |
117 | case *ast.FuncDecl: |
118 | start := v.fset.Position(n.Pos()) |
119 | end := v.fset.Position(n.End()) |
120 | fe := &FuncExtent{ |
121 | name: n.Name.Name, |
122 | startLine: start.Line, |
123 | startCol: start.Column, |
124 | endLine: end.Line, |
125 | endCol: end.Column, |
126 | } |
127 | v.funcs = append(v.funcs, fe) |
128 | } |
129 | return v |
130 | } |
131 | |
132 | // coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. |
133 | func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { |
134 | // We could avoid making this n^2 overall by doing a single scan and annotating the functions, |
135 | // but the sizes of the data structures is never very large and the scan is almost instantaneous. |
136 | var covered, total int64 |
137 | // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. |
138 | for _, b := range profile.Blocks { |
139 | if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { |
140 | // Past the end of the function. |
141 | break |
142 | } |
143 | if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { |
144 | // Before the beginning of the function |
145 | continue |
146 | } |
147 | total += int64(b.NumStmt) |
148 | if b.Count > 0 { |
149 | covered += int64(b.NumStmt) |
150 | } |
151 | } |
152 | if total == 0 { |
153 | total = 1 // Avoid zero denominator. |
154 | } |
155 | return covered, total |
156 | } |
157 | |
158 | // findFile finds the location of the named file in GOROOT, GOPATH etc. |
159 | func findFile(file string) (string, error) { |
160 | dir, file := filepath.Split(file) |
161 | pkg, err := build.Import(dir, ".", build.FindOnly) |
162 | if err != nil { |
163 | return "", fmt.Errorf("can't find %q: %v", file, err) |
164 | } |
165 | return filepath.Join(pkg.Dir, file), nil |
166 | } |
167 |
Members