Skip to content

Commit 67d990c

Browse files
Seppli11guillaume-dequenne
authored andcommitted
SCANPY-106 Create a ScannerEngineAPI class holding the logic to call meaningful APIs (#126)
1 parent e6548d5 commit 67d990c

File tree

8 files changed

+341
-7
lines changed

8 files changed

+341
-7
lines changed

poetry.lock

Lines changed: 47 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ licenseheaders = '>=0.8.8'
4040
mypy = '>=1.7.1'
4141
pytest = '8.3.5'
4242
pytest-cov = '6.0.0'
43+
pytest-subtests = "^0.14.1"
4344

4445
[[tool.poetry.packages]]
4546
from = 'src'

src/pysonar_scanner/api.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 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+
from dataclasses import dataclass
21+
from pysonar_scanner.configuration import Configuration
22+
from pysonar_scanner.utils import remove_trailing_slash
23+
24+
25+
@dataclass(frozen=True)
26+
class BaseUrls:
27+
base_url: str
28+
api_base_url: str
29+
is_sonar_qube_cloud: bool
30+
31+
def __post_init__(self):
32+
object.__setattr__(self, "base_url", remove_trailing_slash(self.base_url))
33+
object.__setattr__(self, "api_base_url", remove_trailing_slash(self.api_base_url))
34+
35+
36+
def get_base_urls(config: Configuration) -> BaseUrls:
37+
def is_sq_cloud_url(sonar_host_url: str) -> bool:
38+
sq_cloud_url = config.sonar.scanner.sonarcloud_url.strip() or "https://sonarcloud.io"
39+
return remove_trailing_slash(sonar_host_url) == remove_trailing_slash(sq_cloud_url)
40+
41+
def is_blank(str) -> bool:
42+
return str.strip() == ""
43+
44+
def region_with_dot(region: str) -> str:
45+
return region + "." if not is_blank(region) else ""
46+
47+
sonar_host_url = remove_trailing_slash(config.sonar.host_url)
48+
if is_blank(sonar_host_url) or is_sq_cloud_url(sonar_host_url):
49+
region = region_with_dot(config.sonar.region)
50+
sonar_host_url = config.sonar.scanner.sonarcloud_url or f"https://{region}sonarcloud.io"
51+
api_base_url = config.sonar.scanner.api_base_url or f"https://api.{region}sonarcloud.io"
52+
return BaseUrls(base_url=sonar_host_url, api_base_url=api_base_url, is_sonar_qube_cloud=True)
53+
else:
54+
api_base_url = config.sonar.scanner.api_base_url or f"{sonar_host_url}/api/v2"
55+
return BaseUrls(base_url=sonar_host_url, api_base_url=api_base_url, is_sonar_qube_cloud=False)
56+
57+
58+
class SonarQubeApi:
59+
def __init__(self, base_urls: BaseUrls):
60+
self.base_url = base_urls

src/pysonar_scanner/configuration.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 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+
from dataclasses import dataclass
21+
22+
23+
@dataclass(frozen=True)
24+
class Scanner:
25+
sonarcloud_url: str = ""
26+
api_base_url: str = ""
27+
28+
29+
@dataclass(frozen=True)
30+
class Sonar:
31+
scanner: Scanner = Scanner()
32+
host_url: str = ""
33+
region: str = ""
34+
35+
36+
@dataclass(frozen=True)
37+
class Configuration:
38+
sonar: Sonar = Sonar()
39+
40+
41+
class ConfigurationLoader:
42+
pass

tests/test_dummy.py renamed to src/pysonar_scanner/scannerengine.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,5 @@
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-
import unittest
21-
22-
23-
class DummyTest(unittest.TestCase):
24-
def test_dummy(self):
25-
self.assertTrue(True)
20+
class ScannerEngine:
21+
pass

src/pysonar_scanner/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 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+
def remove_trailing_slash(url: str) -> str:
21+
return url.rstrip("/ ").lstrip()

tests/test_api.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 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+
from typing import TypedDict
21+
import unittest
22+
23+
from pysonar_scanner.api import BaseUrls, get_base_urls
24+
from pysonar_scanner.configuration import Configuration, Scanner, Sonar
25+
26+
27+
class ApiTest(unittest.TestCase):
28+
def test_BaseUrls_normalization(self):
29+
self.assertEqual(
30+
BaseUrls("test1/", "test2/", is_sonar_qube_cloud=True),
31+
BaseUrls("test1", "test2", is_sonar_qube_cloud=True),
32+
)
33+
34+
def test_get_base_urls(self):
35+
class TestCaseDict(TypedDict):
36+
name: str
37+
config: Configuration
38+
expected: BaseUrls
39+
40+
cases: list[TestCaseDict] = [
41+
# SQ:Cloud tests
42+
{
43+
"name": "default configuration defaults to SQ:cloud base urls",
44+
"config": Configuration(),
45+
"expected": BaseUrls(
46+
base_url="https://sonarcloud.io", api_base_url="https://api.sonarcloud.io", is_sonar_qube_cloud=True
47+
),
48+
},
49+
{
50+
"name": "sonar.host.url with whitespaces uses the SQ:cloud base urls",
51+
"config": Configuration(sonar=Sonar(host_url=" ")),
52+
"expected": BaseUrls(
53+
base_url="https://sonarcloud.io", api_base_url="https://api.sonarcloud.io", is_sonar_qube_cloud=True
54+
),
55+
},
56+
{
57+
"name": "when host_url is set to SQ:cloud, use SQ:cloud base urls",
58+
"config": Configuration(sonar=Sonar(host_url="https://sonarcloud.io")),
59+
"expected": BaseUrls(
60+
base_url="https://sonarcloud.io", api_base_url="https://api.sonarcloud.io", is_sonar_qube_cloud=True
61+
),
62+
},
63+
{
64+
"name": "When both host_url and sonarcloud_url are set, use sonarcloud_url to check if host is SQ:cloud",
65+
"config": Configuration(
66+
sonar=Sonar(
67+
host_url="https://sonarcloud.io",
68+
scanner=Scanner(
69+
sonarcloud_url="https://custom-sq-cloud.io",
70+
),
71+
)
72+
),
73+
"expected": BaseUrls(
74+
base_url="https://sonarcloud.io",
75+
api_base_url="https://sonarcloud.io/api/v2",
76+
is_sonar_qube_cloud=False,
77+
),
78+
},
79+
{
80+
"name": "when host_url with trailing slash is set to SQ:cloud, use SQ:cloud base urls",
81+
"config": Configuration(sonar=Sonar(host_url="https://sonarcloud.io/")),
82+
"expected": BaseUrls(
83+
base_url="https://sonarcloud.io", api_base_url="https://api.sonarcloud.io", is_sonar_qube_cloud=True
84+
),
85+
},
86+
# SQ:Cloud region tests
87+
{
88+
"name": "When region is set, use region in base urls",
89+
"config": Configuration(sonar=Sonar(host_url="https://sonarcloud.io", region="us")),
90+
"expected": BaseUrls(
91+
base_url="https://us.sonarcloud.io",
92+
api_base_url="https://api.us.sonarcloud.io",
93+
is_sonar_qube_cloud=True,
94+
),
95+
},
96+
{
97+
"name": "Ignore region when sonarcloud_url and api_base_url is set",
98+
"config": Configuration(
99+
sonar=Sonar(
100+
host_url="https://custom-sq-cloud.io",
101+
region="us",
102+
scanner=Scanner(
103+
sonarcloud_url="https://custom-sq-cloud.io",
104+
api_base_url="https://other-api.custom-sq-cloud.io",
105+
),
106+
)
107+
),
108+
"expected": BaseUrls(
109+
base_url="https://custom-sq-cloud.io",
110+
api_base_url="https://other-api.custom-sq-cloud.io",
111+
is_sonar_qube_cloud=True,
112+
),
113+
},
114+
# SQ:Server tests
115+
{
116+
"name": "When host_url is set to SQ:server, use SQ:server base urls",
117+
"config": Configuration(sonar=Sonar(host_url="https://sq.home")),
118+
"expected": BaseUrls(
119+
base_url="https://sq.home", api_base_url="https://sq.home/api/v2", is_sonar_qube_cloud=False
120+
),
121+
},
122+
{
123+
"name": "When host_url with trailing slash is set to SQ:server, use SQ:server base urls",
124+
"config": Configuration(sonar=Sonar(host_url="https://sq.home/")),
125+
"expected": BaseUrls(
126+
base_url="https://sq.home", api_base_url="https://sq.home/api/v2", is_sonar_qube_cloud=False
127+
),
128+
},
129+
]
130+
131+
for case in cases:
132+
expected = case["expected"]
133+
with self.subTest(case["name"], config=case["config"], expected=expected):
134+
base_urls = get_base_urls(case["config"])
135+
self.assertEqual(base_urls.base_url, expected.base_url)
136+
self.assertEqual(base_urls.api_base_url, expected.api_base_url)
137+
self.assertEqual(base_urls.is_sonar_qube_cloud, expected.is_sonar_qube_cloud)
138+
self.assertEqual(base_urls, expected)

tests/test_utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 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+
22+
from pysonar_scanner.utils import remove_trailing_slash
23+
24+
25+
class UtilsTest(unittest.TestCase):
26+
def test_removing_trailinlg_slash(self):
27+
self.assertEqual(remove_trailing_slash("test/"), "test")
28+
self.assertEqual(remove_trailing_slash(" test/ "), "test")
29+
self.assertEqual(remove_trailing_slash(" test / "), "test")
30+
self.assertEqual(remove_trailing_slash("test"), "test")

0 commit comments

Comments
 (0)