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  }