github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/cover/profile.go (about) 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 "github.com/powerman/golang-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 }