Skip to content

Commit 33f72bb

Browse files
authored
SCANPY-177 SONAR_SCANNER_JAVA_OPTS should be used for the spawned JRE (#211)
1 parent e69965e commit 33f72bb

File tree

2 files changed

+138
-3
lines changed

2 files changed

+138
-3
lines changed

src/pysonar_scanner/scannerengine.py

Lines changed: 21 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,35 @@ 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+
# 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]
162177
return json.dumps({"scannerProperties": scanner_properties})
178+
179+
def __decompose_java_opts(self, java_opts: str) -> list[str]:
180+
return shlex.split(java_opts.strip())

tests/unit/test_scannerengine.py

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

167284
class TestScannerEngineProvisioner(pyfakefs.TestCase):
168285
def setUp(self):

0 commit comments

Comments
 (0)