| # Copyright 2023 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Utilities for code coverage related processings.""" |
| |
| import logging |
| import os |
| import posixpath |
| import shutil |
| import subprocess |
| |
| from devil import base_error |
| from pylib import constants |
| |
| # These are use for code coverage. |
| LLVM_PROFDATA_PATH = os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', |
| 'llvm-build', 'Release+Asserts', 'bin', |
| 'llvm-profdata') |
| # Name of the file extension for profraw data files. |
| _PROFRAW_FILE_EXTENSION = 'profraw' |
| # Name of the file where profraw data files are merged. |
| _MERGE_PROFDATA_FILE_NAME = 'coverage_merged.' + _PROFRAW_FILE_EXTENSION |
| |
| |
| def GetDeviceClangCoverageDir(device): |
| """Gets the directory to generate clang coverage data on device. |
| |
| Args: |
| device: The working device. |
| |
| Returns: |
| The directory path on the device. |
| """ |
| return posixpath.join(device.GetExternalStoragePath(), 'chrome', 'test', |
| 'coverage', 'profraw') |
| |
| |
| def PullAndMaybeMergeClangCoverageFiles(device, device_coverage_dir, output_dir, |
| output_subfolder_name): |
| """Pulls and possibly merges clang coverage file to a single file. |
| |
| Only merges when llvm-profdata tool exists. If so, Merged file is at |
| `output_dir/coverage_merged.profraw`and raw profraw files before merging |
| are deleted. |
| |
| Args: |
| device: The working device. |
| device_coverage_dir: The directory storing coverage data on device. |
| output_dir: The output directory on host to store the |
| coverage_merged.profraw file. |
| output_subfolder_name: The subfolder in |output_dir| to pull |
| |device_coverage_dir| into. It will be deleted after merging if |
| merging happens. |
| """ |
| if not device.PathExists(device_coverage_dir, retries=0): |
| logging.warning('Clang coverage data folder does not exist on device: %s', |
| device_coverage_dir) |
| return |
| # Host side dir to pull device coverage profraw folder into. |
| profraw_parent_dir = os.path.join(output_dir, output_subfolder_name) |
| # Note: The function pulls |device_coverage_dir| folder, |
| # instead of profraw files, into |profraw_parent_dir|. the |
| # function also removes |device_coverage_dir| from device. |
| PullClangCoverageFiles(device, device_coverage_dir, profraw_parent_dir) |
| # Merge data into one merged file if llvm-profdata tool exists. |
| if os.path.isfile(LLVM_PROFDATA_PATH): |
| profraw_folder_name = os.path.basename( |
| os.path.normpath(device_coverage_dir)) |
| profraw_dir = os.path.join(profraw_parent_dir, profraw_folder_name) |
| MergeClangCoverageFiles(output_dir, profraw_dir) |
| shutil.rmtree(profraw_parent_dir) |
| |
| |
| def PullClangCoverageFiles(device, device_coverage_dir, output_dir): |
| """Pulls clang coverage files on device to host directory. |
| |
| Args: |
| device: The working device. |
| device_coverage_dir: The directory to store coverage data on device. |
| output_dir: The output directory on host. |
| """ |
| try: |
| if not os.path.exists(output_dir): |
| os.makedirs(output_dir) |
| device.PullFile(device_coverage_dir, output_dir) |
| if not os.listdir(os.path.join(output_dir, 'profraw')): |
| logging.warning('No clang coverage data was generated for this run') |
| except (OSError, base_error.BaseError) as e: |
| logging.warning('Failed to pull clang coverage data, error: %s', e) |
| finally: |
| device.RemovePath(device_coverage_dir, force=True, recursive=True) |
| |
| |
| def MergeClangCoverageFiles(coverage_dir, profdata_dir): |
| """Merge coverage data files. |
| |
| Each instrumentation activity generates a separate profraw data file. This |
| merges all profraw files in profdata_dir into a single file in |
| coverage_dir. This happens after each test, rather than waiting until after |
| all tests are ran to reduce the memory footprint used by all the profraw |
| files. |
| |
| Args: |
| coverage_dir: The path to the coverage directory. |
| profdata_dir: The directory where the profraw data file(s) are located. |
| |
| Return: |
| None |
| """ |
| # profdata_dir may not exist if pulling coverage files failed. |
| if not os.path.exists(profdata_dir): |
| logging.debug('Profraw directory does not exist: %s', profdata_dir) |
| return |
| |
| merge_file = os.path.join(coverage_dir, _MERGE_PROFDATA_FILE_NAME) |
| profraw_files = [ |
| os.path.join(profdata_dir, f) for f in os.listdir(profdata_dir) |
| if f.endswith(_PROFRAW_FILE_EXTENSION) |
| ] |
| |
| try: |
| logging.debug('Merging target profraw files into merged profraw file.') |
| subprocess_cmd = [ |
| LLVM_PROFDATA_PATH, |
| 'merge', |
| '-o', |
| merge_file, |
| '-sparse=true', |
| ] |
| # Grow the merge file by merging it with itself and the new files. |
| if os.path.exists(merge_file): |
| subprocess_cmd.append(merge_file) |
| subprocess_cmd.extend(profraw_files) |
| output = subprocess.check_output(subprocess_cmd).decode('utf8') |
| logging.debug('Merge output: %s', output) |
| |
| except subprocess.CalledProcessError: |
| # Don't raise error as that will kill the test run. When code coverage |
| # generates a report, that will raise the error in the report generation. |
| logging.error( |
| 'Failed to merge target profdata files to create ' |
| 'merged profraw file for files: %s', profraw_files) |
| |
| # Free up memory space on bot as all data is in the merge file. |
| for f in profraw_files: |
| os.remove(f) |