diff options
Diffstat (limited to 'mesonbuild/scripts/coverage.py')
-rw-r--r-- | mesonbuild/scripts/coverage.py | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/mesonbuild/scripts/coverage.py b/mesonbuild/scripts/coverage.py new file mode 100644 index 0000000..5e78639 --- /dev/null +++ b/mesonbuild/scripts/coverage.py @@ -0,0 +1,202 @@ +# Copyright 2017 The Meson development team + +# 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 + +# http://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. +from __future__ import annotations + +from mesonbuild import environment, mesonlib + +import argparse, re, sys, os, subprocess, pathlib, stat +import typing as T + +def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build_root: str, log_dir: str, use_llvm_cov: bool) -> int: + outfiles = [] + exitcode = 0 + + (gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe) = environment.find_coverage_tools() + + # load config files for tools if available in the source tree + # - lcov requires manually specifying a per-project config + # - gcovr picks up the per-project config, and also supports filtering files + # so don't exclude subprojects ourselves, if the project has a config, + # because they either don't want that, or should set it themselves + lcovrc = os.path.join(source_root, '.lcovrc') + if os.path.exists(lcovrc): + lcov_config = ['--config-file', lcovrc] + else: + lcov_config = [] + + gcovr_config = ['-e', re.escape(subproject_root)] + + # gcovr >= 4.2 requires a different syntax for out of source builds + if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'): + gcovr_base_cmd = [gcovr_exe, '-r', source_root, build_root] + # it also started supporting the config file + if os.path.exists(os.path.join(source_root, 'gcovr.cfg')): + gcovr_config = [] + else: + gcovr_base_cmd = [gcovr_exe, '-r', build_root] + + if use_llvm_cov: + gcov_exe_args = ['--gcov-executable', llvm_cov_exe + ' gcov'] + else: + gcov_exe_args = [] + + if not outputs or 'xml' in outputs: + if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'): + subprocess.check_call(gcovr_base_cmd + gcovr_config + + ['-x', + '-o', os.path.join(log_dir, 'coverage.xml') + ] + gcov_exe_args) + outfiles.append(('Xml', pathlib.Path(log_dir, 'coverage.xml'))) + elif outputs: + print('gcovr >= 3.3 needed to generate Xml coverage report') + exitcode = 1 + + if not outputs or 'sonarqube' in outputs: + if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'): + subprocess.check_call(gcovr_base_cmd + gcovr_config + + ['--sonarqube', + '-o', os.path.join(log_dir, 'sonarqube.xml'), + ] + gcov_exe_args) + outfiles.append(('Sonarqube', pathlib.Path(log_dir, 'sonarqube.xml'))) + elif outputs: + print('gcovr >= 4.2 needed to generate Xml coverage report') + exitcode = 1 + + if not outputs or 'text' in outputs: + if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'): + subprocess.check_call(gcovr_base_cmd + gcovr_config + + ['-o', os.path.join(log_dir, 'coverage.txt')] + + gcov_exe_args) + outfiles.append(('Text', pathlib.Path(log_dir, 'coverage.txt'))) + elif outputs: + print('gcovr >= 3.3 needed to generate text coverage report') + exitcode = 1 + + if not outputs or 'html' in outputs: + if lcov_exe and genhtml_exe: + htmloutdir = os.path.join(log_dir, 'coveragereport') + covinfo = os.path.join(log_dir, 'coverage.info') + initial_tracefile = covinfo + '.initial' + run_tracefile = covinfo + '.run' + raw_tracefile = covinfo + '.raw' + if use_llvm_cov: + # Create a shim to allow using llvm-cov as a gcov tool. + if mesonlib.is_windows(): + llvm_cov_shim_path = os.path.join(log_dir, 'llvm-cov.bat') + with open(llvm_cov_shim_path, 'w', encoding='utf-8') as llvm_cov_bat: + llvm_cov_bat.write(f'@"{llvm_cov_exe}" gcov %*') + else: + llvm_cov_shim_path = os.path.join(log_dir, 'llvm-cov.sh') + with open(llvm_cov_shim_path, 'w', encoding='utf-8') as llvm_cov_sh: + llvm_cov_sh.write(f'#!/usr/bin/env sh\nexec "{llvm_cov_exe}" gcov $@') + os.chmod(llvm_cov_shim_path, os.stat(llvm_cov_shim_path).st_mode | stat.S_IEXEC) + gcov_tool_args = ['--gcov-tool', llvm_cov_shim_path] + else: + gcov_tool_args = [] + subprocess.check_call([lcov_exe, + '--directory', build_root, + '--capture', + '--initial', + '--output-file', + initial_tracefile] + + lcov_config + + gcov_tool_args) + subprocess.check_call([lcov_exe, + '--directory', build_root, + '--capture', + '--output-file', run_tracefile, + '--no-checksum', + '--rc', 'lcov_branch_coverage=1'] + + lcov_config + + gcov_tool_args) + # Join initial and test results. + subprocess.check_call([lcov_exe, + '-a', initial_tracefile, + '-a', run_tracefile, + '--rc', 'lcov_branch_coverage=1', + '-o', raw_tracefile] + lcov_config) + # Remove all directories outside the source_root from the covinfo + subprocess.check_call([lcov_exe, + '--extract', raw_tracefile, + os.path.join(source_root, '*'), + '--rc', 'lcov_branch_coverage=1', + '--output-file', covinfo] + lcov_config) + # Remove all directories inside subproject dir + subprocess.check_call([lcov_exe, + '--remove', covinfo, + os.path.join(subproject_root, '*'), + '--rc', 'lcov_branch_coverage=1', + '--output-file', covinfo] + lcov_config) + subprocess.check_call([genhtml_exe, + '--prefix', build_root, + '--prefix', source_root, + '--output-directory', htmloutdir, + '--title', 'Code coverage', + '--legend', + '--show-details', + '--branch-coverage', + covinfo]) + outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html'))) + elif gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'): + htmloutdir = os.path.join(log_dir, 'coveragereport') + if not os.path.isdir(htmloutdir): + os.mkdir(htmloutdir) + subprocess.check_call(gcovr_base_cmd + gcovr_config + + ['--html', + '--html-details', + '--print-summary', + '-o', os.path.join(htmloutdir, 'index.html'), + ]) + outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html'))) + elif outputs: + print('lcov/genhtml or gcovr >= 3.3 needed to generate Html coverage report') + exitcode = 1 + + if not outputs and not outfiles: + print('Need gcovr or lcov/genhtml to generate any coverage reports') + exitcode = 1 + + if outfiles: + print('') + for (filetype, path) in outfiles: + print(filetype + ' coverage report can be found at', path.as_uri()) + + return exitcode + +def run(args: T.List[str]) -> int: + if not os.path.isfile('build.ninja'): + print('Coverage currently only works with the Ninja backend.') + return 1 + parser = argparse.ArgumentParser(description='Generate coverage reports') + parser.add_argument('--text', dest='outputs', action='append_const', + const='text', help='generate Text report') + parser.add_argument('--xml', dest='outputs', action='append_const', + const='xml', help='generate Xml report') + parser.add_argument('--sonarqube', dest='outputs', action='append_const', + const='sonarqube', help='generate Sonarqube Xml report') + parser.add_argument('--html', dest='outputs', action='append_const', + const='html', help='generate Html report') + parser.add_argument('--use_llvm_cov', action='store_true', + help='use llvm-cov') + parser.add_argument('source_root') + parser.add_argument('subproject_root') + parser.add_argument('build_root') + parser.add_argument('log_dir') + options = parser.parse_args(args) + return coverage(options.outputs, options.source_root, + options.subproject_root, options.build_root, + options.log_dir, options.use_llvm_cov) + +if __name__ == '__main__': + sys.exit(run(sys.argv[1:])) |