| # Copyright 2018 The Chromium Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # | |
| # The script intentionally does not have a shebang, as it is Py2/Py3 compatible. | |
| import argparse | |
| from collections import defaultdict | |
| import functools | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| # Appends third_party/ so that coverage_utils can import jinja2 from | |
| # third_party/. | |
| sys.path.append( | |
| os.path.join( | |
| os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'third_party' | |
| ) | |
| ) | |
| import jinja2 | |
| # The default name of the html coverage report for a directory. | |
| DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html']) | |
| # Name of the html index files for different views. | |
| COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html']) | |
| DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html']) | |
| FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html']) | |
| INDEX_HTML_FILE = os.extsep.join(['index', 'html']) | |
| REPORT_DIR = 'coverage' | |
| class CoverageSummary(object): | |
| """Encapsulates coverage summary representation.""" | |
| def __init__( | |
| self, | |
| regions_total=0, | |
| regions_covered=0, | |
| functions_total=0, | |
| functions_covered=0, | |
| lines_total=0, | |
| lines_covered=0, | |
| ): | |
| """Initializes CoverageSummary object.""" | |
| self._summary = { | |
| 'regions': {'total': regions_total, 'covered': regions_covered}, | |
| 'functions': {'total': functions_total, 'covered': functions_covered}, | |
| 'lines': {'total': lines_total, 'covered': lines_covered}, | |
| } | |
| def Get(self): | |
| """Returns summary as a dictionary.""" | |
| return self._summary | |
| def AddSummary(self, other_summary): | |
| """Adds another summary to this one element-wise.""" | |
| for feature in self._summary: | |
| self._summary[feature]['total'] += other_summary.Get()[feature]['total'] | |
| self._summary[feature]['covered'] += other_summary.Get()[feature][ | |
| 'covered' | |
| ] | |
| class CoverageReportHtmlGenerator(object): | |
| """Encapsulates coverage html report generation. | |
| The generated html has a table that contains links to other coverage reports. | |
| """ | |
| def __init__(self, output_dir, output_path, table_entry_type): | |
| """Initializes _CoverageReportHtmlGenerator object. | |
| Args: | |
| output_dir: Path to the dir for writing coverage report to. | |
| output_path: Path to the html report that will be generated. | |
| table_entry_type: Type of the table entries to be displayed in the table | |
| header. For example: 'Path', 'Component'. | |
| """ | |
| css_file_name = os.extsep.join(['style', 'css']) | |
| css_absolute_path = os.path.join(output_dir, css_file_name) | |
| assert os.path.exists(css_absolute_path), ( | |
| 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" ' | |
| 'is called first, and the css file is generated at: "%s".' | |
| % css_absolute_path | |
| ) | |
| self._css_absolute_path = css_absolute_path | |
| self._output_dir = output_dir | |
| self._output_path = output_path | |
| self._table_entry_type = table_entry_type | |
| self._table_entries = [] | |
| self._total_entry = {} | |
| source_dir = os.path.dirname(os.path.realpath(__file__)) | |
| template_dir = os.path.join(source_dir, 'html_templates') | |
| jinja_env = jinja2.Environment( | |
| loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True | |
| ) | |
| self._header_template = jinja_env.get_template('header.html') | |
| self._table_template = jinja_env.get_template('table.html') | |
| self._footer_template = jinja_env.get_template('footer.html') | |
| self._style_overrides = open( | |
| os.path.join(source_dir, 'static', 'css', 'style.css') | |
| ).read() | |
| def AddLinkToAnotherReport(self, html_report_path, name, summary): | |
| """Adds a link to another html report in this report. | |
| The link to be added is assumed to be an entry in this directory. | |
| """ | |
| # Use relative paths instead of absolute paths to make the generated reports | |
| # portable. | |
| html_report_relative_path = GetRelativePathToDirectoryOfFile( | |
| html_report_path, self._output_path | |
| ) | |
| table_entry = self._CreateTableEntryFromCoverageSummary( | |
| summary, | |
| html_report_relative_path, | |
| name, | |
| os.path.basename(html_report_path) == DIRECTORY_COVERAGE_HTML_REPORT_NAME, | |
| ) | |
| self._table_entries.append(table_entry) | |
| def CreateTotalsEntry(self, summary): | |
| """Creates an entry corresponds to the 'Totals' row in the html report.""" | |
| self._total_entry = self._CreateTableEntryFromCoverageSummary(summary) | |
| def _CreateTableEntryFromCoverageSummary( | |
| self, summary, href=None, name=None, is_dir=None | |
| ): | |
| """Creates an entry to display in the html report.""" | |
| assert (href is None and name is None and is_dir is None) or ( | |
| href is not None and name is not None and is_dir is not None | |
| ), ( | |
| 'The only scenario when href or name or is_dir can be None is when ' | |
| 'creating an entry for the Totals row, and in that case, all three ' | |
| 'attributes must be None.' | |
| ) | |
| entry = {} | |
| if href is not None: | |
| entry['href'] = href | |
| if name is not None: | |
| entry['name'] = name | |
| if is_dir is not None: | |
| entry['is_dir'] = is_dir | |
| summary_dict = summary.Get() | |
| for feature in summary_dict: | |
| if summary_dict[feature]['total'] == 0: | |
| percentage = 0.0 | |
| else: | |
| percentage = ( | |
| float(summary_dict[feature]['covered']) | |
| / summary_dict[feature]['total'] | |
| * 100 | |
| ) | |
| color_class = self._GetColorClass(percentage) | |
| entry[feature] = { | |
| 'total': summary_dict[feature]['total'], | |
| 'covered': summary_dict[feature]['covered'], | |
| 'percentage': '{:6.2f}'.format(percentage), | |
| 'color_class': color_class, | |
| } | |
| return entry | |
| def _GetColorClass(self, percentage): | |
| """Returns the css color class based on coverage percentage.""" | |
| if percentage >= 0 and percentage < 80: | |
| return 'red' | |
| if percentage >= 80 and percentage < 100: | |
| return 'yellow' | |
| if percentage == 100: | |
| return 'green' | |
| assert False, 'Invalid coverage percentage: "%d".' % percentage | |
| def WriteHtmlCoverageReport(self, no_component_view, no_file_view): | |
| """Writes html coverage report. | |
| In the report, sub-directories are displayed before files and within each | |
| category, entries are sorted alphabetically. | |
| """ | |
| def EntryCmp(left, right): | |
| """Compare function for table entries.""" | |
| if left['is_dir'] != right['is_dir']: | |
| return -1 if left['is_dir'] == True else 1 | |
| return -1 if left['name'] < right['name'] else 1 | |
| self._table_entries = sorted( | |
| self._table_entries, key=functools.cmp_to_key(EntryCmp) | |
| ) | |
| css_path = os.path.join(self._output_dir, os.extsep.join(['style', 'css'])) | |
| directory_view_path = GetDirectoryViewPath(self._output_dir) | |
| directory_view_href = GetRelativePathToDirectoryOfFile( | |
| directory_view_path, self._output_path | |
| ) | |
| component_view_href = None | |
| if not no_component_view: | |
| component_view_path = GetComponentViewPath(self._output_dir) | |
| component_view_href = GetRelativePathToDirectoryOfFile( | |
| component_view_path, self._output_path | |
| ) | |
| # File view is optional in the report. | |
| file_view_href = None | |
| if not no_file_view: | |
| file_view_path = GetFileViewPath(self._output_dir) | |
| file_view_href = GetRelativePathToDirectoryOfFile( | |
| file_view_path, self._output_path | |
| ) | |
| html_header = self._header_template.render( | |
| css_path=GetRelativePathToDirectoryOfFile(css_path, self._output_path), | |
| directory_view_href=directory_view_href, | |
| component_view_href=component_view_href, | |
| file_view_href=file_view_href, | |
| style_overrides=self._style_overrides, | |
| ) | |
| html_table = self._table_template.render( | |
| entries=self._table_entries, | |
| total_entry=self._total_entry, | |
| table_entry_type=self._table_entry_type, | |
| ) | |
| html_footer = self._footer_template.render() | |
| if not os.path.exists(os.path.dirname(self._output_path)): | |
| os.makedirs(os.path.dirname(self._output_path)) | |
| with open(self._output_path, 'w') as html_file: | |
| html_file.write(html_header + html_table + html_footer) | |
| class CoverageReportPostProcessor(object): | |
| """Post processing of code coverage reports produced by llvm-cov.""" | |
| def __init__( | |
| self, | |
| output_dir, | |
| src_root_dir, | |
| summary_data, | |
| no_component_view, | |
| no_file_view, | |
| component_mappings={}, | |
| path_equivalence=None, | |
| ): | |
| """Initializes CoverageReportPostProcessor object.""" | |
| # Caller provided parameters. | |
| self.output_dir = output_dir | |
| self.src_root_dir = os.path.normpath(GetFullPath(src_root_dir)) | |
| if not self.src_root_dir.endswith(os.sep): | |
| self.src_root_dir += os.sep | |
| self.summary_data = json.loads(summary_data) | |
| assert len(self.summary_data['data']) == 1 | |
| self.no_component_view = no_component_view | |
| self.no_file_view = no_file_view | |
| # Mapping from components to directories | |
| self.component_to_directories = None | |
| if component_mappings: | |
| self._ExtractComponentToDirectoriesMapping(component_mappings) | |
| # The root directory that contains all generated coverage html reports. | |
| self.report_root_dir = GetCoverageReportRootDirPath(self.output_dir) | |
| # The root directory that all coverage html files generated by llvm-cov. | |
| self.html_file_root_dir = GetHtmlFileRootDirPath(self.output_dir) | |
| # Path to the HTML file for the component view. | |
| self.component_view_path = GetComponentViewPath(self.output_dir) | |
| # Path to the HTML file for the directory view. | |
| self.directory_view_path = GetDirectoryViewPath(self.output_dir) | |
| # Path to the HTML file for the file view. | |
| self.file_view_path = GetFileViewPath(self.output_dir) | |
| # Path to the main HTML index file. | |
| self.html_index_path = GetHtmlIndexPath(self.output_dir) | |
| self.path_map = [] | |
| if path_equivalence: | |
| def _PreparePath(path): | |
| path = os.path.normpath(path) | |
| if not path.endswith(os.sep): | |
| # A normalized path does not end with '/', unless it is a root dir. | |
| path += os.sep | |
| return path | |
| if isinstance(path_equivalence, str): | |
| path_equivalence = [path_equivalence] | |
| for pe in path_equivalence: | |
| parts = [_PreparePath(p) for p in pe.split(',')] | |
| assert len(parts) == 2, 'Path equivalence argument is incorrect.' | |
| self.path_map.append(parts) | |
| def _ExtractComponentToDirectoriesMapping(self, component_mappings): | |
| """Initializes a mapping from components to directories.""" | |
| directory_to_component = component_mappings['dir-to-component'] | |
| self.component_to_directories = defaultdict(list) | |
| for directory in sorted(directory_to_component): | |
| component = directory_to_component[directory] | |
| # Check if we already added the parent directory of this directory. If | |
| # yes,skip this sub-directory to avoid double-counting. | |
| found_parent_directory = False | |
| for component_directory in self.component_to_directories[component]: | |
| if directory.startswith(component_directory + '/'): | |
| found_parent_directory = True | |
| break | |
| if not found_parent_directory: | |
| self.component_to_directories[component].append(directory) | |
| def _MapToLocal(self, path): | |
| """Maps a path from the coverage data to a local path.""" | |
| for mapping in self.path_map: | |
| if path.startswith(mapping[0]): | |
| return path.replace(mapping[0], mapping[1], 1) | |
| return path | |
| def _MapFromLocal(self, path): | |
| """Maps a local path back to the path from the coverage data.""" | |
| for mapping in self.path_map: | |
| if path.startswith(mapping[1]): | |
| return path.replace(mapping[1], mapping[0], 1) | |
| return path | |
| def CalculatePerDirectoryCoverageSummary(self, per_file_coverage_summary): | |
| """Calculates per directory coverage summary.""" | |
| logging.debug('Calculating per-directory coverage summary.') | |
| per_directory_coverage_summary = defaultdict(lambda: CoverageSummary()) | |
| for file_path in per_file_coverage_summary: | |
| summary = per_file_coverage_summary[file_path] | |
| parent_dir = os.path.dirname(file_path) | |
| while True: | |
| per_directory_coverage_summary[parent_dir].AddSummary(summary) | |
| if os.path.normpath(parent_dir) == os.path.normpath(self.src_root_dir): | |
| break | |
| parent_dir = os.path.dirname(parent_dir) | |
| logging.debug('Finished calculating per-directory coverage summary.') | |
| return per_directory_coverage_summary | |
| def CalculatePerComponentCoverageSummary( | |
| self, per_directory_coverage_summary | |
| ): | |
| """Calculates per component coverage summary.""" | |
| logging.debug('Calculating per-component coverage summary.') | |
| per_component_coverage_summary = defaultdict(lambda: CoverageSummary()) | |
| for component in self.component_to_directories: | |
| for directory in self.component_to_directories[component]: | |
| absolute_directory_path = GetFullPath(directory) | |
| if absolute_directory_path in per_directory_coverage_summary: | |
| per_component_coverage_summary[component].AddSummary( | |
| per_directory_coverage_summary[absolute_directory_path] | |
| ) | |
| logging.debug('Finished calculating per-component coverage summary.') | |
| return per_component_coverage_summary | |
| def GeneratePerComponentCoverageInHtml( | |
| self, per_component_coverage_summary, per_directory_coverage_summary | |
| ): | |
| """Generates per-component coverage reports in html.""" | |
| logging.debug('Writing per-component coverage html reports.') | |
| for component in per_component_coverage_summary: | |
| self.GenerateCoverageInHtmlForComponent( | |
| component, | |
| per_component_coverage_summary, | |
| per_directory_coverage_summary, | |
| ) | |
| logging.debug('Finished writing per-component coverage html reports.') | |
| def GenerateComponentViewHtmlIndexFile(self, per_component_coverage_summary): | |
| """Generates the html index file for component view.""" | |
| component_view_index_file_path = self.component_view_path | |
| logging.debug( | |
| 'Generating component view html index file as: "%s".', | |
| component_view_index_file_path, | |
| ) | |
| html_generator = CoverageReportHtmlGenerator( | |
| self.output_dir, component_view_index_file_path, 'Component' | |
| ) | |
| for component in per_component_coverage_summary: | |
| html_generator.AddLinkToAnotherReport( | |
| self.GetCoverageHtmlReportPathForComponent(component), | |
| component, | |
| per_component_coverage_summary[component], | |
| ) | |
| # Do not create a totals row for the component view as the value is | |
| # incorrect due to failure to account for UNKNOWN component and some paths | |
| # belonging to multiple components. | |
| html_generator.WriteHtmlCoverageReport( | |
| self.no_component_view, self.no_file_view | |
| ) | |
| logging.debug('Finished generating component view html index file.') | |
| def GenerateCoverageInHtmlForComponent( | |
| self, | |
| component_name, | |
| per_component_coverage_summary, | |
| per_directory_coverage_summary, | |
| ): | |
| """Generates coverage html report for a component.""" | |
| component_html_report_path = self.GetCoverageHtmlReportPathForComponent( | |
| component_name | |
| ) | |
| component_html_report_dir = os.path.dirname(component_html_report_path) | |
| if not os.path.exists(component_html_report_dir): | |
| os.makedirs(component_html_report_dir) | |
| html_generator = CoverageReportHtmlGenerator( | |
| self.output_dir, component_html_report_path, 'Path' | |
| ) | |
| for dir_path in self.component_to_directories[component_name]: | |
| dir_absolute_path = GetFullPath(dir_path) | |
| if dir_absolute_path not in per_directory_coverage_summary: | |
| # Any directory without an exercised file shouldn't be included into | |
| # the report. | |
| continue | |
| html_generator.AddLinkToAnotherReport( | |
| self.GetCoverageHtmlReportPathForDirectory(dir_path), | |
| os.path.relpath(dir_path, self.src_root_dir), | |
| per_directory_coverage_summary[dir_absolute_path], | |
| ) | |
| html_generator.CreateTotalsEntry( | |
| per_component_coverage_summary[component_name] | |
| ) | |
| html_generator.WriteHtmlCoverageReport( | |
| self.no_component_view, self.no_file_view | |
| ) | |
| def GetCoverageHtmlReportPathForComponent(self, component_name): | |
| """Given a component, returns the corresponding html report path.""" | |
| component_file_name = component_name.lower().replace('>', '-') | |
| html_report_name = os.extsep.join([component_file_name, 'html']) | |
| return os.path.join(self.report_root_dir, 'components', html_report_name) | |
| def GetCoverageHtmlReportPathForDirectory(self, dir_path): | |
| """Given a directory path, returns the corresponding html report path.""" | |
| assert os.path.isdir(self._MapToLocal(dir_path)), ( | |
| '"%s" is not a directory.' % dir_path | |
| ) | |
| html_report_path = os.path.join( | |
| GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME | |
| ) | |
| return self.CombineAbsolutePaths(self.report_root_dir, html_report_path) | |
| def GetCoverageHtmlReportPathForFile(self, file_path): | |
| """Given a file path, returns the corresponding html report path.""" | |
| assert os.path.isfile(self._MapToLocal(file_path)), ( | |
| '"%s" is not a file.' % file_path | |
| ) | |
| unmapped_file_path = self._MapFromLocal(file_path) | |
| html_report_path = os.extsep.join([GetFullPath(unmapped_file_path), 'html']) | |
| return self.CombineAbsolutePaths(self.html_file_root_dir, html_report_path) | |
| def CombineAbsolutePaths(self, path1, path2): | |
| if GetHostPlatform() == 'win': | |
| # Absolute paths in Windows may start with a drive letter and colon. | |
| # Remove them from the second path before appending to the first. | |
| _, path2 = os.path.splitdrive(path2) | |
| # '+' is used instead of os.path.join because both of them are absolute | |
| # paths and os.path.join ignores the first path. | |
| return path1 + path2 | |
| def GenerateFileViewHtmlIndexFile( | |
| self, per_file_coverage_summary, file_view_index_file_path | |
| ): | |
| """Generates html index file for file view.""" | |
| logging.debug( | |
| 'Generating file view html index file as: "%s".', | |
| file_view_index_file_path, | |
| ) | |
| html_generator = CoverageReportHtmlGenerator( | |
| self.output_dir, file_view_index_file_path, 'Path' | |
| ) | |
| totals_coverage_summary = CoverageSummary() | |
| for file_path in per_file_coverage_summary: | |
| if not os.path.isfile(self._MapToLocal(file_path)): | |
| logging.warning('%s is not a file.', file_path) | |
| continue | |
| totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path]) | |
| html_generator.AddLinkToAnotherReport( | |
| self.GetCoverageHtmlReportPathForFile(file_path), | |
| os.path.relpath(file_path, self.src_root_dir), | |
| per_file_coverage_summary[file_path], | |
| ) | |
| html_generator.CreateTotalsEntry(totals_coverage_summary) | |
| html_generator.WriteHtmlCoverageReport( | |
| self.no_component_view, self.no_file_view | |
| ) | |
| logging.debug('Finished generating file view html index file.') | |
| def GeneratePerFileCoverageSummary(self): | |
| """Generate per file coverage summary using coverage data in JSON format.""" | |
| files_coverage_data = self.summary_data['data'][0]['files'] | |
| per_file_coverage_summary = {} | |
| for file_coverage_data in files_coverage_data: | |
| file_path = self._MapToLocal( | |
| os.path.normpath(file_coverage_data['filename']) | |
| ) | |
| assert file_path.startswith(self.src_root_dir), ( | |
| 'File path "%s" in coverage summary is outside source checkout.' | |
| % file_path | |
| ) | |
| summary = file_coverage_data['summary'] | |
| if summary['lines']['count'] == 0: | |
| continue | |
| per_file_coverage_summary[file_path] = CoverageSummary( | |
| regions_total=summary['regions']['count'], | |
| regions_covered=summary['regions']['covered'], | |
| functions_total=summary['functions']['count'], | |
| functions_covered=summary['functions']['covered'], | |
| lines_total=summary['lines']['count'], | |
| lines_covered=summary['lines']['covered'], | |
| ) | |
| logging.debug('Finished generating per-file code coverage summary.') | |
| return per_file_coverage_summary | |
| def GeneratePerDirectoryCoverageInHtml( | |
| self, per_directory_coverage_summary, per_file_coverage_summary | |
| ): | |
| """Generates per directory coverage breakdown in html.""" | |
| logging.debug('Writing per-directory coverage html reports.') | |
| for dir_path in per_directory_coverage_summary: | |
| self.GenerateCoverageInHtmlForDirectory( | |
| dir_path, per_directory_coverage_summary, per_file_coverage_summary | |
| ) | |
| logging.debug('Finished writing per-directory coverage html reports.') | |
| def GenerateCoverageInHtmlForDirectory( | |
| self, dir_path, per_directory_coverage_summary, per_file_coverage_summary | |
| ): | |
| """Generates coverage html report for a single directory.""" | |
| html_generator = CoverageReportHtmlGenerator( | |
| self.output_dir, | |
| self.GetCoverageHtmlReportPathForDirectory(dir_path), | |
| 'Path', | |
| ) | |
| for entry_name in os.listdir(self._MapToLocal(dir_path)): | |
| entry_path = os.path.normpath(os.path.join(dir_path, entry_name)) | |
| if entry_path in per_file_coverage_summary: | |
| entry_html_report_path = self.GetCoverageHtmlReportPathForFile( | |
| entry_path | |
| ) | |
| entry_coverage_summary = per_file_coverage_summary[entry_path] | |
| elif entry_path in per_directory_coverage_summary: | |
| entry_html_report_path = self.GetCoverageHtmlReportPathForDirectory( | |
| entry_path | |
| ) | |
| entry_coverage_summary = per_directory_coverage_summary[entry_path] | |
| else: | |
| # Any file without executable lines shouldn't be included into the | |
| # report. For example, OWNER and README.md files. | |
| continue | |
| html_generator.AddLinkToAnotherReport( | |
| entry_html_report_path, | |
| os.path.basename(entry_path), | |
| entry_coverage_summary, | |
| ) | |
| html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path]) | |
| html_generator.WriteHtmlCoverageReport( | |
| self.no_component_view, self.no_file_view | |
| ) | |
| def GenerateDirectoryViewHtmlIndexFile(self): | |
| """Generates the html index file for directory view. | |
| Note that the index file is already generated under src_root_dir, so this | |
| file simply redirects to it, and the reason of this extra layer is for | |
| structural consistency with other views. | |
| """ | |
| directory_view_index_file_path = self.directory_view_path | |
| logging.debug( | |
| 'Generating directory view html index file as: "%s".', | |
| directory_view_index_file_path, | |
| ) | |
| src_root_html_report_path = self.GetCoverageHtmlReportPathForDirectory( | |
| self.src_root_dir | |
| ) | |
| WriteRedirectHtmlFile( | |
| directory_view_index_file_path, src_root_html_report_path | |
| ) | |
| logging.debug('Finished generating directory view html index file.') | |
| def OverwriteHtmlReportsIndexFile(self): | |
| """Overwrites the root index file to redirect to the default view.""" | |
| html_index_file_path = self.html_index_path | |
| directory_view_index_file_path = self.directory_view_path | |
| WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path) | |
| def CleanUpOutputDir(self): | |
| """Perform a cleanup of the output dir.""" | |
| # Remove the default index.html file produced by llvm-cov. | |
| index_path = os.path.join(self.output_dir, INDEX_HTML_FILE) | |
| if os.path.exists(index_path): | |
| os.remove(index_path) | |
| def PrepareHtmlReport(self): | |
| per_file_coverage_summary = self.GeneratePerFileCoverageSummary() | |
| if not self.no_file_view: | |
| self.GenerateFileViewHtmlIndexFile( | |
| per_file_coverage_summary, self.file_view_path | |
| ) | |
| per_directory_coverage_summary = self.CalculatePerDirectoryCoverageSummary( | |
| per_file_coverage_summary | |
| ) | |
| self.GeneratePerDirectoryCoverageInHtml( | |
| per_directory_coverage_summary, per_file_coverage_summary | |
| ) | |
| self.GenerateDirectoryViewHtmlIndexFile() | |
| if not self.no_component_view: | |
| per_component_coverage_summary = ( | |
| self.CalculatePerComponentCoverageSummary( | |
| per_directory_coverage_summary | |
| ) | |
| ) | |
| self.GeneratePerComponentCoverageInHtml( | |
| per_component_coverage_summary, per_directory_coverage_summary | |
| ) | |
| self.GenerateComponentViewHtmlIndexFile(per_component_coverage_summary) | |
| # The default index file is generated only for the list of source files, | |
| # needs to overwrite it to display per directory coverage view by default. | |
| self.OverwriteHtmlReportsIndexFile() | |
| self.CleanUpOutputDir() | |
| html_index_file_path = 'file://' + GetFullPath(self.html_index_path) | |
| logging.info( | |
| 'Index file for html report is generated as: "%s".', html_index_file_path | |
| ) | |
| def ConfigureLogging(verbose=False, log_file=None): | |
| """Configures logging settings for later use.""" | |
| log_level = logging.DEBUG if verbose else logging.INFO | |
| log_format = '[%(asctime)s %(levelname)s] %(message)s' | |
| logging.basicConfig(filename=log_file, level=log_level, format=log_format) | |
| def GetComponentViewPath(output_dir): | |
| """Path to the HTML file for the component view.""" | |
| return os.path.join( | |
| GetCoverageReportRootDirPath(output_dir), COMPONENT_VIEW_INDEX_FILE | |
| ) | |
| def GetCoverageReportRootDirPath(output_dir): | |
| """The root directory that contains all generated coverage html reports.""" | |
| return os.path.join(output_dir, GetHostPlatform()) | |
| def GetHtmlFileRootDirPath(output_dir): | |
| """The directory that contains llvm-cov generated coverage html reports.""" | |
| return os.path.join(output_dir, REPORT_DIR) | |
| def GetDirectoryViewPath(output_dir): | |
| """Path to the HTML file for the directory view.""" | |
| return os.path.join( | |
| GetCoverageReportRootDirPath(output_dir), DIRECTORY_VIEW_INDEX_FILE | |
| ) | |
| def GetFileViewPath(output_dir): | |
| """Path to the HTML file for the file view.""" | |
| return os.path.join( | |
| GetCoverageReportRootDirPath(output_dir), FILE_VIEW_INDEX_FILE | |
| ) | |
| def GetHtmlIndexPath(output_dir): | |
| """Path to the main HTML index file.""" | |
| return os.path.join(GetCoverageReportRootDirPath(output_dir), INDEX_HTML_FILE) | |
| def GetFullPath(path): | |
| """Return full absolute path.""" | |
| return os.path.abspath(os.path.expandvars(os.path.expanduser(path))) | |
| def GetHostPlatform(): | |
| """Returns the host platform. | |
| This is separate from the target platform/os that coverage is running for. | |
| """ | |
| if sys.platform == 'win32' or sys.platform == 'cygwin': | |
| return 'win' | |
| if sys.platform.startswith('linux'): | |
| return 'linux' | |
| else: | |
| assert sys.platform == 'darwin' | |
| return 'mac' | |
| def GetRelativePathToDirectoryOfFile(target_path, base_path): | |
| """Returns a target path relative to the directory of base_path. | |
| This method requires base_path to be a file, otherwise, one should call | |
| os.path.relpath directly. | |
| """ | |
| assert os.path.dirname(base_path) != base_path, ( | |
| 'Base path: "%s" is a directory, please call os.path.relpath directly.' | |
| % base_path | |
| ) | |
| base_dir = os.path.dirname(base_path) | |
| return os.path.relpath(target_path, base_dir) | |
| def GetSharedLibraries(binary_paths, build_dir, otool_path): | |
| """Returns list of shared libraries used by specified binaries.""" | |
| logging.info('Finding shared libraries for targets (if any).') | |
| shared_libraries = [] | |
| cmd = [] | |
| shared_library_re = None | |
| if sys.platform.startswith('linux'): | |
| cmd.extend(['ldd']) | |
| shared_library_re = re.compile( | |
| r'.*\.so[.0-9]*\s=>\s(.*' + build_dir + r'.*\.so[.0-9]*)\s.*' | |
| ) | |
| elif sys.platform.startswith('darwin'): | |
| otool = otool_path if otool_path else 'otool' | |
| cmd.extend([otool, '-L']) | |
| shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*') | |
| else: | |
| assert False, 'Cannot detect shared libraries used by the given targets.' | |
| assert shared_library_re is not None | |
| cmd.extend(binary_paths) | |
| output = subprocess.check_output(cmd).decode('utf-8', 'ignore') | |
| for line in output.splitlines(): | |
| m = shared_library_re.match(line) | |
| if not m: | |
| continue | |
| shared_library_path = m.group(1) | |
| if sys.platform.startswith('darwin'): | |
| # otool outputs "@rpath" macro instead of the dirname of the given binary. | |
| shared_library_path = shared_library_path.replace('@rpath', build_dir) | |
| if shared_library_path in shared_libraries: | |
| continue | |
| assert os.path.exists(shared_library_path), ( | |
| 'Shared library "%s" used by ' | |
| 'the given target(s) does not ' | |
| 'exist.' % shared_library_path | |
| ) | |
| with open(shared_library_path, 'rb') as f: | |
| data = f.read() | |
| # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs. | |
| if b'__llvm_cov' in data: | |
| shared_libraries.append(shared_library_path) | |
| logging.debug( | |
| 'Found shared libraries (%d): %s.', len(shared_libraries), shared_libraries | |
| ) | |
| logging.info('Finished finding shared libraries for targets.') | |
| return shared_libraries | |
| def WriteRedirectHtmlFile(from_html_path, to_html_path): | |
| """Writes a html file that redirects to another html file.""" | |
| to_html_relative_path = GetRelativePathToDirectoryOfFile( | |
| to_html_path, from_html_path | |
| ) | |
| content = ( | |
| """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <!-- HTML meta refresh URL redirection --> | |
| <meta http-equiv="refresh" content="0; url=%s"> | |
| </head> | |
| </html>""" | |
| % to_html_relative_path | |
| ) | |
| with open(from_html_path, 'w') as f: | |
| f.write(content) | |
| def _CmdSharedLibraries(args): | |
| """Handles 'shared_libs' command.""" | |
| if not args.object: | |
| logging.error('No binaries are specified.') | |
| return 1 | |
| library_paths = GetSharedLibraries(args.object, args.build_dir, None) | |
| if not library_paths: | |
| return 0 | |
| # Print output in the format that can be passed to llvm-cov tool. | |
| output = ' '.join( | |
| '-object=%s' % os.path.normpath(path) for path in library_paths | |
| ) | |
| print(output) | |
| return 0 | |
| def _CmdPostProcess(args): | |
| """Handles 'post_process' command.""" | |
| with open(args.summary_file) as f: | |
| summary_data = f.read() | |
| processor = CoverageReportPostProcessor( | |
| args.output_dir, | |
| args.src_root_dir, | |
| summary_data, | |
| no_component_view=True, | |
| no_file_view=False, | |
| path_equivalence=args.path_equivalence, | |
| ) | |
| processor.PrepareHtmlReport() | |
| def Main(): | |
| parser = argparse.ArgumentParser( | |
| 'coverage_utils', description='Code coverage utils.' | |
| ) | |
| parser.add_argument( | |
| '-v', | |
| '--verbose', | |
| action='store_true', | |
| help='Prints additional debug output.', | |
| ) | |
| subparsers = parser.add_subparsers(dest='command') | |
| shared_libs_parser = subparsers.add_parser( | |
| 'shared_libs', help='Detect shared libraries.' | |
| ) | |
| shared_libs_parser.add_argument( | |
| '-build-dir', help='Path to the build dir.', required=True | |
| ) | |
| shared_libs_parser.add_argument( | |
| '-object', | |
| action='append', | |
| help='Path to the binary using shared libs.', | |
| required=True, | |
| ) | |
| post_processing_parser = subparsers.add_parser( | |
| 'post_process', help='Post process a report.' | |
| ) | |
| post_processing_parser.add_argument( | |
| '-output-dir', help='Path to the report dir.', required=True | |
| ) | |
| post_processing_parser.add_argument( | |
| '-src-root-dir', help='Path to the src root dir.', required=True | |
| ) | |
| post_processing_parser.add_argument( | |
| '-summary-file', help='Path to the summary file.', required=True | |
| ) | |
| post_processing_parser.add_argument( | |
| '-path-equivalence', | |
| help='Map the paths in the coverage data to local ' | |
| 'source files path (=<from>,<to>)', | |
| ) | |
| args = parser.parse_args() | |
| ConfigureLogging(args.verbose) | |
| if args.command == 'shared_libs': | |
| return _CmdSharedLibraries(args) | |
| elif args.command == 'post_process': | |
| return _CmdPostProcess(args) | |
| else: | |
| parser.print_help(sys.stderr) | |
| if __name__ == '__main__': | |
| sys.exit(Main()) |