1# Copyright 2017 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from mesonbuild import environment, mesonlib
16
17import argparse, re, sys, os, subprocess, pathlib, stat
18import typing as T
19
20def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build_root: str, log_dir: str, use_llvm_cov: bool) -> int:
21    outfiles = []
22    exitcode = 0
23
24    (gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe) = environment.find_coverage_tools()
25
26    # gcovr >= 4.2 requires a different syntax for out of source builds
27    if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'):
28        gcovr_base_cmd = [gcovr_exe, '-r', source_root, build_root]
29    else:
30        gcovr_base_cmd = [gcovr_exe, '-r', build_root]
31
32    if use_llvm_cov:
33        gcov_exe_args = ['--gcov-executable', llvm_cov_exe + ' gcov']
34    else:
35        gcov_exe_args = []
36
37    if not outputs or 'xml' in outputs:
38        if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
39            subprocess.check_call(gcovr_base_cmd +
40                                  ['-x',
41                                   '-e', re.escape(subproject_root),
42                                   '-o', os.path.join(log_dir, 'coverage.xml')
43                                   ] + gcov_exe_args)
44            outfiles.append(('Xml', pathlib.Path(log_dir, 'coverage.xml')))
45        elif outputs:
46            print('gcovr >= 3.3 needed to generate Xml coverage report')
47            exitcode = 1
48
49    if not outputs or 'sonarqube' in outputs:
50        if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'):
51            subprocess.check_call(gcovr_base_cmd +
52                                  ['--sonarqube',
53                                   '-o', os.path.join(log_dir, 'sonarqube.xml'),
54                                   '-e', re.escape(subproject_root)
55                                   ] + gcov_exe_args)
56            outfiles.append(('Sonarqube', pathlib.Path(log_dir, 'sonarqube.xml')))
57        elif outputs:
58            print('gcovr >= 4.2 needed to generate Xml coverage report')
59            exitcode = 1
60
61    if not outputs or 'text' in outputs:
62        if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
63            subprocess.check_call(gcovr_base_cmd +
64                                  ['-e', re.escape(subproject_root),
65                                   '-o', os.path.join(log_dir, 'coverage.txt')
66                                   ] + gcov_exe_args)
67            outfiles.append(('Text', pathlib.Path(log_dir, 'coverage.txt')))
68        elif outputs:
69            print('gcovr >= 3.3 needed to generate text coverage report')
70            exitcode = 1
71
72    if not outputs or 'html' in outputs:
73        if lcov_exe and genhtml_exe:
74            htmloutdir = os.path.join(log_dir, 'coveragereport')
75            covinfo = os.path.join(log_dir, 'coverage.info')
76            initial_tracefile = covinfo + '.initial'
77            run_tracefile = covinfo + '.run'
78            raw_tracefile = covinfo + '.raw'
79            if use_llvm_cov:
80                # Create a shim to allow using llvm-cov as a gcov tool.
81                if mesonlib.is_windows():
82                    llvm_cov_shim_path = os.path.join(log_dir, 'llvm-cov.bat')
83                    with open(llvm_cov_shim_path, 'w', encoding='utf-8') as llvm_cov_bat:
84                        llvm_cov_bat.write(f'@"{llvm_cov_exe}" gcov %*')
85                else:
86                    llvm_cov_shim_path = os.path.join(log_dir, 'llvm-cov.sh')
87                    with open(llvm_cov_shim_path, 'w', encoding='utf-8') as llvm_cov_sh:
88                        llvm_cov_sh.write(f'#!/usr/bin/env sh\nexec "{llvm_cov_exe}" gcov $@')
89                    os.chmod(llvm_cov_shim_path, os.stat(llvm_cov_shim_path).st_mode | stat.S_IEXEC)
90                gcov_tool_args = ['--gcov-tool', llvm_cov_shim_path]
91            else:
92                gcov_tool_args = []
93            subprocess.check_call([lcov_exe,
94                                   '--directory', build_root,
95                                   '--capture',
96                                   '--initial',
97                                   '--output-file',
98                                   initial_tracefile] +
99                                  gcov_tool_args)
100            subprocess.check_call([lcov_exe,
101                                   '--directory', build_root,
102                                   '--capture',
103                                   '--output-file', run_tracefile,
104                                   '--no-checksum',
105                                   '--rc', 'lcov_branch_coverage=1'] +
106                                  gcov_tool_args)
107            # Join initial and test results.
108            subprocess.check_call([lcov_exe,
109                                   '-a', initial_tracefile,
110                                   '-a', run_tracefile,
111                                   '--rc', 'lcov_branch_coverage=1',
112                                   '-o', raw_tracefile])
113            # Remove all directories outside the source_root from the covinfo
114            subprocess.check_call([lcov_exe,
115                                   '--extract', raw_tracefile,
116                                   os.path.join(source_root, '*'),
117                                   '--rc', 'lcov_branch_coverage=1',
118                                   '--output-file', covinfo])
119            # Remove all directories inside subproject dir
120            subprocess.check_call([lcov_exe,
121                                   '--remove', covinfo,
122                                   os.path.join(subproject_root, '*'),
123                                   '--rc', 'lcov_branch_coverage=1',
124                                   '--output-file', covinfo])
125            subprocess.check_call([genhtml_exe,
126                                   '--prefix', build_root,
127                                   '--prefix', source_root,
128                                   '--output-directory', htmloutdir,
129                                   '--title', 'Code coverage',
130                                   '--legend',
131                                   '--show-details',
132                                   '--branch-coverage',
133                                   covinfo])
134            outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html')))
135        elif gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
136            htmloutdir = os.path.join(log_dir, 'coveragereport')
137            if not os.path.isdir(htmloutdir):
138                os.mkdir(htmloutdir)
139            subprocess.check_call(gcovr_base_cmd +
140                                  ['--html',
141                                   '--html-details',
142                                   '--print-summary',
143                                   '-e', re.escape(subproject_root),
144                                   '-o', os.path.join(htmloutdir, 'index.html'),
145                                   ])
146            outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html')))
147        elif outputs:
148            print('lcov/genhtml or gcovr >= 3.3 needed to generate Html coverage report')
149            exitcode = 1
150
151    if not outputs and not outfiles:
152        print('Need gcovr or lcov/genhtml to generate any coverage reports')
153        exitcode = 1
154
155    if outfiles:
156        print('')
157        for (filetype, path) in outfiles:
158            print(filetype + ' coverage report can be found at', path.as_uri())
159
160    return exitcode
161
162def run(args: T.List[str]) -> int:
163    if not os.path.isfile('build.ninja'):
164        print('Coverage currently only works with the Ninja backend.')
165        return 1
166    parser = argparse.ArgumentParser(description='Generate coverage reports')
167    parser.add_argument('--text', dest='outputs', action='append_const',
168                        const='text', help='generate Text report')
169    parser.add_argument('--xml', dest='outputs', action='append_const',
170                        const='xml', help='generate Xml report')
171    parser.add_argument('--sonarqube', dest='outputs', action='append_const',
172                        const='sonarqube', help='generate Sonarqube Xml report')
173    parser.add_argument('--html', dest='outputs', action='append_const',
174                        const='html', help='generate Html report')
175    parser.add_argument('--use_llvm_cov', action='store_true',
176                        help='use llvm-cov')
177    parser.add_argument('source_root')
178    parser.add_argument('subproject_root')
179    parser.add_argument('build_root')
180    parser.add_argument('log_dir')
181    options = parser.parse_args(args)
182    return coverage(options.outputs, options.source_root,
183                    options.subproject_root, options.build_root,
184                    options.log_dir, options.use_llvm_cov)
185
186if __name__ == '__main__':
187    sys.exit(run(sys.argv[1:]))
188