blob: ca1bf341da7bd9bfbb0417b82047f34c03cd3c2f [file] [log] [blame]
#!/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)