Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/pysonar_scanner/configuration/configuration_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
SONAR_PROJECT_KEY,
SONAR_TOKEN,
SONAR_PROJECT_BASE_DIR,
SONAR_EXCLUSIONS,
SONAR_SOURCES,
SONAR_TESTS,
SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED,
Key,
Expand Down Expand Up @@ -80,12 +82,36 @@ 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"

sources_defaulted = SONAR_SOURCES not in resolved_properties
if sources_defaulted:
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:
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'. "
"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,
)
Comment on lines +101 to +113
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Exclusions added for auto-detected tests even when sources is narrow

When tests_auto_detected is True but the user explicitly set sonar.sources to a narrow path like "src", the condition on line 98 still evaluates to True (via the tests_auto_detected branch), and test directory exclusion patterns are appended to sonar.exclusions. These exclusions are harmless but unnecessary when the source path doesn't overlap with the test directories.

For example: --sonar-sources src with auto-detected tests directory → sonar.exclusions gets tests/** even though src never included tests.

This is a minor concern since extra exclusions don't cause incorrect behavior, but the log message "Adding test directories to sonar.exclusions to avoid overlap with sonar.sources" would be misleading when there's no actual overlap.

Only add test exclusions when sources actually includes '.' (the root), which is the only case where overlap is likely:

if SONAR_TESTS in resolved_properties and (
    sources_defaulted or resolved_properties.get(SONAR_SOURCES) == "."
):
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎


return resolved_properties

@staticmethod
Expand Down
44 changes: 43 additions & 1 deletion tests/unit/test_configuration_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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"])
Expand All @@ -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: ".",
},
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -440,6 +443,45 @@ 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_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."""
Expand Down
Loading