Skip to content

Commit b01ec0d

Browse files
SCANPY-155 Provide properties computed during bootstrapping to the scanner engine (#166)
1 parent d339b7a commit b01ec0d

File tree

5 files changed

+163
-90
lines changed

5 files changed

+163
-90
lines changed

src/pysonar_scanner/__main__.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,20 @@
2020

2121
from pysonar_scanner import app_logging
2222
from pysonar_scanner import cache
23-
from pysonar_scanner.api import get_base_urls, SonarQubeApi
23+
from pysonar_scanner.api import get_base_urls, SonarQubeApi, BaseUrls, MIN_SUPPORTED_SQ_VERSION
2424
from pysonar_scanner.configuration import configuration_loader
2525
from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader
26-
from pysonar_scanner.configuration.properties import SONAR_VERBOSE
27-
from pysonar_scanner.scannerengine import ScannerEngine
26+
from pysonar_scanner.configuration.properties import (
27+
SONAR_VERBOSE,
28+
SONAR_HOST_URL,
29+
SONAR_SCANNER_API_BASE_URL,
30+
SONAR_SCANNER_SONARCLOUD_URL,
31+
SONAR_SCANNER_PROXY_PORT,
32+
SONAR_SCANNER_JAVA_EXE_PATH,
33+
)
34+
from pysonar_scanner.exceptions import SQTooOldException
35+
from pysonar_scanner.jre import JREResolvedPath, JREProvisioner, JREResolver, JREResolverConfiguration
36+
from pysonar_scanner.scannerengine import ScannerEngine, ScannerEngineProvisioner
2837

2938

3039
def scan():
@@ -34,16 +43,53 @@ def scan():
3443
set_logging_options(config)
3544

3645
cache_manager = cache.get_default()
37-
api = __build_api(config)
38-
scanner = ScannerEngine(api, cache_manager)
46+
47+
api = build_api(config)
48+
check_version(api)
49+
update_config_with_api_urls(config, api.base_urls)
50+
51+
scanner = create_scanner_engine(api, cache_manager, config)
52+
3953
return scanner.run(config)
4054

4155

4256
def set_logging_options(config):
4357
app_logging.configure_logging_level(verbose=config.get(SONAR_VERBOSE, False))
4458

4559

46-
def __build_api(config: dict[str, any]) -> SonarQubeApi:
60+
def build_api(config: dict[str, any]) -> SonarQubeApi:
4761
token = configuration_loader.get_token(config)
4862
base_urls = get_base_urls(config)
4963
return SonarQubeApi(base_urls, token)
64+
65+
66+
def check_version(api):
67+
if api.is_sonar_qube_cloud():
68+
return
69+
version = api.get_analysis_version()
70+
if not version.does_support_bootstrapping():
71+
raise SQTooOldException(
72+
f"Only SonarQube versions >= {MIN_SUPPORTED_SQ_VERSION} are supported, but got {version}"
73+
)
74+
75+
76+
def update_config_with_api_urls(config, base_urls: BaseUrls):
77+
config[SONAR_HOST_URL] = base_urls.base_url
78+
config[SONAR_SCANNER_API_BASE_URL] = base_urls.api_base_url
79+
if base_urls.is_sonar_qube_cloud:
80+
config[SONAR_SCANNER_SONARCLOUD_URL] = base_urls.base_url
81+
config[SONAR_SCANNER_PROXY_PORT] = "443" if base_urls.base_url.startswith("https") else "80"
82+
83+
84+
def create_scanner_engine(api, cache_manager, config):
85+
jre_path = create_jre(api, cache_manager, config)
86+
config[SONAR_SCANNER_JAVA_EXE_PATH] = str(jre_path.path)
87+
scanner_engine_path = ScannerEngineProvisioner(api, cache_manager).provision()
88+
scanner = ScannerEngine(jre_path, scanner_engine_path)
89+
return scanner
90+
91+
92+
def create_jre(api, cache, config: dict[str, any]) -> JREResolvedPath:
93+
jre_provisioner = JREProvisioner(api, cache)
94+
jre_resolver = JREResolver(JREResolverConfiguration.from_dict(config), jre_provisioner)
95+
return jre_resolver.resolve_jre()

src/pysonar_scanner/jre.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
SONAR_SCANNER_SKIP_JRE_PROVISIONING,
3939
SONAR_SCANNER_OS,
4040
Key,
41+
SONAR_SCANNER_ARCH,
4142
)
4243

4344

