From 1a5a38c20ef2b264adf268cafc7161c48a6e2f8c Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Fri, 22 May 2026 11:39:44 +0200 Subject: [PATCH 1/3] Default sonar.sources to current directory when not provided --- .../configuration/configuration_loader.py | 5 +++++ tests/unit/test_configuration_loader.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pysonar_scanner/configuration/configuration_loader.py b/src/pysonar_scanner/configuration/configuration_loader.py index 9857f1d1..341e195b 100644 --- a/src/pysonar_scanner/configuration/configuration_loader.py +++ b/src/pysonar_scanner/configuration/configuration_loader.py @@ -28,6 +28,7 @@ SONAR_PROJECT_KEY, SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, + SONAR_SOURCES, SONAR_TESTS, SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED, Key, @@ -86,6 +87,10 @@ def load() -> dict[Key, Any]: if disable_heuristic and SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED not in resolved_properties: resolved_properties[SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED] = "true" + if SONAR_SOURCES not in resolved_properties: + logging.info("sonar.sources is not set; defaulting to the current directory '.'") + resolved_properties[SONAR_SOURCES] = "." + return resolved_properties @staticmethod diff --git a/tests/unit/test_configuration_loader.py b/tests/unit/test_configuration_loader.py index 5d36a47b..433a5bf0 100644 --- a/tests/unit/test_configuration_loader.py +++ b/tests/unit/test_configuration_loader.py @@ -79,6 +79,7 @@ def test_defaults(self, mock_get_os, mock_get_arch): expected_configuration = { SONAR_TOKEN: "myToken", SONAR_PROJECT_KEY: "myProjectKey", + SONAR_SOURCES: ".", SONAR_SCANNER_APP: "python", SONAR_SCANNER_APP_VERSION: "1.0", SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], @@ -103,7 +104,7 @@ def test_no_defaults_in_configuration_loaders( self, get_static_default_properties_mock, mock_load, mock_get_os, mock_get_arch ): config = ConfigurationLoader.load() - self.assertDictEqual(config, {}) + self.assertDictEqual(config, {SONAR_SOURCES: "."}) @patch("pysonar_scanner.configuration.configuration_loader.get_static_default_properties", return_value={}) @patch("sys.argv", ["myscript.py"]) @@ -115,6 +116,7 @@ def test_dynamic_defaults_are_loaded(self, get_static_default_properties_mock, m SONAR_PROJECT_BASE_DIR: os.getcwd(), SONAR_SCANNER_OS: Os.LINUX.value, SONAR_SCANNER_ARCH: Arch.X64.value, + SONAR_SOURCES: ".", }, ) @@ -404,6 +406,7 @@ def test_load_coveragerc_properties(self, mock_get_os, mock_get_arch): SONAR_SCANNER_OS: Os.LINUX.value, SONAR_SCANNER_ARCH: Arch.X64.value, SONAR_COVERAGE_EXCLUSIONS: "*/.local/*, /usr/*, utils/tirefire.py", + SONAR_SOURCES: ".", SONAR_SCANNER_DRY_RUN: False, } self.assertDictEqual(configuration, expected_configuration) @@ -440,6 +443,18 @@ def test_explicit_sonar_tests_overrides_auto_detection(self, mock_get_os, mock_g configuration = ConfigurationLoader.load() self.assertEqual(configuration[SONAR_TESTS], "src/test") + @patch("sys.argv", ["myscript.py"]) + def test_default_sources_when_not_set(self, mock_get_os, mock_get_arch): + """sonar.sources defaults to '.' when not set in any configuration source.""" + configuration = ConfigurationLoader.load() + self.assertEqual(configuration[SONAR_SOURCES], ".") + + @patch("sys.argv", ["myscript.py", "--sonar-sources", "src"]) + def test_explicit_sources_overrides_default(self, mock_get_os, mock_get_arch): + """An explicit sonar.sources must not be overridden by the default.""" + configuration = ConfigurationLoader.load() + self.assertEqual(configuration[SONAR_SOURCES], "src") + @patch("sys.argv", ["myscript.py"]) def test_sonar_project_properties_sonar_tests_overrides_auto_detection(self, mock_get_os, mock_get_arch): """sonar.tests in sonar-project.properties must override auto-detected value from filesystem/pytest config.""" From 5e7f0d6fc5ba0dff33c4c97275557ee7eccc49d1 Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Fri, 22 May 2026 11:45:19 +0200 Subject: [PATCH 2/3] Ensure no overlap with auto-detected tests --- .../configuration/configuration_loader.py | 18 ++++++++++++- tests/unit/test_configuration_loader.py | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/pysonar_scanner/configuration/configuration_loader.py b/src/pysonar_scanner/configuration/configuration_loader.py index 341e195b..bedd4492 100644 --- a/src/pysonar_scanner/configuration/configuration_loader.py +++ b/src/pysonar_scanner/configuration/configuration_loader.py @@ -28,6 +28,7 @@ SONAR_PROJECT_KEY, SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, + SONAR_EXCLUSIONS, SONAR_SOURCES, SONAR_TESTS, SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED, @@ -81,16 +82,31 @@ def load() -> dict[Key, Any]: heuristic_disabled = ( str(resolved_properties.get(SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED, "")).lower() == "true" ) + tests_auto_detected = False if SONAR_TESTS not in resolved_properties and not heuristic_disabled: inferred_props, disable_heuristic = test_paths_loader.load(base_dir) resolved_properties.update(inferred_props) + tests_auto_detected = SONAR_TESTS in resolved_properties if disable_heuristic and SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED not in resolved_properties: resolved_properties[SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED] = "true" - if SONAR_SOURCES not in resolved_properties: + sources_defaulted = SONAR_SOURCES not in resolved_properties + if sources_defaulted: logging.info("sonar.sources is not set; defaulting to the current directory '.'") resolved_properties[SONAR_SOURCES] = "." + if (sources_defaulted or tests_auto_detected) and SONAR_TESTS in resolved_properties: + test_dirs = [d.strip() for d in resolved_properties[SONAR_TESTS].split(",") if d.strip()] + test_exclusion_patterns = ",".join(f"{d}/**" for d in test_dirs) + existing = resolved_properties.get(SONAR_EXCLUSIONS, "") + resolved_properties[SONAR_EXCLUSIONS] = ( + f"{existing},{test_exclusion_patterns}" if existing else test_exclusion_patterns + ) + logging.info( + "Adding test directories to sonar.exclusions to avoid overlap with sonar.sources: '%s'", + test_exclusion_patterns, + ) + return resolved_properties @staticmethod diff --git a/tests/unit/test_configuration_loader.py b/tests/unit/test_configuration_loader.py index 433a5bf0..32254bf3 100644 --- a/tests/unit/test_configuration_loader.py +++ b/tests/unit/test_configuration_loader.py @@ -455,6 +455,33 @@ def test_explicit_sources_overrides_default(self, mock_get_os, mock_get_arch): configuration = ConfigurationLoader.load() self.assertEqual(configuration[SONAR_SOURCES], "src") + @patch("sys.argv", ["myscript.py"]) + def test_default_sources_adds_test_dirs_to_exclusions(self, mock_get_os, mock_get_arch): + """When sonar.sources defaults to '.' and sonar.tests is set, test dirs must be excluded from sources.""" + self.fs.create_dir("tests") + configuration = ConfigurationLoader.load() + self.assertEqual(configuration[SONAR_SOURCES], ".") + self.assertEqual(configuration[SONAR_TESTS], "tests") + self.assertEqual(configuration[SONAR_EXCLUSIONS], "tests/**") + + @patch("sys.argv", ["myscript.py"]) + def test_default_sources_appends_test_dirs_to_existing_exclusions(self, mock_get_os, mock_get_arch): + """When sonar.exclusions is already set, test dirs are appended rather than replacing it.""" + self.fs.create_dir("tests") + self.fs.create_file("sonar-project.properties", contents="sonar.exclusions=generated/**\n") + configuration = ConfigurationLoader.load() + self.assertEqual(configuration[SONAR_SOURCES], ".") + self.assertEqual(configuration[SONAR_EXCLUSIONS], "generated/**,tests/**") + + @patch("sys.argv", ["myscript.py", "--sonar-sources", "."]) + def test_auto_detected_tests_add_exclusions_even_when_sources_explicit(self, mock_get_os, mock_get_arch): + """Auto-detected test dirs are added to sonar.exclusions even when sonar.sources was explicitly set.""" + self.fs.create_dir("tests") + configuration = ConfigurationLoader.load() + self.assertEqual(configuration[SONAR_SOURCES], ".") + self.assertEqual(configuration[SONAR_TESTS], "tests") + self.assertEqual(configuration[SONAR_EXCLUSIONS], "tests/**") + @patch("sys.argv", ["myscript.py"]) def test_sonar_project_properties_sonar_tests_overrides_auto_detection(self, mock_get_os, mock_get_arch): """sonar.tests in sonar-project.properties must override auto-detected value from filesystem/pytest config.""" From 07eb3efd5c9ce882cc3bfd19f64376dc7be788b1 Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Fri, 22 May 2026 16:24:40 +0200 Subject: [PATCH 3/3] Add guideline to override behavior --- .../configuration/configuration_loader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pysonar_scanner/configuration/configuration_loader.py b/src/pysonar_scanner/configuration/configuration_loader.py index bedd4492..e27cfd1f 100644 --- a/src/pysonar_scanner/configuration/configuration_loader.py +++ b/src/pysonar_scanner/configuration/configuration_loader.py @@ -92,7 +92,10 @@ def load() -> dict[Key, Any]: sources_defaulted = SONAR_SOURCES not in resolved_properties if sources_defaulted: - logging.info("sonar.sources is not set; defaulting to the current directory '.'") + logging.info( + "sonar.sources is not set; defaulting to the current directory '.'. " + "Set sonar.sources explicitly to override this behavior." + ) resolved_properties[SONAR_SOURCES] = "." if (sources_defaulted or tests_auto_detected) and SONAR_TESTS in resolved_properties: @@ -103,7 +106,9 @@ def load() -> dict[Key, Any]: f"{existing},{test_exclusion_patterns}" if existing else test_exclusion_patterns ) logging.info( - "Adding test directories to sonar.exclusions to avoid overlap with sonar.sources: '%s'", + "Adding test directories to sonar.exclusions to avoid overlap with sonar.sources: '%s'. " + "To manage this manually, set sonar.sources to a path that does not include the test directories, " + "or set sonar.tests explicitly to disable auto-detection.", test_exclusion_patterns, )