Skip to content

Commit ff0e44d

Browse files
authored
SCANPY-178: The scanner should respect the SONAR_SCANNER_OPTS environment variable (#219)
1 parent 7190fcd commit ff0e44d

File tree

5 files changed

+139
-23
lines changed

5 files changed

+139
-23
lines changed

src/pysonar_scanner/configuration/environment_variables.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ def load_properties_env_variables():
6363
env_var_name = prop.env_variable_name()
6464
if env_var_name in os.environ:
6565
properties[prop.name] = os.environ[env_var_name]
66+
if prop.deprecation_message:
67+
logging.warning(prop.deprecation_message)
6668
return properties

src/pysonar_scanner/configuration/properties.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
# along with this program; if not, write to the Free Software Foundation,
1818
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
#
20-
import time
2120
import argparse
21+
import time
2222
from dataclasses import dataclass
2323
from typing import Any, Callable, Optional
2424

@@ -93,14 +93,17 @@
9393
SONAR_WORKING_DIRECTORY: Key = "sonar.working.directory"
9494
SONAR_SCM_FORCE_RELOAD_ALL: Key = "sonar.scm.forceReloadAll"
9595
SONAR_MODULES: Key = "sonar.modules"
96-
SONAR_PYTHON_XUNIT_REPORT_PATH = "sonar.python.xunit.reportPath"
97-
SONAR_PYTHON_XUNIT_SKIP_DETAILS = "sonar.python.xunit.skipDetails"
98-
SONAR_PYTHON_MYPY_REPORT_PATHS = "sonar.python.mypy.reportPaths"
99-
SONAR_PYTHON_BANDIT_REPORT_PATHS = "sonar.python.bandit.reportPaths"
100-
SONAR_PYTHON_FLAKE8_REPORT_PATHS = "sonar.python.flake8.reportPaths"
101-
SONAR_PYTHON_RUFF_REPORT_PATHS = "sonar.python.ruff.reportPaths"
96+
SONAR_PYTHON_XUNIT_REPORT_PATH: Key = "sonar.python.xunit.reportPath"
97+
SONAR_PYTHON_XUNIT_SKIP_DETAILS: Key = "sonar.python.xunit.skipDetails"
98+
SONAR_PYTHON_MYPY_REPORT_PATHS: Key = "sonar.python.mypy.reportPaths"
99+
SONAR_PYTHON_BANDIT_REPORT_PATHS: Key = "sonar.python.bandit.reportPaths"
100+
SONAR_PYTHON_FLAKE8_REPORT_PATHS: Key = "sonar.python.flake8.reportPaths"
101+
SONAR_PYTHON_RUFF_REPORT_PATHS: Key = "sonar.python.ruff.reportPaths"
102102
TOML_PATH: Key = "toml-path"
103103

104+
# ============ DEPRECATED ==============
105+
SONAR_SCANNER_OPTS: Key = "sonar.scanner.opts"
106+
104107

105108
@dataclass
106109
class Property:
@@ -113,6 +116,8 @@ class Property:
113116
cli_getter: Optional[Callable[[argparse.Namespace], Any]] = None
114117
"""function to get the value from the CLI arguments namespace. If None, the property is not settable via CLI"""
115118

119+
deprecation_message: Optional[str] = None
120+
116121
def python_name(self) -> str:
117122
"""Convert Java-style camel case name to Python-style kebab-case name."""
118123
result = []
@@ -524,6 +529,12 @@ def env_variable_name(self) -> str:
524529
name=SONAR_MODULES,
525530
default_value=None,
526531
cli_getter=lambda args: args.sonar_modules
532+
),
533+
Property(
534+
name=SONAR_SCANNER_OPTS,
535+
default_value=None,
536+
cli_getter=None,
537+
deprecation_message="SONAR_SCANNER_OPTS is deprecated, please use SONAR_SCANNER_JAVA_OPTS instead."
527538
)
528539
]
529540
# fmt: on

src/pysonar_scanner/scannerengine.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@
2020
import json
2121
import logging
2222
import pathlib
23-
from dataclasses import dataclass
2423
import shlex
25-
from subprocess import Popen, PIPE
24+
from dataclasses import dataclass
25+
from subprocess import PIPE, Popen
2626
from threading import Thread
2727
from typing import IO, Any, Callable, Optional
2828

2929
from pysonar_scanner.api import EngineInfo, SonarQubeApi
3030
from pysonar_scanner.cache import Cache, CacheFile
31-
from pysonar_scanner.configuration.properties import SONAR_SCANNER_JAVA_OPTS
31+
from pysonar_scanner.configuration.properties import (
32+
SONAR_SCANNER_JAVA_OPTS,
33+
SONAR_SCANNER_OPTS,
34+
)
3235
from pysonar_scanner.exceptions import ChecksumException
3336
from pysonar_scanner.jre import JREResolvedPath
3437

