Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/pysonar_scanner/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
Key,
)
from pysonar_scanner.exceptions import MissingKeyException, SonarQubeApiException
from pysonar_scanner.utils import Arch, Os, remove_trailing_slash
from pysonar_scanner.utils import remove_trailing_slash, OsStr, ArchStr


@dataclass(frozen=True)
Expand Down Expand Up @@ -211,12 +211,9 @@ def download_analysis_engine(self, handle: typing.BinaryIO) -> None:
except requests.RequestException as e:
raise SonarQubeApiException("Error while fetching the analysis engine") from e

def get_analysis_jres(self, os: Optional[Os] = None, arch: Optional[Arch] = None) -> list[JRE]:
def get_analysis_jres(self, os: OsStr, arch: ArchStr) -> list[JRE]:
try:
params = {
"os": os.value if os else None,
"arch": arch.value if arch else None,
}
params = {"os": os, "arch": arch}
res = requests.get(
f"{self.base_urls.api_base_url}/analysis/jres",
auth=self.auth,
Expand Down
4 changes: 3 additions & 1 deletion src/pysonar_scanner/configuration/configuration_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from pysonar_scanner.configuration.pyproject_toml import TomlConfigurationLoader
from pysonar_scanner.configuration.properties import SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, Key
from pysonar_scanner.configuration.properties import PROPERTIES
from pysonar_scanner.configuration import sonar_project_properties, environment_variables
from pysonar_scanner.configuration import sonar_project_properties, environment_variables, dynamic_defaults_loader

from pysonar_scanner.exceptions import MissingKeyException

Expand All @@ -38,6 +38,7 @@ def load() -> dict[Key, any]:
# each property loader is required to return NO default values.
# E.g. if no property has been set, an empty dict must be returned.
# Default values should be set through the get_static_default_properties() method

cli_properties = CliConfigurationLoader.load()
# CLI properties have a higher priority than properties file,
# but we need to resolve them first to load the properties file
Expand All @@ -48,6 +49,7 @@ def load() -> dict[Key, any]:
toml_properties = TomlConfigurationLoader.load(toml_dir)

resolved_properties = get_static_default_properties()
resolved_properties.update(dynamic_defaults_loader.load())
resolved_properties.update(toml_properties.project_properties)
resolved_properties.update(sonar_project_properties.load(base_dir))
resolved_properties.update(toml_properties.sonar_properties)
Expand Down
36 changes: 36 additions & 0 deletions src/pysonar_scanner/configuration/dynamic_defaults_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#
# Sonar Scanner Python
# Copyright (C) 2011-2024 SonarSource SA.
# mailto:info AT sonarsource DOT com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
#
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
import os
from typing import Dict

from pysonar_scanner.configuration.properties import Key, SONAR_SCANNER_OS, SONAR_SCANNER_ARCH, SONAR_PROJECT_BASE_DIR
from pysonar_scanner import utils


def load() -> Dict[Key, any]:
"""
Load dynamically computed default properties
"""
properties = {
SONAR_SCANNER_OS: utils.get_os().value,
SONAR_SCANNER_ARCH: utils.get_arch().value,
SONAR_PROJECT_BASE_DIR: os.getcwd(),
}
return properties
2 changes: 1 addition & 1 deletion src/pysonar_scanner/configuration/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def env_variable_name(self) -> str:
),
Property(
name=SONAR_USER_HOME,
default_value="~/.sonar",
default_value=None,
cli_getter=lambda args: args.sonar_user_home
),
Property(
Expand Down
10 changes: 6 additions & 4 deletions src/pysonar_scanner/jre.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ def from_string(path: str) -> "JREResolvedPath":


class JREProvisioner:
def __init__(self, api: SonarQubeApi, cache: Cache):
def __init__(self, api: SonarQubeApi, cache: Cache, sonar_scanner_os: str, sonar_scanner_arch: str):
self.api = api
self.cache = cache
self.sonar_scanner_os = sonar_scanner_os
self.sonar_scanner_arch = sonar_scanner_arch

def provision(self) -> JREResolvedPath:
jre, resolved_path = self.__attempt_provisioning_jre_with_retry()
Expand All @@ -67,7 +69,7 @@ def __attempt_provisioning_jre_with_retry(self) -> tuple[JRE, pathlib.Path]:
jre_and_resolved_path = self.__attempt_provisioning_jre()
if jre_and_resolved_path is None:
raise ChecksumException(
f"Failed to download and verify JRE for {utils.get_os().value} and {utils.get_arch().value}"
f"Failed to download and verify JRE for {self.sonar_scanner_os} and {self.sonar_scanner_arch}"
)

return jre_and_resolved_path
Expand All @@ -83,10 +85,10 @@ def __attempt_provisioning_jre(self) -> Optional[tuple[JRE, pathlib.Path]]:
return (jre, jre_path) if jre_path is not None else None

def __get_available_jre(self) -> JRE:
jres = self.api.get_analysis_jres(os=utils.get_os(), arch=utils.get_arch())
jres = self.api.get_analysis_jres(os=self.sonar_scanner_os, arch=self.sonar_scanner_arch)
if len(jres) == 0:
raise NoJreAvailableException(
f"No JREs are available for {utils.get_os().value} and {utils.get_arch().value}"
f"No JREs are available for {self.sonar_scanner_os} and {self.sonar_scanner_arch}"
)
return jres[0]

Expand Down
3 changes: 2 additions & 1 deletion src/pysonar_scanner/scannerengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

from pysonar_scanner.api import SonarQubeApi
from pysonar_scanner.cache import Cache, CacheFile
from pysonar_scanner.configuration.properties import SONAR_SCANNER_OS, SONAR_SCANNER_ARCH
from pysonar_scanner.exceptions import ChecksumException, SQTooOldException
from pysonar_scanner.jre import JREProvisioner, JREResolvedPath, JREResolver, JREResolverConfiguration
from subprocess import Popen, PIPE
Expand Down Expand Up @@ -175,6 +176,6 @@ def __version_check(self):
)

