github.com/thediveo/gons@v0.9.9/reexec/testing/profile.go (about) 1 // Copyright 2020 Harald Albrecht. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package testing 16 17 import ( 18 "bufio" 19 "fmt" 20 "os" 21 "regexp" 22 "sort" 23 "strconv" 24 ) 25 26 // coverageProfile represents coverage profile data for a specific coverage 27 // profile data file. 28 type coverageProfile struct { 29 // Mode of coverage profile: "atomic", "count", or "set". 30 Mode string 31 // Sources with block coverage data, indexed by source file name. 32 Sources map[string]*coverageProfileSource 33 } 34 35 // newCoverageProfile returns a new and correctly initialized coverageProfile. 36 func newCoverageProfile() *coverageProfile { 37 return &coverageProfile{ 38 Sources: map[string]*coverageProfileSource{}, 39 } 40 } 41 42 // coverageProfileSource represents the coverage blocks of a single source 43 // file. 44 type coverageProfileSource struct { 45 Blocks []coverageProfileBlock // coverage blocks per source file. 46 } 47 48 // coverageProfileBlockByStart is a type alias for sorting slices of 49 // coverageProfileBlocks. 50 type coverageProfileBlockByStart []coverageProfileBlock 51 52 func (b coverageProfileBlockByStart) Len() int { return len(b) } 53 func (b coverageProfileBlockByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 54 func (b coverageProfileBlockByStart) Less(i, j int) bool { 55 bi, bj := b[i], b[j] 56 return bi.StartLine < bj.StartLine || 57 (bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol) 58 } 59 60 // coverageProfileBlock represents a single block of coverage profiling data. 61 type coverageProfileBlock struct { 62 StartLine uint32 // line number for block start. 63 StartCol uint16 // column number for block start. 64 EndLine uint32 // line number for block end. 65 EndCol uint16 // column number for block end. 66 NumStmts uint16 // number of statements included in this block. 67 Counts uint32 // number of times this block was executed. 68 } 69 70 // modeRe specifies the format of the first "mode:" text line of a coverage 71 // profile data file. 72 var modeRe = regexp.MustCompile(`^mode: ([[:alpha:]]+)$`) 73 74 // lineRe specifies the format of the block text lines in coverage profile 75 // data files. 76 var lineRe = regexp.MustCompile( 77 `^(.+):([0-9]+).([0-9]+),([0-9]+).([0-9]+) ([0-9]+) ([0-9]+)$`) 78 79 // mergeCoverageFile reads coverage profile data from the file specified in 80 // the path parameter and merges it with the summary coverage profile in 81 // sumcp. 82 func mergeCoverageFile(path string, sumcp *coverageProfile) { 83 // Phase I: read in the specified coverage profile data file, before we 84 // can attempt to merge it. 85 cp := readcovfile(path) 86 if cp == nil { 87 return 88 } 89 // Phase II: check for the proper coverage profile mode; if not set yet 90 // for the results, then accept the one from the coverage profile just 91 // read. Normally, this will be the "main" coverage profile file created 92 // by the process under test, as we'll read in the other profiles from 93 // re-executed children only later. 94 if sumcp.Mode == "" { 95 sumcp.Mode = cp.Mode 96 } else if cp.Mode != sumcp.Mode { 97 panic(fmt.Sprintf("expected mode %q, got mode %q", sumcp.Mode, cp.Mode)) 98 } 99 // Phase III: for each source, append the new blocks to the existing ones, 100 // sort them, and then finally merge blocks. 101 setmode := sumcp.Mode == "set" 102 for srcname, source := range cp.Sources { 103 // Look up the corresponding source in the summary coverage profile, 104 // or create a new one, if not already present. 105 var sumsource *coverageProfileSource 106 var ok bool 107 if sumsource, ok = sumcp.Sources[srcname]; !ok { 108 sumsource = &coverageProfileSource{Blocks: source.Blocks} 109 sumcp.Sources[srcname] = sumsource 110 } else { 111 sumsource.Blocks = append(sumsource.Blocks, source.Blocks...) 112 } 113 // We might not merge, but we do sort anyway. While not strictly 114 // necessary, this helps our tests to have a well-defined result 115 // order. 116 mergecovblocks(sumsource, setmode) 117 } 118 } 119 120 // readcovfile reads a coverage profile data file and returns it as a 121 // coverageProfile. Returns nil if no such coverage profile file exists or is 122 // empty. If the file turns out to be unparseable for some other reason, it 123 // simply panics. 124 func readcovfile(path string) *coverageProfile { 125 cpf, err := os.Open(toOutputDir(path)) 126 if err != nil { 127 if os.IsNotExist(err) { 128 // Silently skip the situation when a re-execution did not create 129 // a coverage profile data file. 130 return nil 131 } 132 panic(fmt.Sprintf( 133 "unable to merge coverage profile data file %q: %s", 134 toOutputDir(path), err.Error())) 135 } 136 defer cpf.Close() 137 scan := bufio.NewScanner(cpf) 138 if !scan.Scan() { 139 return nil 140 } 141 // Phase I: read in the specified coverage profile data file, before we 142 // can attempt to merge it. 143 cp := newCoverageProfile() 144 // The first line of a coverage profile data file is the mode how 145 // coverage data was gathered; either "atomic", "count", or "set". 146 line := scan.Text() 147 m := modeRe.FindStringSubmatch(line) 148 if m == nil { 149 panic(fmt.Sprintf( 150 "line %q doesn't match expected mode: line format", line)) 151 } 152 cp.Mode = m[1] 153 // The remaining lines contain coverage profile block data. We optimize 154 // here on the basis that Go's testing/coverage.go writes coverage profile 155 // data files where the coverage block data for the same source file is 156 // continuous (instead of being scattered around). However, the code 157 // blocks are not sorted. 158 var srcname string // caches most recent source filename. 159 var source *coverageProfileSource // caches most recent source data. 160 for scan.Scan() { 161 line = scan.Text() 162 m := lineRe.FindStringSubmatch(line) 163 if m == nil { 164 panic(fmt.Sprintf( 165 "line %q doesn't match expected block line format", line)) 166 } 167 if m[1] != srcname { 168 // If we haven't seen this source filename yet, allocate a 169 // coverage data source element and put into the map of known 170 // sources. 171 srcname = m[1] 172 source = &coverageProfileSource{} 173 cp.Sources[srcname] = source 174 } 175 // Append the block data from the coverage profile data file line, the 176 // sequence of blocks is yet unsorted. 177 source.Blocks = append(source.Blocks, coverageProfileBlock{ 178 StartLine: toUint32(m[2]), 179 StartCol: toUint16(m[3]), 180 EndLine: toUint32(m[4]), 181 EndCol: toUint16(m[5]), 182 NumStmts: toUint16(m[6]), 183 Counts: toUint32(m[7]), 184 }) 185 } 186 return cp 187 } 188 189 // toUint32 converts a textual int value into its binary uint32 190 // representation. If the specified text doesn't represent a valid uint32 191 // value, toUint32 panics. 192 func toUint32(s string) uint32 { 193 if v, err := strconv.ParseUint(s, 10, 32); err != nil { 194 panic(err.Error()) 195 } else { 196 return uint32(v) 197 } 198 } 199 200 // toUint16 converts a textual int value into its binary uint16 201 // representation. If the specified text doesn't represent a valid uint16 202 // value, toUint16 panics. 203 func toUint16(s string) uint16 { 204 if v, err := strconv.ParseUint(s, 10, 16); err != nil { 205 panic(err.Error()) 206 } else { 207 return uint16(v) 208 } 209 } 210 211 // mergecovblocks merges coverage blocks for the same code blocks. 212 func mergecovblocks(sumsource *coverageProfileSource, setmode bool) { 213 // First sort, so that multiple coverages for the same block location will 214 // be adjacent. 215 sort.Sort(coverageProfileBlockByStart(sumsource.Blocks)) 216 mergeidx := 0 217 for idx := mergeidx + 1; idx < len(sumsource.Blocks); idx++ { 218 mergeblock := &sumsource.Blocks[mergeidx] 219 block := &sumsource.Blocks[idx] 220 if mergeblock.StartLine == block.StartLine && 221 mergeblock.StartCol == block.StartCol && 222 mergeblock.EndLine == block.EndLine && 223 mergeblock.EndCol == block.EndCol { 224 // We've found a(nother) matching code block, so update the 225 // first's coverage data. 226 if setmode { 227 mergeblock.Counts |= block.Counts 228 } else { 229 mergeblock.Counts += block.Counts 230 } 231 continue 232 } 233 // We've reached a different code location after a set of mergeable 234 // locations, so move this new location block downwards to the end of 235 // already merged blocks. 236 mergeidx++ 237 if mergeidx != idx { 238 sumsource.Blocks[mergeidx] = *block 239 } 240 } 241 // Shorten the code block locations slice to only, erm, "cover" the unique 242 // (and probably merged) blocks. 243 sumsource.Blocks = sumsource.Blocks[:mergeidx+1] 244 }