github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/coverage/parser.ts (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 import {filter, reduce} from './utils'; 18 19 export interface Pos { 20 line: number; 21 col: number; 22 } 23 24 export interface Block { 25 statements: number; 26 hits: number; 27 start: Pos; 28 end: Pos; 29 } 30 31 export class FileCoverage { 32 private blocks: Map<string, Block> = new Map<string, Block>(); 33 34 constructor(readonly filename: string, readonly fileNumber: number) {} 35 36 public addBlock(block: Block) { 37 const k = this.keyForBlock(block); 38 const oldBlock = this.blocks.get(k); 39 if (oldBlock) { 40 oldBlock.hits += block.hits; 41 } else { 42 this.blocks.set(k, block); 43 } 44 } 45 46 get totalStatements(): number { 47 return reduce(this.blocks.values(), (acc, b) => acc + b.statements, 0); 48 } 49 50 get coveredStatements(): number { 51 return reduce(this.blocks.values(), 52 (acc, b) => acc + (b.hits > 0 ? b.statements : 0), 0); 53 } 54 55 private keyForBlock(block: Block): string { 56 return `${block.start.line}.${block.start.col},${block.end.line}.${block.end.col}`; 57 } 58 } 59 60 export class Coverage { 61 public files = new Map<string, FileCoverage>(); 62 63 constructor(readonly mode: string, readonly prefix = '') {} 64 65 public addFile(file: FileCoverage): void { 66 this.files.set(file.filename, file); 67 } 68 69 public getFile(name: string): FileCoverage|undefined { 70 return this.files.get(name); 71 } 72 73 public getFilesWithPrefix(prefix: string): Map<string, FileCoverage> { 74 return new Map(filter( 75 this.files.entries(), ([k]) => k.startsWith(this.prefix + prefix))); 76 } 77 78 public getCoverageForPrefix(prefix: string): Coverage { 79 const subCoverage = new Coverage(this.mode, this.prefix + prefix); 80 for (const [filename, file] of this.files) { 81 if (filename.startsWith(this.prefix + prefix)) { 82 subCoverage.addFile(file); 83 } 84 } 85 return subCoverage; 86 } 87 88 get children(): Map<string, Coverage> { 89 const children = new Map(); 90 for (const path of this.files.keys()) { 91 // eslint-disable-next-line prefer-const 92 let [dir, rest] = path.substr(this.prefix.length).split('/', 2); 93 if (!children.has(dir)) { 94 if (rest) { 95 dir += '/'; 96 } 97 children.set(dir, this.getCoverageForPrefix(dir)); 98 } 99 } 100 return children; 101 } 102 103 get basename(): string { 104 if (this.prefix.endsWith('/')) { 105 return `${this.prefix.substring(0, this.prefix.length - 1).split('/').pop() 106 }/`; 107 } 108 return this.prefix.split('/').pop()!; 109 } 110 111 get totalStatements(): number { 112 return reduce(this.files.values(), (acc, f) => acc + f.totalStatements, 0); 113 } 114 115 get coveredStatements(): number { 116 return reduce( 117 this.files.values(), (acc, f) => acc + f.coveredStatements, 0); 118 } 119 120 get totalFiles(): number { 121 return this.files.size; 122 } 123 124 get coveredFiles(): number { 125 return reduce( 126 this.files.values(), 127 (acc, f) => acc + (f.coveredStatements > 0 ? 1 : 0), 0); 128 } 129 } 130 131 export function parseCoverage(content: string): Coverage { 132 const lines = content.split('\n'); 133 const modeLine = lines.shift()!; 134 const [modeLabel, mode] = modeLine.split(':').map((x) => x.trim()); 135 if (modeLabel !== 'mode') { 136 throw new Error('Expected to start with mode line.'); 137 } 138 139 // Well-formed coverage files are already sorted alphabetically, but Kubernetes' 140 // `make test` produces ill-formed coverage files. This does actually matter, so 141 // sort it ourselves. 142 lines.sort((a, b) => { 143 a = a.split(':', 2)[0]; 144 b = b.split(':', 2)[0]; 145 if (a < b) { 146 return -1; 147 } else if (a > b) { 148 return 1; 149 } else { 150 return 0; 151 } 152 }); 153 154 const coverage = new Coverage(mode); 155 let fileCounter = 0; 156 for (const line of lines) { 157 if (line === '') { 158 continue; 159 } 160 const {filename, ...block} = parseLine(line); 161 let file = coverage.getFile(filename); 162 if (!file) { 163 file = new FileCoverage(filename, fileCounter++); 164 coverage.addFile(file); 165 } 166 file.addBlock(block); 167 } 168 169 return coverage; 170 } 171 172 function parseLine(line: string): Block&{filename: string} { 173 const [filename, block] = line.split(':'); 174 const [positions, statements, hits] = block.split(' '); 175 const [start, end] = positions.split(','); 176 const [startLine, startCol] = start.split('.').map(parseInt); 177 const [endLine, endCol] = end.split('.').map(parseInt); 178 return { 179 end: { 180 col: endCol, 181 line: endLine, 182 }, 183 filename, 184 hits: Math.max(0, Number(hits)), 185 start: { 186 col: startCol, 187 line: startLine, 188 }, 189 statements: Number(statements), 190 }; 191 }