From 4a8fec7025307432dc362bc9211544e336a4f44d Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Tue, 17 Jun 2025 09:50:56 +0200 Subject: [PATCH 1/6] SCANPY-80 Read ".coveragerc" configuration to exclude files from coverage --- CLI_ARGS.md | 1 + src/pysonar_scanner/configuration/cli.py | 7 ++ .../configuration/configuration_loader.py | 3 + .../configuration/coveragerc_loader.py | 82 +++++++++++++++++ .../configuration/properties.py | 6 ++ tests/unit/test_configuration_cli.py | 5 ++ tests/unit/test_configuration_loader.py | 40 +++++++++ tests/unit/test_coveragerc_loader.py | 88 +++++++++++++++++++ 8 files changed, 232 insertions(+) create mode 100644 src/pysonar_scanner/configuration/coveragerc_loader.py create mode 100644 tests/unit/test_coveragerc_loader.py diff --git a/CLI_ARGS.md b/CLI_ARGS.md index e06ad710..09dd65e7 100644 --- a/CLI_ARGS.md +++ b/CLI_ARGS.md @@ -51,6 +51,7 @@ | `--skip-jre-provisioning`, `-Dsonar.scanner.skipJreProvisioning` | If provided, the provisioning of the JRE will be skipped | | `--sonar-branch-name`, `-Dsonar.branch.name` | Name of the branch being analyzed | | `--sonar-build-string`, `-Dsonar.buildString` | The string passed with this property will be stored with the analysis and available in the results of api/project_analyses/search, thus allowing you to later identify a specific analysis and obtain its key for use with api/new_code_periods/set on the SPECIFIC_ANALYSIS type | +| `--sonar-coverage-exclusions`, `--sonar.coverage.exclusions`, `-Dsonar.coverage.exclusions` | Defines the source files to be excluded from the code coverage analysis. | | `--sonar-cpd-python-minimum-lines`, `-Dsonar.cpd.python.minimumLines` | Minimum number of tokens to be considered as a duplicated block of code | | `--sonar-cpd-python-minimum-tokens`, `-Dsonar.cpd.python.minimumTokens` | Minimum number of tokens to be considered as a duplicated block of code | | `--sonar-links-ci`, `-Dsonar.links.ci` | The URL of the continuous integration system used | diff --git a/src/pysonar_scanner/configuration/cli.py b/src/pysonar_scanner/configuration/cli.py index b5c14506..75f4bbe9 100644 --- a/src/pysonar_scanner/configuration/cli.py +++ b/src/pysonar_scanner/configuration/cli.py @@ -469,6 +469,13 @@ def __create_parser(cls): type=str, help="Comma-delimited list of paths to coverage reports in the Cobertura XML format.", ) + reports_group.add_argument( + "--sonar-coverage-exclusions", + "--sonar.coverage.exclusions", + "-Dsonar.coverage.exclusions", + type=str, + help="Defines the source files to be excluded from the code coverage analysis.", + ) reports_group.add_argument( "-Dsonar.python.skipUnchanged", type=bool, diff --git a/src/pysonar_scanner/configuration/configuration_loader.py b/src/pysonar_scanner/configuration/configuration_loader.py index 6b7b4768..d2b6f5dc 100644 --- a/src/pysonar_scanner/configuration/configuration_loader.py +++ b/src/pysonar_scanner/configuration/configuration_loader.py @@ -22,6 +22,7 @@ from typing import Any from pysonar_scanner.configuration.cli import CliConfigurationLoader +from pysonar_scanner.configuration.coveragerc_loader import CoverageRCConfigurationLoader from pysonar_scanner.configuration.pyproject_toml import TomlConfigurationLoader from pysonar_scanner.configuration.properties import SONAR_PROJECT_KEY, SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, Key from pysonar_scanner.configuration.properties import PROPERTIES @@ -50,9 +51,11 @@ def load() -> dict[Key, Any]: toml_path_property = cli_properties.get("toml-path", ".") toml_dir = Path(toml_path_property) if "toml-path" in cli_properties else base_dir toml_properties = TomlConfigurationLoader.load(toml_dir) + coverage_properties = CoverageRCConfigurationLoader.load(base_dir) resolved_properties = get_static_default_properties() resolved_properties.update(dynamic_defaults_loader.load()) + resolved_properties.update(coverage_properties) resolved_properties.update(toml_properties.project_properties) resolved_properties.update(sonar_project_properties.load(base_dir)) resolved_properties.update(toml_properties.sonar_properties) diff --git a/src/pysonar_scanner/configuration/coveragerc_loader.py b/src/pysonar_scanner/configuration/coveragerc_loader.py new file mode 100644 index 00000000..0356ea76 --- /dev/null +++ b/src/pysonar_scanner/configuration/coveragerc_loader.py @@ -0,0 +1,82 @@ +# +# Sonar Scanner Python +# Copyright (C) 2011-2024 SonarSource SA. +# mailto:info AT sonarsource DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +import configparser +import logging +import pathlib +from typing import Any + + +class CoverageRCConfigurationLoader: + + @staticmethod + def load(base_dir: pathlib.Path) -> dict[str, str]: + config_file_path = base_dir / ".coveragerc" + result_dict: dict[str, str] = {} + + coverage_properties = CoverageRCConfigurationLoader.__read_config(config_file_path) + if len(coverage_properties) == 0: + return result_dict + exclusion_properties = CoverageRCConfigurationLoader.__read_coverage_exclusions_properties( + config_file_path, coverage_properties + ) + result_dict.update(exclusion_properties) + return result_dict + + @staticmethod + def __read_config(config_file_path: pathlib.Path) -> dict[str, Any]: + config_dict: dict[str, Any] = {} + if not config_file_path.exists(): + logging.debug(f"Configuration file not found: {config_file_path}") + return config_dict + + try: + config_parser = configparser.ConfigParser() + config_parser.read(config_file_path) + + # Iterate over sections and options + for section in config_parser.sections(): + section_values = {} + for key, value in config_parser.items(section): + section_values[key] = value + + config_dict[section] = section_values + except Exception as e: + logging.debug(f"Error decoding coverage file {config_file_path}: {e}") + return config_dict + + @staticmethod + def __read_coverage_exclusions_properties( + config_file_path: pathlib.Path, coverage_properties: dict[str, Any] + ) -> dict[str, Any]: + result_dict: dict[str, Any] = {} + if "run" not in coverage_properties: + logging.debug(f"The run key was not found in {config_file_path}") + return result_dict + + if "omit" not in coverage_properties["run"]: + logging.debug(f"The run.omit key was not found in {config_file_path}") + return result_dict + + omit_exclusions = coverage_properties["run"]["omit"] + patterns_list = [patterns.strip() for patterns in omit_exclusions.splitlines() if patterns.strip()] + translated_exclusions = ", ".join(patterns_list) + + result_dict["sonar.coverage.exclusions"] = translated_exclusions + return result_dict diff --git a/src/pysonar_scanner/configuration/properties.py b/src/pysonar_scanner/configuration/properties.py index fd26884b..c185e1f2 100644 --- a/src/pysonar_scanner/configuration/properties.py +++ b/src/pysonar_scanner/configuration/properties.py @@ -85,6 +85,7 @@ SONAR_PYTHON_VERSION: Key = "sonar.python.version" SONAR_PYTHON_PYLINT_REPORT_PATH: Key = "sonar.python.pylint.reportPath" SONAR_PYTHON_COVERAGE_REPORT_PATHS: Key = "sonar.python.coverage.reportPaths" +SONAR_COVERAGE_EXCLUSIONS: Key = "sonar.coverage.exclusions" SONAR_PYTHON_SKIP_UNCHANGED: Key = "sonar.python.skipUnchanged" SONAR_NEWCODE_REFERENCE_BRANCH: Key = "sonar.newCode.referenceBranch" SONAR_SCM_REVISION: Key = "sonar.scm.revision" @@ -490,6 +491,11 @@ def env_variable_name(self) -> str: default_value=None, cli_getter=lambda args: args.sonar_python_coverage_report_paths ), + Property( + name=SONAR_COVERAGE_EXCLUSIONS, + default_value=None, + cli_getter=lambda args: args.sonar_coverage_exclusions + ), Property( name=SONAR_PYTHON_SKIP_UNCHANGED, default_value=None, diff --git a/tests/unit/test_configuration_cli.py b/tests/unit/test_configuration_cli.py index 1efd0ae4..b9e3ea20 100644 --- a/tests/unit/test_configuration_cli.py +++ b/tests/unit/test_configuration_cli.py @@ -88,6 +88,7 @@ SONAR_SCM_FORCE_RELOAD_ALL, SONAR_PYTHON_PYLINT_REPORT_PATH, SONAR_PYTHON_COVERAGE_REPORT_PATHS, + SONAR_COVERAGE_EXCLUSIONS, SONAR_PYTHON_SKIP_UNCHANGED, SONAR_PYTHON_XUNIT_REPORT_PATH, SONAR_PYTHON_XUNIT_SKIP_DETAILS, @@ -156,6 +157,7 @@ SONAR_SCM_FORCE_RELOAD_ALL: True, SONAR_PYTHON_PYLINT_REPORT_PATH: "path/to/pylint/report", SONAR_PYTHON_COVERAGE_REPORT_PATHS: "path/to/coverage1,path/to/coverage2", + SONAR_COVERAGE_EXCLUSIONS: "*/.local/*,/usr/*,utils/tirefire.py", SONAR_PYTHON_SKIP_UNCHANGED: True, SONAR_PYTHON_XUNIT_REPORT_PATH: "path/to/xunit/report", SONAR_PYTHON_XUNIT_SKIP_DETAILS: True, @@ -384,6 +386,8 @@ def test_impossible_os_choice(self): "path/to/pylint/report", "--sonar-python-coverage-report-paths", "path/to/coverage1,path/to/coverage2", + "--sonar-coverage-exclusions", + "*/.local/*,/usr/*,utils/tirefire.py", "--sonar-python-skip-unchanged", "--sonar-python-xunit-report-path", "path/to/xunit/report", @@ -469,6 +473,7 @@ def test_all_cli_args(self): "-Dsonar.log.level=INFO", "-Dsonar.python.pylint.reportPath=path/to/pylint/report", "-Dsonar.python.coverage.reportPaths=path/to/coverage1,path/to/coverage2", + "-Dsonar.coverage.exclusions=*/.local/*,/usr/*,utils/tirefire.py", "-Dsonar.python.skipUnchanged=true", "-Dsonar.python.xunit.reportPath=path/to/xunit/report", "-Dsonar.python.xunit.skipDetails=true", diff --git a/tests/unit/test_configuration_loader.py b/tests/unit/test_configuration_loader.py index 2ef35e8d..a3846186 100644 --- a/tests/unit/test_configuration_loader.py +++ b/tests/unit/test_configuration_loader.py @@ -51,6 +51,7 @@ SONAR_SCANNER_JAVA_OPTS, SONAR_SCANNER_ARCH, SONAR_SCANNER_OS, + SONAR_COVERAGE_EXCLUSIONS, ) from pysonar_scanner.utils import Arch, Os 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): } self.assertDictEqual(configuration, expected_configuration) + @patch( + "sys.argv", + [ + "myscript.py", + ], + ) + def test_load_coveragerc_properties(self, mock_get_os, mock_get_arch): + self.fs.create_dir("custom/path") + self.fs.create_file( + ".coveragerc", + contents=( + """ + [run] + omit = + */.local/* + /usr/* + utils/tirefire.py + """ + ), + ) + configuration = ConfigurationLoader.load() + expected_configuration = { + SONAR_SCANNER_APP: "python", + SONAR_SCANNER_APP_VERSION: "1.0", + SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], + SONAR_VERBOSE: False, + SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, + SONAR_PROJECT_BASE_DIR: os.getcwd(), + SONAR_SCANNER_CONNECT_TIMEOUT: 5, + SONAR_SCANNER_SOCKET_TIMEOUT: 60, + SONAR_SCANNER_RESPONSE_TIMEOUT: 0, + SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", + SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, + SONAR_COVERAGE_EXCLUSIONS: "*/.local/*, /usr/*, utils/tirefire.py", + } + self.assertDictEqual(configuration, expected_configuration) + @patch("sys.argv", ["myscript.py"]) @patch.dict("os.environ", {"SONAR_TOKEN": "TokenFromEnv", "SONAR_PROJECT_KEY": "KeyFromEnv"}, clear=True) def test_load_from_env_variables_only(self, mock_get_os, mock_get_arch): diff --git a/tests/unit/test_coveragerc_loader.py b/tests/unit/test_coveragerc_loader.py new file mode 100644 index 00000000..394fed8a --- /dev/null +++ b/tests/unit/test_coveragerc_loader.py @@ -0,0 +1,88 @@ +# +# Sonar Scanner Python +# Copyright (C) 2011-2024 SonarSource SA. +# mailto:info AT sonarsource DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +from pathlib import Path +from unittest import mock +from unittest.mock import MagicMock, patch + +from pyfakefs.fake_filesystem_unittest import TestCase +from pysonar_scanner.configuration.pyproject_toml import TomlConfigurationLoader +from pysonar_scanner.configuration.coveragerc_loader import CoverageRCConfigurationLoader + + +class TestCoverageRcFile(TestCase): + def setUp(self): + self.setUpPyfakefs() + + def test_load_coverage_file(self): + self.fs.create_file( + ".coveragerc", + contents=""" + [run] + omit = + */.local/* + /usr/* + utils/tirefire.py + """, + ) + properties = CoverageRCConfigurationLoader.load(Path(".")) + + self.assertEqual(properties["sonar.coverage.exclusions"], "*/.local/*, /usr/*, utils/tirefire.py") + + @patch("pysonar_scanner.configuration.coveragerc_loader.logging") + def test_load_missing_file(self, mock_logging): + properties = CoverageRCConfigurationLoader.load(Path(".")) + self.assertEqual(len(properties), 0) + mock_logging.debug.assert_called_with("Configuration file not found: .coveragerc") + + @patch("pysonar_scanner.configuration.coveragerc_loader.logging") + def test_load_without_run_section(self, mock_logging): + self.fs.create_file( + ".coveragerc", + contents=""" + [something_else] + """, + ) + properties = CoverageRCConfigurationLoader.load(Path(".")) + self.assertEqual(len(properties), 0) + mock_logging.debug.assert_called_with("The run key was not found in .coveragerc") + + @patch("pysonar_scanner.configuration.coveragerc_loader.logging") + def test_load_without_exclusions_property(self, mock_logging): + self.fs.create_file( + ".coveragerc", + contents=""" + [run] + """, + ) + properties = CoverageRCConfigurationLoader.load(Path(".")) + self.assertEqual(len(properties), 0) + mock_logging.debug.assert_called_with("The run.omit key was not found in .coveragerc") + + @patch("pysonar_scanner.configuration.coveragerc_loader.logging") + def test_load_malformed_file(self, mock_logging): + self.fs.create_file( + ".coveragerc", + contents=""" + [run + omit = + """, + ) + properties = CoverageRCConfigurationLoader.load(Path(".")) + self.assertEqual(len(properties), 0) From 7e0278b9c2cf767f5203d1c9467a5f459c36b3d3 Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Tue, 17 Jun 2025 15:41:30 +0200 Subject: [PATCH 2/6] CR fixes --- .../configuration/configuration_loader.py | 2 +- .../configuration/coveragerc_loader.py | 38 +++++++------------ tests/unit/test_coveragerc_loader.py | 24 +++--------- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/src/pysonar_scanner/configuration/configuration_loader.py b/src/pysonar_scanner/configuration/configuration_loader.py index d2b6f5dc..b09a1718 100644 --- a/src/pysonar_scanner/configuration/configuration_loader.py +++ b/src/pysonar_scanner/configuration/configuration_loader.py @@ -51,7 +51,7 @@ def load() -> dict[Key, Any]: toml_path_property = cli_properties.get("toml-path", ".") toml_dir = Path(toml_path_property) if "toml-path" in cli_properties else base_dir toml_properties = TomlConfigurationLoader.load(toml_dir) - coverage_properties = CoverageRCConfigurationLoader.load(base_dir) + coverage_properties = CoverageRCConfigurationLoader.load_exclusion_properties(base_dir) resolved_properties = get_static_default_properties() resolved_properties.update(dynamic_defaults_loader.load()) diff --git a/src/pysonar_scanner/configuration/coveragerc_loader.py b/src/pysonar_scanner/configuration/coveragerc_loader.py index 0356ea76..ea9671c6 100644 --- a/src/pysonar_scanner/configuration/coveragerc_loader.py +++ b/src/pysonar_scanner/configuration/coveragerc_loader.py @@ -26,57 +26,47 @@ class CoverageRCConfigurationLoader: @staticmethod - def load(base_dir: pathlib.Path) -> dict[str, str]: + def load_exclusion_properties(base_dir: pathlib.Path) -> dict[str, str]: config_file_path = base_dir / ".coveragerc" - result_dict: dict[str, str] = {} - coverage_properties = CoverageRCConfigurationLoader.__read_config(config_file_path) if len(coverage_properties) == 0: - return result_dict - exclusion_properties = CoverageRCConfigurationLoader.__read_coverage_exclusions_properties( + return {} + translated_exclusions = CoverageRCConfigurationLoader.__extract_coverage_exclusion_patterns( config_file_path, coverage_properties ) - result_dict.update(exclusion_properties) - return result_dict + if translated_exclusions is None: + return {} + + return {"sonar.coverage.exclusions": translated_exclusions} @staticmethod def __read_config(config_file_path: pathlib.Path) -> dict[str, Any]: config_dict: dict[str, Any] = {} if not config_file_path.exists(): - logging.debug(f"Configuration file not found: {config_file_path}") + logging.debug(f"Coverage file not found: {config_file_path}") return config_dict try: config_parser = configparser.ConfigParser() config_parser.read(config_file_path) - - # Iterate over sections and options for section in config_parser.sections(): section_values = {} for key, value in config_parser.items(section): section_values[key] = value - config_dict[section] = section_values except Exception as e: logging.debug(f"Error decoding coverage file {config_file_path}: {e}") return config_dict @staticmethod - def __read_coverage_exclusions_properties( + def __extract_coverage_exclusion_patterns( config_file_path: pathlib.Path, coverage_properties: dict[str, Any] - ) -> dict[str, Any]: + ) -> str | None: result_dict: dict[str, Any] = {} - if "run" not in coverage_properties: - logging.debug(f"The run key was not found in {config_file_path}") - return result_dict - - if "omit" not in coverage_properties["run"]: - logging.debug(f"The run.omit key was not found in {config_file_path}") - return result_dict + if "run" not in coverage_properties or "omit" not in coverage_properties["run"]: + logging.debug(f"Coverage file has no exclusion properties") + return None omit_exclusions = coverage_properties["run"]["omit"] patterns_list = [patterns.strip() for patterns in omit_exclusions.splitlines() if patterns.strip()] - translated_exclusions = ", ".join(patterns_list) - - result_dict["sonar.coverage.exclusions"] = translated_exclusions - return result_dict + return ", ".join(patterns_list) diff --git a/tests/unit/test_coveragerc_loader.py b/tests/unit/test_coveragerc_loader.py index 394fed8a..62233367 100644 --- a/tests/unit/test_coveragerc_loader.py +++ b/tests/unit/test_coveragerc_loader.py @@ -41,15 +41,15 @@ def test_load_coverage_file(self): utils/tirefire.py """, ) - properties = CoverageRCConfigurationLoader.load(Path(".")) + properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) self.assertEqual(properties["sonar.coverage.exclusions"], "*/.local/*, /usr/*, utils/tirefire.py") @patch("pysonar_scanner.configuration.coveragerc_loader.logging") def test_load_missing_file(self, mock_logging): - properties = CoverageRCConfigurationLoader.load(Path(".")) + properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) self.assertEqual(len(properties), 0) - mock_logging.debug.assert_called_with("Configuration file not found: .coveragerc") + mock_logging.debug.assert_called_with("Coverage file not found: .coveragerc") @patch("pysonar_scanner.configuration.coveragerc_loader.logging") def test_load_without_run_section(self, mock_logging): @@ -59,21 +59,9 @@ def test_load_without_run_section(self, mock_logging): [something_else] """, ) - properties = CoverageRCConfigurationLoader.load(Path(".")) + properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) self.assertEqual(len(properties), 0) - mock_logging.debug.assert_called_with("The run key was not found in .coveragerc") - - @patch("pysonar_scanner.configuration.coveragerc_loader.logging") - def test_load_without_exclusions_property(self, mock_logging): - self.fs.create_file( - ".coveragerc", - contents=""" - [run] - """, - ) - properties = CoverageRCConfigurationLoader.load(Path(".")) - self.assertEqual(len(properties), 0) - mock_logging.debug.assert_called_with("The run.omit key was not found in .coveragerc") + mock_logging.debug.assert_called_with("Coverage file has no exclusion properties") @patch("pysonar_scanner.configuration.coveragerc_loader.logging") def test_load_malformed_file(self, mock_logging): @@ -84,5 +72,5 @@ def test_load_malformed_file(self, mock_logging): omit = """, ) - properties = CoverageRCConfigurationLoader.load(Path(".")) + properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) self.assertEqual(len(properties), 0) From ad3748c1084523b486e7b4903dfea7dd6bb3f315 Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Tue, 17 Jun 2025 15:43:48 +0200 Subject: [PATCH 3/6] add missing test --- tests/unit/test_coveragerc_loader.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/test_coveragerc_loader.py b/tests/unit/test_coveragerc_loader.py index 62233367..381cc14b 100644 --- a/tests/unit/test_coveragerc_loader.py +++ b/tests/unit/test_coveragerc_loader.py @@ -63,6 +63,18 @@ def test_load_without_run_section(self, mock_logging): self.assertEqual(len(properties), 0) mock_logging.debug.assert_called_with("Coverage file has no exclusion properties") + @patch("pysonar_scanner.configuration.coveragerc_loader.logging") + def test_load_without_exclusions_property(self, mock_logging): + self.fs.create_file( + ".coveragerc", + contents=""" + [run] + """, + ) + properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) + self.assertEqual(len(properties), 0) + mock_logging.debug.assert_called_with("Coverage file has no exclusion properties") + @patch("pysonar_scanner.configuration.coveragerc_loader.logging") def test_load_malformed_file(self, mock_logging): self.fs.create_file( From 639d071f087d0a3d923836720c74b802a5dc996a Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Tue, 17 Jun 2025 15:45:03 +0200 Subject: [PATCH 4/6] code fixes --- src/pysonar_scanner/configuration/coveragerc_loader.py | 3 +-- tests/unit/test_coveragerc_loader.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pysonar_scanner/configuration/coveragerc_loader.py b/src/pysonar_scanner/configuration/coveragerc_loader.py index ea9671c6..8c85796b 100644 --- a/src/pysonar_scanner/configuration/coveragerc_loader.py +++ b/src/pysonar_scanner/configuration/coveragerc_loader.py @@ -62,9 +62,8 @@ def __read_config(config_file_path: pathlib.Path) -> dict[str, Any]: def __extract_coverage_exclusion_patterns( config_file_path: pathlib.Path, coverage_properties: dict[str, Any] ) -> str | None: - result_dict: dict[str, Any] = {} if "run" not in coverage_properties or "omit" not in coverage_properties["run"]: - logging.debug(f"Coverage file has no exclusion properties") + logging.debug(f"Coverage file {config_file_path} has no exclusion properties") return None omit_exclusions = coverage_properties["run"]["omit"] diff --git a/tests/unit/test_coveragerc_loader.py b/tests/unit/test_coveragerc_loader.py index 381cc14b..5b92c7ca 100644 --- a/tests/unit/test_coveragerc_loader.py +++ b/tests/unit/test_coveragerc_loader.py @@ -61,7 +61,7 @@ def test_load_without_run_section(self, mock_logging): ) properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) self.assertEqual(len(properties), 0) - mock_logging.debug.assert_called_with("Coverage file has no exclusion properties") + mock_logging.debug.assert_called_with("Coverage file .coveragerc has no exclusion properties") @patch("pysonar_scanner.configuration.coveragerc_loader.logging") def test_load_without_exclusions_property(self, mock_logging): @@ -73,7 +73,7 @@ def test_load_without_exclusions_property(self, mock_logging): ) properties = CoverageRCConfigurationLoader.load_exclusion_properties(Path(".")) self.assertEqual(len(properties), 0) - mock_logging.debug.assert_called_with("Coverage file has no exclusion properties") + mock_logging.debug.assert_called_with("Coverage file .coveragerc has no exclusion properties") @patch("pysonar_scanner.configuration.coveragerc_loader.logging") def test_load_malformed_file(self, mock_logging): From 238bb6cae8c1a84a6725d9ecb7fd5832e544d864 Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Tue, 17 Jun 2025 15:51:01 +0200 Subject: [PATCH 5/6] fix --- .../configuration/coveragerc_loader.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pysonar_scanner/configuration/coveragerc_loader.py b/src/pysonar_scanner/configuration/coveragerc_loader.py index 8c85796b..3744e219 100644 --- a/src/pysonar_scanner/configuration/coveragerc_loader.py +++ b/src/pysonar_scanner/configuration/coveragerc_loader.py @@ -34,10 +34,10 @@ def load_exclusion_properties(base_dir: pathlib.Path) -> dict[str, str]: translated_exclusions = CoverageRCConfigurationLoader.__extract_coverage_exclusion_patterns( config_file_path, coverage_properties ) - if translated_exclusions is None: + if len(translated_exclusions) == 0: return {} - return {"sonar.coverage.exclusions": translated_exclusions} + return {"sonar.coverage.exclusions": ", ".join(translated_exclusions)} @staticmethod def __read_config(config_file_path: pathlib.Path) -> dict[str, Any]: @@ -61,11 +61,10 @@ def __read_config(config_file_path: pathlib.Path) -> dict[str, Any]: @staticmethod def __extract_coverage_exclusion_patterns( config_file_path: pathlib.Path, coverage_properties: dict[str, Any] - ) -> str | None: + ) -> list[str]: if "run" not in coverage_properties or "omit" not in coverage_properties["run"]: logging.debug(f"Coverage file {config_file_path} has no exclusion properties") - return None + return [] omit_exclusions = coverage_properties["run"]["omit"] - patterns_list = [patterns.strip() for patterns in omit_exclusions.splitlines() if patterns.strip()] - return ", ".join(patterns_list) + return [patterns.strip() for patterns in omit_exclusions.splitlines() if patterns.strip()] From 4ecd7772620eaf52e273598c0a3aad81ec4717c0 Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Tue, 17 Jun 2025 15:52:28 +0200 Subject: [PATCH 6/6] optimise imports --- tests/unit/test_coveragerc_loader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_coveragerc_loader.py b/tests/unit/test_coveragerc_loader.py index 5b92c7ca..473222cd 100644 --- a/tests/unit/test_coveragerc_loader.py +++ b/tests/unit/test_coveragerc_loader.py @@ -18,11 +18,10 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # from pathlib import Path -from unittest import mock -from unittest.mock import MagicMock, patch +from unittest.mock import patch from pyfakefs.fake_filesystem_unittest import TestCase -from pysonar_scanner.configuration.pyproject_toml import TomlConfigurationLoader + from pysonar_scanner.configuration.coveragerc_loader import CoverageRCConfigurationLoader