tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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())