1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
|
# 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)
|