@@ -148,6 +151,7 @@ def __init__(self, jre_path: JREResolvedPath, scanner_engine_path: pathlib.Path)
148151
def run(self, config: dict[str, Any]):
149152
# Extract Java options if present; they must influence the JVM invocation, not the scanner engine itself
150153
java_opts = config.get(SONAR_SCANNER_JAVA_OPTS)
154+
java_opts = config.get(SONAR_SCANNER_OPTS) if not java_opts else java_opts
151155

152156
cmd = self.__build_command(self.jre_path, self.scanner_engine_path, java_opts)
153157
logging.debug(f"Command: {cmd}")
@@ -173,7 +177,11 @@ def __build_command(
173177

174178
def __config_to_json(self, config: dict[str, Any]) -> str:
175179
# SONAR_SCANNER_JAVA_OPTS are properties that shouldn't be passed to the engine, only to the JVM
176-
scanner_properties = [{"key": k, "value": v} for k, v in config.items() if k != SONAR_SCANNER_JAVA_OPTS]
180+
scanner_properties = [
181+
{"key": k, "value": v}
182+
for k, v in config.items()
183+
if k != SONAR_SCANNER_JAVA_OPTS and k != SONAR_SCANNER_OPTS
184+
]
177185
return json.dumps({"scannerProperties": scanner_properties})
178186

179187
def __decompose_java_opts(self, java_opts: str) -> list[str]:

tests/test_environment_variables.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
SONAR_TOKEN,
3232
SONAR_USER_HOME,
3333
SONAR_PROJECT_KEY,
34+
SONAR_SCANNER_OPTS,
3435
)
3536

3637

@@ -134,3 +135,27 @@ def test_environment_variables_priority_over_json_params(self):
134135
}
135136
self.assertEqual(len(properties), 3)
136137
self.assertDictEqual(properties, expected_properties)
138+
139+
@patch("pysonar_scanner.configuration.environment_variables.logging")
140+
def test_SONAR_SCANNER_OPTS(self, mock_logging):
141+
env = {
142+
"SONAR_TOKEN": "my-token",
143+
"SONAR_HOST_URL": "https://sonarqube.example.com",
144+
"SONAR_USER_HOME": "/custom/sonar/home",
145+
"SONAR_SCANNER_OPTS": "-Xmx1024m -XX:MaxPermSize=256m",
146+
"SONAR_REGION": "us",
147+
}
148+
with patch.dict("os.environ", env, clear=True):
149+
properties = environment_variables.load()
150+
expected_properties = {
151+
SONAR_TOKEN: "my-token",
152+
SONAR_HOST_URL: "https://sonarqube.example.com",
153+
SONAR_USER_HOME: "/custom/sonar/home",
154+
SONAR_SCANNER_OPTS: "-Xmx1024m -XX:MaxPermSize=256m",
155+
SONAR_REGION: "us",
156+
}
157+
self.assertEqual(len(properties), 5)
158+
self.assertDictEqual(properties, expected_properties)
159+
mock_logging.warning.assert_called_once_with(
160+
"SONAR_SCANNER_OPTS is deprecated, please use SONAR_SCANNER_JAVA_OPTS instead.",
161+
)

tests/unit/test_scannerengine.py

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
import pathlib
2323
import unittest
2424
from subprocess import PIPE
25-
from unittest.mock import Mock
26-
from unittest.mock import patch, MagicMock
25+
from unittest.mock import MagicMock, Mock, patch
2726

2827
import pyfakefs.fake_filesystem_unittest as pyfakefs
2928

