Skip to content

Commit 1ebf649

Browse files
Refactor-error-handling-part-2 (#2004)
* 2nd pass of exceptions refactoring * Remove useless parens for walrus ops * Formatting * Remove unused imports and vars due to centralized exception handling * Refactor * New tests for branches * Remove unused imports * Clear cache when object not found * More robust clear when object not found * Better tests * Use platform exception handling * More centralized exception handling * Quality pass * Add docstring * Quality pass * Add more tests * Add more tests * Move Levenshtein distance calc in utilities * Fix docstrings * add test_exists() * Formatting * Small refactoring * Fix get_findings() * Add get_findings() * Adjust test_exists() for CB * Adjust test_rename() for variable main branch name * Adjust test_request_error() for connection error * Detect unsupported operation from unknown URL * Adjust to variable main branch name * Adjust to 9.9 that can't set main branch * Remove non class get_object() * Remove non class rule get_object() * Remove noisy log * Add cache class * Add cache class * Temporarily remove portfolio export test * Clear caches after corrupting branches or projects * Quality pass * Change max issues in sonar-tools * Add more tests * Fix bug * Fix test * Fix tests sync * Add branches to test project * Formatting --------- Co-authored-by: Olivier Korach <[email protected]>
1 parent a4b7889 commit 1ebf649

39 files changed

+593
-572
lines changed

cli/projects_cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
2525
"""
2626

27-
import sys
2827
import json
2928

3029
from requests import RequestException

conf/prep_all_tests.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ function create_fresh_project {
3838
fi
3939
curl -X POST -u "${usertoken}:" "${url}/api/projects/delete?project=${key}"
4040
conf/run_scanner.sh "${opts[@]}" -Dsonar.projectKey="${key}" -Dsonar.projectName="${key}" -Dsonar.host.url="${url}" "${opt_token}" "${opt_org}"
41+
conf/run_scanner.sh "${opts[@]}" -Dsonar.projectKey="${key}" -Dsonar.projectName="${key}" -Dsonar.host.url="${url}" "${opt_token}" "${opt_org}" -Dsonar.branch.name=develop
42+
conf/run_scanner.sh "${opts[@]}" -Dsonar.projectKey="${key}" -Dsonar.projectName="${key}" -Dsonar.host.url="${url}" "${opt_token}" "${opt_org}" -Dsonar.branch.name=release-3.x
4143
return 0
4244
}
4345

sonar/app_branches.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
from typing import Optional
2525

2626
import json
27-
from http import HTTPStatus
28-
from requests import RequestException
2927
from requests.utils import quote
3028

3129
import sonar.logging as log
@@ -112,11 +110,7 @@ def create(cls, app: object, name: str, project_branches: list[Branch]) -> Appli
112110
else: # Default main branch of project
113111
params["project"].append(obj.key)
114112
params["projectBranch"].append("")
115-
try:
116-
app.endpoint.post(ApplicationBranch.API[c.CREATE], params=params)
117-
except (ConnectionError, RequestException) as e:
118-
utilities.handle_error(e, f"creating branch {name} of {str(app)}", catch_http_statuses=(HTTPStatus.BAD_REQUEST,))
119-
raise exceptions.ObjectAlreadyExists(f"{str(app)} branch '{name}", e.response.text)
113+
app.endpoint.post(ApplicationBranch.API[c.CREATE], params=params)
120114
return ApplicationBranch(app=app, name=name, project_branches=project_branches)
121115

122116
@classmethod
@@ -201,10 +195,9 @@ def update(self, name: str, project_branches: list[Branch]) -> bool:
201195
params["projectBranch"].append(br_name)
202196
try:
203197
ok = self.post(ApplicationBranch.API[c.UPDATE], params=params).ok
204-
except (ConnectionError, RequestException) as e:
205-
utilities.handle_error(e, f"updating {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,))
198+
except exceptions.ObjectNotFound:
206199
ApplicationBranch.CACHE.pop(self)
207-
raise exceptions.ObjectNotFound(str(self), e.response.text)
200+
raise
208201

209202
self.name = name
210203
self._project_branches = project_branches

sonar/applications.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,7 @@ def get_object(cls, endpoint: pf.Platform, key: str) -> Application:
9393
o = Application.CACHE.get(key, endpoint.local_url)
9494
if o:
9595
return o
96-
try:
97-
data = json.loads(endpoint.get(Application.API[c.GET], params={"application": key}).text)["application"]
98-
except (ConnectionError, RequestException) as e:
99-
util.handle_error(e, f"searching application {key}", catch_http_statuses=(HTTPStatus.NOT_FOUND,))
100-
raise exceptions.ObjectNotFound(key, f"Application key '{key}' not found")
96+
data = json.loads(endpoint.get(Application.API[c.GET], params={"application": key}).text)["application"]
10197
return cls.load(endpoint, data)
10298

10399
@classmethod
@@ -132,11 +128,7 @@ def create(cls, endpoint: pf.Platform, key: str, name: str) -> Application:
132128
:rtype: Application
133129
"""
134130
check_supported(endpoint)
135-
try:
136-
endpoint.post(Application.API["CREATE"], params={"key": key, "name": name})
137-
except (ConnectionError, RequestException) as e:
138-
util.handle_error(e, f"creating application {key}", catch_http_statuses=(HTTPStatus.BAD_REQUEST,))
139-
raise exceptions.ObjectAlreadyExists(key, e.response.text)
131+
endpoint.post(Application.API["CREATE"], params={"key": key, "name": name})
140132
log.info("Creating object")
141133
return Application(endpoint=endpoint, key=key, name=name)
142134

@@ -151,10 +143,9 @@ def refresh(self) -> None:
151143
self.reload(json.loads(self.get("navigation/component", params={"component": self.key}).text))
152144
self.reload(json.loads(self.get(Application.API[c.GET], params=self.api_params(c.GET)).text)["application"])
153145
self.projects()
154-
except (ConnectionError, RequestException) as e:
155-
util.handle_error(e, f"refreshing {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,))
146+
except exceptions.ObjectNotFound:
156147
Application.CACHE.pop(self)
157-
raise exceptions.ObjectNotFound(self.key, f"{str(self)} not found")
148+
raise
158149

159150
def __str__(self) -> str:
160151
"""String name of object"""

sonar/branches.py

Lines changed: 55 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
from http import HTTPStatus
2525
from typing import Optional
2626
import json
27+
import re
2728
from urllib.parse import unquote
28-
from requests import HTTPError, RequestException
2929
import requests.utils
3030

3131
from sonar import platform
@@ -89,16 +89,11 @@ def get_object(cls, concerned_object: projects.Project, branch_name: str) -> Bra
8989
o = Branch.CACHE.get(concerned_object.key, branch_name, concerned_object.base_url())
9090
if o:
9191
return o
92-
try:
93-
data = json.loads(concerned_object.get(Branch.API[c.LIST], params={"project": concerned_object.key}).text)
94-
except (ConnectionError, RequestException) as e:
95-
util.handle_error(e, f"searching {str(concerned_object)} for branch '{branch_name}'", catch_http_statuses=(HTTPStatus.NOT_FOUND,))
96-
raise exceptions.ObjectNotFound(concerned_object.key, f"{str(concerned_object)} not found")
97-
98-
for br in data.get("branches", []):
99-
if br["name"] == branch_name:
100-
return cls.load(concerned_object, branch_name, br)
101-
raise exceptions.ObjectNotFound(branch_name, f"Branch '{branch_name}' of {str(concerned_object)} not found")
92+
data = json.loads(concerned_object.get(Branch.API[c.LIST], params={"project": concerned_object.key}).text)
93+
br = next((b for b in data.get("branches", []) if b["name"] == branch_name), None)
94+
if not br:
95+
raise exceptions.ObjectNotFound(branch_name, f"Branch '{branch_name}' of {str(concerned_object)} not found")
96+
return cls.load(concerned_object, branch_name, br)
10297

10398
@classmethod
10499
def load(cls, concerned_object: projects.Project, branch_name: str, data: types.ApiPayload) -> Branch:
@@ -112,9 +107,11 @@ def load(cls, concerned_object: projects.Project, branch_name: str, data: types.
112107
"""
113108
branch_name = unquote(branch_name)
114109
o = Branch.CACHE.get(concerned_object.key, branch_name, concerned_object.base_url())
110+
br_data = next((br for br in data.get("branches", []) if br["name"] == branch_name), None)
115111
if not o:
116112
o = cls(concerned_object, branch_name)
117-
o._load(data)
113+
if br_data:
114+
o._load(br_data)
118115
return o
119116

120117
def __str__(self) -> str:
@@ -135,44 +132,33 @@ def refresh(self) -> Branch:
135132
:return: itself
136133
:rtype: Branch
137134
"""
138-
try:
139-
data = json.loads(self.get(Branch.API[c.LIST], params=self.api_params(c.LIST)).text)
140-
except (ConnectionError, RequestException) as e:
141-
util.handle_error(e, f"refreshing {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,))
142-
Branch.CACHE.pop(self)
143-
raise exceptions.ObjectNotFound(self.key, f"{str(self)} not found in SonarQube")
144-
for br in data.get("branches", []):
145-
if br["name"] == self.name:
146-
self._load(br)
147-
else:
148-
# While we're there let's load other branches with up to date branch data
149-
Branch.load(self.concerned_object, br["name"], data)
135+
data = json.loads(self.get(Branch.API[c.LIST], params=self.api_params(c.LIST)).text)
136+
br_data = next((br for br in data.get("branches", []) if br["name"] == self.name), None)
137+
if not br_data:
138+
Branch.CACHE.clear()
139+
raise exceptions.ObjectNotFound(self.name, f"{str(self)} not found")
140+
self._load(br_data)
141+
# While we're there let's load other branches with up to date branch data
142+
for br in [b for b in data.get("branches", []) if b["name"] != self.name]:
143+
Branch.load(self.concerned_object, br["name"], data)
150144
return self
151145

152146
def _load(self, data: types.ApiPayload) -> None:
153-
if self.sq_json is None:
154-
self.sq_json = data
155-
else:
156-
self.sq_json.update(data)
147+
log.debug("Loading %s with data %s", self, data)
148+
self.sq_json = (self.sq_json or {}) | data
157149
self._is_main = self.sq_json["isMain"]
158150
self._last_analysis = util.string_to_date(self.sq_json.get("analysisDate", None))
159151
self._keep_when_inactive = self.sq_json.get("excludedFromPurge", False)
160152
self._is_main = self.sq_json.get("isMain", False)
161153

162154
def is_kept_when_inactive(self) -> bool:
163-
"""
164-
:return: Whether the branch is kept when inactive
165-
:rtype: bool
166-
"""
155+
"""Returns whether the branch is kept when inactive"""
167156
if self._keep_when_inactive is None or self.sq_json is None:
168157
self.refresh()
169158
return self._keep_when_inactive
170159

171160
def is_main(self) -> bool:
172-
"""
173-
:return: Whether the branch is the project main branch
174-
:rtype: bool
175-
"""
161+
"""Returns whether the branch is the project main branch"""
176162
if self._is_main is None or self.sq_json is None:
177163
self.refresh()
178164
return self._is_main
@@ -186,11 +172,32 @@ def delete(self) -> bool:
186172
"""
187173
try:
188174
return super().delete()
189-
except (ConnectionError, RequestException) as e:
190-
if isinstance(e, HTTPError) and e.response.status_code == HTTPStatus.BAD_REQUEST:
191-
log.warning("Can't delete %s, it's the main branch", str(self))
175+
except exceptions.SonarException as e:
176+
log.warning(e.message)
192177
return False
193178

179+
def get(
180+
self, api: str, params: types.ApiParams = None, data: Optional[str] = None, mute: tuple[HTTPStatus] = (), **kwargs: str
181+
) -> requests.Response:
182+
"""Performs an HTTP GET request for the object"""
183+
try:
184+
return super().get(api=api, params=params, data=data, mute=mute, **kwargs)
185+
except exceptions.ObjectNotFound as e:
186+
if re.match(r"Project .+ not found", e.message):
187+
log.warning("Clearing project cache")
188+
projects.Project.CACHE.clear()
189+
raise
190+
191+
def post(self, api: str, params: types.ApiParams = None, mute: tuple[HTTPStatus] = (), **kwargs: str) -> requests.Response:
192+
"""Performs an HTTP POST request for the object"""
193+
try:
194+
return super().post(api=api, params=params, mute=mute, **kwargs)
195+
except exceptions.ObjectNotFound as e:
196+
if re.match(r"Project .+ not found", e.message):
197+
log.warning("Clearing project cache")
198+
projects.Project.CACHE.clear()
199+
raise
200+
194201
def new_code(self) -> str:
195202
"""
196203
:return: The branch new code period definition
@@ -199,13 +206,7 @@ def new_code(self) -> str:
199206
if self._new_code is None and self.endpoint.is_sonarcloud():
200207
self._new_code = settings.new_code_to_string({"inherited": True})
201208
elif self._new_code is None:
202-
try:
203-
data = json.loads(self.get(api=Branch.API["get_new_code"], params=self.api_params(c.LIST)).text)
204-
except (ConnectionError, RequestException) as e:
205-
util.handle_error(e, f"getting new code period of {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND,))
206-
Branch.CACHE.pop(self)
207-
raise exceptions.ObjectNotFound(self.concerned_object.key, f"{str(self.concerned_object)} not found")
208-
209+
data = json.loads(self.get(api=Branch.API["get_new_code"], params=self.api_params(c.LIST)).text)
209210
for b in data["newCodePeriods"]:
210211
new_code = settings.new_code_to_string(b)
211212
if b["branchKey"] == self.name:
@@ -245,24 +246,17 @@ def set_keep_when_inactive(self, keep: bool) -> bool:
245246
:return: Whether the operation was successful
246247
"""
247248
log.info("Setting %s keep when inactive to %s", self, keep)
248-
try:
249-
self.post("project_branches/set_automatic_deletion_protection", params=self.api_params() | {"value": str(keep).lower()})
249+
ok = self.post("project_branches/set_automatic_deletion_protection", params=self.api_params() | {"value": str(keep).lower()}).ok
250+
if ok:
250251
self._keep_when_inactive = keep
251-
except (ConnectionError, RequestException) as e:
252-
util.handle_error(e, f"setting {str(self)} keep when inactive to {keep}", catch_all=True)
253-
return False
254252
return True
255253

256254
def set_as_main(self) -> bool:
257255
"""Sets the branch as the main branch of the project
258256
259257
:return: Whether the operation was successful
260258
"""
261-
try:
262-
self.post("api/project_branches/set_main", params=self.api_params())
263-
except (ConnectionError, RequestException) as e:
264-
util.handle_error(e, f"setting {str(self)} as main branch", catch_all=True)
265-
return False
259+
self.post("api/project_branches/set_main", params=self.api_params())
266260
for b in self.concerned_object.branches().values():
267261
b._is_main = b.name == self.name
268262
return True
@@ -317,30 +311,21 @@ def rename(self, new_name: str) -> bool:
317311
log.debug("Skipping rename %s with same new name", str(self))
318312
return False
319313
log.info("Renaming main branch of %s from '%s' to '%s'", str(self.concerned_object), self.name, new_name)
320-
try:
321-
self.post(Branch.API[c.RENAME], params={"project": self.concerned_object.key, "name": new_name})
322-
except (ConnectionError, RequestException) as e:
323-
util.handle_error(e, f"Renaming {str(self)}", catch_http_statuses=(HTTPStatus.NOT_FOUND, HTTPStatus.BAD_REQUEST))
324-
if isinstance(e, HTTPError):
325-
if e.response.status_code == HTTPStatus.NOT_FOUND:
326-
Branch.CACHE.pop(self)
327-
raise exceptions.ObjectNotFound(self.concerned_object.key, f"str{self.concerned_object} not found")
328-
if e.response.status_code == HTTPStatus.BAD_REQUEST:
329-
return False
314+
self.post(Branch.API[c.RENAME], params={"project": self.concerned_object.key, "name": new_name})
330315
Branch.CACHE.pop(self)
331316
self.name = new_name
332317
Branch.CACHE.put(self)
333318
return True
334319

335-
def get_findings(self) -> dict[str, object]:
320+
def get_findings(self, filters: Optional[types.ApiParams] = None) -> dict[str, object]:
336321
"""Returns a branch list of findings
337322
338323
:return: dict of Findings, with finding key as key
339324
:rtype: dict{key: Finding}
340325
"""
341-
findings = self.get_issues()
342-
findings.update(self.get_hotspots())
343-
return findings
326+
if not filters:
327+
return self.concerned_object.get_findings(branch=self.name)
328+
return self.get_issues(filters) | self.get_hotspots(filters)
344329

345330
def component_data(self) -> dict[str, str]:
346331
"""Returns key data"""

sonar/devops.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,8 @@
2222

2323
from __future__ import annotations
2424
from typing import Optional, Union
25-
from http import HTTPStatus
2625
import json
2726

28-
from requests import RequestException
29-
3027
import sonar.logging as log
3128
from sonar.util import types, cache
3229
from sonar import platform
@@ -109,11 +106,10 @@ def create(cls, endpoint: platform.Platform, key: str, plt_type: str, url_or_wor
109106
elif plt_type == "bitbucketcloud":
110107
params.update({"clientSecret": _TO_BE_SET, "clientId": _TO_BE_SET, "workspace": url_or_workspace})
111108
endpoint.post(_CREATE_API_BBCLOUD, params=params)
112-
except (ConnectionError, RequestException) as e:
113-
util.handle_error(e, f"creating devops platform {key}/{plt_type}/{url_or_workspace}", catch_http_statuses=(HTTPStatus.BAD_REQUEST,))
109+
except exceptions.SonarException as e:
114110
if endpoint.edition() in (c.CE, c.DE):
115111
log.warning("Can't set DevOps platform '%s', don't you have more that 1 of that type?", key)
116-
raise exceptions.UnsupportedOperation(f"Can't set DevOps platform '{key}', don't you have more that 1 of that type?")
112+
raise exceptions.UnsupportedOperation(e.message) from e
117113
o = DevopsPlatform(endpoint=endpoint, key=key, platform_type=plt_type)
118114
o.refresh()
119115
return o
@@ -185,8 +181,7 @@ def update(self, **kwargs) -> bool:
185181
ok = self.post(f"alm_settings/update_{alm_type}", params=params).ok
186182
self.url = kwargs["url"]
187183
self._specific = {k: v for k, v in params.items() if k not in ("key", "url")}
188-
except (ConnectionError, RequestException) as e:
189-
util.handle_error(e, f"updating devops platform {self.key}/{alm_type}", catch_http_statuses=(HTTPStatus.BAD_REQUEST,))
184+
except exceptions.SonarException:
190185
ok = False
191186
return ok
192187

sonar/findings.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
from datetime import datetime
2525
from typing import Optional
2626
import re
27-
from http import HTTPStatus
28-
from requests import RequestException
2927
import Levenshtein
3028

3129
import sonar.logging as log
@@ -184,7 +182,7 @@ def assign(self, assignee: Optional[str] = None) -> str:
184182

185183
def language(self) -> str:
186184
"""Returns the finding language"""
187-
return rules.get_object(endpoint=self.endpoint, key=self.rule).language
185+
return rules.Rule.get_object(endpoint=self.endpoint, key=self.rule).language
188186

189187
def to_csv(self, without_time: bool = False) -> list[str]:
190188
"""
@@ -460,13 +458,10 @@ def search_siblings(
460458
def do_transition(self, transition: str) -> bool:
461459
try:
462460
return self.post("issues/do_transition", {"issue": self.key, "transition": transition}).ok
463-
except (ConnectionError, RequestException) as e:
464-
util.handle_error(e, f"applying transition {transition}", catch_http_statuses=(HTTPStatus.BAD_REQUEST, HTTPStatus.NOT_FOUND))
465461
except exceptions.SonarException as e:
466462
if re.match(r"Transition from state [A-Za-z]+ does not exist", e.message):
467463
raise exceptions.UnsupportedOperation(e.message) from e
468464
raise
469-
return False
470465

471466
def get_branch_and_pr(self, data: types.ApiPayload) -> tuple[Optional[str], Optional[str]]:
472467
"""

0 commit comments

Comments
 (0)