| 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