/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import {filter, reduce} from './utils'; export interface Pos { line: number; col: number; } export interface Block { statements: number; hits: number; start: Pos; end: Pos; } export class FileCoverage { private blocks: Map = new Map(); constructor(readonly filename: string, readonly fileNumber: number) {} public addBlock(block: Block) { const k = this.keyForBlock(block); const oldBlock = this.blocks.get(k); if (oldBlock) { oldBlock.hits += block.hits; } else { this.blocks.set(k, block); } } get totalStatements(): number { return reduce(this.blocks.values(), (acc, b) => acc + b.statements, 0); } get coveredStatements(): number { return reduce(this.blocks.values(), (acc, b) => acc + (b.hits > 0 ? b.statements : 0), 0); } private keyForBlock(block: Block): string { return `${block.start.line}.${block.start.col},${block.end.line}.${block.end.col}`; } } export class Coverage { public files = new Map(); constructor(readonly mode: string, readonly prefix = '') {} public addFile(file: FileCoverage): void { this.files.set(file.filename, file); } public getFile(name: string): FileCoverage|undefined { return this.files.get(name); } public getFilesWithPrefix(prefix: string): Map { return new Map(filter( this.files.entries(), ([k]) => k.startsWith(this.prefix + prefix))); } public getCoverageForPrefix(prefix: string): Coverage { const subCoverage = new Coverage(this.mode, this.prefix + prefix); for (const [filename, file] of this.files) { if (filename.startsWith(this.prefix + prefix)) { subCoverage.addFile(file); } } return subCoverage; } get children(): Map { const children = new Map(); for (const path of this.files.keys()) { // tslint:disable-next-line:prefer-const let [dir, rest] = path.substr(this.prefix.length).split('/', 2); if (!children.has(dir)) { if (rest) { dir += '/'; } children.set(dir, this.getCoverageForPrefix(dir)); } } return children; } get basename(): string { if (this.prefix.endsWith('/')) { return this.prefix.substring(0, this.prefix.length - 1).split('/').pop() + '/'; } return this.prefix.split('/').pop()!; } get totalStatements(): number { return reduce(this.files.values(), (acc, f) => acc + f.totalStatements, 0); } get coveredStatements(): number { return reduce( this.files.values(), (acc, f) => acc + f.coveredStatements, 0); } get totalFiles(): number { return this.files.size; } get coveredFiles(): number { return reduce( this.files.values(), (acc, f) => acc + (f.coveredStatements > 0 ? 1 : 0), 0); } } export function parseCoverage(content: string): Coverage { const lines = content.split('\n'); const modeLine = lines.shift()!; const [modeLabel, mode] = modeLine.split(':').map((x) => x.trim()); if (modeLabel !== 'mode') { throw new Error('Expected to start with mode line.'); } // Well-formed coverage files are already sorted alphabetically, but Kubernetes' // `make test` produces ill-formed coverage files. This does actually matter, so // sort it ourselves. lines.sort((a, b) => { a = a.split(':', 2)[0]; b = b.split(':', 2)[0]; if (a < b) { return -1; } else if (a > b) { return 1; } else { return 0; } }); const coverage = new Coverage(mode); let fileCounter = 0; for (const line of lines) { if (line === '') { continue; } const {filename, ...block} = parseLine(line); let file = coverage.getFile(filename); if (!file) { file = new FileCoverage(filename, fileCounter++); coverage.addFile(file); } file.addBlock(block); } return coverage; } function parseLine(line: string): Block&{filename: string} { const [filename, block] = line.split(':'); const [positions, statements, hits] = block.split(' '); const [start, end] = positions.split(','); const [startLine, startCol] = start.split('.').map(parseInt); const [endLine, endCol] = end.split('.').map(parseInt); return { end: { col: endCol, line: endLine, }, filename, hits: Math.max(0, Number(hits)), start: { col: startCol, line: startLine, }, statements: Number(statements), }; }