[Code Coverage] Refactor the script logic into Py2/Py3 compatible module.

- Move all report post processing logic to a separate coverage_utils file.
- Add switch for disabling component view.
- Fix shared libraries regexp to match versioned libraries.
- Move HTML post processing methods into a class (to reduce passing a bunch
  of parameters back and forth).
- Add _Main() to coverage_utils.py that can be used outside of Chromium.

For example, to use that script on OSS-Fuzz we would need to do the following:
1) git clone https://chromium.googlesource.com/chromium/src/tools/code_coverage somewhere
2) echo -en '#!/bin/bash\npython3 somewhere/coverage_utils.py $@\n' \
       > /usr/local/bin/coverage_helper

Bug: 817798
Change-Id: I8de8cec2d118f022eb43634dd85b96e4126f5e63
Reviewed-on: https://chromium-review.googlesource.com/1180364
Commit-Queue: Max Moroz <[email protected]>
Reviewed-by: Oliver Chang <[email protected]>
Cr-Commit-Position: refs/heads/master@{#584726}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index 3cac5f32..688605d 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -91,9 +91,10 @@
     os.path.join(
         os.path.dirname(__file__), os.path.pardir, os.path.pardir,
         'third_party'))
-import jinja2
 from collections import defaultdict
 
+import coverage_utils
+
 # Absolute path to the code coverage tools binary. These paths can be
 # overwritten by user specified coverage tool paths.
 LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
@@ -128,15 +129,6 @@
 # Build arg required for generating code coverage data.
 CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
 
-# 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'])
-
 LOGS_DIR_NAME = 'logs'
 
 # Used to extract a mapping between directories and components.
@@ -161,214 +153,10 @@
 LLVM_PROFILE_FILE_PATH_SUBSTITUTION = '<llvm_profile_file_path>'
 
 
-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_path, table_entry_type):
-    """Initializes _CoverageReportHtmlGenerator object.
-
-    Args:
-      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_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_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, cmp=EntryCmp)
-
-    css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
-
-    directory_view_path = _GetDirectoryViewPath()
-    directory_view_href = _GetRelativePathToDirectoryOfFile(
-        directory_view_path, self._output_path)
-    component_view_path = _GetComponentViewPath()
-    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()
-      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()
-
-    with open(self._output_path, 'w') as html_file:
-      html_file.write(html_header + html_table + html_footer)
-
-
-def _ConfigureLogging(args):
-  """Configures logging settings for later use."""
-  log_level = logging.DEBUG if args.verbose else logging.INFO
-  log_format = '[%(asctime)s %(levelname)s] %(message)s'
-  log_file = args.log_file if args.log_file else None
-  logging.basicConfig(filename=log_file, level=log_level, format=log_format)
-
-
 def _ConfigureLLVMCoverageTools(args):
   """Configures llvm coverage tools."""
   if args.coverage_tools_dir:
-    llvm_bin_dir = _GetFullPath(args.coverage_tools_dir)
+    llvm_bin_dir = coverage_utils.GetFullPath(args.coverage_tools_dir)
     global LLVM_COV_PATH
     global LLVM_PROFDATA_PATH
     LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
@@ -383,71 +171,6 @@
                                     LLVM_COV_PATH, LLVM_PROFDATA_PATH)
 
 
-def _GetSharedLibraries(binary_paths):
-  """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\s=>\s(.*' + BUILD_DIR +
-                                   r'.*\.so)\s.*')
-  elif sys.platform.startswith('darwin'):
-    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)
-
-  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) as f:
-      data = f.read()
-
-    # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
-    if '__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 _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 _GetPathWithLLVMSymbolizerDir():
   """Add llvm-symbolizer directory to path for symbolized stacks."""
   path = os.getenv('PATH')
@@ -500,7 +223,7 @@
       clang_revision_str, clang_sub_revision_str = package_version.split('-')
       return int(clang_revision_str), int(clang_sub_revision_str)
 
-  host_platform = _GetHostPlatform()
+  host_platform = coverage_utils.GetHostPlatform()
   clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
       clang_update.STAMP_FILE)
 
@@ -568,7 +291,7 @@
   subprocess_cmd.extend(
       ['-object=' + binary_path for binary_path in binary_paths[1:]])
   _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
-  if _GetHostPlatform() in ['linux', 'mac']:
+  if coverage_utils.GetHostPlatform() in ['linux', 'mac']:
     subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
   subprocess_cmd.extend(filters)
   if ignore_filename_regex:
@@ -576,333 +299,27 @@
 
   subprocess.check_call(subprocess_cmd)
 
