1#!/usr/bin/env python
2# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3
4from __future__ import print_function
5
6import optparse
7import re
8import sys
9
10# the gcov report follows certain pattern. Each file will have two lines
11# of report, from which we can extract the file name, total lines and coverage
12# percentage.
13def parse_gcov_report(gcov_input):
14    per_file_coverage = {}
15    total_coverage = None
16
17    for line in sys.stdin:
18        line = line.strip()
19
20        # --First line of the coverage report (with file name in it)?
21        match_obj = re.match("^File '(.*)'$", line)
22        if match_obj:
23            # fetch the file name from the first line of the report.
24            current_file = match_obj.group(1)
25            continue
26
27        # -- Second line of the file report (with coverage percentage)
28        match_obj = re.match("^Lines executed:(.*)% of (.*)", line)
29
30        if match_obj:
31            coverage = float(match_obj.group(1))
32            lines = int(match_obj.group(2))
33
34            if current_file is not None:
35                per_file_coverage[current_file] = (coverage, lines)
36                current_file = None
37            else:
38                # If current_file is not set, we reach the last line of report,
39                # which contains the summarized coverage percentage.
40                total_coverage = (coverage, lines)
41            continue
42
43        # If the line's pattern doesn't fall into the above categories. We
44        # can simply ignore them since they're either empty line or doesn't
45        # find executable lines of the given file.
46        current_file = None
47
48    return per_file_coverage, total_coverage
49
50def get_option_parser():
51    usage = "Parse the gcov output and generate more human-readable code " +\
52            "coverage report."
53    parser = optparse.OptionParser(usage)
54
55    parser.add_option(
56        "--interested-files", "-i",
57        dest="filenames",
58        help="Comma separated files names. if specified, we will display " +
59             "the coverage report only for interested source files. " +
60             "Otherwise we will display the coverage report for all " +
61             "source files."
62    )
63    return parser
64
65def display_file_coverage(per_file_coverage, total_coverage):
66    # To print out auto-adjustable column, we need to know the longest
67    # length of file names.
68    max_file_name_length = max(
69        len(fname) for fname in per_file_coverage.keys()
70    )
71
72    # -- Print header
73    # size of separator is determined by 3 column sizes:
74    # file name, coverage percentage and lines.
75    header_template = \
76        "%" + str(max_file_name_length) + "s\t%s\t%s"
77    separator = "-" * (max_file_name_length + 10 + 20)
78    print(header_template % ("Filename", "Coverage", "Lines"))  # noqa: E999 T25377293 Grandfathered in
79    print(separator)
80
81    # -- Print body
82    # template for printing coverage report for each file.
83    record_template = "%" + str(max_file_name_length) + "s\t%5.2f%%\t%10d"
84
85    for fname, coverage_info in per_file_coverage.items():
86        coverage, lines = coverage_info
87        print(record_template % (fname, coverage, lines))
88
89    # -- Print footer
90    if total_coverage:
91        print(separator)
92        print(record_template % ("Total", total_coverage[0], total_coverage[1]))
93
94def report_coverage():
95    parser = get_option_parser()
96    (options, args) = parser.parse_args()
97
98    interested_files = set()
99    if options.filenames is not None:
100        interested_files = set(f.strip() for f in options.filenames.split(','))
101
102    # To make things simple, right now we only read gcov report from the input
103    per_file_coverage, total_coverage = parse_gcov_report(sys.stdin)
104
105    # Check if we need to display coverage info for interested files.
106    if len(interested_files):
107        per_file_coverage = dict(
108            (fname, per_file_coverage[fname]) for fname in interested_files
109            if fname in per_file_coverage
110        )
111        # If we only interested in several files, it makes no sense to report
112        # the total_coverage
113        total_coverage = None
114
115    if not len(per_file_coverage):
116        print("Cannot find coverage info for the given files.", file=sys.stderr)
117        return
118    display_file_coverage(per_file_coverage, total_coverage)
119
120if __name__ == "__main__":
121    report_coverage()
122