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