-  # llvm-cov creates "coverage" subdir in the output dir. We would like to use
-  # the platform name instead, as it simplifies the report dir structure when
-  # the same report is generated for different platforms.
-  default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
-  platform_report_subdir_path = _GetCoverageReportRootDirPath()
-  _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
-
   logging.debug('Finished running "llvm-cov show" command.')
 
 
-def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
-  """Generates html index file for file view."""
-  file_view_index_file_path = _GetFileViewPath()
-  logging.debug('Generating file view html index file as: "%s".',
-                file_view_index_file_path)
-  html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
-                                                'Path')
-  totals_coverage_summary = _CoverageSummary()
-
-  for file_path in per_file_coverage_summary:
-    totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
-
-    html_generator.AddLinkToAnotherReport(
-        _GetCoverageHtmlReportPathForFile(file_path),
-        os.path.relpath(file_path, SRC_ROOT_PATH),
-        per_file_coverage_summary[file_path])
-
-  html_generator.CreateTotalsEntry(totals_coverage_summary)
-  html_generator.WriteHtmlCoverageReport(no_file_view=False)
-  logging.debug('Finished generating file view html index file.')
-
-
-def _CalculatePerDirectoryCoverageSummary(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 parent_dir == SRC_ROOT_PATH:
-        break
-      parent_dir = os.path.dirname(parent_dir)
-
-  logging.debug('Finished calculating per-directory coverage summary.')
-  return per_directory_coverage_summary
-
-
-def _GeneratePerDirectoryCoverageInHtml(
-    per_directory_coverage_summary, per_file_coverage_summary, no_file_view):
-  """Generates per directory coverage breakdown in html."""
-  logging.debug('Writing per-directory coverage html reports.')
-  for dir_path in per_directory_coverage_summary:
-    _GenerateCoverageInHtmlForDirectory(dir_path,
-                                        per_directory_coverage_summary,
-                                        per_file_coverage_summary, no_file_view)
-
-  logging.debug('Finished writing per-directory coverage html reports.')
-
-
-def _GenerateCoverageInHtmlForDirectory(
-    dir_path, per_directory_coverage_summary, per_file_coverage_summary,
-    no_file_view):
-  """Generates coverage html report for a single directory."""
-  html_generator = _CoverageReportHtmlGenerator(
-      _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
-
-  for entry_name in os.listdir(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 = _GetCoverageHtmlReportPathForFile(entry_path)
-      entry_coverage_summary = per_file_coverage_summary[entry_path]
-    elif entry_path in per_directory_coverage_summary:
-      entry_html_report_path = _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(no_file_view)
-
-
-def _GenerateDirectoryViewHtmlIndexFile():
-  """Generates the html index file for directory view.
-
-  Note that the index file is already generated under SRC_ROOT_PATH, 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 = _GetDirectoryViewPath()
-  logging.debug('Generating directory view html index file as: "%s".',
-                directory_view_index_file_path)
-  src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
-      SRC_ROOT_PATH)
-  _WriteRedirectHtmlFile(directory_view_index_file_path,
-                         src_root_html_report_path)
-  logging.debug('Finished generating directory view html index file.')
-
-
-def _CalculatePerComponentCoverageSummary(component_to_directories,
-                                          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 component_to_directories:
-    for directory in 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 _ExtractComponentToDirectoriesMapping():
-  """Returns a mapping from components to directories."""
-  component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
-  directory_to_component = component_mappings['dir-to-component']
-
-  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 component_to_directories[component]:
-      if directory.startswith(component_directory + '/'):
-        found_parent_directory = True
-        break
-
-    if not found_parent_directory:
-      component_to_directories[component].append(directory)
-
-  return component_to_directories
-
-
-def _GeneratePerComponentCoverageInHtml(
-    per_component_coverage_summary, component_to_directories,
-    per_directory_coverage_summary, no_file_view):
-  """Generates per-component coverage reports in html."""
-  logging.debug('Writing per-component coverage html reports.')
-  for component in per_component_coverage_summary:
-    _GenerateCoverageInHtmlForComponent(
-        component, per_component_coverage_summary, component_to_directories,
-        per_directory_coverage_summary, no_file_view)
-
-  logging.debug('Finished writing per-component coverage html reports.')
-
-
-def _GenerateCoverageInHtmlForComponent(
-    component_name, per_component_coverage_summary, component_to_directories,
-    per_directory_coverage_summary, no_file_view):
-  """Generates coverage html report for a component."""
-  component_html_report_path = _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(component_html_report_path,
-                                                'Path')
-
-  for dir_path in 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 excercised file shouldn't be included into the
-      # report.
-      continue
-
-    html_generator.AddLinkToAnotherReport(
-        _GetCoverageHtmlReportPathForDirectory(dir_path),
-        os.path.relpath(dir_path, SRC_ROOT_PATH),
-        per_directory_coverage_summary[dir_absolute_path])
-
-  html_generator.CreateTotalsEntry(
-      per_component_coverage_summary[component_name])
-  html_generator.WriteHtmlCoverageReport(no_file_view)
-
-
-def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
-                                        no_file_view):
-  """Generates the html index file for component view."""
-  component_view_index_file_path = _GetComponentViewPath()
-  logging.debug('Generating component view html index file as: "%s".',
-                component_view_index_file_path)
-  html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
-                                                'Component')
-  for component in per_component_coverage_summary:
-    html_generator.AddLinkToAnotherReport(
-        _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(no_file_view)
-  logging.debug('Finished generating component view html index file.')
-
-
-def _MergeTwoDirectories(src_path, dst_path):
-  """Merge src_path directory into dst_path directory."""
-  for filename in os.listdir(src_path):
-    dst_path = os.path.join(dst_path, filename)
-    if os.path.exists(dst_path):
-      shutil.rmtree(dst_path)
-    os.rename(os.path.join(src_path, filename), dst_path)
-  shutil.rmtree(src_path)
-
-
-def _OverwriteHtmlReportsIndexFile():
-  """Overwrites the root index file to redirect to the default view."""
-  html_index_file_path = _GetHtmlIndexPath()
-  directory_view_index_file_path = _GetDirectoryViewPath()
-  _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
-
-
-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 _CleanUpOutputDir():
-  """Perform a cleanup of the output dir."""
-  # Remove the default index.html file produced by llvm-cov.
-  index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
-  if os.path.exists(index_path):
-    os.remove(index_path)
-
-
-def _GetCoverageHtmlReportPathForFile(file_path):
-  """Given a file path, returns the corresponding html report path."""
-  assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
-  html_report_path = os.extsep.join([_GetFullPath(file_path), 'html'])
-
-  # '+' is used instead of os.path.join because both of them are absolute paths
-  # and os.path.join ignores the first path.
-  # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
-  return _GetCoverageReportRootDirPath() + html_report_path
-
-
-def _GetCoverageHtmlReportPathForDirectory(dir_path):
-  """Given a directory path, returns the corresponding html report path."""
-  assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
-  html_report_path = os.path.join(
-      _GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
-
-  # '+' is used instead of os.path.join because both of them are absolute paths
-  # and os.path.join ignores the first path.
-  # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
-  return _GetCoverageReportRootDirPath() + html_report_path
-
-
-def _GetCoverageHtmlReportPathForComponent(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(_GetCoverageReportRootDirPath(), 'components',
-                      html_report_name)
-
-
-def _GetCoverageReportRootDirPath():
-  """The root directory that contains all generated coverage html reports."""
-  return os.path.join(OUTPUT_DIR, _GetHostPlatform())
-
-
-def _GetComponentViewPath():
-  """Path to the HTML file for the component view."""
-  return os.path.join(_GetCoverageReportRootDirPath(),
-                      COMPONENT_VIEW_INDEX_FILE)
-
-
-def _GetDirectoryViewPath():
-  """Path to the HTML file for the directory view."""
-  return os.path.join(_GetCoverageReportRootDirPath(),
-                      DIRECTORY_VIEW_INDEX_FILE)
-
-
-def _GetFileViewPath():
-  """Path to the HTML file for the file view."""
-  return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
-
-
 def _GetLogsDirectoryPath():
   """Path to the logs directory."""
-  return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
-
-
-def _GetHtmlIndexPath():
-  """Path to the main HTML index file."""
-  return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
+  return os.path.join(
+      coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR), LOGS_DIR_NAME)
 
 
 def _GetProfdataFilePath():
   """Path to the resulting .profdata file."""
-  return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
+  return os.path.join(
+      coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
+      PROFDATA_FILE_NAME)
 
 
 def _GetSummaryFilePath():
   """The JSON file that contains coverage summary written by llvm-cov export."""
-  return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
+  return os.path.join(
+      coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
+      SUMMARY_FILE_NAME)
 
 
 def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
@@ -976,9 +393,10 @@
   logging.debug('Executing the test commands.')
 
   # Remove existing profraw data files.
-  for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
+  report_root_dir = coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR)
+  for file_or_dir in os.listdir(report_root_dir):
     if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
-      os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
+      os.remove(os.path.join(report_root_dir, file_or_dir))
 
   # Ensure that logs directory exists.
   if not os.path.exists(_GetLogsDirectoryPath()):
@@ -1009,10 +427,10 @@
       if _IsIOS():
         profraw_file_paths = [_GetProfrawDataFileByParsingOutput(output)]
       else:
-        for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
+        for file_or_dir in os.listdir(report_root_dir):
           if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
             profraw_file_paths.append(
-                os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
+                os.path.join(report_root_dir, file_or_dir))
 
       assert profraw_file_paths, (
           'Running target "%s" failed to generate any profraw data file, '
@@ -1020,8 +438,8 @@
           'does not crash. %s' % (target, FILE_BUG_MESSAGE))
 
       assert isinstance(profraw_file_paths, list), (
-        'Variable \'profraw_file_paths\' is expected to be of type \'list\', '
-        'but it is a %s. %s' % (type(profraw_file_paths), FILE_BUG_MESSAGE))
+          'Variable \'profraw_file_paths\' is expected to be of type \'list\', '
+          'but it is a %s. %s' % (type(profraw_file_paths), FILE_BUG_MESSAGE))
 
       try:
         profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
@@ -1079,8 +497,9 @@
   profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
   expected_profraw_file_name = os.extsep.join(
       [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
-  expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
-                                            expected_profraw_file_name)
+  expected_profraw_file_path = os.path.join(
+      coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
+      expected_profraw_file_name)
   command = command.replace(LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
                             expected_profraw_file_path)
 
@@ -1186,7 +605,7 @@
     subprocess_cmd.extend(profdata_file_paths)
 
     output = subprocess.check_output(subprocess_cmd)
-    logging.debug('Merge output: %s' % output)
+    logging.debug('Merge output: %s', output)
   except subprocess.CalledProcessError as error:
     logging.error(
         'Failed to merge target profdata files to create coverage profdata. %s',
@@ -1224,7 +643,7 @@
     subprocess_cmd.extend(profraw_file_paths)
 
     output = subprocess.check_output(subprocess_cmd)
-    logging.debug('Merge output: %s' % output)
+    logging.debug('Merge output: %s', output)
   except subprocess.CalledProcessError as error:
     logging.error(
         'Failed to merge target profraw files to create target profdata.')
@@ -1262,31 +681,7 @@
   with open(_GetSummaryFilePath(), 'w') as f:
     f.write(export_output)
 
-  json_output = json.loads(export_output)
-  assert len(json_output['data']) == 1
-  files_coverage_data = json_output['data'][0]['files']
-
-  per_file_coverage_summary = {}
-  for file_coverage_data in files_coverage_data:
-    file_path = file_coverage_data['filename']
-    assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
-        '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
+  return export_output
 
 
 def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
@@ -1350,7 +745,7 @@
   the given build directory."""
   for command in commands:
     binary_path = _GetBinaryPath(command)
-    binary_absolute_path = _GetFullPath(binary_path)
+    binary_absolute_path = coverage_utils.GetFullPath(binary_path)
     assert binary_absolute_path.startswith(BUILD_DIR + os.sep), (
         'Target executable "%s" in command: "%s" is outside of '
         'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
@@ -1372,7 +767,7 @@
   if target_os:
     current_platform = target_os
   else:
-    current_platform = _GetHostPlatform()
+    current_platform = coverage_utils.GetHostPlatform()
 
   assert current_platform in [
       'linux', 'mac', 'chromeos', 'ios'
@@ -1429,19 +824,6 @@
   return absolute_paths
 
 
-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 _GetBinaryPathsFromTargets(targets, build_dir):
   """Return binary paths from target names."""
   # FIXME: Derive output binary from target build definitions rather than
@@ -1449,7 +831,7 @@
   binary_paths = []
   for target in targets:
     binary_path = os.path.join(build_dir, target)
-    if _GetHostPlatform() == 'win':
+    if coverage_utils.GetHostPlatform() == 'win':
       binary_path += '.exe'
 
     if os.path.exists(binary_path):
@@ -1462,12 +844,6 @@
   return binary_paths
 
 
-def _GetFullPath(path):
-  """Return full absolute path."""
-  return (os.path.abspath(
-      os.path.realpath(os.path.expandvars(os.path.expanduser(path)))))
-
-
 def _GetCommandForWebTests(arguments):
   """Return command to run for blink web tests."""
   command_list = [
@@ -1475,8 +851,7 @@
       'third_party/blink/tools/run_web_tests.py',
       '--additional-driver-flag=--no-sandbox',
       '--additional-env-var=LLVM_PROFILE_FILE=%s' %
-      LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
-      '--batch-size=1',
+      LLVM_PROFILE_FILE_PATH_SUBSTITUTION, '--batch-size=1',
       '--child-processes=%d' % max(1, int(multiprocessing.cpu_count() / 2)),
       '--disable-breakpad', '--no-show-results', '--skip-failing-tests',
       '--target=%s' % os.path.basename(BUILD_DIR), '--time-out-ms=30000'
@@ -1488,7 +863,7 @@
 
 def _GetBinaryPathForWebTests():
   """Return binary path used to run blink web tests."""
-  host_platform = _GetHostPlatform()
+  host_platform = coverage_utils.GetHostPlatform()
   if host_platform == 'win':
     return os.path.join(BUILD_DIR, 'content_shell.exe')
   elif host_platform == 'linux':
@@ -1506,7 +881,7 @@
     shutil.rmtree(OUTPUT_DIR)
 
   # Creates |OUTPUT_DIR| and its platform sub-directory.
-  os.makedirs(_GetCoverageReportRootDirPath())
+  os.makedirs(coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR))
 
 
 def _ParseCommandArguments():
@@ -1586,6 +961,11 @@
       'cause the browser to freeze, and this argument comes handy.')
 
   arg_parser.add_argument(
+      '--no-component-view',
+      action='store_true',
+      help='Don\'t generate the component view in the coverage report.')
+
+  arg_parser.add_argument(
       '--coverage-tools-dir',
       type=str,
       help='Path of the directory where LLVM coverage tools (llvm-cov, '
@@ -1633,19 +1013,19 @@
 
   # Change directory to source root to aid in relative paths calculations.
   global SRC_ROOT_PATH
-  SRC_ROOT_PATH = _GetFullPath(
+  SRC_ROOT_PATH = coverage_utils.GetFullPath(
       os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
   os.chdir(SRC_ROOT_PATH)
 
   args = _ParseCommandArguments()
-  _ConfigureLogging(args)
+  coverage_utils.ConfigureLogging(verbose=args.verbose, log_file=args.log_file)
   _ConfigureLLVMCoverageTools(args)
 
   global BUILD_DIR
-  BUILD_DIR = _GetFullPath(args.build_dir)
+  BUILD_DIR = coverage_utils.GetFullPath(args.build_dir)
 
   global OUTPUT_DIR
-  OUTPUT_DIR = _GetFullPath(args.output_dir)
+  OUTPUT_DIR = coverage_utils.GetFullPath(args.output_dir)
 
   assert args.web_tests or args.command or args.profdata_file, (
       'Need to either provide commands to run using -c/--command option OR '
@@ -1691,43 +1071,31 @@
     profdata_file_path = args.profdata_file
     binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
 
-  binary_paths.extend(_GetSharedLibraries(binary_paths))
+  binary_paths.extend(
+      coverage_utils.GetSharedLibraries(binary_paths, BUILD_DIR))
 
   logging.info('Generating code coverage report in html (this can take a while '
                'depending on size of target!).')
-  per_file_coverage_summary = _GeneratePerFileCoverageSummary(
+  per_file_summary_data = _GeneratePerFileCoverageSummary(
       binary_paths, profdata_file_path, absolute_filter_paths,
       args.ignore_filename_regex)
   _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
                                            absolute_filter_paths,
                                            args.ignore_filename_regex)
-  if not args.no_file_view:
-    _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
+  component_mappings = None
+  if not args.no_component_view:
+    component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
 
-  per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
-      per_file_coverage_summary)
-  _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
-                                      per_file_coverage_summary,
-                                      args.no_file_view)
-  _GenerateDirectoryViewHtmlIndexFile()
+  # Call prepare here.
+  processor = coverage_utils.CoverageReportPostProcessor(
+      OUTPUT_DIR,
+      SRC_ROOT_PATH,
+      per_file_summary_data,
+      no_component_view=args.no_component_view,
+      no_file_view=args.no_file_view,
+      component_mappings=component_mappings)
 
-  component_to_directories = _ExtractComponentToDirectoriesMapping()
-  per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
-      component_to_directories, per_directory_coverage_summary)
-  _GeneratePerComponentCoverageInHtml(
-      per_component_coverage_summary, component_to_directories,
-      per_directory_coverage_summary, args.no_file_view)
-  _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
-                                      args.no_file_view)
-
-  # 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.
-  _OverwriteHtmlReportsIndexFile()
-  _CleanUpOutputDir()
-
-  html_index_file_path = 'file://' + _GetFullPath(_GetHtmlIndexPath())
-  logging.info('Index file for html report is generated as: "%s".',
-               html_index_file_path)
+  processor.PrepareHtmlReport()
 
 
 if __name__ == '__main__':