From 81763436ecc6b3b7906b86201bbb6f687b66e58e Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Wed, 26 Mar 2025 11:16:00 +0100 Subject: [PATCH 1/4] SCANPY-145 Dynamically infer the OS and Architecture properties --- src/pysonar_scanner/api.py | 9 +-- .../configuration/configuration_loader.py | 4 +- .../configuration/dynamic_defaults_loader.py | 36 ++++++++++ .../configuration/properties.py | 2 +- src/pysonar_scanner/jre.py | 10 +-- src/pysonar_scanner/scannerengine.py | 3 +- src/pysonar_scanner/utils.py | 38 ++++------- tests/sq_api_utils.py | 3 - tests/test_api.py | 6 +- tests/test_configuration_loader.py | 65 +++++++++++++++---- tests/test_jre.py | 49 +++++++------- tests/test_scannerengine.py | 4 ++ tests/test_utils.py | 16 ++--- 13 files changed, 153 insertions(+), 92 deletions(-) create mode 100644 src/pysonar_scanner/configuration/dynamic_defaults_loader.py diff --git a/src/pysonar_scanner/api.py b/src/pysonar_scanner/api.py index 7d152289..45ec3046 100644 --- a/src/pysonar_scanner/api.py +++ b/src/pysonar_scanner/api.py @@ -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) @@ -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, diff --git a/src/pysonar_scanner/configuration/configuration_loader.py b/src/pysonar_scanner/configuration/configuration_loader.py index ce037066..bdcb90fd 100644 --- a/src/pysonar_scanner/configuration/configuration_loader.py +++ b/src/pysonar_scanner/configuration/configuration_loader.py @@ -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 @@ -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 @@ -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) diff --git a/src/pysonar_scanner/configuration/dynamic_defaults_loader.py b/src/pysonar_scanner/configuration/dynamic_defaults_loader.py new file mode 100644 index 00000000..ebfbbb7a --- /dev/null +++ b/src/pysonar_scanner/configuration/dynamic_defaults_loader.py @@ -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(), + SONAR_SCANNER_ARCH: utils.get_arch(), + SONAR_PROJECT_BASE_DIR: os.getcwd(), + } + return properties diff --git a/src/pysonar_scanner/configuration/properties.py b/src/pysonar_scanner/configuration/properties.py index c8ca1bed..67f6370d 100644 --- a/src/pysonar_scanner/configuration/properties.py +++ b/src/pysonar_scanner/configuration/properties.py @@ -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( diff --git a/src/pysonar_scanner/jre.py b/src/pysonar_scanner/jre.py index 2cc1d9d4..1ec3fc88 100644 --- a/src/pysonar_scanner/jre.py +++ b/src/pysonar_scanner/jre.py @@ -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() @@ -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 @@ -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] diff --git a/src/pysonar_scanner/scannerengine.py b/src/pysonar_scanner/scannerengine.py index 34169b76..c980fc0d 100644 --- a/src/pysonar_scanner/scannerengine.py +++ b/src/pysonar_scanner/scannerengine.py @@ -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 @@ -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() diff --git a/src/pysonar_scanner/utils.py b/src/pysonar_scanner/utils.py index 4bd7d115..aec20e0b 100644 --- a/src/pysonar_scanner/utils.py +++ b/src/pysonar_scanner/utils.py @@ -21,7 +21,9 @@ import pathlib import platform import typing -from enum import Enum + +OsStr = typing.Literal["windows", "linux", "mac", "alpine", "other"] +ArchStr = typing.Literal["x86_64", "aarch64", "x86"] def remove_trailing_slash(url: str) -> str: @@ -35,15 +37,7 @@ def calculate_checksum(filehandle: typing.BinaryIO) -> str: return sha256_hash.hexdigest() -class Os(Enum): - WINDOWS = "windows" - LINUX = "linux" - MACOS = "mac" - ALPINE = "alpine" - OTHER = "other" - - -def get_os() -> Os: +def get_os() -> OsStr: def is_alpine() -> bool: try: os_release = pathlib.Path("/etc/os-release") @@ -58,30 +52,24 @@ def is_alpine() -> bool: os_name = platform.system() if os_name == "Windows": - return Os.WINDOWS + return "windows" elif os_name == "Darwin": - return Os.MACOS + return "mac" elif os_name == "Linux": if is_alpine(): - return Os.ALPINE + return "alpine" else: - return Os.LINUX - return Os.OTHER - - -class Arch(Enum): - X64 = "x64" - AARCH64 = "aarch64" - OTHER = "other" + return "linux" + return "other" -def get_arch() -> Arch: +def get_arch() -> ArchStr: machine = platform.machine().lower() if machine in ["amd64", "x86_64"]: - return Arch.X64 + return "x64" elif machine == "arm64": - return Arch.AARCH64 - return Arch.OTHER + return "aarch64" + return "other" def filter_none_values(dictionary: dict) -> dict: diff --git a/tests/sq_api_utils.py b/tests/sq_api_utils.py index 41b8c1b4..2b6214b4 100644 --- a/tests/sq_api_utils.py +++ b/tests/sq_api_utils.py @@ -79,8 +79,6 @@ 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, - arch_matcher: Optional[str] = None, status: int = 200, ) -> responses.BaseResponse: return self.rsps.get( @@ -89,7 +87,6 @@ def mock_analysis_jres( status=status, match=[ matchers.header_matcher({"Accept": "application/json"}), - matchers.query_param_matcher(utils.filter_none_values({"os": os_matcher, "arch": arch_matcher})), ], ) diff --git a/tests/test_api.py b/tests/test_api.py index a19e7aae..11eadb12 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -320,7 +320,7 @@ def test_get_analysis_jres(self): with self.subTest("get_analysis_jres works"), 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="x86_64") self.assertEqual(actual_jres, expected_jres) with ( @@ -329,7 +329,7 @@ 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="x86_64") with ( self.subTest("get_analysis_jres returns error when keys are missing"), @@ -337,7 +337,7 @@ def test_get_analysis_jres(self): self.assertRaises(SonarQubeApiException), ): mocker.mock_analysis_jres([{"id": "jre1"}]) - self.sq.get_analysis_jres() + self.sq.get_analysis_jres(os="linux", arch="x86_64") def test_download_analysis_jre(self): jre_id = "jre1" diff --git a/tests/test_configuration_loader.py b/tests/test_configuration_loader.py index b5b493c2..005a393f 100644 --- a/tests/test_configuration_loader.py +++ b/tests/test_configuration_loader.py @@ -46,11 +46,16 @@ SONAR_PYTHON_VERSION, SONAR_HOST_URL, SONAR_SCANNER_JAVA_OPTS, + SONAR_SCANNER_ARCH, + SONAR_SCANNER_OS, ) from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader, SONAR_PROJECT_BASE_DIR from pysonar_scanner.exceptions import MissingKeyException +# Mock utils.get_os and utils.get_arch at the module level +@patch("pysonar_scanner.utils.get_arch", return_value="x64") +@patch("pysonar_scanner.utils.get_os", return_value="linux") class TestConfigurationLoader(pyfakefs.TestCase): def setUp(self): self.maxDiff = None @@ -59,7 +64,7 @@ def setUp(self): self.env_patcher.start() @patch("sys.argv", ["myscript.py", "--token", "myToken", "--sonar-project-key", "myProjectKey"]) - def test_defaults(self): + def test_defaults(self, mock_get_os, mock_get_arch): configuration = ConfigurationLoader.load() expected_configuration = { SONAR_TOKEN: "myToken", @@ -69,22 +74,40 @@ def test_defaults(self): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_USER_HOME: "~/.sonar", + SONAR_PROJECT_BASE_DIR: "/", SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", + SONAR_SCANNER_OS: "linux", + SONAR_SCANNER_ARCH: "x64", } self.assertDictEqual(configuration, expected_configuration) @patch("pysonar_scanner.configuration.configuration_loader.get_static_default_properties", return_value={}) + @patch("pysonar_scanner.configuration.dynamic_defaults_loader.load", return_value={}) @patch("sys.argv", ["myscript.py"]) - def test_no_defaults_in_configuration_loaders(self, get_static_default_properties_mock): + def test_no_defaults_in_configuration_loaders( + self, get_static_default_properties_mock, mock_load, mock_get_os, mock_get_arch + ): config = ConfigurationLoader.load() self.assertDictEqual(config, {}) - def test_get_token(self): + @patch("pysonar_scanner.configuration.configuration_loader.get_static_default_properties", return_value={}) + @patch("sys.argv", ["myscript.py"]) + def test_dynamic_defaults_are_loaded(self, get_static_default_properties_mock, mock_get_os, mock_get_arch): + config = ConfigurationLoader.load() + self.assertDictEqual( + config, + { + SONAR_PROJECT_BASE_DIR: "/", + SONAR_SCANNER_OS: "linux", + SONAR_SCANNER_ARCH: "x64", + }, + ) + + def test_get_token(self, mock_get_os, mock_get_arch): with self.subTest("Token is present"): self.assertEqual(configuration_loader.get_token({SONAR_TOKEN: "myToken"}), "myToken") @@ -92,7 +115,7 @@ def test_get_token(self): configuration_loader.get_token({}) @patch("sys.argv", ["myscript.py", "--token", "myToken", "--sonar-project-key", "myProjectKey"]) - def test_load_sonar_project_properties(self): + def test_load_sonar_project_properties(self, mock_get_os, mock_get_arch): self.fs.create_file( "sonar-project.properties", @@ -117,12 +140,14 @@ def test_load_sonar_project_properties(self): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_USER_HOME: "~/.sonar", + SONAR_PROJECT_BASE_DIR: "/", SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", + SONAR_SCANNER_OS: "linux", + SONAR_SCANNER_ARCH: "x64", } self.assertDictEqual(configuration, expected_configuration) @@ -138,7 +163,7 @@ def test_load_sonar_project_properties(self): "custom/path", ], ) - def test_load_sonar_project_properties_from_custom_path(self): + def test_load_sonar_project_properties_from_custom_path(self, mock_get_os, mock_get_arch): self.fs.create_dir("custom/path") self.fs.create_file( "custom/path/sonar-project.properties", @@ -164,12 +189,13 @@ def test_load_sonar_project_properties_from_custom_path(self): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_USER_HOME: "~/.sonar", SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", + SONAR_SCANNER_OS: "linux", + SONAR_SCANNER_ARCH: "x64", } self.assertDictEqual(configuration, expected_configuration) @@ -185,7 +211,7 @@ def test_load_sonar_project_properties_from_custom_path(self): "custom/path", ], ) - def test_load_pyproject_toml_from_base_dir(self): + def test_load_pyproject_toml_from_base_dir(self, mock_get_os, mock_get_arch): self.fs.create_dir("custom/path") self.fs.create_file( "custom/path/pyproject.toml", @@ -212,12 +238,13 @@ def test_load_pyproject_toml_from_base_dir(self): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_USER_HOME: "~/.sonar", SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", + SONAR_SCANNER_OS: "linux", + SONAR_SCANNER_ARCH: "x64", } self.assertDictEqual(configuration, expected_configuration) @@ -233,7 +260,7 @@ def test_load_pyproject_toml_from_base_dir(self): "custom/path", ], ) - def test_load_pyproject_toml_from_toml_path(self): + def test_load_pyproject_toml_from_toml_path(self, mock_get_os, mock_get_arch): self.fs.create_dir("custom/path") self.fs.create_file( "custom/path/pyproject.toml", @@ -259,19 +286,21 @@ def test_load_pyproject_toml_from_toml_path(self): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_USER_HOME: "~/.sonar", + SONAR_PROJECT_BASE_DIR: "/", SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", + SONAR_SCANNER_OS: "linux", + SONAR_SCANNER_ARCH: "x64", TOML_PATH: "custom/path", } self.assertDictEqual(configuration, expected_configuration) @patch("sys.argv", ["myscript.py"]) @patch.dict("os.environ", {"SONAR_TOKEN": "TokenFromEnv", "SONAR_PROJECT_KEY": "KeyFromEnv"}, clear=True) - def test_load_from_env_variables_only(self): + def test_load_from_env_variables_only(self, mock_get_os, mock_get_arch): """Test that configuration can be loaded exclusively from environment variables""" configuration = ConfigurationLoader.load() @@ -303,7 +332,7 @@ def test_load_from_env_variables_only(self): }, clear=True, ) - def test_properties_priority(self): + def test_properties_priority(self, mock_get_os, mock_get_arch): """Test the priority order of different configuration sources: 1. CLI args (highest) 2. Environment variables @@ -375,3 +404,11 @@ def test_properties_priority(self): # CLI args have highest priority self.assertEqual(configuration[SONAR_PROJECT_KEY], "ProjectKeyFromCLI") self.assertEqual(configuration[SONAR_TOKEN], "myToken") # CLI overrides env var + + +# If you have test functions outside of classes, use patch as a decorator for each function +@patch("pysonar_scanner.utils.get_arch", return_value="x64") +@patch("pysonar_scanner.utils.get_os", return_value="linux") +def test_standalone_function(mock_get_os, mock_get_arch): + # ...existing test code... + pass diff --git a/tests/test_jre.py b/tests/test_jre.py index 33acadde..5531e2a9 100644 --- a/tests/test_jre.py +++ b/tests/test_jre.py @@ -39,8 +39,8 @@ import zipfile -@patch("pysonar_scanner.utils.get_os", return_value=utils.Os.LINUX) -@patch("pysonar_scanner.utils.get_arch", return_value=utils.Arch.AARCH64) +@patch("pysonar_scanner.utils.get_os", return_value="linux") +@patch("pysonar_scanner.utils.get_arch", return_value="aarch64") class TestJREProvisioner(pyfakefs.TestCase): def setUp(self): self.setUpPyfakefs(allow_root_user=False) @@ -56,8 +56,8 @@ def setUp(self): filename="fake_jre.zip", sha256="fakechecksum", java_path="fake_java", - os=utils.Os.WINDOWS.value, - arch=utils.Arch.X64.value, + os="windows", + arch="x64", download_url="http://example.com/fake_jre.zip", ) @@ -113,8 +113,8 @@ def __setup_tar_file(self): ) def test_if_patching_worked(self, get_os_mock, get_arch_mock): - self.assertEqual(utils.get_os(), utils.Os.LINUX) - self.assertEqual(utils.get_arch(), utils.Arch.AARCH64) + self.assertEqual(utils.get_os(), "linux") + self.assertEqual(utils.get_arch(), "aarch64") def test_successfully_downloading_jre(self, get_os_mock, get_arch_mock): class JRETestCase(TypedDict): @@ -138,11 +138,10 @@ class JRETestCase(TypedDict): jres = [testcase_jre, self.other_jre] with self.subTest(jre=testcase), sq_api_utils.sq_api_mocker() as mocker: mocker.mock_analysis_jres( - body=[sq_api_utils.jre_to_dict(jre) for jre in jres], os_matcher="linux", arch_matcher="aarch64" - ) + body=[sq_api_utils.jre_to_dict(jre) for jre in jres]) mocker.mock_analysis_jre_download(id=testcase_jre.id, body=testcase["bytes"], status=200) - provisioner = JREProvisioner(self.api, self.cache) + provisioner = JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()) jre_path = provisioner.provision() cache_file = self.cache.get_file(testcase_jre.filename, testcase["checksum"]) @@ -159,10 +158,10 @@ def test_invalid_checksum(self, *args): with self.assertRaises(ChecksumException), sq_api_utils.sq_api_mocker() as mocker: jre_dict = sq_api_utils.jre_to_dict(self.zip_jre) jre_dict["sha256"] = "invalid" - mocker.mock_analysis_jres(body=[jre_dict], os_matcher="linux", arch_matcher="aarch64") + mocker.mock_analysis_jres(body=[jre_dict]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) - JREProvisioner(self.api, self.cache).provision() + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() def test_retry_mechanism(self, *args): with sq_api_utils.sq_api_mocker() as mocker: @@ -170,11 +169,11 @@ def test_retry_mechanism(self, *args): jre_dict_with_invalid_checksum["sha256"] = "invalid" jre_dict = sq_api_utils.jre_to_dict(self.zip_jre) - mocker.mock_analysis_jres(body=[jre_dict_with_invalid_checksum], os_matcher="linux", arch_matcher="aarch64") - mocker.mock_analysis_jres(body=[jre_dict], os_matcher="linux", arch_matcher="aarch64") + mocker.mock_analysis_jres(body=[jre_dict_with_invalid_checksum]) + mocker.mock_analysis_jres(body=[jre_dict]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) - JREProvisioner(self.api, self.cache).provision() + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() cache_file = self.cache.get_file(self.zip_jre.filename, self.zip_checksum) self.assertTrue(cache_file.is_valid()) @@ -182,13 +181,13 @@ def test_retry_mechanism(self, *args): def test_already_cached(self, *args): with sq_api_utils.sq_api_mocker(assert_all_requests_are_fired=False) as mocker: jre_dict = sq_api_utils.jre_to_dict(self.zip_jre) - metadata_rsps = mocker.mock_analysis_jres(body=[jre_dict], os_matcher="linux", arch_matcher="aarch64") + metadata_rsps = mocker.mock_analysis_jres(body=[jre_dict]) download_rsps = mocker.mock_analysis_jre_download(id="zip_jre", status=500) with self.cache.get_file(self.zip_jre.filename, self.zip_checksum).open(mode="wb") as f: f.write(self.zip_bytes) - JREProvisioner(self.api, self.cache).provision() + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() cache_file = self.cache.get_file(self.zip_jre.filename, self.zip_checksum) self.assertTrue(cache_file.is_valid()) @@ -199,7 +198,7 @@ def test_already_cached(self, *args): def test_file_already_exists_with_invalid_checksum(self, *args): with sq_api_utils.sq_api_mocker() as mocker: jre_dict = sq_api_utils.jre_to_dict(self.zip_jre) - mocker.mock_analysis_jres(body=[jre_dict], os_matcher="linux", arch_matcher="aarch64") + mocker.mock_analysis_jres(body=[jre_dict]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) with self.cache.get_file(self.zip_jre.filename, self.zip_checksum).open(mode="wb") as f: @@ -209,20 +208,19 @@ def test_file_already_exists_with_invalid_checksum(self, *args): cache_file.is_valid(), msg="Cache file should have invalid checksum before provisioner ran" ) - JREProvisioner(self.api, self.cache).provision() + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() self.assertTrue(cache_file.is_valid(), msg="Cache file should have valid checksum after provisioner ran") def test_no_jre_available(self, *args): with self.assertRaises(NoJreAvailableException), sq_api_utils.sq_api_mocker() as mocker: - mocker.mock_analysis_jres(body=[], os_matcher="linux", arch_matcher="aarch64") - JREProvisioner(self.api, self.cache).provision() + mocker.mock_analysis_jres(body=[]) + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() def test_unzip_dir_already_exists(self, *args): with sq_api_utils.sq_api_mocker() as mocker: mocker.mock_analysis_jres( - body=[sq_api_utils.jre_to_dict(self.zip_jre)], os_matcher="linux", arch_matcher="aarch64" - ) + body=[sq_api_utils.jre_to_dict(self.zip_jre)]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) unzip_dir = self.cache.get_file_path("jre.zip_unzip") @@ -231,7 +229,7 @@ def test_unzip_dir_already_exists(self, *args): old_text_file = unzip_dir / "subdir/test.txt" old_text_file.write_text("test") - JREProvisioner(self.api, self.cache).provision() + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() self.assertTrue(unzip_dir.exists()) self.assertTrue((unzip_dir / "readme.md").exists()) @@ -251,11 +249,10 @@ def test_unsupported_jre(self, *args): with self.assertRaises(UnsupportedArchiveFormat), sq_api_utils.sq_api_mocker() as mocker: mocker.mock_analysis_jres( - body=[sq_api_utils.jre_to_dict(unsupported_archive_jre)], os_matcher="linux", arch_matcher="aarch64" - ) + body=[sq_api_utils.jre_to_dict(unsupported_archive_jre)]) mocker.mock_analysis_jre_download(id="unsupported", body=self.zip_bytes, status=200) - JREProvisioner(self.api, self.cache).provision() + JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() class TestJREResolvedPath(unittest.TestCase): diff --git a/tests/test_scannerengine.py b/tests/test_scannerengine.py index e165fce7..a6badc82 100644 --- a/tests/test_scannerengine.py +++ b/tests/test_scannerengine.py @@ -147,6 +147,8 @@ def test_command_building(self, provision_mock, resolve_jre_mock, execute_mock): config = { "sonar.token": "myToken", "sonar.projectKey": "myProjectKey", + "sonar.scanner.os": "linux", + "sonar.scanner.arch": "x64", } expected_std_in = json.dumps( @@ -154,6 +156,8 @@ def test_command_building(self, provision_mock, resolve_jre_mock, execute_mock): "scannerProperties": [ {"key": "sonar.token", "value": "myToken"}, {"key": "sonar.projectKey", "value": "myProjectKey"}, + {"key": "sonar.scanner.os", "value": "linux"}, + {"key": "sonar.scanner.arch", "value": "x64"}, ] } ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 37743e99..b24983e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -23,7 +23,7 @@ import unittest.mock import pyfakefs.fake_filesystem_unittest as pyfakefs -from pysonar_scanner.utils import Arch, Os, get_arch, get_os, remove_trailing_slash, calculate_checksum +from pysonar_scanner.utils import get_arch, get_os, remove_trailing_slash, calculate_checksum class TestUtils(unittest.TestCase): @@ -35,10 +35,10 @@ def test_removing_trailinlg_slash(self): def test_get_os(self): with self.subTest("os=Windows"), unittest.mock.patch("platform.system", return_value="Windows"): - self.assertEqual(get_os(), Os.WINDOWS) + self.assertEqual(get_os(), "windows") with self.subTest("os=Darwin"), unittest.mock.patch("platform.system", return_value="Darwin"): - self.assertEqual(get_os(), Os.MACOS) + self.assertEqual(get_os(), "mac") def test_get_arch(self): x64_machine_strs = ["amd64", "AmD64", "x86_64", "X86_64"] @@ -46,13 +46,13 @@ def test_get_arch(self): with self.subTest("amd64", machine_str=machine_str), unittest.mock.patch( "platform.machine", return_value=machine_str ): - self.assertEqual(get_arch(), Arch.X64) + self.assertEqual(get_arch(), "x64") arm_machine_strs = ["arm64", "ARm64"] for machine_str in arm_machine_strs: with self.subTest("arm", machine_str=machine_str), unittest.mock.patch( "platform.machine", return_value=machine_str ): - self.assertEqual(get_arch(), Arch.AARCH64) + self.assertEqual(get_arch(), "aarch64") class TestAlpineDetection(unittest.TestCase): @@ -99,7 +99,7 @@ def test_os_release_for_alpine(self): ): assert patcher.fs is not None patcher.fs.create_file(os_release_location, contents=alpine_text) - self.assertEqual(get_os(), Os.ALPINE) + self.assertEqual(get_os(), "alpine") def test_os_release_for_generic_linux(self): for os_release_location in self.os_release_locations: @@ -110,7 +110,7 @@ def test_os_release_for_generic_linux(self): ): assert patcher.fs is not None patcher.fs.create_file(os_release_location, contents=self.ubuntu_text) - self.assertEqual(get_os(), Os.LINUX) + self.assertEqual(get_os(), "linux") def test_os_release_does_not_exist(self): with ( @@ -120,7 +120,7 @@ def test_os_release_does_not_exist(self): ): self.assertFalse(pathlib.Path("/etc/os-release").exists()) self.assertFalse(pathlib.Path("/usr/lib/os-release").exists()) - self.assertEqual(get_os(), Os.LINUX) + self.assertEqual(get_os(), "linux") class TestCalculateChecksum(unittest.TestCase): From 644549fd55753dc75555c1fb7eb938d6fa3c910b Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Wed, 26 Mar 2025 14:23:14 +0100 Subject: [PATCH 2/4] Retain Os and Arch classes for stronger typing --- .../configuration/dynamic_defaults_loader.py | 4 +- src/pysonar_scanner/utils.py | 35 +++++++---- tests/test_configuration_loader.py | 33 ++++++----- tests/test_jre.py | 59 +++++++++---------- tests/test_utils.py | 16 ++--- 5 files changed, 80 insertions(+), 67 deletions(-) diff --git a/src/pysonar_scanner/configuration/dynamic_defaults_loader.py b/src/pysonar_scanner/configuration/dynamic_defaults_loader.py index ebfbbb7a..04fcbdf4 100644 --- a/src/pysonar_scanner/configuration/dynamic_defaults_loader.py +++ b/src/pysonar_scanner/configuration/dynamic_defaults_loader.py @@ -29,8 +29,8 @@ def load() -> Dict[Key, any]: Load dynamically computed default properties """ properties = { - SONAR_SCANNER_OS: utils.get_os(), - SONAR_SCANNER_ARCH: utils.get_arch(), + SONAR_SCANNER_OS: utils.get_os().value, + SONAR_SCANNER_ARCH: utils.get_arch().value, SONAR_PROJECT_BASE_DIR: os.getcwd(), } return properties diff --git a/src/pysonar_scanner/utils.py b/src/pysonar_scanner/utils.py index aec20e0b..e3e13faa 100644 --- a/src/pysonar_scanner/utils.py +++ b/src/pysonar_scanner/utils.py @@ -21,6 +21,7 @@ import pathlib import platform import typing +from enum import Enum OsStr = typing.Literal["windows", "linux", "mac", "alpine", "other"] ArchStr = typing.Literal["x86_64", "aarch64", "x86"] @@ -37,7 +38,15 @@ def calculate_checksum(filehandle: typing.BinaryIO) -> str: return sha256_hash.hexdigest() -def get_os() -> OsStr: +class Os(Enum): + WINDOWS: OsStr = "windows" + LINUX: OsStr = "linux" + MACOS: OsStr = "mac" + ALPINE: OsStr = "alpine" + OTHER: OsStr = "other" + + +def get_os() -> Os: def is_alpine() -> bool: try: os_release = pathlib.Path("/etc/os-release") @@ -52,24 +61,30 @@ def is_alpine() -> bool: os_name = platform.system() if os_name == "Windows": - return "windows" + return Os.WINDOWS elif os_name == "Darwin": - return "mac" + return Os.MACOS elif os_name == "Linux": if is_alpine(): - return "alpine" + return Os.ALPINE else: - return "linux" - return "other" + return Os.LINUX + return Os.OTHER + + +class Arch(Enum): + X64: ArchStr = "x64" + AARCH64: ArchStr = "aarch64" + OTHER: ArchStr = "other" -def get_arch() -> ArchStr: +def get_arch() -> Arch: machine = platform.machine().lower() if machine in ["amd64", "x86_64"]: - return "x64" + return Arch.X64 elif machine == "arm64": - return "aarch64" - return "other" + return Arch.AARCH64 + return Arch.OTHER def filter_none_values(dictionary: dict) -> dict: diff --git a/tests/test_configuration_loader.py b/tests/test_configuration_loader.py index 005a393f..02ffcd73 100644 --- a/tests/test_configuration_loader.py +++ b/tests/test_configuration_loader.py @@ -51,11 +51,12 @@ ) from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader, SONAR_PROJECT_BASE_DIR from pysonar_scanner.exceptions import MissingKeyException +from pysonar_scanner.utils import Arch, Os # Mock utils.get_os and utils.get_arch at the module level -@patch("pysonar_scanner.utils.get_arch", return_value="x64") -@patch("pysonar_scanner.utils.get_os", return_value="linux") +@patch("pysonar_scanner.utils.get_arch", return_value=Arch.X64) +@patch("pysonar_scanner.utils.get_os", return_value=Os.LINUX) class TestConfigurationLoader(pyfakefs.TestCase): def setUp(self): self.maxDiff = None @@ -80,8 +81,8 @@ def test_defaults(self, mock_get_os, mock_get_arch): SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", - SONAR_SCANNER_OS: "linux", - SONAR_SCANNER_ARCH: "x64", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, } self.assertDictEqual(configuration, expected_configuration) @@ -102,8 +103,8 @@ def test_dynamic_defaults_are_loaded(self, get_static_default_properties_mock, m config, { SONAR_PROJECT_BASE_DIR: "/", - SONAR_SCANNER_OS: "linux", - SONAR_SCANNER_ARCH: "x64", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, }, ) @@ -146,8 +147,8 @@ def test_load_sonar_project_properties(self, mock_get_os, mock_get_arch): SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", - SONAR_SCANNER_OS: "linux", - SONAR_SCANNER_ARCH: "x64", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, } self.assertDictEqual(configuration, expected_configuration) @@ -194,8 +195,8 @@ def test_load_sonar_project_properties_from_custom_path(self, mock_get_os, mock_ SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", - SONAR_SCANNER_OS: "linux", - SONAR_SCANNER_ARCH: "x64", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, } self.assertDictEqual(configuration, expected_configuration) @@ -243,8 +244,8 @@ def test_load_pyproject_toml_from_base_dir(self, mock_get_os, mock_get_arch): SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", - SONAR_SCANNER_OS: "linux", - SONAR_SCANNER_ARCH: "x64", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, } self.assertDictEqual(configuration, expected_configuration) @@ -292,8 +293,8 @@ def test_load_pyproject_toml_from_toml_path(self, mock_get_os, mock_get_arch): SONAR_SCANNER_RESPONSE_TIMEOUT: 0, SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit", SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit", - SONAR_SCANNER_OS: "linux", - SONAR_SCANNER_ARCH: "x64", + SONAR_SCANNER_OS: Os.LINUX.value, + SONAR_SCANNER_ARCH: Arch.X64.value, TOML_PATH: "custom/path", } self.assertDictEqual(configuration, expected_configuration) @@ -407,8 +408,8 @@ def test_properties_priority(self, mock_get_os, mock_get_arch): # If you have test functions outside of classes, use patch as a decorator for each function -@patch("pysonar_scanner.utils.get_arch", return_value="x64") -@patch("pysonar_scanner.utils.get_os", return_value="linux") +@patch("pysonar_scanner.utils.get_arch", return_value=Arch.X64.value) +@patch("pysonar_scanner.utils.get_os", return_value=Os.LINUX.value) def test_standalone_function(mock_get_os, mock_get_arch): # ...existing test code... pass diff --git a/tests/test_jre.py b/tests/test_jre.py index 5531e2a9..703153ed 100644 --- a/tests/test_jre.py +++ b/tests/test_jre.py @@ -34,13 +34,14 @@ ) from pysonar_scanner.exceptions import ChecksumException, NoJreAvailableException, UnsupportedArchiveFormat from pysonar_scanner.jre import JREProvisioner, JREResolvedPath, JREResolver, JREResolverConfiguration +from pysonar_scanner.utils import Os, Arch from tests import sq_api_utils import zipfile -@patch("pysonar_scanner.utils.get_os", return_value="linux") -@patch("pysonar_scanner.utils.get_arch", return_value="aarch64") +@patch("pysonar_scanner.utils.get_os", return_value=Os.LINUX) +@patch("pysonar_scanner.utils.get_arch", return_value=Arch.AARCH64) class TestJREProvisioner(pyfakefs.TestCase): def setUp(self): self.setUpPyfakefs(allow_root_user=False) @@ -56,7 +57,7 @@ def setUp(self): filename="fake_jre.zip", sha256="fakechecksum", java_path="fake_java", - os="windows", + os=Os.WINDOWS.value, arch="x64", download_url="http://example.com/fake_jre.zip", ) @@ -74,8 +75,8 @@ def __setup_zip_file(self): filename=self.zip_name, sha256=self.zip_checksum, java_path="java", - os="linux", - arch="aarch64", + os=Os.LINUX.value, + arch=Arch.AARCH64.value, download_url=None, ) @@ -94,8 +95,8 @@ def __setup_tar_file(self): filename=self.tar_gz_name, sha256=self.tar_gz_checksum, java_path="java", - os="linux", - arch="aarch64", + os=Os.LINUX.value, + arch=Arch.AARCH64.value, download_url=None, ) @@ -107,14 +108,14 @@ def __setup_tar_file(self): filename=self.tgz_name, sha256=self.tgz_checksum, java_path="java", - os="linux", - arch="aarch64", + os=Os.LINUX.value, + arch=Arch.AARCH64.value, download_url=None, ) def test_if_patching_worked(self, get_os_mock, get_arch_mock): - self.assertEqual(utils.get_os(), "linux") - self.assertEqual(utils.get_arch(), "aarch64") + self.assertEqual(utils.get_os(), Os.LINUX) + self.assertEqual(utils.get_arch(), Arch.AARCH64) def test_successfully_downloading_jre(self, get_os_mock, get_arch_mock): class JRETestCase(TypedDict): @@ -137,11 +138,10 @@ class JRETestCase(TypedDict): testcase_jre = testcase["jre"] jres = [testcase_jre, self.other_jre] with self.subTest(jre=testcase), sq_api_utils.sq_api_mocker() as mocker: - mocker.mock_analysis_jres( - body=[sq_api_utils.jre_to_dict(jre) for jre in jres]) + mocker.mock_analysis_jres(body=[sq_api_utils.jre_to_dict(jre) for jre in jres]) mocker.mock_analysis_jre_download(id=testcase_jre.id, body=testcase["bytes"], status=200) - provisioner = JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()) + provisioner = JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value) jre_path = provisioner.provision() cache_file = self.cache.get_file(testcase_jre.filename, testcase["checksum"]) @@ -161,7 +161,7 @@ def test_invalid_checksum(self, *args): mocker.mock_analysis_jres(body=[jre_dict]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() def test_retry_mechanism(self, *args): with sq_api_utils.sq_api_mocker() as mocker: @@ -173,7 +173,7 @@ def test_retry_mechanism(self, *args): mocker.mock_analysis_jres(body=[jre_dict]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() cache_file = self.cache.get_file(self.zip_jre.filename, self.zip_checksum) self.assertTrue(cache_file.is_valid()) @@ -187,7 +187,7 @@ def test_already_cached(self, *args): with self.cache.get_file(self.zip_jre.filename, self.zip_checksum).open(mode="wb") as f: f.write(self.zip_bytes) - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() cache_file = self.cache.get_file(self.zip_jre.filename, self.zip_checksum) self.assertTrue(cache_file.is_valid()) @@ -208,19 +208,18 @@ def test_file_already_exists_with_invalid_checksum(self, *args): cache_file.is_valid(), msg="Cache file should have invalid checksum before provisioner ran" ) - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() self.assertTrue(cache_file.is_valid(), msg="Cache file should have valid checksum after provisioner ran") def test_no_jre_available(self, *args): with self.assertRaises(NoJreAvailableException), sq_api_utils.sq_api_mocker() as mocker: mocker.mock_analysis_jres(body=[]) - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() def test_unzip_dir_already_exists(self, *args): with sq_api_utils.sq_api_mocker() as mocker: - mocker.mock_analysis_jres( - body=[sq_api_utils.jre_to_dict(self.zip_jre)]) + mocker.mock_analysis_jres(body=[sq_api_utils.jre_to_dict(self.zip_jre)]) mocker.mock_analysis_jre_download(id="zip_jre", body=self.zip_bytes, status=200) unzip_dir = self.cache.get_file_path("jre.zip_unzip") @@ -229,7 +228,7 @@ def test_unzip_dir_already_exists(self, *args): old_text_file = unzip_dir / "subdir/test.txt" old_text_file.write_text("test") - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() self.assertTrue(unzip_dir.exists()) self.assertTrue((unzip_dir / "readme.md").exists()) @@ -242,17 +241,15 @@ def test_unsupported_jre(self, *args): filename="jre.txt", sha256=self.zip_checksum, java_path="java", - os="linux", - arch="aarch64", + os=Os.LINUX.value, + arch=Arch.AARCH64.value, download_url=None, ) with self.assertRaises(UnsupportedArchiveFormat), sq_api_utils.sq_api_mocker() as mocker: - mocker.mock_analysis_jres( - body=[sq_api_utils.jre_to_dict(unsupported_archive_jre)]) + mocker.mock_analysis_jres(body=[sq_api_utils.jre_to_dict(unsupported_archive_jre)]) mocker.mock_analysis_jre_download(id="unsupported", body=self.zip_bytes, status=200) - - JREProvisioner(self.api, self.cache, utils.get_os(), utils.get_arch()).provision() + JREProvisioner(self.api, self.cache, utils.get_os().value, utils.get_arch().value).provision() class TestJREResolvedPath(unittest.TestCase): @@ -279,13 +276,13 @@ def test(self): { SONAR_SCANNER_JAVA_EXE_PATH: "a/b", SONAR_SCANNER_SKIP_JRE_PROVISIONING: True, - SONAR_SCANNER_OS: "windows", + SONAR_SCANNER_OS: Os.WINDOWS.value, } ) self.assertEqual(config.sonar_scanner_java_exe_path, "a/b") self.assertTrue(config.sonar_scanner_skip_jre_provisioning) - self.assertEqual(config.sonar_scanner_os, "windows") + self.assertEqual(config.sonar_scanner_os, Os.WINDOWS.value) class TestJREResolver(unittest.TestCase): @@ -339,7 +336,7 @@ class TestCaseDict(TypedDict): { "name": "if skip_jre_provisioning is True and java_home is not set return the default for windows", "config": JREResolverConfiguration( - sonar_scanner_os="windows", + sonar_scanner_os=Os.WINDOWS.value, sonar_scanner_skip_jre_provisioning=True, sonar_scanner_java_exe_path=None, ), diff --git a/tests/test_utils.py b/tests/test_utils.py index b24983e5..37743e99 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -23,7 +23,7 @@ import unittest.mock import pyfakefs.fake_filesystem_unittest as pyfakefs -from pysonar_scanner.utils import get_arch, get_os, remove_trailing_slash, calculate_checksum +from pysonar_scanner.utils import Arch, Os, get_arch, get_os, remove_trailing_slash, calculate_checksum class TestUtils(unittest.TestCase): @@ -35,10 +35,10 @@ def test_removing_trailinlg_slash(self): def test_get_os(self): with self.subTest("os=Windows"), unittest.mock.patch("platform.system", return_value="Windows"): - self.assertEqual(get_os(), "windows") + self.assertEqual(get_os(), Os.WINDOWS) with self.subTest("os=Darwin"), unittest.mock.patch("platform.system", return_value="Darwin"): - self.assertEqual(get_os(), "mac") + self.assertEqual(get_os(), Os.MACOS) def test_get_arch(self): x64_machine_strs = ["amd64", "AmD64", "x86_64", "X86_64"] @@ -46,13 +46,13 @@ def test_get_arch(self): with self.subTest("amd64", machine_str=machine_str), unittest.mock.patch( "platform.machine", return_value=machine_str ): - self.assertEqual(get_arch(), "x64") + self.assertEqual(get_arch(), Arch.X64) arm_machine_strs = ["arm64", "ARm64"] for machine_str in arm_machine_strs: with self.subTest("arm", machine_str=machine_str), unittest.mock.patch( "platform.machine", return_value=machine_str ): - self.assertEqual(get_arch(), "aarch64") + self.assertEqual(get_arch(), Arch.AARCH64) class TestAlpineDetection(unittest.TestCase): @@ -99,7 +99,7 @@ def test_os_release_for_alpine(self): ): assert patcher.fs is not None patcher.fs.create_file(os_release_location, contents=alpine_text) - self.assertEqual(get_os(), "alpine") + self.assertEqual(get_os(), Os.ALPINE) def test_os_release_for_generic_linux(self): for os_release_location in self.os_release_locations: @@ -110,7 +110,7 @@ def test_os_release_for_generic_linux(self): ): assert patcher.fs is not None patcher.fs.create_file(os_release_location, contents=self.ubuntu_text) - self.assertEqual(get_os(), "linux") + self.assertEqual(get_os(), Os.LINUX) def test_os_release_does_not_exist(self): with ( @@ -120,7 +120,7 @@ def test_os_release_does_not_exist(self): ): self.assertFalse(pathlib.Path("/etc/os-release").exists()) self.assertFalse(pathlib.Path("/usr/lib/os-release").exists()) - self.assertEqual(get_os(), "linux") + self.assertEqual(get_os(), Os.LINUX) class TestCalculateChecksum(unittest.TestCase): From 3ef267ee83773186141fb910d1d5eddcd07dc6a3 Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Wed, 26 Mar 2025 17:17:00 +0100 Subject: [PATCH 3/4] Fix after review --- tests/test_configuration_loader.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_configuration_loader.py b/tests/test_configuration_loader.py index 02ffcd73..fdcd3476 100644 --- a/tests/test_configuration_loader.py +++ b/tests/test_configuration_loader.py @@ -17,7 +17,7 @@ # 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 unittest.mock import patch import pyfakefs.fake_filesystem_unittest as pyfakefs @@ -26,6 +26,7 @@ from pysonar_scanner.configuration.properties import ( SONAR_PROJECT_KEY, SONAR_PROJECT_NAME, + SONAR_PROJECT_BASE_DIR, SONAR_SCANNER_APP, SONAR_SCANNER_APP_VERSION, SONAR_SCANNER_BOOTSTRAP_START_TIME, @@ -49,7 +50,7 @@ SONAR_SCANNER_ARCH, SONAR_SCANNER_OS, ) -from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader, SONAR_PROJECT_BASE_DIR +from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader from pysonar_scanner.exceptions import MissingKeyException from pysonar_scanner.utils import Arch, Os @@ -66,6 +67,9 @@ def setUp(self): @patch("sys.argv", ["myscript.py", "--token", "myToken", "--sonar-project-key", "myProjectKey"]) def test_defaults(self, mock_get_os, mock_get_arch): + custom_dir = "/my_analysis_directory" + self.fs.create_dir(custom_dir) + os.chdir(custom_dir) configuration = ConfigurationLoader.load() expected_configuration = { SONAR_TOKEN: "myToken", @@ -75,7 +79,7 @@ def test_defaults(self, mock_get_os, mock_get_arch): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_PROJECT_BASE_DIR: "/", + SONAR_PROJECT_BASE_DIR: "/my_analysis_directory", SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, @@ -102,7 +106,7 @@ def test_dynamic_defaults_are_loaded(self, get_static_default_properties_mock, m self.assertDictEqual( config, { - SONAR_PROJECT_BASE_DIR: "/", + SONAR_PROJECT_BASE_DIR: os.getcwd(), SONAR_SCANNER_OS: Os.LINUX.value, SONAR_SCANNER_ARCH: Arch.X64.value, }, @@ -141,7 +145,7 @@ def test_load_sonar_project_properties(self, mock_get_os, mock_get_arch): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_PROJECT_BASE_DIR: "/", + SONAR_PROJECT_BASE_DIR: os.getcwd(), SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, @@ -287,7 +291,7 @@ def test_load_pyproject_toml_from_toml_path(self, mock_get_os, mock_get_arch): SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME], SONAR_VERBOSE: False, SONAR_SCANNER_SKIP_JRE_PROVISIONING: False, - SONAR_PROJECT_BASE_DIR: "/", + SONAR_PROJECT_BASE_DIR: os.getcwd(), SONAR_SCANNER_CONNECT_TIMEOUT: 5, SONAR_SCANNER_SOCKET_TIMEOUT: 60, SONAR_SCANNER_RESPONSE_TIMEOUT: 0, From dbb74bf3a46072b5273dd402aecfada250f86d13 Mon Sep 17 00:00:00 2001 From: Guillaume Dequenne Date: Thu, 27 Mar 2025 09:23:00 +0100 Subject: [PATCH 4/4] Further fix after review --- src/pysonar_scanner/utils.py | 2 +- tests/sq_api_utils.py | 3 +++ tests/test_api.py | 15 +++++++++++---- tests/test_jre.py | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/pysonar_scanner/utils.py b/src/pysonar_scanner/utils.py index e3e13faa..96ffab34 100644 --- a/src/pysonar_scanner/utils.py +++ b/src/pysonar_scanner/utils.py @@ -24,7 +24,7 @@ from enum import Enum OsStr = typing.Literal["windows", "linux", "mac", "alpine", "other"] -ArchStr = typing.Literal["x86_64", "aarch64", "x86"] +ArchStr = typing.Literal["x64", "aarch64", "other"] def remove_trailing_slash(url: str) -> str: diff --git a/tests/sq_api_utils.py b/tests/sq_api_utils.py index 2b6214b4..beced83d 100644 --- a/tests/sq_api_utils.py +++ b/tests/sq_api_utils.py @@ -80,6 +80,8 @@ def mock_analysis_jres( self, body: Optional[list[dict]] = 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", @@ -87,6 +89,7 @@ def mock_analysis_jres( status=status, match=[ matchers.header_matcher({"Accept": "application/json"}), + matchers.query_param_matcher(utils.filter_none_values({"os": os_matcher, "arch": arch_matcher})), ], ) diff --git a/tests/test_api.py b/tests/test_api.py index 11eadb12..9b952836 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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(os="linux", arch="x86_64") + 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 ( @@ -329,7 +336,7 @@ def test_get_analysis_jres(self): self.assertRaises(SonarQubeApiException), ): mocker.mock_analysis_jres(status=404) - self.sq.get_analysis_jres(os="linux", arch="x86_64") + self.sq.get_analysis_jres(os="linux", arch="x64") with ( self.subTest("get_analysis_jres returns error when keys are missing"), @@ -337,7 +344,7 @@ def test_get_analysis_jres(self): self.assertRaises(SonarQubeApiException), ): mocker.mock_analysis_jres([{"id": "jre1"}]) - self.sq.get_analysis_jres(os="linux", arch="x86_64") + self.sq.get_analysis_jres(os="linux", arch="x64") def test_download_analysis_jre(self): jre_id = "jre1" diff --git a/tests/test_jre.py b/tests/test_jre.py index 703153ed..77d013ba 100644 --- a/tests/test_jre.py +++ b/tests/test_jre.py @@ -41,7 +41,7 @@ @patch("pysonar_scanner.utils.get_os", return_value=Os.LINUX) -@patch("pysonar_scanner.utils.get_arch", return_value=Arch.AARCH64) +@patch("pysonar_scanner.utils.get_arch", return_value=Arch.X64) class TestJREProvisioner(pyfakefs.TestCase): def setUp(self): self.setUpPyfakefs(allow_root_user=False) @@ -115,7 +115,7 @@ def __setup_tar_file(self): def test_if_patching_worked(self, get_os_mock, get_arch_mock): self.assertEqual(utils.get_os(), Os.LINUX) - self.assertEqual(utils.get_arch(), Arch.AARCH64) + self.assertEqual(utils.get_arch(), Arch.X64) def test_successfully_downloading_jre(self, get_os_mock, get_arch_mock): class JRETestCase(TypedDict):