Skip to content

Commit 6d0c9ba

Browse files
committed
SCANPY-177 SONAR_SCANNER_JAVA_OPTS should be used for the spawned JRE
1 parent aa51f25 commit 6d0c9ba

File tree

2 files changed

+153
-3
lines changed

2 files changed

+153
-3
lines changed

src/pysonar_scanner/scannerengine.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import logging
2222
import pathlib
2323
from dataclasses import dataclass
24+
import shlex
2425
from subprocess import Popen, PIPE
2526
from threading import Thread
2627
from typing import IO, Any, Callable, Optional
2728

2829
from pysonar_scanner.api import EngineInfo, SonarQubeApi
2930
from pysonar_scanner.cache import Cache, CacheFile
31+
from pysonar_scanner.configuration.properties import SONAR_SCANNER_JAVA_OPTS
3032
from pysonar_scanner.exceptions import ChecksumException
3133
from pysonar_scanner.jre import JREResolvedPath
3234

@@ -144,19 +146,39 @@ def __init__(self, jre_path: JREResolvedPath, scanner_engine_path: pathlib.Path)
144146
self.scanner_engine_path = scanner_engine_path
145147

146148
def run(self, config: dict[str, Any]):
147-
cmd = self.__build_command(self.jre_path, self.scanner_engine_path)
149+
# Extract Java options if present; they must influence the JVM invocation, not the scanner engine itself
150+
java_opts = config.get(SONAR_SCANNER_JAVA_OPTS)
151+
152+
cmd = self.__build_command(self.jre_path, self.scanner_engine_path, java_opts)
148153
logging.debug(f"Command: {cmd}")
149154
properties_str = self.__config_to_json(config)
150155
logging.debug(f"Properties: {properties_str}")
151156
return CmdExecutor(cmd, properties_str).execute()
152157

153-
def __build_command(self, jre_path: JREResolvedPath, scanner_engine_path: pathlib.Path) -> list[str]:
158+
def __build_command(
159+
self,
160+
jre_path: JREResolvedPath,
161+
scanner_engine_path: pathlib.Path,
162+
java_opts: Optional[str] = None,
163+
) -> list[str]:
154164
cmd: list[str] = []
155165
cmd.append(str(jre_path.path))
166+
167+
if java_opts:
168+
cmd.extend(self.__decompose_java_opts(java_opts))
169+
156170
cmd.append("-jar")
157171
cmd.append(str(scanner_engine_path))
158172
return cmd
159173

160174
def __config_to_json(self, config: dict[str, Any]) -> str:
161-
scanner_properties = [{"key": k, "value": v} for k, v in config.items()]
175+
# Remove sonar.scanner.javaOpts from the properties passed to the engine, as per guidelines
176+
scanner_properties = [
177+
{"key": k, "value": v}
178+
for k, v in config.items()
179+
if k != SONAR_SCANNER_JAVA_OPTS
180+
]
162181
return json.dumps({"scannerProperties": scanner_properties})
182+
183+
def __decompose_java_opts(self, java_opts: str) -> list[str]:
184+
return shlex.split(java_opts.strip())

tests/unit/test_scannerengine.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
from pysonar_scanner import cache
3131
from pysonar_scanner import scannerengine
32+
from pysonar_scanner.configuration.properties import SONAR_SCANNER_JAVA_OPTS
3233
from pysonar_scanner.exceptions import ChecksumException
3334
from pysonar_scanner.scannerengine import (
3435
LogLine,
@@ -163,6 +164,133 @@ def test_command_building(self, execute_mock):
163164
[str(java_path), "-jar", str(pathlib.Path("/test/scanner-engine.jar"))], expected_std_in
164165
)
165166

