| #!/usr/bin/env python3 |
| # |
| # Copyright 2013 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Instruments classes and jar files. |
| |
| This script corresponds to the 'jacoco_instr' action in the Java build process. |
| Depending on whether jacoco_instrument is set, the 'jacoco_instr' action will |
| call the instrument command which accepts a jar and instruments it using |
| jacococli.jar. |
| |
| """ |
| |
| from __future__ import print_function |
| |
| import argparse |
| import json |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import zipfile |
| |
| from util import build_utils |
| |
| |
| def _AddArguments(parser): |
| """Adds arguments related to instrumentation to parser. |
| |
| Args: |
| parser: ArgumentParser object. |
| """ |
| parser.add_argument( |
| '--input-path', |
| required=True, |
| help='Path to input file(s). Either the classes ' |
| 'directory, or the path to a jar.') |
| parser.add_argument( |
| '--output-path', |
| required=True, |
| help='Path to output final file(s) to. Either the ' |
| 'final classes directory, or the directory in ' |
| 'which to place the instrumented/copied jar.') |
| parser.add_argument( |
| '--sources-json-file', |
| required=True, |
| help='File to create with the list of source directories ' |
| 'and input path.') |
| parser.add_argument( |
| '--java-sources-file', |
| required=True, |
| help='File containing newline-separated .java paths') |
| parser.add_argument( |
| '--jacococli-jar', required=True, help='Path to jacococli.jar.') |
| parser.add_argument( |
| '--files-to-instrument', |
| help='Path to a file containing which source files are affected.') |
| |
| |
| def _GetSourceDirsFromSourceFiles(source_files): |
| """Returns list of directories for the files in |source_files|. |
| |
| Args: |
| source_files: List of source files. |
| |
| Returns: |
| List of source directories. |
| """ |
| return list(set(os.path.dirname(source_file) for source_file in source_files)) |
| |
| |
| def _CreateSourcesJsonFile(source_dirs, input_path, sources_json_file, |
| src_root): |
| """Adds all normalized source directories and input path to |
| |sources_json_file|. |
| |
| Args: |
| source_dirs: List of source directories. |
| input_path: The input path to non-instrumented class files. |
| sources_json_file: File into which to write the list of source directories |
| and input path. |
| src_root: Root which sources added to the file should be relative to. |
| |
| Returns: |
| An exit code. |
| """ |
| src_root = os.path.abspath(src_root) |
| relative_sources = [] |
| for s in source_dirs: |
| abs_source = os.path.abspath(s) |
| if abs_source[:len(src_root)] != src_root: |
| print('Error: found source directory not under repository root: %s %s' % |
| (abs_source, src_root)) |
| return 1 |
| rel_source = os.path.relpath(abs_source, src_root) |
| |
| relative_sources.append(rel_source) |
| |
| data = {} |
| data['source_dirs'] = relative_sources |
| data['input_path'] = [] |
| if input_path: |
| data['input_path'].append(os.path.abspath(input_path)) |
| with open(sources_json_file, 'w') as f: |
| json.dump(data, f) |
| |
| |
| def _GetAffectedClasses(jar_file, source_files): |
| """Gets affected classes by affected source files to a jar. |
| |
| Args: |
| jar_file: The jar file to get all members. |
| source_files: The list of affected source files. |
| |
| Returns: |
| A tuple of affected classes and unaffected members. |
| """ |
| with zipfile.ZipFile(jar_file) as f: |
| members = f.namelist() |
| |
| affected_classes = [] |
| unaffected_members = [] |
| |
| for member in members: |
| if not member.endswith('.class'): |
| unaffected_members.append(member) |
| continue |
| |
| is_affected = False |
| index = member.find('$') |
| if index == -1: |
| index = member.find('.class') |
| for source_file in source_files: |
| if source_file.endswith(member[:index] + '.java'): |
| affected_classes.append(member) |
| is_affected = True |
| break |
| if not is_affected: |
| unaffected_members.append(member) |
| |
| return affected_classes, unaffected_members |
| |
| |
| def _InstrumentClassFiles(instrument_cmd, |
| input_path, |
| output_path, |
| temp_dir, |
| affected_source_files=None): |
| """Instruments class files from input jar. |
| |
| Args: |
| instrument_cmd: JaCoCo instrument command. |
| input_path: The input path to non-instrumented jar. |
| output_path: The output path to instrumented jar. |
| temp_dir: The temporary directory. |
| affected_source_files: The affected source file paths to input jar. |
| Default is None, which means instrumenting all class files in jar. |
| """ |
| affected_classes = None |
| unaffected_members = None |
| if affected_source_files: |
| affected_classes, unaffected_members = _GetAffectedClasses( |
| input_path, affected_source_files) |
| |
| # Extract affected class files. |
| with zipfile.ZipFile(input_path) as f: |
| f.extractall(temp_dir, affected_classes) |
| |
| instrumented_dir = os.path.join(temp_dir, 'instrumented') |
| |
| # Instrument extracted class files. |
| instrument_cmd.extend([temp_dir, '--dest', instrumented_dir]) |
| build_utils.CheckOutput(instrument_cmd) |
| |
| if affected_source_files and unaffected_members: |
| # Extract unaffected members to instrumented_dir. |
| with zipfile.ZipFile(input_path) as f: |
| f.extractall(instrumented_dir, unaffected_members) |
| |
| # Zip all files to output_path |
| build_utils.ZipDir(output_path, instrumented_dir) |
| |
| |
| def _RunInstrumentCommand(parser): |
| """Instruments class or Jar files using JaCoCo. |
| |
| Args: |
| parser: ArgumentParser object. |
| |
| Returns: |
| An exit code. |
| """ |
| args = parser.parse_args() |
| |
| source_files = [] |
| if args.java_sources_file: |
| source_files.extend(build_utils.ReadSourcesList(args.java_sources_file)) |
| |
| with build_utils.TempDir() as temp_dir: |
| instrument_cmd = build_utils.JavaCmd() + [ |
| '-jar', args.jacococli_jar, 'instrument' |
| ] |
| |
| if not args.files_to_instrument: |
| _InstrumentClassFiles(instrument_cmd, args.input_path, args.output_path, |
| temp_dir) |
| else: |
| affected_files = build_utils.ReadSourcesList(args.files_to_instrument) |
| source_set = set(source_files) |
| affected_source_files = [f for f in affected_files if f in source_set] |
| |
| # Copy input_path to output_path and return if no source file affected. |
| if not affected_source_files: |
| shutil.copyfile(args.input_path, args.output_path) |
| # Create a dummy sources_json_file. |
| _CreateSourcesJsonFile([], None, args.sources_json_file, |
| build_utils.DIR_SOURCE_ROOT) |
| return 0 |
| else: |
| _InstrumentClassFiles(instrument_cmd, args.input_path, args.output_path, |
| temp_dir, affected_source_files) |
| |
| source_dirs = _GetSourceDirsFromSourceFiles(source_files) |
| # TODO(GYP): In GN, we are passed the list of sources, detecting source |
| # directories, then walking them to re-establish the list of sources. |
| # This can obviously be simplified! |
| _CreateSourcesJsonFile(source_dirs, args.input_path, args.sources_json_file, |
| build_utils.DIR_SOURCE_ROOT) |
| |
| return 0 |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| _AddArguments(parser) |
| _RunInstrumentCommand(parser) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |