| #!/usr/bin/env python |
| # utils/coverage/coverage-generate-data - Generate, parse test run profdata |
| # |
| # This source file is part of the Swift.org open source project |
| # |
| # Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors |
| # Licensed under Apache License v2.0 with Runtime Library Exception |
| # |
| # See https://swift.org/LICENSE.txt for license information |
| # See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| |
| import argparse |
| import logging |
| import multiprocessing |
| import os |
| import pipes |
| import subprocess |
| import sys |
| import timeit |
| from multiprocessing import Pool |
| |
| NUM_CORES = multiprocessing.cpu_count() |
| |
| logging_format = '%(asctime)s %(levelname)s %(message)s' |
| logging.basicConfig(level=logging.DEBUG, |
| format=logging_format, |
| filename='/tmp/%s.log' % os.path.basename(__file__), |
| filemode='w') |
| console = logging.StreamHandler() |
| console.setLevel(logging.INFO) |
| formatter = logging.Formatter(logging_format) |
| console.setFormatter(formatter) |
| logging.getLogger().addHandler(console) |
| |
| global_build_subdir = '' |
| |
| |
| def quote_shell_cmd(cmd): |
| """Return `cmd` as a properly quoted shell string""" |
| return ' '.join([pipes.quote(a) for a in cmd]) |
| |
| |
| def call(cmd, verbose=True, show_cmd=True): |
| """Call `cmd` and optionally log debug info""" |
| formatted_cmd = quote_shell_cmd(cmd) if isinstance(cmd, list) else cmd |
| if show_cmd: |
| logging.info('$ ' + formatted_cmd) |
| start_time = timeit.default_timer() |
| process = subprocess.Popen( |
| cmd, |
| shell=(not isinstance(cmd, list)), |
| bufsize=1, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT |
| ) |
| for line in iter(process.stdout.readline, b''): |
| if verbose: |
| logging.info('STDOUT: ' + line.rstrip()) |
| end_time = timeit.default_timer() |
| logging.debug('END $ ' + formatted_cmd) |
| logging.debug('Return code: %s', process.returncode) |
| logging.debug('Elapsed time: %s', end_time - start_time) |
| return process.returncode |
| |
| |
| def check_output(cmd, verbose=True, show_cmd=True): |
| """Return output of calling `cmd` and optionally log debug info""" |
| output = [] |
| formatted_cmd = quote_shell_cmd(cmd) if isinstance(cmd, list) else cmd |
| if show_cmd: |
| logging.info('$ ' + formatted_cmd) |
| start_time = timeit.default_timer() |
| process = subprocess.Popen( |
| cmd, |
| shell=(not isinstance(cmd, list)), |
| bufsize=1, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT |
| ) |
| for line in iter(process.stdout.readline, b''): |
| if verbose: |
| logging.info('STDOUT: ' + line.rstrip()) |
| output.append(line) |
| end_time = timeit.default_timer() |
| logging.debug('Return code: %s', process.returncode) |
| logging.debug('Elapsed time: %s', end_time - start_time) |
| return (process.returncode, ''.join(output)) |
| |
| |
| def xcrun_find(cmd): |
| """Return path of `cmd` using xcrun -f""" |
| return check_output(['xcrun', '-f', cmd])[1].strip() |
| |
| |
| llvm_cov = xcrun_find('llvm-cov') |
| llvm_profdata = xcrun_find('llvm-profdata') |
| |
| |
| def dump_coverage_data(merged_file): |
| """Dump coverage data of file at path `merged_file` using llvm-cov""" |
| try: |
| swift = os.path.join(global_build_subdir, |
| 'swift-macosx-x86_64/bin/swift') |
| coverage_log = os.path.join(os.path.dirname(merged_file), |
| 'coverage.log') |
| testname = os.path.basename(os.path.dirname(merged_file)) |
| logging.info('Searching for covered files: %s', testname) |
| (returncode, output) = check_output( |
| [llvm_cov, 'report', '-instr-profile=%s' % merged_file, swift], |
| verbose=False, |
| show_cmd=False |
| ) |
| output = [line.split()[0] |
| for line in output.split() |
| if '0.00' not in line and '/swift' in line] |
| with open(coverage_log, 'w') as f: |
| logging.info('Dumping coverage data: %s', testname) |
| (returncode2, dumped) = check_output( |
| quote_shell_cmd( |
| [llvm_cov, 'show', '-line-coverage-gt=0', |
| '-instr-profile=%s' % merged_file, swift] + |
| output |
| ), |
| verbose=False, |
| show_cmd=False |
| ) |
| f.write(dumped) |
| except Exception as e: |
| logging.debug(str(e)) |
| |
| |
| def find_folders(root_path, suffix): |
| """Return a list of folder paths ending in `suffix` rooted at |
| `root_path`""" |
| found_folders = [] |
| for root, folders, files in os.walk(root_path): |
| for folder in folders: |
| if folder.endswith(suffix): |
| folderpath = os.path.join(root, folder) |
| logging.debug('Found %s', folderpath) |
| found_folders.append(folderpath) |
| logging.info('Found %s "%s" folders', len(found_folders), suffix) |
| return found_folders |
| |
| |
| def find_files(root_path, suffix): |
| """Return a list of file paths ending in `suffix` rooted at |
| `root_path`""" |
| found_files = [] |
| for root, folders, files in os.walk(root_path): |
| for f in files: |
| if f.endswith(suffix): |
| fpath = os.path.join(root, f) |
| logging.debug('Found %s', fpath) |
| found_files.append(fpath) |
| logging.info('Found %s "%s" files', len(found_files), suffix) |
| return found_files |
| |
| |
| def merge_profdir(profdir_path): |
| """Merge swift-*.profraw files contained in `profdir_path` into |
| merged.profraw""" |
| logging.info('Merging %s', profdir_path) |
| if not os.path.exists(os.path.join(profdir_path, 'merged.profraw')): |
| call('set -x; ' |
| 'cd %s; ' |
| '%s merge -output merged.profraw swift-*.profraw && ' |
| 'rm swift-*.profraw' % (profdir_path, llvm_profdata)) |
| |
| |
| def demangle_coverage_data(coverage_log_path): |
| """Demangle coverage dump at `coverage_log_path` using c++filt""" |
| logging.info('Demangling %s', coverage_log_path) |
| cppfilt = '/usr/bin/c++filt' |
| demangled_log_path = coverage_log_path + '.demangled' |
| returncode = 1 |
| with open(coverage_log_path) as cf, open(demangled_log_path, 'w') as df: |
| process = subprocess.Popen( |
| [cppfilt, '-n'], |
| stdin=subprocess.PIPE, |
| stdout=df, |
| stderr=subprocess.PIPE |
| ) |
| for line in cf: |
| process.stdin.write(line) |
| process.stdin.close() |
| returncode = process.wait() |
| return returncode |
| |
| |
| def main(): |
| global global_build_subdir |
| |
| parser = argparse.ArgumentParser( |
| description='Generate, parse test run profdata') |
| parser.add_argument('swift_dir', metavar='swift-dir') |
| parser.add_argument('--log', |
| help='the level of information to log (default: info)', |
| metavar='LEVEL', |
| default='info', |
| choices=['info', 'debug', 'warning', 'error', |
| 'critical']) |
| args = parser.parse_args() |
| |
| console.setLevel(level=args.log.upper()) |
| logging.debug(args) |
| |
| swift_dir = os.path.realpath(os.path.abspath(args.swift_dir)) |
| build_dir = os.path.realpath(os.path.join(os.path.dirname(swift_dir), |
| 'build')) |
| build_subdir = os.path.join(build_dir, 'buildbot_incremental_coverage') |
| |
| global_build_subdir = build_subdir |
| |
| build_script_cmd = [ |
| os.path.join(swift_dir, 'utils/build-script'), |
| '--preset=buildbot_incremental,tools=RDA,stdlib=RDA,coverage', |
| ] |
| |
| call(build_script_cmd) |
| |
| assert global_build_subdir |
| |
| pool = Pool(NUM_CORES) |
| |
| logging.info('Starting merge on %s', build_dir) |
| folders = find_folders(build_dir, '.profdir') |
| pool.map_async(merge_profdir, folders).get(999999) |
| |
| logging.info('Starting coverage data dump...') |
| merged_profraw_files = find_files(build_dir, 'merged.profraw') |
| pool.map_async(dump_coverage_data, merged_profraw_files).get(999999) |
| |
| logging.info('Starting coverage data dump demangling...') |
| coverage_log_files = find_files(build_dir, 'coverage.log') |
| pool.map_async(demangle_coverage_data, coverage_log_files).get(999999) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| try: |
| sys.exit(main()) |
| except Exception as e: |
| logging.debug(str(e)) |
| sys.exit(1) |