| #!/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() |