def __resolve_jre(self, config: dict[str, any]) -> JREResolvedPath:
jre_provisionner = JREProvisioner(self.api, self.cache)
jre_provisionner = JREProvisioner(self.api, self.cache, config[SONAR_SCANNER_OS], config[SONAR_SCANNER_ARCH])
jre_resolver = JREResolver(JREResolverConfiguration.from_dict(config), jre_provisionner)
return jre_resolver.resolve_jre()
19 changes: 11 additions & 8 deletions src/pysonar_scanner/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import typing
from enum import Enum

OsStr = typing.Literal["windows", "linux", "mac", "alpine", "other"]
ArchStr = typing.Literal["x64", "aarch64", "other"]


def remove_trailing_slash(url: str) -> str:
return url.rstrip("/ ").lstrip()
Expand All @@ -36,11 +39,11 @@ def calculate_checksum(filehandle: typing.BinaryIO) -> str:


class Os(Enum):
WINDOWS = "windows"
LINUX = "linux"
MACOS = "mac"
ALPINE = "alpine"
OTHER = "other"
WINDOWS: OsStr = "windows"
LINUX: OsStr = "linux"
MACOS: OsStr = "mac"
ALPINE: OsStr = "alpine"
OTHER: OsStr = "other"


def get_os() -> Os:
Expand Down Expand Up @@ -70,9 +73,9 @@ def is_alpine() -> bool:


class Arch(Enum):
X64 = "x64"
AARCH64 = "aarch64"
OTHER = "other"
X64: ArchStr = "x64"
AARCH64: ArchStr = "aarch64"
OTHER: ArchStr = "other"


def get_arch() -> Arch:
Expand Down
4 changes: 2 additions & 2 deletions tests/sq_api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ def mock_analysis_engine_download(self, body: bytes = b"", status: int = 200) ->
def mock_analysis_jres(
self,
body: Optional[list[dict]] = None,
os_matcher: Optional[str] = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why this was removed...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In absolute, it probably makes sense to keep it. However, doing so breaks the existing tests where no matcher is defined because we'll now always provide os and arch.
Since we already have quite a few tests for the configuration of the resolver, I felt it didn't really make sense to perform a complete refactoring of the tests and add more to have specific matchers for os/architectures, so I decided to drop it.
If you think we still need it, I can revisit and adapt the tests though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I added the matchers back, with default values, as well as a small test using different OS/Arch combination to make sure that part works as well. Thanks for the feedback!

arch_matcher: Optional[str] = None,
status: int = 200,
os_matcher: Optional[str] = "linux",
arch_matcher: Optional[str] = "x64",
) -> responses.BaseResponse:
return self.rsps.get(
url=f"{self.api_url}/analysis/jres",
Expand Down
15 changes: 11 additions & 4 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,17 @@ def test_get_analysis_jres(self):
),
]

with self.subTest("get_analysis_jres works"), sq_api_mocker() as mocker:
with self.subTest("get_analysis_jres works (linux)"), sq_api_mocker() as mocker:
mocker.mock_analysis_jres([sq_api_utils.jre_to_dict(jre) for jre in expected_jres])

actual_jres = self.sq.get_analysis_jres()
actual_jres = self.sq.get_analysis_jres(os="linux", arch="x64")
self.assertEqual(actual_jres, expected_jres)

with self.subTest("get_analysis_jres works (windows)"), sq_api_mocker() as mocker:
mocker.mock_analysis_jres(
[sq_api_utils.jre_to_dict(jre) for jre in expected_jres], os_matcher="windows", arch_matcher="aarch64"
)
actual_jres = self.sq.get_analysis_jres(os="windows", arch="aarch64")
self.assertEqual(actual_jres, expected_jres)

with (
Expand All @@ -329,15 +336,15 @@ def test_get_analysis_jres(self):
self.assertRaises(SonarQubeApiException),
):
mocker.mock_analysis_jres(status=404)
self.sq.get_analysis_jres()
self.sq.get_analysis_jres(os="linux", arch="x64")

with (
self.subTest("get_analysis_jres returns error when keys are missing"),
sq_api_mocker() as mocker,
self.assertRaises(SonarQubeApiException),
):
mocker.mock_analysis_jres([{"id": "jre1"}])
self.sq.get_analysis_jres()
self.sq.get_analysis_jres(os="linux", arch="x64")

def test_download_analysis_jre(self):
jre_id = "jre1"
Expand Down
Loading