Skip to content

Commit 1c1deca

Browse files
authored
PYSCAN-4: Add tests for environment (#5)
1 parent f5aac36 commit 1c1deca

File tree

7 files changed

+313
-28
lines changed

7 files changed

+313
-28
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ classifiers = [
2020
]
2121

2222
dependencies = [
23-
"toml>=0.10.2",
23+
"toml>=0.10.2",
24+
"pyfiglet"
2425
]
2526

2627
[project.urls]

src/py_sonar_scanner/environment.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,65 +20,79 @@
2020
from __future__ import annotations
2121
import os
2222
import platform
23+
import pyfiglet
2324
import shutil
2425
import urllib.request
25-
import zipfile
26+
from urllib.error import HTTPError
2627

28+
from py_sonar_scanner.logger import ApplicationLogger
2729
from py_sonar_scanner.configuration import Configuration
30+
from py_sonar_scanner.utils.binaries_utils import write_binaries, unzip_binaries
2831
from py_sonar_scanner.scanner import Scanner
2932

30-
systems = {
31-
'Darwin': 'macosx',
32-
'Windows': 'windows'
33-
}
33+
systems = {"Darwin": "macosx", "Windows": "windows"}
3434

3535

3636
class Environment:
3737
cfg: Configuration
3838

39+
# a full download path for a scanner has the following shape:
40+
# https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${version}-${os}.zip
41+
scanner_base_url: str = "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli"
42+
3943
def __init__(self, cfg: Configuration):
4044
self.cfg = cfg
45+
self.log = ApplicationLogger.get_logger()
4146

4247
def setup(self):
4348
self.cleanup()
4449
if self._is_sonar_scanner_on_path():
45-
self.cfg.sonar_scanner_executable_path = 'sonar-scanner'
50+
self.cfg.sonar_scanner_executable_path = "sonar-scanner"
4651
else:
47-
system_name = systems.get(platform.uname().system, 'linux')
52+
system_name = systems.get(platform.uname().system, "linux")
4853
self._install_scanner(system_name)
49-
sonar_scanner_home = os.path.join(self.cfg.sonar_scanner_path,
50-
f'sonar-scanner-{self.cfg.sonar_scanner_version}-{system_name}')
51-
self.cfg.sonar_scanner_executable_path = os.path.join(sonar_scanner_home, 'bin', 'sonar-scanner')
54+
sonar_scanner_home = os.path.join(
55+
self.cfg.sonar_scanner_path,
56+
f"sonar-scanner-{self.cfg.sonar_scanner_version}-{system_name}",
57+
)
58+
self.cfg.sonar_scanner_executable_path = os.path.join(sonar_scanner_home, "bin", "sonar-scanner")
5259

53-
print(self.cfg.sonar_scanner_executable_path)
60+
ascii_banner = pyfiglet.figlet_format("Sonar Scanner")
61+
self.log.info(ascii_banner)
5462

55-
def _install_scanner(self, system_name: str):
56-
os.mkdir(self.cfg.sonar_scanner_path)
57-
# Download the binaries and unzip them
58-
# https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${version}-${os}.zip
59-
scanner_res = urllib.request.urlopen(
60-
f'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-{self.cfg.sonar_scanner_version}-{system_name}.zip')
61-
scanner_zip_path = os.path.join(self.cfg.sonar_scanner_path, 'scanner.zip')
62-
with open(scanner_zip_path, 'wb') as output:
63-
output.write(scanner_res.read())
64-
with zipfile.ZipFile(scanner_zip_path, "r") as zip_ref:
65-
zip_ref.extractall(self.cfg.sonar_scanner_path)
66-
os.remove(scanner_zip_path)
67-
self._change_permissions_recursive(self.cfg.sonar_scanner_path, 0o777)
63+
def scanner(self) -> Scanner:
64+
return Scanner(self.cfg)
6865

6966
def _is_sonar_scanner_on_path(self) -> bool:
70-
return shutil.which('sonar-scanner') is not None
67+
return shutil.which("sonar-scanner") is not None
7168

7269
def cleanup(self):
7370
if os.path.exists(self.cfg.sonar_scanner_path):
7471
shutil.rmtree(self.cfg.sonar_scanner_path)
7572

73+
def _install_scanner(self, system_name: str):
74+
os.mkdir(self.cfg.sonar_scanner_path)
75+
# Download the binaries and unzip them
76+
scanner_zip_path = self._download_scanner_binaries(
77+
self.cfg.sonar_scanner_path, self.cfg.sonar_scanner_version, system_name
78+
)
79+
unzip_binaries(scanner_zip_path, self.cfg.sonar_scanner_path)
80+
os.remove(scanner_zip_path)
81+
self._change_permissions_recursive(self.cfg.sonar_scanner_path, 0o777)
82+
7683
def _change_permissions_recursive(self, path, mode):
7784
for root, dirs, files in os.walk(path, topdown=False):
7885
for dir in [os.path.join(root, d) for d in dirs]:
7986
os.chmod(dir, mode)
8087
for file in [os.path.join(root, f) for f in files]:
8188
os.chmod(file, mode)
8289

83-
def scanner(self) -> Scanner:
84-
return Scanner(self.cfg)
90+
def _download_scanner_binaries(self, destination: str, scanner_version: str, system_name: str) -> str:
91+
try:
92+
scanner_res = urllib.request.urlopen(f"{self.scanner_base_url}-{scanner_version}-{system_name}.zip")
93+
scanner_zip_path = os.path.join(destination, "scanner.zip")
94+
write_binaries(scanner_res, scanner_zip_path)
95+
return scanner_zip_path
96+
except HTTPError as error:
97+
self.log.error(f"ERROR: could not download scanner binaries - {error.code} - {error.msg}")
98+
raise error

src/py_sonar_scanner/logger.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2023 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
import logging
21+
from logging import Logger
22+
from typing import Optional
23+
24+
class ApplicationLogger():
25+
26+
_log: Optional[Logger] = None
27+
28+
@classmethod
29+
def get_logger(cls) -> Logger:
30+
if not cls._log:
31+
cls._log = logging.getLogger("main")
32+
cls._setup_logger(cls._log)
33+
return cls._log
34+
35+
@staticmethod
36+
def _setup_logger(log: Logger):
37+
log.setLevel(logging.INFO)
38+
handler = logging.StreamHandler()
39+
handler.terminator = ""
40+
log.addHandler(handler)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2023 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2023 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
import zipfile
21+
22+
def write_binaries(scanner_res: bytes, destination: str):
23+
with open(destination, "wb") as output:
24+
output.write(scanner_res.read())
25+
26+
def unzip_binaries(scanner_zip_path: str, destination: str):
27+
with zipfile.ZipFile(scanner_zip_path, "r") as zip_ref:
28+
zip_ref.extractall(destination)

tests/test_environment.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2023 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
import unittest
21+
from unittest.mock import patch, Mock
22+
from urllib.error import HTTPError
23+
from py_sonar_scanner.configuration import Configuration
24+
from py_sonar_scanner.environment import Environment
25+
26+
27+
class TestEnvironment(unittest.TestCase):
28+
29+
@patch("py_sonar_scanner.environment.write_binaries")
30+
@patch("py_sonar_scanner.environment.urllib.request.urlopen")
31+
def test_download_scanner(self, mock_urlopen, mock_write_binaries):
32+
cfg = Configuration()
33+
environment = Environment(cfg)
34+
environment.scanner_base_url = "http://scanner.com/download"
35+
mock_urlopen.return_value = bytes()
36+
37+
expected_destination = "destination/scanner.zip"
38+
destination = environment._download_scanner_binaries("destination", "test_version", "os_name")
39+
40+
mock_urlopen.assert_called_once_with("http://scanner.com/download-test_version-os_name.zip")
41+
mock_write_binaries.assert_called_once_with(bytes(), expected_destination)
42+
assert destination == expected_destination
43+
44+
@patch("py_sonar_scanner.environment.write_binaries")
45+
@patch("py_sonar_scanner.environment.urllib.request.urlopen")
46+
def test_download_scanner_http_error(self, mock_urlopen, mock_write_binaries):
47+
cfg = Configuration()
48+
environment = Environment(cfg)
49+
environment.scanner_base_url = "http://scanner.com/download"
50+
url = "http://scanner.com/download-test_version-os_name.zip"
51+
mock_urlopen.side_effect = Mock(side_effect=HTTPError(url, 504, "Test", {}, None))
52+
53+
with self.assertLogs(environment.log) as log:
54+
with self.assertRaises(HTTPError):
55+
environment._download_scanner_binaries("destination", "test_version", "os_name")
56+
mock_urlopen.assert_called_once_with(url)
57+
assert not mock_write_binaries.called
58+
expected_error_message = "ERROR: could not download scanner binaries - 504 - Test"
59+
assert log.records[0].getMessage() == expected_error_message
60+
61+
@patch("py_sonar_scanner.environment.unzip_binaries")
62+
@patch("py_sonar_scanner.environment.os")
63+
def test_install_scanner(self, mock_os, mock_unzip_binaries):
64+
cfg = Configuration()
65+
scanner_path = "scanner_path"
66+
scanner_version = "1"
67+
cfg.sonar_scanner_path = scanner_path
68+
cfg.sonar_scanner_version = scanner_version
69+
70+
environment = Environment(cfg)
71+
mock_os.remove = Mock()
72+
mock_os.mkdir = Mock()
73+
74+
download_destination = "path"
75+
environment._download_scanner_binaries = Mock(return_value=download_destination)
76+
environment._change_permissions_recursive = Mock()
77+
78+
system_name = "test"
79+
environment._install_scanner(system_name)
80+
81+
mock_os.mkdir.assert_called_once_with(scanner_path)
82+
environment._download_scanner_binaries.assert_called_once_with(
83+
scanner_path, scanner_version, system_name
84+
)
85+
mock_unzip_binaries.assert_called_once_with(download_destination, scanner_path)
86+
87+
mock_os.remove.assert_called_once_with(download_destination)
88+
environment._change_permissions_recursive.assert_called_once_with(
89+
scanner_path, 0o777
90+
)
91+
92+
def test_setup_when_scanner_is_on_path(self):
93+
cfg = Configuration()
94+
environment = Environment(cfg)
95+
environment.cleanup = Mock()
96+
environment._is_sonar_scanner_on_path = Mock(return_value=True)
97+
98+
environment.setup()
99+
100+
environment.cleanup.assert_called_once()
101+
assert cfg.sonar_scanner_executable_path == "sonar-scanner"
102+
103+
@patch("py_sonar_scanner.environment.systems")
104+
def test_setup_when_scanner_is_not_on_path(self, mock_systems):
105+
cfg = Configuration()
106+
cfg.sonar_scanner_path = "path"
107+
cfg.sonar_scanner_version = "4.1.2"
108+
environment = Environment(cfg)
109+
environment.cleanup = Mock()
110+
system_name = "test"
111+
mock_systems.get = Mock(return_value=system_name)
112+
environment._is_sonar_scanner_on_path = Mock(return_value=False)
113+
environment._install_scanner = Mock()
114+
expected_path = "path/sonar-scanner-4.1.2-test/bin/sonar-scanner"
115+
116+
environment.setup()
117+
118+
environment.cleanup.assert_called_once()
119+
mock_systems.get.assert_called_once()
120+
environment._install_scanner.assert_called_once_with(system_name)
121+
122+
assert cfg.sonar_scanner_executable_path == expected_path
123+
124+
@patch("py_sonar_scanner.environment.os.path")
125+
@patch("py_sonar_scanner.environment.shutil")
126+
def test_cleanup_when_scanner_path_exists(self, mock_shutil, mock_os_path):
127+
cfg = Configuration()
128+
scanner_path = "path"
129+
cfg.sonar_scanner_path = scanner_path
130+
environment = Environment(cfg)
131+
mock_os_path.exists = Mock(return_value=True)
132+
mock_shutil.rmtree = Mock()
133+
134+
environment.cleanup()
135+
136+
mock_os_path.exists.assert_called_once_with(scanner_path)
137+
mock_shutil.rmtree.assert_called_once_with(scanner_path)
138+
139+
@patch("py_sonar_scanner.environment.os.path")
140+
@patch("py_sonar_scanner.environment.shutil")
141+
def test_cleanup_when_scanner_path_does_not_exist(self, mock_shutil, mock_os_path):
142+
cfg = Configuration()
143+
scanner_path = "path"
144+
cfg.sonar_scanner_path = scanner_path
145+
environment = Environment(cfg)
146+
mock_os_path.exists = Mock(return_value=False)
147+
mock_shutil.rmtree = Mock()
148+
149+
environment.cleanup()
150+
151+
mock_os_path.exists.assert_called_once_with(scanner_path)
152+
assert not mock_shutil.rmtree.called
153+
154+
@patch("py_sonar_scanner.environment.shutil")
155+
def test_is_sonar_scanner_on_path(self, mock_shutil):
156+
cfg = Configuration()
157+
scanner_path = "path"
158+
cfg.sonar_scanner_path = scanner_path
159+
environment = Environment(cfg)
160+
mock_shutil.which = Mock()
161+
162+
environment._is_sonar_scanner_on_path()
163+
164+
mock_shutil.which.assert_called_once_with("sonar-scanner")

tests/test_scanner.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2023 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
120
import unittest
221
from unittest.mock import Mock
322
import threading

0 commit comments

Comments
 (0)