1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17import {filter, reduce} from './utils';
18
19export interface Pos {
20 line: number;
21 col: number;
22}
23
24export interface Block {
25 statements: number;
26 hits: number;
27 start: Pos;
28 end: Pos;
29}
30
31export 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
60export 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 // tslint: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
131export 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
172function 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}
View as plain text