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 provides support for parsing coverage profiles 6 // generated by "go test -coverprofile=cover.out". 7 // It is a copy of golang.org/x/tools/cover/profile.go. 8 9 package main 10 11 import ( 12 "bufio" 13 "fmt" 14 "math" 15 "os" 16 "regexp" 17 "sort" 18 "strconv" 19 "strings" 20 ) 21 22 // Profile represents the profiling data for a specific file. 23 type Profile struct { 24 FileName string 25 Mode string 26 Blocks []ProfileBlock 27 } 28 29 // ProfileBlock represents a single block of profiling data. 30 type ProfileBlock struct { 31 StartLine, StartCol int 32 EndLine, EndCol int 33 NumStmt, Count int 34 } 35 36 type byFileName []*Profile 37 38 func (p byFileName) Len() int { return len(p) } 39 func (p byFileName) Less(i, j int) bool { return p[i].FileName < p[j].FileName } 40 func (p byFileName) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 41 42 // ParseProfiles parses profile data in the specified file and returns a 43 // Profile for each source file described therein. 44 func ParseProfiles(fileName string) ([]*Profile, error) { 45 pf, err := os.Open(fileName) 46 if err != nil { 47 return nil, err 48 } 49 defer pf.Close() 50 51 files := make(map[string]*Profile) 52 buf := bufio.NewReader(pf) 53 // First line is "mode: foo", where foo is "set", "count", or "atomic". 54 // Rest of file is in the format 55 // encoding/base64/base64.go:34.44,37.40 3 1 56 // where the fields are: name.go:line.column,line.column numberOfStatements count 57 s := bufio.NewScanner(buf) 58 mode := "" 59 for s.Scan() { 60 line := s.Text() 61 if mode == "" { 62 const p = "mode: " 63 if !strings.HasPrefix(line, p) || line == p { 64 return nil, fmt.Errorf("bad mode line: %v", line) 65 } 66 mode = line[len(p):] 67 continue 68 } 69 m := lineRe.FindStringSubmatch(line) 70 if m == nil { 71 return nil, fmt.Errorf("line %q doesn't match expected format: %v", m, lineRe) 72 } 73 fn := m[1] 74 p := files[fn] 75 if p == nil { 76 p = &Profile{ 77 FileName: fn, 78 Mode: mode, 79 } 80 files[fn] = p 81 } 82 p.Blocks = append(p.Blocks, ProfileBlock{ 83 StartLine: toInt(m[2]), 84 StartCol: toInt(m[3]), 85 EndLine: toInt(m[4]), 86 EndCol: toInt(m[5]), 87 NumStmt: toInt(m[6]), 88 Count: toInt(m[7]), 89 }) 90 } 91 if err := s.Err(); err != nil { 92 return nil, err 93 } 94 for _, p := range files { 95 sort.Sort(blocksByStart(p.Blocks)) 96 } 97 // Generate a sorted slice. 98 profiles := make([]*Profile, 0, len(files)) 99 for _, profile := range files { 100 profiles = append(profiles, profile) 101 } 102 sort.Sort(byFileName(profiles)) 103 return profiles, nil 104 } 105 106 type blocksByStart []ProfileBlock 107 108 func (b blocksByStart) Len() int { return len(b) } 109 func (b blocksByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 110 func (b blocksByStart) Less(i, j int) bool { 111 bi, bj := b[i], b[j] 112 return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol 113 } 114 115 var lineRe = regexp.MustCompile(`^(.+):([0-9]+).([0-9]+),([0-9]+).([0-9]+) ([0-9]+) ([0-9]+)$`) 116 117 func toInt(s string) int { 118 i, err := strconv.Atoi(s) 119 if err != nil { 120 panic(err) 121 } 122 return i 123 } 124 125 // Boundary represents the position in a source file of the beginning or end of a 126 // block as reported by the coverage profile. In HTML mode, it will correspond to 127 // the opening or closing of a <span> tag and will be used to colorize the source 128 type Boundary struct { 129 Offset int // Location as a byte offset in the source file. 130 Start bool // Is this the start of a block? 131 Count int // Event count from the cover profile. 132 Norm float64 // Count normalized to [0..1]. 133 } 134 135 // Boundaries returns a Profile as a set of Boundary objects within the provided src. 136 func (p *Profile) Boundaries(src []byte) (boundaries []Boundary) { 137 // Find maximum count. 138 max := 0 139 for _, b := range p.Blocks { 140 if b.Count > max { 141 max = b.Count 142 } 143 } 144 // Divisor for normalization. 145 divisor := math.Log(float64(max)) 146 147 // boundary returns a Boundary, populating the Norm field with a normalized Count. 148 boundary := func(offset int, start bool, count int) Boundary { 149 b := Boundary{Offset: offset, Start: start, Count: count} 150 if !start || count == 0 { 151 return b 152 } 153 if max <= 1 { 154 b.Norm = 0.8 // Profile is in"set" mode; we want a heat map. Use cov8 in the CSS. 155 } else if count > 0 { 156 b.Norm = math.Log(float64(count)) / divisor 157 } 158 return b 159 } 160 161 line, col := 1, 2 // TODO: Why is this 2? 162 for si, bi := 0, 0; si < len(src) && bi < len(p.Blocks); { 163 b := p.Blocks[bi] 164 if b.StartLine == line && b.StartCol == col { 165 boundaries = append(boundaries, boundary(si, true, b.Count)) 166 } 167 if b.EndLine == line && b.EndCol == col || line > b.EndLine { 168 boundaries = append(boundaries, boundary(si, false, 0)) 169 bi++ 170 continue // Don't advance through src; maybe the next block starts here. 171 } 172 if src[si] == '\n' { 173 line++ 174 col = 0 175 } 176 col++ 177 si++ 178 } 179 sort.Sort(boundariesByPos(boundaries)) 180 return 181 } 182 183 type boundariesByPos []Boundary 184 185 func (b boundariesByPos) Len() int { return len(b) } 186 func (b boundariesByPos) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 187 func (b boundariesByPos) Less(i, j int) bool { 188 if b[i].Offset == b[j].Offset { 189 return !b[i].Start && b[j].Start 190 } 191 return b[i].Offset < b[j].Offset 192 } 193