|  | #!/usr/bin/env python2.7 | 
|  |  | 
|  | # 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 IN-427. | 
|  |  | 
|  | 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() |