167+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
168+
def test_command_building_with_java_opts_basic(self, execute_mock):
169+
"""Test that java_opts are added to the command"""
170+
config = {
171+
"sonar.token": "myToken",
172+
"sonar.projectKey": "myProjectKey",
173+
SONAR_SCANNER_JAVA_OPTS: "-Xmx1024m",
174+
}
175+
176+
java_path = pathlib.Path("jre/bin/java")
177+
jre_resolve_path_mock = Mock()
178+
jre_resolve_path_mock.path = java_path
179+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
180+
181+
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(
182+
config
183+
)
184+
185+
called_args = execute_mock.call_args[0]
186+
actual_command = called_args[0]
187+
188+
expected_command = [
189+
str(java_path),
190+
"-Xmx1024m",
191+
"-jar",
192+
str(scanner_engine_mock),
193+
]
194+
self.assertEqual(actual_command, expected_command)
195+
196+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
197+
def test_command_building_with_java_opts_multiple(self, execute_mock):
198+
"""Test java_opts with multiple space-separated arguments"""
199+
config = {
200+
"sonar.token": "myToken",
201+
"sonar.projectKey": "myProjectKey",
202+
SONAR_SCANNER_JAVA_OPTS: "-Xmx1024m -XX:MaxPermSize=256m",
203+
}
204+
205+
java_path = pathlib.Path("jre/bin/java")
206+
jre_resolve_path_mock = Mock()
207+
jre_resolve_path_mock.path = java_path
208+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
209+
210+
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(
211+
config
212+
)
213+
214+
called_args = execute_mock.call_args[0]
215+
actual_command = called_args[0]
216+
217+
expected_command = [
218+
str(java_path),
219+
"-Xmx1024m",
220+
"-XX:MaxPermSize=256m",
221+
"-jar",
222+
str(scanner_engine_mock),
223+
]
224+
self.assertEqual(actual_command, expected_command)
225+
226+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
227+
def test_java_opts_filtered_from_properties(self, execute_mock):
228+
"""Test that SONAR_SCANNER_JAVA_OPTS is excluded from scanner properties JSON"""
229+
config = {
230+
"sonar.token": "myToken",
231+
"sonar.projectKey": "myProjectKey",
232+
SONAR_SCANNER_JAVA_OPTS: "-Xmx1024m",
233+
"sonar.host.url": "https://sonar.example.com",
234+
}
235+
236+
java_path = pathlib.Path("jre/bin/java")
237+
jre_resolve_path_mock = Mock()
238+
jre_resolve_path_mock.path = java_path
239+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
240+
241+
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(
242+
config
243+
)
244+
245+
called_args = execute_mock.call_args[0]
246+
actual_properties_str = called_args[1]
247+
actual_properties = json.loads(actual_properties_str)
248+
249+
property_keys = [prop["key"] for prop in actual_properties["scannerProperties"]]
250+
self.assertNotIn(SONAR_SCANNER_JAVA_OPTS, property_keys)
251+
252+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
253+
def test_java_opts_edge_cases(self, execute_mock):
254+
"""Test edge cases for java_opts handling"""
255+
java_path = pathlib.Path("jre/bin/java")
256+
jre_resolve_path_mock = Mock()
257+
jre_resolve_path_mock.path = java_path
258+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
259+
260+
test_cases = [
261+
# (java_opts_value, expected_command_length, description)
262+
(None, 3, "None java_opts"),
263+
("", 3, "Empty string java_opts"),
264+
(" ", 3, "Whitespace-only java_opts"),
265+
]
266+
267+
for java_opts_value, expected_length, description in test_cases:
268+
with self.subTest(description=description):
269+
execute_mock.reset_mock()
270+
271+
config = {
272+
"sonar.token": "myToken",
273+
"sonar.projectKey": "myProjectKey",
274+
}
275+
if java_opts_value is not None:
276+
config[SONAR_SCANNER_JAVA_OPTS] = java_opts_value
277+
278+
scannerengine.ScannerEngine(
279+
jre_resolve_path_mock, scanner_engine_mock
280+
).run(config)
281+
282+
called_args = execute_mock.call_args[0]
283+
actual_command = called_args[0]
284+
285+
self.assertEqual(
286+
len(actual_command),
287+
expected_length,
288+
f"Expected command length {expected_length} for {description}",
289+
)
290+
291+
self.assertEqual(actual_command[0], str(java_path))
292+
self.assertEqual(actual_command[-2], "-jar")
293+
self.assertEqual(actual_command[-1], str(scanner_engine_mock))
166294

167295
class TestScannerEngineProvisioner(pyfakefs.TestCase):
168296
def setUp(self):

0 commit comments

Comments
 (0)