src/pysonar_scanner/scannerengine.py

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,18 @@
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-
from enum import Enum
2120
import json
2221
import logging
23-
from operator import le
2422
import pathlib
23+
from dataclasses import dataclass
24+
from subprocess import Popen, PIPE
2525
from threading import Thread
2626
from typing import IO, Callable, Optional
2727

28-
from dataclasses import dataclass
29-
30-
import pysonar_scanner.api as api
31-
3228
from pysonar_scanner.api import SonarQubeApi
3329
from pysonar_scanner.cache import Cache, CacheFile
34-
from pysonar_scanner.exceptions import ChecksumException, SQTooOldException
35-
from pysonar_scanner.jre import JREProvisioner, JREResolvedPath, JREResolver, JREResolverConfiguration
36-
from subprocess import Popen, PIPE
30+
from pysonar_scanner.exceptions import ChecksumException
31+
from pysonar_scanner.jre import JREResolvedPath
3732

3833

3934
@dataclass(frozen=True)
@@ -139,18 +134,12 @@ def __download_scanner_engine(self, cache_file: CacheFile) -> None:
139134

140135

141136
class ScannerEngine:
142-
def __init__(self, api: SonarQubeApi, cache: Cache):
143-
self.api = api
144-
self.cache = cache
145-
146-
def __fetch_scanner_engine(self) -> pathlib.Path:
147-
return ScannerEngineProvisioner(self.api, self.cache).provision()
137+
def __init__(self, jre_path: JREResolvedPath, scanner_engine_path: pathlib.Path):
138+
self.jre_path = jre_path
139+
self.scanner_engine_path = scanner_engine_path
148140

149141
def run(self, config: dict[str, any]):
150-
self.__version_check()
151-
jre_path = self.__resolve_jre(config)
152-
scanner_engine_path = self.__fetch_scanner_engine()
153-
cmd = self.__build_command(jre_path, scanner_engine_path)
142+
cmd = self.__build_command(self.jre_path, self.scanner_engine_path)
154143
properties_str = self.__config_to_json(config)
155144
return CmdExecutor(cmd, properties_str).execute()
156145

