Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2018 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | """Adds code coverage flags to the invocations of the Clang C/C++ compiler. |
| 6 | |
| 7 | This script is used to instrument a subset of the source files, and the list of |
| 8 | files to instrument is specified by an input file that is passed to this script |
| 9 | as a command-line argument. |
| 10 | |
| 11 | The path to the coverage instrumentation input file should be relative to the |
| 12 | root build directory, and the file consists of multiple lines where each line |
| 13 | represents a path to a source file, and the specified paths must be relative to |
| 14 | the root build directory. e.g. ../../base/task/post_task.cc for build |
| 15 | directory 'out/Release'. |
| 16 | |
| 17 | One caveat with this compiler wrapper is that it may introduce unexpected |
| 18 | behaviors in incremental builds when the file path to the coverage |
| 19 | instrumentation input file changes between consecutive runs, so callers of this |
| 20 | script are strongly advised to always use the same path such as |
| 21 | "${root_build_dir}/coverage_instrumentation_input.txt". |
| 22 | |
| 23 | It's worth noting on try job builders, if the contents of the instrumentation |
| 24 | file changes so that a file doesn't need to be instrumented any longer, it will |
| 25 | be recompiled automatically because if try job B runs after try job A, the files |
| 26 | that were instrumented in A will be updated (i.e., reverted to the checked in |
| 27 | version) in B, and so they'll be considered out of date by ninja and recompiled. |
| 28 | |
| 29 | Example usage: |
| 30 | clang_code_coverage_wrapper.py \\ |
| 31 | --files-to-instrument=coverage_instrumentation_input.txt |
| 32 | """ |
| 33 | |
| 34 | import argparse |
| 35 | import os |
| 36 | import subprocess |
| 37 | import sys |
| 38 | |
| 39 | # Flags used to enable coverage instrumentation. |
Yuke Liao | 5eff782 | 2019-02-28 03:56:24 | [diff] [blame] | 40 | _COVERAGE_FLAGS = [ |
Sajjad Mirza | 49c00e3 | 2019-03-01 22:46:52 | [diff] [blame^] | 41 | '-fprofile-instr-generate', '-fcoverage-mapping', |
| 42 | # Following experimental flags remove unused header functions from the |
| 43 | # coverage mapping data embedded in the test binaries, and the reduction |
| 44 | # of binary size enables building Chrome's large unit test targets on |
| 45 | # MacOS. Please refer to crbug.com/796290 for more details. |
| 46 | '-mllvm', '-limited-coverage-experimental=true' |
Yuke Liao | 5eff782 | 2019-02-28 03:56:24 | [diff] [blame] | 47 | ] |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 48 | |
| 49 | |
| 50 | def main(): |
| 51 | # TODO(crbug.com/898695): Make this wrapper work on Windows platform. |
| 52 | arg_parser = argparse.ArgumentParser() |
| 53 | arg_parser.usage = __doc__ |
| 54 | arg_parser.add_argument( |
| 55 | '--files-to-instrument', |
| 56 | type=str, |
| 57 | required=True, |
| 58 | help='Path to a file that contains a list of file names to instrument.') |
| 59 | arg_parser.add_argument('args', nargs=argparse.REMAINDER) |
| 60 | parsed_args = arg_parser.parse_args() |
| 61 | |
| 62 | if not os.path.isfile(parsed_args.files_to_instrument): |
| 63 | raise Exception('Path to the coverage instrumentation file: "%s" doesn\'t ' |
| 64 | 'exist.' % parsed_args.files_to_instrument) |
| 65 | |
| 66 | compile_command = parsed_args.args |
| 67 | try: |
| 68 | # The command is assumed to use Clang as the compiler, and the path to the |
| 69 | # source file is behind the -c argument, and the path to the source path is |
| 70 | # relative to the root build directory. For example: |
| 71 | # clang++ -fvisibility=hidden -c ../../base/files/file_path.cc -o \ |
| 72 | # obj/base/base/file_path.o |
| 73 | index_dash_c = compile_command.index('-c') |
| 74 | except ValueError: |
| 75 | print '-c argument is not found in the compile command.' |
| 76 | raise |
| 77 | |
| 78 | if index_dash_c + 1 >= len(compile_command): |
| 79 | raise Exception('Source file to be compiled is missing from the command.') |
| 80 | |
| 81 | compile_source_file = compile_command[index_dash_c + 1] |
| 82 | with open(parsed_args.files_to_instrument) as f: |
| 83 | if compile_source_file + '\n' in f.read(): |
| 84 | compile_command.extend(_COVERAGE_FLAGS) |
| 85 | |
| 86 | return subprocess.call(compile_command) |
| 87 | |
| 88 | |
| 89 | if __name__ == '__main__': |
| 90 | sys.exit(main()) |