#!/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('--build-dir') parser.add_argument('--build-subdir', default='coverage') 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)) if args.build_dir: build_dir = os.path.realpath(os.path.abspath(args.build_dir)) else: build_dir = os.path.realpath(os.path.join(os.path.dirname(swift_dir), 'build')) build_subdir = os.path.join(build_dir, args.build_subdir) global_build_subdir = build_subdir build_script_cmd = [ os.path.join(swift_dir, 'utils/build-script'), '--release', '--no-assertions', '--swift-analyze-code-coverage', 'not-merged', '--test', '--validation-test', '--skip-test-ios', '--skip-test-tvos', '--skip-test-watchos', '--skip-build-benchmark', '--verbose-build', '--lit-args=-v', '--reconfigure', '--build-ninja', '--build-subdir', build_subdir ] if args.build_dir: build_script_cmd += ['--build-dir', build_dir] 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(9999999) 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(9999999) 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(9999999) return 0 if __name__ == '__main__': try: sys.exit(main()) except Exception as e: logging.debug(str(e)) sys.exit(1)