@@ -164,17 +153,3 @@ def __build_command(self, jre_path: JREResolvedPath, scanner_engine_path: pathli
164153
def __config_to_json(self, config: dict[str, any]) -> str:
165154
scanner_properties = [{"key": k, "value": v} for k, v in config.items()]
166155
return json.dumps({"scannerProperties": scanner_properties})
167-
168-
def __version_check(self):
169-
if self.api.is_sonar_qube_cloud():
170-
return
171-
version = self.api.get_analysis_version()
172-
if not version.does_support_bootstrapping():
173-
raise SQTooOldException(
174-
f"Only SonarQube versions >= {api.MIN_SUPPORTED_SQ_VERSION} are supported, but got {version}"
175-
)
176-
177-
def __resolve_jre(self, config: dict[str, any]) -> JREResolvedPath:
178-
jre_provisionner = JREProvisioner(self.api, self.cache)
179-
jre_resolver = JREResolver(JREResolverConfiguration.from_dict(config), jre_provisionner)
180-
return jre_resolver.resolve_jre()

tests/unit/test_main.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,96 @@
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-
from unittest.mock import patch
20+
import pathlib
21+
from pathlib import Path
22+
from unittest.mock import patch, Mock
2123

2224
from pyfakefs import fake_filesystem_unittest as pyfakefs
2325

24-
from pysonar_scanner.__main__ import scan
26+
from pysonar_scanner.__main__ import scan, check_version, create_jre
27+
from pysonar_scanner.api import SQVersion, SonarQubeApi
28+
from pysonar_scanner.cache import Cache
2529
from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader
26-
from pysonar_scanner.configuration.properties import SONAR_PROJECT_KEY, SONAR_TOKEN
27-
from pysonar_scanner.scannerengine import ScannerEngine
30+
from pysonar_scanner.configuration.properties import (
31+
SONAR_PROJECT_KEY,
32+
SONAR_TOKEN,
33+
SONAR_HOST_URL,
34+
SONAR_SCANNER_API_BASE_URL,
35+
SONAR_SCANNER_SONARCLOUD_URL,
36+
SONAR_SCANNER_PROXY_PORT,
37+
SONAR_SCANNER_OS,
38+
SONAR_SCANNER_ARCH,
39+
SONAR_SCANNER_JAVA_EXE_PATH,
40+
)
41+
from pysonar_scanner.exceptions import SQTooOldException
42+
from pysonar_scanner.jre import JREResolvedPath, JREResolver
43+
from pysonar_scanner.scannerengine import ScannerEngine, ScannerEngineProvisioner
44+
from tests.unit import sq_api_utils
2845

2946

3047
class TestMain(pyfakefs.TestCase):
31-
@patch.object(ConfigurationLoader, "load", return_value={SONAR_TOKEN: "myToken", SONAR_PROJECT_KEY: "myProjectKey"})
48+
@patch.object(
49+
ConfigurationLoader,
50+
"load",
51+
return_value={
52+
SONAR_TOKEN: "myToken",
53+
SONAR_PROJECT_KEY: "myProjectKey",
54+
SONAR_SCANNER_OS: "linux",
55+
SONAR_SCANNER_ARCH: "x64",
56+
},
57+
)
58+
@patch.object(ScannerEngineProvisioner, "provision", return_value=JREResolvedPath(Path("scanner_engine_path")))
59+
@patch("pysonar_scanner.__main__.create_jre", return_value=JREResolvedPath(Path("jre_path")))
3260
@patch.object(ScannerEngine, "run", return_value=0)
33-
def test_minimal_success_run(self, load_mock, run_mock):
61+
def test_minimal_success_run(self, run_mock, create_jre_mock, provision_mock, load_mock):
3462
exitcode = scan()
3563
self.assertEqual(exitcode, 0)
64+
65+
# Verify that run was called with the expected configuration
66+
run_mock.assert_called_once()
67+
config = run_mock.call_args[0][0] # Extract the configuration arg
68+
69+
# Check expected configuration with a single assertion
70+
expected_config = {
71+
SONAR_TOKEN: "myToken",
72+
SONAR_PROJECT_KEY: "myProjectKey",
73+
SONAR_SCANNER_OS: "linux",
74+
SONAR_SCANNER_ARCH: "x64",
75+
SONAR_HOST_URL: "https://sonarcloud.io",
76+
SONAR_SCANNER_API_BASE_URL: "https://api.sonarcloud.io",
77+
SONAR_SCANNER_SONARCLOUD_URL: "https://sonarcloud.io",
78+
SONAR_SCANNER_PROXY_PORT: "443",
79+
SONAR_SCANNER_JAVA_EXE_PATH: "jre_path",
80+
}
81+
82+
self.assertEqual(expected_config, config)
83+
84+
def test_version_check_outdated_sonarqube(self):
85+
sq_cloud_api = sq_api_utils.get_sq_server()
86+
sq_cloud_api.get_analysis_version = Mock(return_value=SQVersion.from_str("9.9.9"))
87+
88+
with self.assertRaises(SQTooOldException):
89+
check_version(sq_cloud_api)
90+
91+
def test_version_check_recent_sonarqube(self):
92+
sq_cloud_api = sq_api_utils.get_sq_server()
93+
sq_cloud_api.get_analysis_version = Mock(return_value=SQVersion.from_str("10.7"))
94+
95+
check_version(sq_cloud_api)
96+
sq_cloud_api.get_analysis_version.assert_called_once()
97+
98+
def test_version_check_sonarqube_cloud(self):
99+
sq_cloud_api = sq_api_utils.get_sq_cloud()
100+
sq_cloud_api.get_analysis_version = Mock()
101+
102+
check_version(sq_cloud_api)
103+
sq_cloud_api.get_analysis_version.assert_not_called()
104+
105+
106+
@patch("pysonar_scanner.scannerengine.CmdExecutor")
107+
@patch.object(JREResolver, "resolve_jre")
108+
def test_get_jre(self, resolve_jre_mock):
109+
resolve_jre_mock.return_value = JREResolvedPath(pathlib.Path("jre/bin/java"))
110+
api = SonarQubeApi(Mock(), Mock())
111+
cache = Cache(Mock())
112+
create_jre(api, cache, {SONAR_SCANNER_OS: "linux", SONAR_SCANNER_ARCH: "x64"})

tests/unit/test_scannerengine.py

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,26 @@
1919
#
2020
import json
2121
import logging
22-
from math import log
23-
from subprocess import PIPE
24-
import unittest
2522
import pathlib
23+
import unittest
24+
from subprocess import PIPE
25+
from unittest.mock import Mock
26+
from unittest.mock import patch, MagicMock
27+
2628
import pyfakefs.fake_filesystem_unittest as pyfakefs
2729

28-
from pysonar_scanner import app_logging, cache
30+
from pysonar_scanner import cache
2931
from pysonar_scanner import scannerengine
32+
from pysonar_scanner.api import SQVersion
3033
from pysonar_scanner.exceptions import ChecksumException, SQTooOldException
31-
from pysonar_scanner.jre import JREProvisioner, JREResolvedPath, JREResolver
34+
from pysonar_scanner.jre import JREResolvedPath, JREResolver
3235
from pysonar_scanner.scannerengine import (
33-
CmdExecutor,
3436
LogLine,
3537
ScannerEngine,
3638
ScannerEngineProvisioner,
3739
default_log_line_listener,
38-
parse_log_line,
3940
)
40-
from unittest.mock import Mock
41-
42-
from pysonar_scanner.api import SQVersion
4341
from tests.unit import sq_api_utils
44-
from unittest.mock import patch, MagicMock
4542

4643

4744
class TestLogLine(unittest.TestCase):
@@ -138,59 +135,36 @@ def setUp(self):
138135
self.setUpPyfakefs()
139136

140137
@patch("pysonar_scanner.scannerengine.CmdExecutor")
141-
@patch.object(JREResolver, "resolve_jre")
142-
@patch.object(ScannerEngineProvisioner, "provision")
143-
def test_command_building(self, provision_mock, resolve_jre_mock, execute_mock):
144-
provision_mock.return_value = pathlib.Path("/test/scanner-engine.jar")
145-
resolve_jre_mock.return_value = JREResolvedPath(pathlib.Path("jre/bin/java"))
146-
138+
def test_command_building(self, execute_mock):
147139
config = {
148140
"sonar.token": "myToken",
149141
"sonar.projectKey": "myProjectKey",
142+
"sonar.scanner.os": "linux",
143+
"sonar.scanner.arch": "x64",
144+
"sonar.scanner.javaExePath": "jre/bin/java",
150145
}
151146

152147
expected_std_in = json.dumps(
153148
{
154149
"scannerProperties": [
155150
{"key": "sonar.token", "value": "myToken"},
156151
{"key": "sonar.projectKey", "value": "myProjectKey"},
152+
{"key": "sonar.scanner.os", "value": "linux"},
153+
{"key": "sonar.scanner.arch", "value": "x64"},
154+
{"key": "sonar.scanner.javaExePath", "value": "jre/bin/java"},
157155
]
158156
}
159157
)
158+
jre_resolve_path_mock = Mock()
159+
jre_resolve_path_mock.path = pathlib.Path("jre/bin/java")
160+
scanner_engine_mock = pathlib.Path("/test/scanner-engine.jar")
160161

161-
scannerengine.ScannerEngine(sq_api_utils.get_sq_cloud(), cache.get_default()).run(config)
162+
scannerengine.ScannerEngine(jre_resolve_path_mock, scanner_engine_mock).run(config)
162163

163164
execute_mock.assert_called_once_with(
164165
[pathlib.Path("jre/bin/java"), "-jar", pathlib.Path("/test/scanner-engine.jar")], expected_std_in
165166
)
166167

167-
def test_version_check(self):
168-
with self.subTest("SQ:Server is too old"):
169-
sq_cloud_api = sq_api_utils.get_sq_server()
170-
sq_cloud_api.get_analysis_version = Mock(return_value=SQVersion.from_str("9.9.9"))
171-
scannerengine = ScannerEngine(sq_cloud_api, cache.get_default())
172-
173-
with self.assertRaises(SQTooOldException):
174-
scannerengine._ScannerEngine__version_check()
175-
176-
with self.subTest("SQ:Server that is new than 10.6"):
177-
sq_cloud_api = sq_api_utils.get_sq_server()
178-
sq_cloud_api.get_analysis_version = Mock(return_value=SQVersion.from_str("10.7"))
179-
scannerengine = ScannerEngine(sq_cloud_api, cache.get_default())
180-
181-
scannerengine._ScannerEngine__version_check()
182-
183-
sq_cloud_api.get_analysis_version.assert_called_once()
184-
185-
with self.subTest("SQ:Cloud "):
186-
sq_cloud_api = sq_api_utils.get_sq_cloud()
187-
sq_cloud_api.get_analysis_version = Mock()
188-
scannerengine = ScannerEngine(sq_cloud_api, cache.get_default())
189-
190-
scannerengine._ScannerEngine__version_check()
191-
192-
sq_cloud_api.get_analysis_version.assert_not_called()
193-
194168

195169
class TestScannerEngineProvisioner(pyfakefs.TestCase):
196170
def setUp(self):

0 commit comments

Comments
 (0)