xref: /aosp_15_r20/external/angle/build/android/pylib/utils/code_coverage_utils.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Utilities for code coverage related processings."""
5
6import logging
7import os
8import posixpath
9import shutil
10import subprocess
11
12from devil import base_error
13from pylib import constants
14
15# These are use for code coverage.
16LLVM_PROFDATA_PATH = os.path.join(constants.DIR_SOURCE_ROOT, 'third_party',
17                                  'llvm-build', 'Release+Asserts', 'bin',
18                                  'llvm-profdata')
19# Name of the file extension for profraw data files.
20_PROFRAW_FILE_EXTENSION = 'profraw'
21# Name of the file where profraw data files are merged.
22_MERGE_PROFDATA_FILE_NAME = 'coverage_merged.' + _PROFRAW_FILE_EXTENSION
23
24
25def GetDeviceClangCoverageDir(device):
26  """Gets the directory to generate clang coverage data on device.
27
28  Args:
29    device: The working device.
30
31  Returns:
32    The directory path on the device.
33  """
34  return posixpath.join(device.GetExternalStoragePath(), 'chrome', 'test',
35                        'coverage', 'profraw')
36
37
38def PullAndMaybeMergeClangCoverageFiles(device, device_coverage_dir, output_dir,
39                                        output_subfolder_name):
40  """Pulls and possibly merges clang coverage file to a single file.
41
42  Only merges when llvm-profdata tool exists. If so, Merged file is at
43  `output_dir/coverage_merged.profraw`and raw profraw files before merging
44  are deleted.
45
46  Args:
47    device: The working device.
48    device_coverage_dir: The directory storing coverage data on device.
49    output_dir: The output directory on host to store the
50        coverage_merged.profraw file.
51    output_subfolder_name: The subfolder in |output_dir| to pull
52        |device_coverage_dir| into. It will be deleted after merging if
53        merging happens.
54  """
55  if not device.PathExists(device_coverage_dir, retries=0):
56    logging.warning('Clang coverage data folder does not exist on device: %s',
57                    device_coverage_dir)
58    return
59  # Host side dir to pull device coverage profraw folder into.
60  profraw_parent_dir = os.path.join(output_dir, output_subfolder_name)
61  # Note: The function pulls |device_coverage_dir| folder,
62  # instead of profraw files, into |profraw_parent_dir|. the
63  # function also removes |device_coverage_dir| from device.
64  PullClangCoverageFiles(device, device_coverage_dir, profraw_parent_dir)
65  # Merge data into one merged file if llvm-profdata tool exists.
66  if os.path.isfile(LLVM_PROFDATA_PATH):
67    profraw_folder_name = os.path.basename(
68        os.path.normpath(device_coverage_dir))
69    profraw_dir = os.path.join(profraw_parent_dir, profraw_folder_name)
70    MergeClangCoverageFiles(output_dir, profraw_dir)
71    shutil.rmtree(profraw_parent_dir)
72
73
74def PullClangCoverageFiles(device, device_coverage_dir, output_dir):
75  """Pulls clang coverage files on device to host directory.
76
77  Args:
78    device: The working device.
79    device_coverage_dir: The directory to store coverage data on device.
80    output_dir: The output directory on host.
81  """
82  try:
83    if not os.path.exists(output_dir):
84      os.makedirs(output_dir)
85    device.PullFile(device_coverage_dir, output_dir)
86    if not os.listdir(os.path.join(output_dir, 'profraw')):
87      logging.warning('No clang coverage data was generated for this run')
88  except (OSError, base_error.BaseError) as e:
89    logging.warning('Failed to pull clang coverage data, error: %s', e)
90  finally:
91    device.RemovePath(device_coverage_dir, force=True, recursive=True)
92
93
94def MergeClangCoverageFiles(coverage_dir, profdata_dir):
95  """Merge coverage data files.
96
97  Each instrumentation activity generates a separate profraw data file. This
98  merges all profraw files in profdata_dir into a single file in
99  coverage_dir. This happens after each test, rather than waiting until after
100  all tests are ran to reduce the memory footprint used by all the profraw
101  files.
102
103  Args:
104    coverage_dir: The path to the coverage directory.
105    profdata_dir: The directory where the profraw data file(s) are located.
106
107  Return:
108    None
109  """
110  # profdata_dir may not exist if pulling coverage files failed.
111  if not os.path.exists(profdata_dir):
112    logging.debug('Profraw directory does not exist: %s', profdata_dir)
113    return
114
115  merge_file = os.path.join(coverage_dir, _MERGE_PROFDATA_FILE_NAME)
116  profraw_files = [
117      os.path.join(profdata_dir, f) for f in os.listdir(profdata_dir)
118      if f.endswith(_PROFRAW_FILE_EXTENSION)
119  ]
120
121  try:
122    logging.debug('Merging target profraw files into merged profraw file.')
123    subprocess_cmd = [
124        LLVM_PROFDATA_PATH,
125        'merge',
126        '-o',
127        merge_file,
128        '-sparse=true',
129    ]
130    # Grow the merge file by merging it with itself and the new files.
131    if os.path.exists(merge_file):
132      subprocess_cmd.append(merge_file)
133    subprocess_cmd.extend(profraw_files)
134    output = subprocess.check_output(subprocess_cmd).decode('utf8')
135    logging.debug('Merge output: %s', output)
136
137  except subprocess.CalledProcessError:
138    # Don't raise error as that will kill the test run. When code coverage
139    # generates a report, that will raise the error in the report generation.
140    logging.error(
141        'Failed to merge target profdata files to create '
142        'merged profraw file for files: %s', profraw_files)
143
144  # Free up memory space on bot as all data is in the merge file.
145  for f in profraw_files:
146    os.remove(f)
147