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 | // Package cover provides support for parsing coverage profiles |
6 | // generated by "go test -coverprofile=cover.out". |
7 | package cover // import "golang.org/x/tools/cover" |
8 | |
9 | import ( |
10 | "bufio" |
11 | "errors" |
12 | "fmt" |
13 | "io" |
14 | "math" |
15 | "os" |
16 | "sort" |
17 | "strconv" |
18 | "strings" |
19 | ) |
20 | |
21 | // Profile represents the profiling data for a specific file. |
22 | type Profile struct { |
23 | FileName string |
24 | Mode string |
25 | Blocks []ProfileBlock |
26 | } |
27 | |
28 | // ProfileBlock represents a single block of profiling data. |
29 | type ProfileBlock struct { |
30 | StartLine, StartCol int |
31 | EndLine, EndCol int |
32 | NumStmt, Count int |
33 | } |
34 | |
35 | type byFileName []*Profile |
36 | |
37 | func (p byFileName) Len() int { return len(p) } |
38 | func (p byFileName) Less(i, j int) bool { return p[i].FileName < p[j].FileName } |
39 | func (p byFileName) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
40 | |
41 | // ParseProfiles parses profile data in the specified file and returns a |
42 | // Profile for each source file described therein. |
43 | func ParseProfiles(fileName string) ([]*Profile, error) { |
44 | pf, err := os.Open(fileName) |
45 | if err != nil { |
46 | return nil, err |
47 | } |
48 | defer pf.Close() |
49 | return ParseProfilesFromReader(pf) |
50 | } |
51 | |
52 | // ParseProfilesFromReader parses profile data from the Reader and |
53 | // returns a Profile for each source file described therein. |
54 | func ParseProfilesFromReader(rd io.Reader) ([]*Profile, error) { |
55 | // First line is "mode: foo", where foo is "set", "count", or "atomic". |
56 | // Rest of file is in the format |
57 | // encoding/base64/base64.go:34.44,37.40 3 1 |
58 | // where the fields are: name.go:line.column,line.column numberOfStatements count |
59 | files := make(map[string]*Profile) |
60 | s := bufio.NewScanner(rd) |
61 | mode := "" |
62 | for s.Scan() { |
63 | line := s.Text() |
64 | if mode == "" { |
65 | const p = "mode: " |
66 | if !strings.HasPrefix(line, p) || line == p { |
67 | return nil, fmt.Errorf("bad mode line: %v", line) |
68 | } |
69 | mode = line[len(p):] |
70 | continue |
71 | } |
72 | fn, b, err := parseLine(line) |
73 | if err != nil { |
74 | return nil, fmt.Errorf("line %q doesn't match expected format: %v", line, err) |
75 | } |
76 | p := files[fn] |
77 | if p == nil { |
78 | p = &Profile{ |
79 | FileName: fn, |
80 | Mode: mode, |
81 | } |
82 | files[fn] = p |
83 | } |
84 | p.Blocks = append(p.Blocks, b) |
85 | } |
86 | if err := s.Err(); err != nil { |
87 | return nil, err |
88 | } |
89 | for _, p := range files { |
90 | sort.Sort(blocksByStart(p.Blocks)) |
91 | // Merge samples from the same location. |
92 | j := 1 |
93 | for i := 1; i < len(p.Blocks); i++ { |
94 | b := p.Blocks[i] |
95 | last := p.Blocks[j-1] |
96 | if b.StartLine == last.StartLine && |
97 | b.StartCol == last.StartCol && |
98 | b.EndLine == last.EndLine && |
99 | b.EndCol == last.EndCol { |
100 | if b.NumStmt != last.NumStmt { |
101 | return nil, fmt.Errorf("inconsistent NumStmt: changed from %d to %d", last.NumStmt, b.NumStmt) |
102 | } |
103 | if mode == "set" { |
104 | p.Blocks[j-1].Count |= b.Count |
105 | } else { |
106 | p.Blocks[j-1].Count += b.Count |
107 | } |
108 | continue |
109 | } |
110 | p.Blocks[j] = b |
111 | j++ |
112 | } |
113 | p.Blocks = p.Blocks[:j] |
114 | } |
115 | // Generate a sorted slice. |
116 | profiles := make([]*Profile, 0, len(files)) |
117 | for _, profile := range files { |
118 | profiles = append(profiles, profile) |
119 | } |
120 | sort.Sort(byFileName(profiles)) |
121 | return profiles, nil |
122 | } |
123 | |
124 | // parseLine parses a line from a coverage file. |
125 | // It is equivalent to the regex |
126 | // ^(.+):([0-9]+)\.([0-9]+),([0-9]+)\.([0-9]+) ([0-9]+) ([0-9]+)$ |
127 | // |
128 | // However, it is much faster: https://golang.org/cl/179377 |
129 | func parseLine(l string) (fileName string, block ProfileBlock, err error) { |
130 | end := len(l) |
131 | |
132 | b := ProfileBlock{} |
133 | b.Count, end, err = seekBack(l, ' ', end, "Count") |
134 | if err != nil { |
135 | return "", b, err |
136 | } |
137 | b.NumStmt, end, err = seekBack(l, ' ', end, "NumStmt") |
138 | if err != nil { |
139 | return "", b, err |
140 | } |
141 | b.EndCol, end, err = seekBack(l, '.', end, "EndCol") |
142 | if err != nil { |
143 | return "", b, err |
144 | } |
145 | b.EndLine, end, err = seekBack(l, ',', end, "EndLine") |
146 | if err != nil { |
147 | return "", b, err |
148 | } |
149 | b.StartCol, end, err = seekBack(l, '.', end, "StartCol") |
150 | if err != nil { |
151 | return "", b, err |
152 | } |
153 | b.StartLine, end, err = seekBack(l, ':', end, "StartLine") |
154 | if err != nil { |
155 | return "", b, err |
156 | } |
157 | fn := l[0:end] |
158 | if fn == "" { |
159 | return "", b, errors.New("a FileName cannot be blank") |
160 | } |
161 | return fn, b, nil |
162 | } |
163 | |
164 | // seekBack searches backwards from end to find sep in l, then returns the |
165 | // value between sep and end as an integer. |
166 | // If seekBack fails, the returned error will reference what. |
167 | func seekBack(l string, sep byte, end int, what string) (value int, nextSep int, err error) { |
168 | // Since we're seeking backwards and we know only ASCII is legal for these values, |
169 | // we can ignore the possibility of non-ASCII characters. |
170 | for start := end - 1; start >= 0; start-- { |
171 | if l[start] == sep { |
172 | i, err := strconv.Atoi(l[start+1 : end]) |
173 | if err != nil { |
174 | return 0, 0, fmt.Errorf("couldn't parse %q: %v", what, err) |
175 | } |
176 | if i < 0 { |
177 | return 0, 0, fmt.Errorf("negative values are not allowed for %s, found %d", what, i) |
178 | } |
179 | return i, start, nil |
180 | } |
181 | } |
182 | return 0, 0, fmt.Errorf("couldn't find a %s before %s", string(sep), what) |
183 | } |
184 | |
185 | type blocksByStart []ProfileBlock |
186 | |
187 | func (b blocksByStart) Len() int { return len(b) } |
188 | func (b blocksByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] } |
189 | func (b blocksByStart) Less(i, j int) bool { |
190 | bi, bj := b[i], b[j] |
191 | return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol |
192 | } |
193 | |
194 | // Boundary represents the position in a source file of the beginning or end of a |
195 | // block as reported by the coverage profile. In HTML mode, it will correspond to |
196 | // the opening or closing of a <span> tag and will be used to colorize the source |
197 | type Boundary struct { |
198 | Offset int // Location as a byte offset in the source file. |
199 | Start bool // Is this the start of a block? |
200 | Count int // Event count from the cover profile. |
201 | Norm float64 // Count normalized to [0..1]. |
202 | Index int // Order in input file. |
203 | } |
204 | |
205 | // Boundaries returns a Profile as a set of Boundary objects within the provided src. |
206 | func (p *Profile) Boundaries(src []byte) (boundaries []Boundary) { |
207 | // Find maximum count. |
208 | max := 0 |
209 | for _, b := range p.Blocks { |
210 | if b.Count > max { |
211 | max = b.Count |
212 | } |
213 | } |
214 | // Divisor for normalization. |
215 | divisor := math.Log(float64(max)) |
216 | |
217 | // boundary returns a Boundary, populating the Norm field with a normalized Count. |
218 | index := 0 |
219 | boundary := func(offset int, start bool, count int) Boundary { |
220 | b := Boundary{Offset: offset, Start: start, Count: count, Index: index} |
221 | index++ |
222 | if !start || count == 0 { |
223 | return b |
224 | } |
225 | if max <= 1 { |
226 | b.Norm = 0.8 // Profile is in"set" mode; we want a heat map. Use cov8 in the CSS. |
227 | } else if count > 0 { |
228 | b.Norm = math.Log(float64(count)) / divisor |
229 | } |
230 | return b |
231 | } |
232 | |
233 | line, col := 1, 2 // TODO: Why is this 2? |
234 | for si, bi := 0, 0; si < len(src) && bi < len(p.Blocks); { |
235 | b := p.Blocks[bi] |
236 | if b.StartLine == line && b.StartCol == col { |
237 | boundaries = append(boundaries, boundary(si, true, b.Count)) |
238 | } |
239 | if b.EndLine == line && b.EndCol == col || line > b.EndLine { |
240 | boundaries = append(boundaries, boundary(si, false, 0)) |
241 | bi++ |
242 | continue // Don't advance through src; maybe the next block starts here. |
243 | } |
244 | if src[si] == '\n' { |
245 | line++ |
246 | col = 0 |
247 | } |
248 | col++ |
249 | si++ |
250 | } |
251 | sort.Sort(boundariesByPos(boundaries)) |
252 | return |
253 | } |
254 | |
255 | type boundariesByPos []Boundary |
256 | |
257 | func (b boundariesByPos) Len() int { return len(b) } |
258 | func (b boundariesByPos) Swap(i, j int) { b[i], b[j] = b[j], b[i] } |
259 | func (b boundariesByPos) Less(i, j int) bool { |
260 | if b[i].Offset == b[j].Offset { |
261 | // Boundaries at the same offset should be ordered according to |
262 | // their original position. |
263 | return b[i].Index < b[j].Index |
264 | } |
265 | return b[i].Offset < b[j].Offset |
266 | } |
267 |
Members