| # Copyright 2023 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Generate a coverage report using llvm-cov.""" |
| |
| import argparse |
| import json |
| import logging |
| import sys |
| import subprocess |
| from pathlib import Path |
| from typing import Any |
| |
| _LOG = logging.getLogger(__name__) |
| |
| |
| def _parser_args() -> dict[str, Any]: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| '--llvm-cov-path', |
| type=Path, |
| required=True, |
| help='Path to the llvm-cov binary to use to generate coverage reports.', |
| ) |
| parser.add_argument( |
| '--format', |
| dest='format_type', |
| type=str, |
| choices=['text', 'html', 'lcov', 'json'], |
| required=True, |
| help='Desired output format of the code coverage report.', |
| ) |
| parser.add_argument( |
| '--test-metadata-path', |
| type=Path, |
| required=True, |
| help='Path to the *.test_metadata.json file that describes all of the ' |
| 'tests being used to generate a coverage report.', |
| ) |
| parser.add_argument( |
| '--profdata-path', |
| type=Path, |
| required=True, |
| help='Path for the output merged profdata file to use with generating a' |
| ' coverage report for the tests described in --test-metadata.', |
| ) |
| parser.add_argument( |
| '--root-dir', |
| type=Path, |
| required=True, |
| help='Path to the project\'s root directory.', |
| ) |
| parser.add_argument( |
| '--build-dir', |
| type=Path, |
| required=True, |
| help='Path to the ninja build directory.', |
| ) |
| parser.add_argument( |
| '--output-dir', |
| type=Path, |
| required=True, |
| help='Path to where the output report should be placed. This must be a ' |
| 'relative path (from the current working directory) to ensure the ' |
| 'depfiles are generated correctly.', |
| ) |
| parser.add_argument( |
| '--depfile-path', |
| type=Path, |
| required=True, |
| help='Path for the output depfile to convey the extra input ' |
| 'requirements from parsing --test-metadata.', |
| ) |
| parser.add_argument( |
| '--filter-path', |
| dest='filter_paths', |
| type=str, |
| action='append', |
| default=[], |
| help='Only these folder paths or files will be included in the output. ' |
| 'To work properly, these must be aboslute paths or relative paths from ' |
| 'the current working directory. No globs or regular expression features' |
| ' are supported.', |
| ) |
| parser.add_argument( |
| '--ignore-filename-pattern', |
| dest='ignore_filename_patterns', |
| type=str, |
| action='append', |
| default=[], |
| help='Any file path that matches one of these regular expression ' |
| 'patterns will be excluded from the output report (possibly even if ' |
| 'that path was included in --filter-paths). The regular expression ' |
| 'engine for these is somewhat primitive and does not support things ' |
| 'like look-ahead or look-behind.', |
| ) |
| return vars(parser.parse_args()) |
| |
| |
| def generate_report( |
| llvm_cov_path: Path, |
| format_type: str, |
| test_metadata_path: Path, |
| profdata_path: Path, |
| root_dir: Path, |
| build_dir: Path, |
| output_dir: Path, |
| depfile_path: Path, |
| filter_paths: list[str], |
| ignore_filename_patterns: list[str], |
| ) -> int: |
| """Generate a coverage report using llvm-cov.""" |
| |
| # Ensure directories that need to be absolute are. |
| root_dir = root_dir.resolve() |
| build_dir = build_dir.resolve() |
| |
| # Open the test_metadata_path, parse it to JSON, and extract out the |
| # test binaries. |
| test_metadata = json.loads(test_metadata_path.read_text()) |
| test_binaries = [ |
| Path(obj['test_directory']) / obj['test_name'] |
| for obj in test_metadata |
| if 'test_type' in obj and obj['test_type'] == 'unit_test' |
| ] |
| |
| # llvm-cov export does not create an output file, so we mimic it by creating |
| # the directory structure and writing to file outself after we run the |
| # command. |
| if format_type in ['lcov', 'json']: |
| export_output_path = ( |
| output_dir / 'report.lcov' |
| if format_type == 'lcov' |
| else output_dir / 'report.json' |
| ) |
| output_dir.mkdir(parents=True, exist_ok=True) |
| |
| # Build the command to the llvm-cov subtool based on provided arguments. |
| command = [str(llvm_cov_path)] |
| if format_type in ['html', 'text']: |
| command += [ |
| 'show', |
| '--format', |
| format_type, |
| '--output-dir', |
| str(output_dir), |
| ] |
| else: # format_type in ['lcov', 'json'] |
| command += [ |
| 'export', |
| '--format', |
| # `text` is JSON format when using `llvm-cov`. |
| format_type if format_type == 'lcov' else 'text', |
| ] |
| # We really need two `--path-equivalence` options to be able to map both the |
| # root directory for coverage files to the absolute path of the project |
| # root_dir and to be able to map "out/" prefix to the provided build_dir. |
| # |
| # llvm-cov does not currently support two `--path-equivalence` options, so |
| # we use `--compilation-dir` and `--path-equivalence` together. This has the |
| # unfortunate consequence of showing file paths as absolute in the JSON, |
| # LCOV, and text reports. |
| # |
| # An unwritten assumption here is that root_dir must be an |
| # absolute path to enable file-path-based filtering. |
| # |
| # This is due to turning all file paths into absolute files here: |
| # https://github.com/llvm-mirror/llvm/blob/2c4ca6832fa6b306ee6a7010bfb80a3f2596f824/tools/llvm-cov/CodeCoverage.cpp#L188. |
| command += [ |
| '--compilation-dir', |
| str(root_dir), |
| ] |
| # Pigweed maps any build directory to out, which causes generated files to |
| # be reported to exist under the out directory, which may not exist if the |
| # build directory is not exactly out. This maps out back to the build |
| # directory so generated files can be found. |
| command += [ |
| '--path-equivalence', |
| f'{str(root_dir)}/out,{str(build_dir)}', |
| ] |
| command += [ |
| '--instr-profile', |
| str(profdata_path), |
| ] |
| command += [ |
| f'--ignore-filename-regex={path}' for path in ignore_filename_patterns |
| ] |
| # The test binary positional argument MUST appear before the filter path |
| # positional arguments. llvm-cov is a horrible interface. |
| command += [str(test_binaries[0])] |
| command += [f'--object={binary}' for binary in test_binaries[1:]] |
| command += [ |
| str(Path(filter_path).resolve()) for filter_path in filter_paths |
| ] |
| |
| _LOG.info('') |
| _LOG.info(' '.join(command)) |
| _LOG.info('') |
| |
| # Generate the coverage report by invoking the command. |
| if format_type in ['html', 'text']: |
| output = subprocess.run(command) |
| if output.returncode != 0: |
| return output.returncode |
| else: # format_type in ['lcov', 'json'] |
| output = subprocess.run(command, capture_output=True) |
| if output.returncode != 0: |
| _LOG.error(output.stderr) |
| return output.returncode |
| export_output_path.write_bytes(output.stdout) |
| |
| # Generate the depfile that describes the dependency on the test binaries |
| # used to create the report output. |
| depfile_target = Path('.') |
| if format_type in ['lcov', 'json']: |
| depfile_target = export_output_path |
| elif format_type == 'text': |
| depfile_target = output_dir / 'index.txt' |
| else: # format_type == 'html' |
| depfile_target = output_dir / 'index.html' |
| depfile_path.write_text( |
| ''.join( |
| [ |
| str(depfile_target), |
| ': \\\n', |
| *[str(binary) + ' \\\n' for binary in test_binaries], |
| ] |
| ) |
| ) |
| |
| return 0 |
| |
| |
| def main() -> int: |
| return generate_report(**_parser_args()) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |