Skip to content

Commit 6d8f9d8

Browse files
SCANPY-80 Read ".coveragerc" configuration to exclude files from coverage
1 parent 0a47f10 commit 6d8f9d8

File tree

6 files changed

+143
-0
lines changed

6 files changed

+143
-0
lines changed

src/pysonar_scanner/configuration/cli.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ def __create_parser(cls):
469469
type=str,
470470
help="Comma-delimited list of paths to coverage reports in the Cobertura XML format.",
471471
)
472+
reports_group.add_argument(
473+
"--sonar-coverage-exclusions",
474+
"--sonar.coverage.exclusions",
475+
"-Dsonar.coverage.exclusions",
476+
type=str,
477+
help="Defines the source files to be excluded from the code coverage analysis.",
478+
)
472479
reports_group.add_argument(
473480
"-Dsonar.python.skipUnchanged",
474481
type=bool,

src/pysonar_scanner/configuration/configuration_loader.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from typing import Any
2323

2424
from pysonar_scanner.configuration.cli import CliConfigurationLoader
25+
from pysonar_scanner.configuration.coveragerc_loader import CoverageRCConfigurationLoader
2526
from pysonar_scanner.configuration.pyproject_toml import TomlConfigurationLoader
2627
from pysonar_scanner.configuration.properties import SONAR_PROJECT_KEY, SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, Key
2728
from pysonar_scanner.configuration.properties import PROPERTIES
@@ -50,9 +51,11 @@ def load() -> dict[Key, Any]:
5051
toml_path_property = cli_properties.get("toml-path", ".")
5152
toml_dir = Path(toml_path_property) if "toml-path" in cli_properties else base_dir
5253
toml_properties = TomlConfigurationLoader.load(toml_dir)
54+
coverage_properties = CoverageRCConfigurationLoader.load(base_dir)
5355

5456
resolved_properties = get_static_default_properties()
5557
resolved_properties.update(dynamic_defaults_loader.load())
58+
resolved_properties.update(coverage_properties)
5659
resolved_properties.update(toml_properties.project_properties)
5760
resolved_properties.update(sonar_project_properties.load(base_dir))
5861
resolved_properties.update(toml_properties.sonar_properties)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
import configparser
21+
import logging
22+
import pathlib
23+
from typing import Any
24+
25+
26+
class CoverageRCConfigurationLoader:
27+
28+
@staticmethod
29+
def load(base_dir: pathlib.Path) -> dict[str, str]:
30+
config_file_path = base_dir / ".coveragerc"
31+
result_dict = {}
32+
33+
coverage_properties = CoverageRCConfigurationLoader.__read_config(config_file_path)
34+
exclusion_properties = CoverageRCConfigurationLoader.__read_coverage_exclusions_properties(
35+
config_file_path, coverage_properties
36+
)
37+
result_dict.update(exclusion_properties)
38+
return result_dict
39+
40+
@staticmethod
41+
def __read_config(config_file_path: pathlib.Path) -> dict[str, Any]:
42+
config_dict = {}
43+
if not config_file_path.exists():
44+
logging.debug(f"Configuration file not found: {config_file_path}")
45+
return config_dict
46+
47+
try:
48+
config_parser = configparser.ConfigParser()
49+
config_parser.read(config_file_path)
50+
51+
# Iterate over sections and options
52+
for section in config_parser.sections():
53+
section_values = {}
54+
for key, value in config_parser.items(section):
55+
section_values[key] = value
56+
57+
config_dict[section] = section_values
58+
except configparser.Error as e:
59+
logging.debug(f"Error decoding coverage file {config_file_path}: {e}")
60+
except Exception as e:
61+
logging.debug(f"An unexpected error occurred while processing {config_file_path}: {e}")
62+
return config_dict
63+
64+
@staticmethod
65+
def __read_coverage_exclusions_properties(
66+
config_file_path: pathlib.Path, coverage_properties: dict[str, Any]
67+
) -> dict[str, Any]:
68+
result_dict = {}
69+
if not "run" in coverage_properties:
70+
logging.debug(f"The run key was not found in {config_file_path}")
71+
return result_dict
72+
73+
if not "omit" in coverage_properties["run"]:
74+
logging.debug(f"The run.omit was not found in {config_file_path}")
75+
return result_dict
76+
77+
omit_exclusions = coverage_properties["run"]["omit"]
78+
patterns_list = [patterns.strip() for patterns in omit_exclusions.splitlines() if patterns.strip()]
79+
translated_exclusions = ", ".join(patterns_list)
80+
81+
result_dict["sonar.coverage.exclusions"] = translated_exclusions
82+
return result_dict

src/pysonar_scanner/configuration/properties.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
SONAR_PYTHON_VERSION: Key = "sonar.python.version"
8686
SONAR_PYTHON_PYLINT_REPORT_PATH: Key = "sonar.python.pylint.reportPath"
8787
SONAR_PYTHON_COVERAGE_REPORT_PATHS: Key = "sonar.python.coverage.reportPaths"
88+
SONAR_COVERAGE_EXCLUSIONS: Key = "sonar.coverage.exclusions"
8889
SONAR_PYTHON_SKIP_UNCHANGED: Key = "sonar.python.skipUnchanged"
8990
SONAR_NEWCODE_REFERENCE_BRANCH: Key = "sonar.newCode.referenceBranch"
9091
SONAR_SCM_REVISION: Key = "sonar.scm.revision"
@@ -490,6 +491,11 @@ def env_variable_name(self) -> str:
490491
default_value=None,
491492
cli_getter=lambda args: args.sonar_python_coverage_report_paths
492493
),
494+
Property(
495+
name=SONAR_COVERAGE_EXCLUSIONS,
496+
default_value=None,
497+
cli_getter=lambda args: args.sonar_coverage_exclusions
498+
),
493499
Property(
494500
name=SONAR_PYTHON_SKIP_UNCHANGED,
495501
default_value=None,

tests/unit/test_configuration_cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
SONAR_SCM_FORCE_RELOAD_ALL,
8989
SONAR_PYTHON_PYLINT_REPORT_PATH,
9090
SONAR_PYTHON_COVERAGE_REPORT_PATHS,
91+
SONAR_COVERAGE_EXCLUSIONS,
9192
SONAR_PYTHON_SKIP_UNCHANGED,
9293
SONAR_PYTHON_XUNIT_REPORT_PATH,
9394
SONAR_PYTHON_XUNIT_SKIP_DETAILS,
@@ -156,6 +157,7 @@
156157
SONAR_SCM_FORCE_RELOAD_ALL: True,
157158
SONAR_PYTHON_PYLINT_REPORT_PATH: "path/to/pylint/report",
158159
SONAR_PYTHON_COVERAGE_REPORT_PATHS: "path/to/coverage1,path/to/coverage2",
160+
SONAR_COVERAGE_EXCLUSIONS: "*/.local/*,/usr/*,utils/tirefire.py",
159161
SONAR_PYTHON_SKIP_UNCHANGED: True,
160162
SONAR_PYTHON_XUNIT_REPORT_PATH: "path/to/xunit/report",
161163
SONAR_PYTHON_XUNIT_SKIP_DETAILS: True,
@@ -384,6 +386,8 @@ def test_impossible_os_choice(self):
384386
"path/to/pylint/report",
385387
"--sonar-python-coverage-report-paths",
386388
"path/to/coverage1,path/to/coverage2",
389+
"--sonar-coverage-exclusions",
390+
"*/.local/*,/usr/*,utils/tirefire.py",
387391
"--sonar-python-skip-unchanged",
388392
"--sonar-python-xunit-report-path",
389393
"path/to/xunit/report",
@@ -469,6 +473,7 @@ def test_all_cli_args(self):
469473
"-Dsonar.log.level=INFO",
470474
"-Dsonar.python.pylint.reportPath=path/to/pylint/report",
471475
"-Dsonar.python.coverage.reportPaths=path/to/coverage1,path/to/coverage2",
476+
"-Dsonar.coverage.exclusions=*/.local/*,/usr/*,utils/tirefire.py",
472477
"-Dsonar.python.skipUnchanged=true",
473478
"-Dsonar.python.xunit.reportPath=path/to/xunit/report",
474479
"-Dsonar.python.xunit.skipDetails=true",

tests/unit/test_configuration_loader.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
SONAR_SCANNER_JAVA_OPTS,
5252
SONAR_SCANNER_ARCH,
5353
SONAR_SCANNER_OS,
54+
SONAR_COVERAGE_EXCLUSIONS,
5455
)
5556
from pysonar_scanner.utils import Arch, Os
5657
from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader, SONAR_PROJECT_BASE_DIR
@@ -307,6 +308,45 @@ def test_load_pyproject_toml_from_toml_path(self, mock_get_os, mock_get_arch):
307308
}
308309
self.assertDictEqual(configuration, expected_configuration)
309310

311+
@patch(
312+
"sys.argv",
313+
[
314+
"myscript.py",
315+
],
316+
)
317+
def test_load_coveragerc_properties(self, mock_get_os, mock_get_arch):
318+
self.fs.create_dir("custom/path")
319+
self.fs.create_file(
320+
".coveragerc",
321+
contents=(
322+
"""
323+
[run]
324+
omit =
325+
*/.local/*
326+
/usr/*
327+
utils/tirefire.py
328+
"""
329+
),
330+
)
331+
configuration = ConfigurationLoader.load()
332+
expected_configuration = {
333+
SONAR_SCANNER_APP: "python",
334+
SONAR_SCANNER_APP_VERSION: "1.0",
335+
SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME],
336+
SONAR_VERBOSE: False,
337+
SONAR_SCANNER_SKIP_JRE_PROVISIONING: False,
338+
SONAR_PROJECT_BASE_DIR: os.getcwd(),
339+
SONAR_SCANNER_CONNECT_TIMEOUT: 5,
340+
SONAR_SCANNER_SOCKET_TIMEOUT: 60,
341+
SONAR_SCANNER_RESPONSE_TIMEOUT: 0,
342+
SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit",
343+
SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit",
344+
SONAR_SCANNER_OS: Os.LINUX.value,
345+
SONAR_SCANNER_ARCH: Arch.X64.value,
346+
SONAR_COVERAGE_EXCLUSIONS: "*/.local/*, /usr/*, utils/tirefire.py",
347+
}
348+
self.assertDictEqual(configuration, expected_configuration)
349+
310350
@patch("sys.argv", ["myscript.py"])
311351
@patch.dict("os.environ", {"SONAR_TOKEN": "TokenFromEnv", "SONAR_PROJECT_KEY": "KeyFromEnv"}, clear=True)
312352
def test_load_from_env_variables_only(self, mock_get_os, mock_get_arch):

0 commit comments

Comments
 (0)