Skip to content

Commit b1b6bf8

Browse files
Prevent heuristics from running when attempt to set test is made
1 parent 3249dd4 commit b1b6bf8

5 files changed

Lines changed: 205 additions & 83 deletions

File tree

src/pysonar_scanner/configuration/configuration_loader.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
SONAR_TOKEN,
3030
SONAR_PROJECT_BASE_DIR,
3131
SONAR_TESTS,
32+
SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED,
3233
Key,
3334
)
3435
from pysonar_scanner.configuration.properties import PROPERTIES
@@ -76,9 +77,12 @@ def load() -> dict[Key, Any]:
7677
# Auto-detect sonar.tests only when the user has not set it in any higher-priority source
7778
# and has not explicitly disabled the sonar-python test file heuristic. When the heuristic
7879
# is disabled the intent is to analyse all files as main code with no test classification.
79-
heuristic_disabled = resolved_properties.get("sonar.python.testFileHeuristic.disabled", "").lower() == "true"
80+
heuristic_disabled = resolved_properties.get(SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED, "").lower() == "true"
8081
if SONAR_TESTS not in resolved_properties and not heuristic_disabled:
81-
resolved_properties.update(test_paths_loader.load(base_dir))
82+
inferred_props, disable_heuristic = test_paths_loader.load(base_dir)
83+
resolved_properties.update(inferred_props)
84+
if disable_heuristic and SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED not in resolved_properties:
85+
resolved_properties[SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED] = "true"
8286

8387
return resolved_properties
8488

src/pysonar_scanner/configuration/properties.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
SONAR_WORKING_DIRECTORY: Key = "sonar.working.directory"
9595
SONAR_SCM_FORCE_RELOAD_ALL: Key = "sonar.scm.forceReloadAll"
9696
SONAR_MODULES: Key = "sonar.modules"
97+
SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED: Key = "sonar.python.testFileHeuristic.disabled"
9798
SONAR_PYTHON_ANALYSIS_PARALLEL: Key = "sonar.python.analysis.parallel"
9899
SONAR_PYTHON_ANALYSIS_THREADS: Key = "sonar.python.analysis.threads"
99100
SONAR_PYTHON_XUNIT_REPORT_PATH: Key = "sonar.python.xunit.reportPath"

src/pysonar_scanner/configuration/test_paths_loader.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,27 @@
3030
_SETUP_CFG_PYTEST_SECTION = "tool:pytest"
3131

3232

33-
def load(base_dir: pathlib.Path) -> dict[str, str]:
33+
def load(base_dir: pathlib.Path) -> tuple[dict[str, str], bool]:
3434
"""Infer sonar.tests from Python tooling configuration and filesystem conventions.
3535
36-
Returns sonar.tests if a test directory can be reliably inferred; empty dict otherwise.
37-
Filesystem convention fallback only runs when no config file declares a testpaths key.
36+
Returns (properties, disable_heuristic) where:
37+
- properties contains sonar.tests if a test directory was reliably inferred
38+
- disable_heuristic is True when a config file declared testpaths but all paths were
39+
invalid — the user expressed intent, so the sonar-python heuristic should not fire
3840
"""
3941
for loader in [_load_from_pyproject_toml, _load_from_pytest_ini, _load_from_tox_ini, _load_from_setup_cfg]:
4042
result = loader(base_dir)
4143
if result is None:
42-
continue # file absent or no testpaths key — try next source
44+
continue # file absent, no testpaths key, or empty testpaths (no restriction) — try next
4345
if result:
44-
return {SONAR_TESTS: result}
45-
return {} # testpaths declared but all paths were invalid — stop chain, set nothing
46+
return {SONAR_TESTS: result}, False
47+
return {}, True # declared but all paths invalid: user expressed intent, disable heuristic
4648

