| """This file provides a parse() method to extract the per-file line coverage |
| from an LCOV ".info" file. |
| """ |
| |
| from typing import Dict, Text, Tuple, Iterable, IO, Optional |
| |
| |
| Lines = Dict[int, bool] |
| |
| def _merge_lines(dst: Lines, src: Lines) -> Lines: |
| """_merge_lines merges `src` into `dst` and returns `dst`. """ |
| for line, coverage in src.items(): |
| if line in dst: |
| dst[line] = dst[line] or coverage |
| else: |
| dst[line] = coverage |
| return dst |
| |
| |
| class Record: |
| """Record holds the data from an LCOV record.""" |
| def __init__(self): |
| self.lines = {} # type: Lines |
| self.file = '' |
| |
| |
| class LcovSyntaxError(Exception): |
| """LcovSyntaxError is raised when there is formatting issues in the ".info" file.""" |
| pass |
| |
| |
| def _parse_line(line: Text) -> Tuple[Text, Text]: |
| """_parse_line takes in a line from a file and returns it in two parts. |
| |
| LCOV lines are of the form |
| <field>:<data> |
| eg: "TN:<test name>" |
| |
| Args: |
| line: line from the input file. |
| |
| Returns: |
| a tuple containing the part before and the part after a ":" in the line. |
| """ |
| try: |
| field, value = line.split(':', 1) |
| except: |
| raise LcovSyntaxError('invalid data line, needs to be of the form' + |
| '\n\tDA:<line #>,<execution count>[,<checksum>]\n ' + |
| 'or\n \tTN:<test name>\ngot: ' + line) |
| |
| return (field, value.strip()) |
| |
| |
| def _read_record(input_file: Iterable[Text]) -> Optional[Record]: |
| """_read_record the next LCOV record from `input_file`. |
| |
| LCOV records are of the form |
| SF:<file name> |
| ... |
| end_of_record |
| |
| Args: |
| input_file: the ".info" file that is being read. |
| |
| Returns: |
| the text Record in the file. |
| |
| Raises: |
| LcovSyntaxError: if the format of the input file doesn't match correct |
| LCOV format. |
| """ |
| rec = Record() |
| |
| for line in input_file: |
| |
| line = line.strip() |
| |
| if line == 'end_of_record': |
| return rec |
| |
| field, value = _parse_line(line) |
| |
| if field == 'SF': |
| rec.file = value |
| |
| elif field == 'DA': |
| parts = value.split(',', 2) |
| if len(parts) < 2: |
| raise LcovSyntaxError('invalid data line, needs to be of the form ' + |
| '\n\tDA:<line #>,<execution count>[,<checksum>]\ngot: ' + line) |
| |
| try: |
| line_num = int(parts[0]) |
| except: |
| raise LcovSyntaxError('invalid data line, needs to be of the form ' + |
| '\n\tDA:<line #>,<execution count>[,<checksum>]\ngot: ' + line) |
| |
| # LCOV explicitly reports a '0' execution count for instrumented lines that don't get run. |
| rec.lines[line_num] = parts[1] != '0' |
| |
| return None |
| |
| |
| def parse(input_file: IO) -> Tuple[Text, Dict[Text, Lines]]: |
| """parse reads an LCOV ".info" file. |
| |
| Args: |
| input_file: the file that is being read. |
| |
| Returns: |
| a tuple containing the test_name and the associated files w/ coverage. |
| |
| Raises: |
| LcovSyntaxError: if the first line of the input file does not start with "TN". |
| """ |
| |
| # the first line in a LCOV report is the test name |
| field, test_name = _parse_line(input_file.readline()) |
| |
| if field != 'TN': |
| raise LcovSyntaxError('first line in LCOV report should be "TN:<name>", got: ' + |
| field + ':' + test_name) |
| |
| files = {} # type: Dict[Text, Lines] |
| |
| while True: |
| rec = _read_record(input_file) |
| if rec is None: |
| break |
| |
| if rec.file in files: |
| files[rec.file] = _merge_lines(files[rec.file], rec.lines) |
| else: |
| files[rec.file] = rec.lines |
| |
| return (test_name, files) |