30-
from pysonar_scanner import cache
31-
from pysonar_scanner import scannerengine
32-
from pysonar_scanner.configuration.properties import SONAR_SCANNER_JAVA_OPTS
29+
from pysonar_scanner import cache, scannerengine
30+
from pysonar_scanner.configuration.properties import (
31+
SONAR_SCANNER_JAVA_OPTS,
32+
SONAR_SCANNER_OPTS,
33+
)
3334
from pysonar_scanner.exceptions import ChecksumException
3435
from pysonar_scanner.scannerengine import (
3536
LogLine,
@@ -48,7 +49,10 @@ def test_without_stacktrace(self):
4849
def test_with_stacktrace(self):
4950
line = '{"level":"INFO","message":"a message", "stacktrace":"a stacktrace"}'
5051
log_line = scannerengine.parse_log_line(line)
51-
self.assertEqual(log_line, LogLine(level="INFO", message="a message", stacktrace="a stacktrace"))
52+
self.assertEqual(
53+
log_line,
54+
LogLine(level="INFO", message="a message", stacktrace="a stacktrace"),
55+
)
5256

5357
def test_invalid_json(self):
5458
line = '"level":"INFO","message":"a message", "stacktrace":"a stacktrace"}'
@@ -62,7 +66,6 @@ def test_no_level(self):
6266

6367

6468
class TestCmdExecutor(unittest.TestCase):
65-
6669
@patch("pysonar_scanner.scannerengine.Popen")
6770
def test_execute_successful(self, mock_popen):
6871
mock_process = MagicMock()
@@ -119,11 +122,17 @@ def test_to_logging_level(self):
119122
self.assertEqual(LogLine(level="UNKNOWN", message="").get_logging_level(), logging.INFO)
120123

121124
def test_default_log_line_listener(self):
122-
with self.subTest("log line without stacktrace"), self.assertLogs(level="INFO") as logs:
125+
with (
126+
self.subTest("log line without stacktrace"),
127+
self.assertLogs(level="INFO") as logs,
128+
):
123129
scannerengine.default_log_line_listener(LogLine(level="INFO", message="info1", stacktrace=None))
124130
self.assertEqual(logs.output, ["INFO:root:info1"])
125131

126-
with self.subTest("log line with stacktrace"), self.assertLogs(level="INFO") as logs:
132+
with (
133+
self.subTest("log line with stacktrace"),
134+
self.assertLogs(level="INFO") as logs,
135+
):
127136
default_log_line_listener(LogLine(level="WARN", message="info2", stacktrace="a stacktrace"))
128137
self.assertEqual(logs.output, ["WARNING:root:info2", "WARNING:root:a stacktrace"])
129138

@@ -161,7 +170,8 @@ def test_command_building(self, execute_mock):
161170
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(config)
162171

163172
execute_mock.assert_called_once_with(
164-
[str(java_path), "-jar", str(pathlib.Path("/test/scanner-engine.jar"))], expected_std_in
173+
[str(java_path), "-jar", str(pathlib.Path("/test/scanner-engine.jar"))],
174+
expected_std_in,
165175
)
166176

167177
@patch("pysonar_scanner.scannerengine.CmdExecutor")
@@ -280,6 +290,61 @@ def test_java_opts_edge_cases(self, execute_mock):
280290
self.assertEqual(actual_command[-2], "-jar")
281291
self.assertEqual(actual_command[-1], str(scanner_engine_mock))
282292

293+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
294+
def test_command_building_with_scanner_opts(self, execute_mock):
295+
config = {
296+
"sonar.token": "myToken",
297+
"sonar.projectKey": "myProjectKey",
298+
SONAR_SCANNER_OPTS: "-Xmx1024m -XX:MaxPermSize=256m",
299+
}
300+
301+
java_path = pathlib.Path("jre/bin/java")
302+
jre_resolve_path_mock = Mock()
303+
jre_resolve_path_mock.path = java_path
304+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
305+
306+
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(config)
307+
308+
called_args = execute_mock.call_args[0]
309+
actual_command = called_args[0]
310+
311+
expected_command = [
312+
str(java_path),
313+
"-Xmx1024m",
314+
"-XX:MaxPermSize=256m",
315+
"-jar",
316+
str(scanner_engine_mock),
317+
]
318+
self.assertEqual(actual_command, expected_command)
319+
320+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
321+
def test_command_ignore_scanner_opts_for_java_opts(self, execute_mock):
322+
config = {
323+
"sonar.token": "myToken",
324+
"sonar.projectKey": "myProjectKey",
325+
SONAR_SCANNER_OPTS: "-Xmx512m -XX:MaxPermSize=128m",
326+
SONAR_SCANNER_JAVA_OPTS: "-Xmx1024m -XX:MaxPermSize=256m",
327+
}
328+
329+
java_path = pathlib.Path("jre/bin/java")
330+
jre_resolve_path_mock = Mock()
331+
jre_resolve_path_mock.path = java_path
332+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
333+
334+
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(config)
335+
336+
called_args = execute_mock.call_args[0]
337+
actual_command = called_args[0]
338+
339+
expected_command = [
340+
str(java_path),
341+
"-Xmx1024m",
342+
"-XX:MaxPermSize=256m",
343+
"-jar",
344+
str(scanner_engine_mock),
345+
]
346+
self.assertEqual(actual_command, expected_command)
347+
283348

284349
class TestScannerEngineProvisioner(pyfakefs.TestCase):
285350
def setUp(self):
@@ -304,7 +369,9 @@ def test_happy_path(self):
304369
def test_happy_path_with_download_url(self):
305370
with sq_api_utils.sq_api_mocker() as mocker:
306371
mocker.mock_analysis_engine(
307-
filename="scanner-engine.jar", sha256=self.test_file_checksum, download_url="http://example.com"
372+
filename="scanner-engine.jar",
373+
sha256=self.test_file_checksum,
374+
download_url="http://example.com",
308375
)
309376
mocker.mock_download_url(url="http://example.com", body=self.test_file_content)
310377

@@ -331,7 +398,10 @@ def test_scanner_engine_is_cached(self):
331398
self.assertEqual(engine_download_rsps.call_count, 0)
332399

333400
def test_checksum_is_invalid(self):
334-
with self.assertRaises(ChecksumException), sq_api_utils.sq_api_mocker() as mocker:
401+
with (
402+
self.assertRaises(ChecksumException),
403+
sq_api_utils.sq_api_mocker() as mocker,
404+
):
335405
mocker.mock_analysis_engine(filename="scanner-engine.jar", sha256="invalid-checksum")
336406
mocker.mock_analysis_engine_download(body=self.test_file_content)
337407

0 commit comments

Comments
 (0)