blob: df9ee893cd8f1522f37f5f81975d6178a4a67787 [file] [log] [blame] [edit]
#!/usr/bin/env python2.7
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This program generates a combined coverage report for all host-side dart tests.
# See example_commands and arg help strings in ParseArgs() for usage.
#
# Implementation sketch:
# Search the host_tests directory for tests that use dart-tools/fuchsia_tester.
# Run each test with --coverage and --coverage-path.
# Combine the coverage data from each test into one.
# Generate an HTML report.
#
# This is all pretty hacky. Longer term efforts to make this more automatic and
# less hacky tracked by fxbug.dev/9302.
from __future__ import print_function
import argparse
import collections
import distutils.spawn
import glob
import os
from multiprocessing.pool import ThreadPool
import paths
import re
import subprocess
import sys
import tempfile
TestResult = collections.namedtuple(
'TestResult', ('exit_code', 'coverage_data_path', 'package_dir'))
DEV_NULL = open('/dev/null', 'w')
LCOV = 'lcov'
GENHTML = 'genhtml'
def ParseArgs():
example_commands = """
Examples:
$ report_coverage.py --report-dir /tmp/cov
$ report_coverage.py --test-patterns 'foo_*_test,bar_test' --report-dir ...
$ report_coverage.py --out-dir out/x64 --report-dir ...
"""
p = argparse.ArgumentParser(
description='Generates a coverage report for dart tests',
epilog=example_commands,
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument(
'--report-dir',
type=str,
help='Where to write the report. Will be created if needed',
required=True)
p.add_argument(
'--test-patterns',
type=str,
help=(
'Comma-separated list of glob patterns to match against test file '
'base names'),
default='*')
p.add_argument('--out-dir', type=str, help='fuchsia build out dir')
return p.parse_args()
def OutDir(args):
if args.out_dir:
out_dir = args.out_dir
if not os.path.isabs(out_dir):
out_dir = os.path.join(paths.FUCHSIA_ROOT, out_dir)
if not os.path.isdir(out_dir):
sys.exit(out_dir + ' is not a directory')
return out_dir
if os.environ.get('FUCHSIA_BUILD_DIR'):
return os.environ.get('FUCHSIA_BUILD_DIR')
fuchsia_dir = os.environ.get('FUCHSIA_DIR', paths.FUCHSIA_ROOT)
fuchsia_config_file = os.path.join(fuchsia_dir, '.fx-build-dir')
if os.path.isfile(fuchsia_config_file):
fuchsia_config = open(fuchsia_config_file).read()
return os.path.join(fuchsia_dir, fuchsia_config.strip())
return None
class TestRunner(object):
def __init__(self, out_dir):
self.out_dir = out_dir
def RunTest(self, test_path):
# This whole function super hacky. Assumes implementation details which are
# not meant to be public.
# test_path actually refers to a script that executes other tests.
# The other tests that get executed go into this list.
leaf_test_paths = []
test_lines = open(test_path, 'r').readlines()
# We expect a script that starts with shebang.
if not test_lines or not test_lines[0].startswith('#!'):
return []
for test_line in test_lines[1:]: # Skip the shebang.
test_line_parts = test_line.strip().split()
if not test_line_parts:
continue
if os.path.join(self.out_dir, 'dartlang',
'gen') in test_line_parts[0]:
leaf_test_paths.append(test_line_parts[0])
results = [self._RunLeafTest(p) for p in leaf_test_paths]
return [result for result in results if result] # filter None
def _RunLeafTest(self, test_path):
test_lines = open(test_path, 'r').readlines()
for test_line in test_lines:
test_line_parts = test_line.strip().split()
if not test_line_parts:
continue
if test_line_parts[0].endswith('dart-tools/fuchsia_tester'):
is_dart_test = True
elif test_line_parts[0].startswith('--test-directory='):
test_directory = test_line_parts[0].split('=')[1]
if not is_dart_test:
return None
if not test_directory:
raise ValueError(
'Failed to find --test-directory arg in %s' % test_path)
coverage_data_handle, coverage_data_path = tempfile.mkstemp()
os.close(coverage_data_handle)
exit_code = subprocess.call(
(
test_path, '--coverage',
'--coverage-path=%s' % coverage_data_path),
stdout=DEV_NULL,
stderr=DEV_NULL)
if not os.stat(coverage_data_path).st_size:
print(
'%s produced no coverage data' % os.path.basename(test_path),
file=sys.stderr)
return None
return TestResult(
exit_code, coverage_data_path, os.path.dirname(test_directory))
def MakeRelativePathsAbsolute(test_result):
"""Change source-file paths from relative-to-the-package to absolute."""
with open(test_result.coverage_data_path, 'r+') as coverage_data_file:
fixed_data = coverage_data_file.read().replace(
'SF:', 'SF:%s/' % test_result.package_dir)
coverage_data_file.seek(0)
coverage_data_file.write(fixed_data)
def CombineCoverageData(test_results):
output_handle, output_path = tempfile.mkstemp()
os.close(output_handle)
lcov_cmd = [LCOV, '--output-file', output_path]
for test_result in test_results:
lcov_cmd.extend(['--add-tracefile', test_result.coverage_data_path])
subprocess.check_call(lcov_cmd, stdout=DEV_NULL, stderr=DEV_NULL)
return output_path
def main():
args = ParseArgs()
out_dir = OutDir(args)
if not out_dir:
sys.exit(
'Couldn\'t find the output directory, pass --out-dir '
'(absolute or relative to Fuchsia root) or set FUCHSIA_BUILD_DIR.')
if not (distutils.spawn.find_executable(LCOV) and
distutils.spawn.find_executable(GENHTML)):
sys.exit('\'lcov\' and \'genhtml\' must be installed and in the PATH')
host_tests_dir = os.path.join(out_dir, 'host_tests')
test_patterns = args.test_patterns.split(',')
test_paths = []
for test_pattern in test_patterns:
test_paths.extend(glob.glob(os.path.join(host_tests_dir, test_pattern)))
thread_pool = ThreadPool()
test_runner = TestRunner(out_dir)
results_lists = thread_pool.map(test_runner.RunTest, test_paths)
# flatten
results = [result for sublist in results_lists for result in sublist]
if not results:
sys.exit('Found no dart tests that produced coverage data')
for result in results:
if result.exit_code:
sys.exit('%s failed' % test_path)
thread_pool.map(MakeRelativePathsAbsolute, results)
combined_coverage_path = CombineCoverageData(results)
subprocess.check_call(
(
GENHTML, combined_coverage_path, '--output-directory',
args.report_dir),
stdout=DEV_NULL,
stderr=DEV_NULL)
print(
'Open file://%s to view the report' %
os.path.join(os.path.abspath(args.report_dir), 'index.html'),
file=sys.stderr)
if __name__ == '__main__':
main()