blob: c623efc244f566224140559194b166750a1227f6 [file] [log] [blame]
"""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)