47-
# No config file declared a testpaths key; fall back to filesystem conventions.
49+
# No config file gave a non-empty testpaths declaration; fall back to filesystem conventions.
4850
filesystem_result = _load_from_filesystem(base_dir)
4951
if filesystem_result:
50-
return {SONAR_TESTS: filesystem_result}
51-
return {}
52+
return {SONAR_TESTS: filesystem_result}, False
53+
return {}, False
5254

5355

5456
def _existing_paths(base_dir: pathlib.Path, paths: list[str]) -> list[str]:
@@ -102,7 +104,7 @@ def _load_from_pyproject_toml(base_dir: pathlib.Path) -> Optional[str]:
102104
testpaths = ini_options["testpaths"]
103105
raw = [str(p) for p in (testpaths if isinstance(testpaths, list) else testpaths.split()) if str(p).strip()]
104106
if not raw:
105-
return "" # testpaths key present but empty — stop chain
107+
return None # testpaths = [] means "no path restriction" — same as key absent, continue chain
106108
paths = _existing_paths(base_dir, raw)
107109
if paths:
108110
result = ",".join(paths)
@@ -129,7 +131,7 @@ def _load_from_ini_file(base_dir: pathlib.Path, filename: str, section: str) ->
129131
return None
130132
raw = [p for p in config[section]["testpaths"].split() if p]
131133
if not raw:
132-
return "" # testpaths key present but emptystop chain
134+
return None # empty testpaths means "no path restriction"same as key absent, continue chain
133135
paths = _existing_paths(base_dir, raw)
134136
if paths:
135137
result = ",".join(paths)

tests/unit/test_configuration_loader.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
SONAR_SCANNER_ARCH,
5454
SONAR_SCANNER_OS,
5555
SONAR_COVERAGE_EXCLUSIONS,
56+
SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED,
5657
)
5758
from pysonar_scanner.utils import Arch, Os
5859
from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader, SONAR_PROJECT_BASE_DIR
@@ -499,6 +500,33 @@ def test_test_paths_loader_not_called_when_heuristic_disabled(self, mock_loader,
499500
ConfigurationLoader.load()
500501
mock_loader.load.assert_not_called()
501502

503+
@patch("sys.argv", ["myscript.py"])
504+
def test_declared_invalid_testpaths_disables_heuristic_in_resolved_properties(self, mock_get_os, mock_get_arch):
505+
"""When testpaths is declared but all paths are invalid, the loader signals intent and
506+
configuration_loader sets sonar.python.testFileHeuristic.disabled=true."""
507+
self.fs.create_file(
508+
"pytest.ini",
509+
contents="[pytest]\ntestpaths = nonexistent\n",
510+
)
511+
configuration = ConfigurationLoader.load()
512+
self.assertEqual(configuration.get(SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED), "true")
513+
self.assertNotIn(SONAR_TESTS, configuration)
514+
515+
@patch("sys.argv", ["myscript.py"])
516+
def test_user_heuristic_disable_not_overridden_by_loader(self, mock_get_os, mock_get_arch):
517+
"""An explicit sonar.python.testFileHeuristic.disabled set by the user is not overridden
518+
by the loader's suggestion, even when testpaths resolves to nothing."""
519+
self.fs.create_file(
520+
"sonar-project.properties",
521+
contents="sonar.python.testFileHeuristic.disabled=false\n",
522+
)
523+
self.fs.create_file(
524+
"pytest.ini",
525+
contents="[pytest]\ntestpaths = nonexistent\n",
526+
)
527+
configuration = ConfigurationLoader.load()
528+
self.assertEqual(configuration.get(SONAR_PYTHON_TEST_FILE_HEURISTIC_DISABLED), "false")
529+
502530
@patch("sys.argv", ["myscript.py"])
503531
@patch.dict("os.environ", {"SONAR_TOKEN": "TokenFromEnv", "SONAR_PROJECT_KEY": "KeyFromEnv"}, clear=True)
504532
def test_load_from_env_variables_only(self, mock_get_os, mock_get_arch):

0 commit comments

Comments
 (0)