Repository: mesonbuild/meson

Issue:
Support project-provided lcovrc script for lcov coverage
lcov has various options for excluding certain lines, directories and functions. In particular, I would like to be able to exclude certain function calls from branch coverage since they are assertions which it makes no sense to test. This uses the `lcov_excl_br_line` option ([example in GLib](https://gitlab.gnome.org/GNOME/glib/blob/master/.gitlab-ci/lcovrc)).

Since that kind of configuration is quite highly project dependent, and needs to be repeated in all the `lcov` commands, I think it might make sense to allow the project to provide an `lcovrc` file which would get passed to all the `coverage.py` `lcov` commands using `--config-file path/to/lcovrc`.

Failing that, it would be useful to be able to pass custom arguments to `lcov` so that projects could at least pass `--config-file` (or excl options) themselves.
Add the function exclude_from_coverage()
The function exclude_from_coverage() allow to pass a list of patterns to exclude them from the code coverage computation.

I am not sure that this code is optimal, so feel free to come with remarks and things to fix in order to make it better.

This patch is intended to fix issue #3287.


Discussion:

One of the design goals of Meson is to keep the build files focused on building and keep all other configuration information away from them. It would be better if this would be the case here, too. Something like #9732 was better in this regard.

Even better would be if there were a standalone file like `.clang-format` et al that you could put in a standard location and that would work generally. I don't think there is a standard for that, though. :(
@jpakkane In fact, #9732 is not far, it is just the diff on `mesonbuild/scripts/coverage.py`, I just extended the code to accept a list of patterns.

But, if I understand well the philosophy of meson, the code of `mesonbuild/scripts/coverage.py` should also look for `.lcovrc` or `gcovr.cfg` file at `self.environment.source_dir` and take it in consideration.

Is it correct ? (if so, it would really simplify my patch)
@jpakkane I think I got your point, I will come back with a more global and headless solution. Thanks for your helpful message.

Resolving patch:
diff --git a/mesonbuild/scripts/coverage.py b/mesonbuild/scripts/coverage.py
--- a/mesonbuild/scripts/coverage.py
+++ b/mesonbuild/scripts/coverage.py
@@ -23,9 +23,26 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
 
     (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]
 
@@ -36,9 +53,8 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
 
     if not outputs or 'xml' in outputs:
         if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
-            subprocess.check_call(gcovr_base_cmd +
+            subprocess.check_call(gcovr_base_cmd + gcovr_config +
                                   ['-x',
-                                   '-e', re.escape(subproject_root),
                                    '-o', os.path.join(log_dir, 'coverage.xml')
                                    ] + gcov_exe_args)
             outfiles.append(('Xml', pathlib.Path(log_dir, 'coverage.xml')))
@@ -48,10 +64,9 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
 
     if not outputs or 'sonarqube' in outputs:
         if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'):
-            subprocess.check_call(gcovr_base_cmd +
+            subprocess.check_call(gcovr_base_cmd + gcovr_config +
                                   ['--sonarqube',
                                    '-o', os.path.join(log_dir, 'sonarqube.xml'),
-                                   '-e', re.escape(subproject_root)
                                    ] + gcov_exe_args)
             outfiles.append(('Sonarqube', pathlib.Path(log_dir, 'sonarqube.xml')))
         elif outputs:
@@ -60,10 +75,9 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
 
     if not outputs or 'text' in outputs:
         if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
-            subprocess.check_call(gcovr_base_cmd +
-                                  ['-e', re.escape(subproject_root),
-                                   '-o', os.path.join(log_dir, 'coverage.txt')
-                                   ] + gcov_exe_args)
+            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')
@@ -96,6 +110,7 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
                                    '--initial',
                                    '--output-file',
                                    initial_tracefile] +
+                                  lcov_config +
                                   gcov_tool_args)
             subprocess.check_call([lcov_exe,
                                    '--directory', build_root,
@@ -103,25 +118,26 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
                                    '--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])
+                                   '-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])
+                                   '--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])
+                                   '--output-file', covinfo] + lcov_config)
             subprocess.check_call([genhtml_exe,
                                    '--prefix', build_root,
                                    '--prefix', source_root,
@@ -136,11 +152,10 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build
             htmloutdir = os.path.join(log_dir, 'coveragereport')
             if not os.path.isdir(htmloutdir):
                 os.mkdir(htmloutdir)
-            subprocess.check_call(gcovr_base_cmd +
+            subprocess.check_call(gcovr_base_cmd + gcovr_config +
                                   ['--html',
                                    '--html-details',
                                    '--print-summary',
-                                   '-e', re.escape(subproject_root),
                                    '-o', os.path.join(htmloutdir, 'index.html'),
                                    ])
             outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html')))


Test patch: