jacoco_instr.py (8641B)
1 #!/usr/bin/env python3 2 # 3 # Copyright 2013 The Chromium Authors 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 """Instruments classes and jar files. 7 8 This script corresponds to the 'jacoco_instr' action in the Java build process. 9 Depending on whether jacoco_instrument is set, the 'jacoco_instr' action will 10 call the instrument command which accepts a jar and instruments it using 11 jacococli.jar. 12 13 """ 14 15 import argparse 16 import json 17 import os 18 import shutil 19 import sys 20 import zipfile 21 22 from util import build_utils 23 import action_helpers 24 import zip_helpers 25 26 27 # This should be same as recipe side token. See bit.ly/3STSPcE. 28 INSTRUMENT_ALL_JACOCO_OVERRIDE_TOKEN = 'INSTRUMENT_ALL_JACOCO' 29 30 31 def _AddArguments(parser): 32 """Adds arguments related to instrumentation to parser. 33 34 Args: 35 parser: ArgumentParser object. 36 """ 37 parser.add_argument( 38 '--root-build-dir', 39 required=True, 40 help='Path to build directory rooted at checkout dir e.g. //out/Release') 41 parser.add_argument('--input-path', 42 required=True, 43 help='Path to input file(s). Either the classes ' 44 'directory, or the path to a jar.') 45 parser.add_argument('--output-path', 46 required=True, 47 help='Path to output final file(s) to. Either the ' 48 'final classes directory, or the directory in ' 49 'which to place the instrumented/copied jar.') 50 parser.add_argument('--sources-json-file', 51 required=True, 52 help='File to create with the list of source directories ' 53 'and input path.') 54 parser.add_argument( 55 '--target-sources-file', 56 required=True, 57 help='File containing newline-separated .java and .kt paths') 58 parser.add_argument( 59 '--jacococli-jar', required=True, help='Path to jacococli.jar.') 60 parser.add_argument( 61 '--files-to-instrument', 62 help='Path to a file containing which source files are affected.') 63 64 65 def _GetSourceDirsFromSourceFiles(source_files): 66 """Returns list of directories for the files in |source_files|. 67 68 Args: 69 source_files: List of source files. 70 71 Returns: 72 List of source directories. 73 """ 74 return list(set(os.path.dirname(source_file) for source_file in source_files)) 75 76 77 def _CreateSourcesJsonFile(source_dirs, input_path, sources_json_file, src_root, 78 root_build_dir): 79 """Adds all normalized source directories and input path to 80 |sources_json_file|. 81 82 Args: 83 source_dirs: List of source directories. 84 input_path: The input path to non-instrumented class files. 85 sources_json_file: File into which to write the list of source directories 86 and input path. 87 src_root: Root which sources added to the file should be relative to. 88 root_build_dir: Build directory path rooted at checkout where 89 sources_json_file is generated e.g. //out/Release 90 91 Returns: 92 An exit code. 93 """ 94 src_root = os.path.abspath(src_root) 95 relative_sources = [] 96 for s in source_dirs: 97 abs_source = os.path.abspath(s) 98 if abs_source[:len(src_root)] != src_root: 99 print('Error: found source directory not under repository root: %s %s' % 100 (abs_source, src_root)) 101 return 1 102 rel_source = os.path.relpath(abs_source, src_root) 103 104 relative_sources.append(rel_source) 105 106 data = {} 107 data['source_dirs'] = relative_sources 108 data['input_path'] = [] 109 build_dir = os.path.join(src_root, root_build_dir[2:]) 110 data['output_dir'] = build_dir 111 if input_path: 112 data['input_path'].append(os.path.abspath(input_path)) 113 with open(sources_json_file, 'w') as f: 114 json.dump(data, f) 115 return 0 116 117 118 def _GetAffectedClasses(jar_file, source_files): 119 """Gets affected classes by affected source files to a jar. 120 121 Args: 122 jar_file: The jar file to get all members. 123 source_files: The list of affected source files. 124 125 Returns: 126 A tuple of affected classes and unaffected members. 127 """ 128 with zipfile.ZipFile(jar_file) as f: 129 members = f.namelist() 130 131 affected_classes = [] 132 unaffected_members = [] 133 134 for member in members: 135 if not member.endswith('.class'): 136 unaffected_members.append(member) 137 continue 138 139 is_affected = False 140 index = member.find('$') 141 if index == -1: 142 index = member.find('.class') 143 for source_file in source_files: 144 if source_file.endswith( 145 (member[:index] + '.java', member[:index] + '.kt')): 146 affected_classes.append(member) 147 is_affected = True 148 break 149 if not is_affected: 150 unaffected_members.append(member) 151 152 return affected_classes, unaffected_members 153 154 155 def _InstrumentClassFiles(instrument_cmd, 156 input_path, 157 output_path, 158 temp_dir, 159 affected_source_files=None): 160 """Instruments class files from input jar. 161 162 Args: 163 instrument_cmd: JaCoCo instrument command. 164 input_path: The input path to non-instrumented jar. 165 output_path: The output path to instrumented jar. 166 temp_dir: The temporary directory. 167 affected_source_files: The affected source file paths to input jar. 168 Default is None, which means instrumenting all class files in jar. 169 """ 170 affected_classes = None 171 unaffected_members = None 172 if affected_source_files: 173 affected_classes, unaffected_members = _GetAffectedClasses( 174 input_path, affected_source_files) 175 176 # Extract affected class files. 177 with zipfile.ZipFile(input_path) as f: 178 f.extractall(temp_dir, affected_classes) 179 180 instrumented_dir = os.path.join(temp_dir, 'instrumented') 181 182 # Instrument extracted class files. 183 instrument_cmd.extend([temp_dir, '--dest', instrumented_dir]) 184 build_utils.CheckOutput(instrument_cmd) 185 186 if affected_source_files and unaffected_members: 187 # Extract unaffected members to instrumented_dir. 188 with zipfile.ZipFile(input_path) as f: 189 f.extractall(instrumented_dir, unaffected_members) 190 191 # Zip all files to output_path 192 with action_helpers.atomic_output(output_path) as f: 193 zip_helpers.zip_directory(f, instrumented_dir) 194 195 196 def _RunInstrumentCommand(parser): 197 """Instruments class or Jar files using JaCoCo. 198 199 Args: 200 parser: ArgumentParser object. 201 202 Returns: 203 An exit code. 204 """ 205 args = parser.parse_args() 206 207 source_files = [] 208 if args.target_sources_file: 209 source_files.extend(build_utils.ReadSourcesList(args.target_sources_file)) 210 211 with build_utils.TempDir() as temp_dir: 212 instrument_cmd = build_utils.JavaCmd() + [ 213 '-jar', args.jacococli_jar, 'instrument' 214 ] 215 216 if not args.files_to_instrument: 217 affected_source_files = None 218 else: 219 affected_files = build_utils.ReadSourcesList(args.files_to_instrument) 220 # Check if coverage recipe decided to instrument everything by overriding 221 # the try builder default setting(selective instrumentation). This can 222 # happen in cases like a DEPS roll of jacoco library 223 224 # Note: This token is preceded by ../../ because the paths to be 225 # instrumented are expected to be relative to the build directory. 226 # See _rebase_paths() at https://bit.ly/40oiixX 227 token = '../../' + INSTRUMENT_ALL_JACOCO_OVERRIDE_TOKEN 228 if token in affected_files: 229 affected_source_files = None 230 else: 231 source_set = set(source_files) 232 affected_source_files = [f for f in affected_files if f in source_set] 233 234 # Copy input_path to output_path and return if no source file affected. 235 if not affected_source_files: 236 shutil.copyfile(args.input_path, args.output_path) 237 # Create a dummy sources_json_file. 238 _CreateSourcesJsonFile([], None, args.sources_json_file, 239 build_utils.DIR_SOURCE_ROOT, 240 args.root_build_dir) 241 return 0 242 _InstrumentClassFiles(instrument_cmd, args.input_path, args.output_path, 243 temp_dir, affected_source_files) 244 245 source_dirs = _GetSourceDirsFromSourceFiles(source_files) 246 # TODO(GYP): In GN, we are passed the list of sources, detecting source 247 # directories, then walking them to re-establish the list of sources. 248 # This can obviously be simplified! 249 _CreateSourcesJsonFile(source_dirs, args.input_path, args.sources_json_file, 250 build_utils.DIR_SOURCE_ROOT, args.root_build_dir) 251 252 return 0 253 254 255 def main(): 256 parser = argparse.ArgumentParser() 257 _AddArguments(parser) 258 _RunInstrumentCommand(parser) 259 260 261 if __name__ == '__main__': 262 sys.exit(main())