Skip to content

Commit 4a1556a

Browse files
authored
Fix MQR mode get/set (#1992)
* Fixes #1991 * Quality pass * Fix reset on 2025.2 and higher for autodetect ai * Return false when setting an unsettable setting * Add tests for custom settings * Refactoring * Add more test for standard settings * Add docstring
1 parent d145c5c commit 4a1556a

File tree

2 files changed

+138
-36
lines changed

2 files changed

+138
-36
lines changed

sonar/settings.py

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class Setting(sqobject.SqObject):
132132
c.LIST: "settings/list_definitions",
133133
"NEW_CODE_GET": "new_code_periods/show",
134134
"NEW_CODE_SET": "new_code_periods/set",
135+
"MQR_MODE": "v2/clean-code-policy/mode",
135136
}
136137

137138
def __init__(self, endpoint: pf.Platform, key: str, component: object = None, data: types.ApiPayload = None) -> None:
@@ -154,19 +155,7 @@ def read(cls, key: str, endpoint: pf.Platform, component: object = None) -> Sett
154155
o = Setting.CACHE.get(key, component, endpoint.local_url)
155156
if o:
156157
return o
157-
if key == NEW_CODE_PERIOD and not endpoint.is_sonarcloud():
158-
params = get_component_params(component, name="project")
159-
data = json.loads(endpoint.get(Setting.API["NEW_CODE_GET"], params=params).text)
160-
else:
161-
if key == NEW_CODE_PERIOD:
162-
key = "sonar.leak.period.type"
163-
params = get_component_params(component)
164-
params.update({"keys": key})
165-
data = json.loads(endpoint.get(Setting.API[c.GET], params=params, with_organization=(component is None)).text)["settings"]
166-
if not endpoint.is_sonarcloud() and len(data) > 0:
167-
data = data[0]
168-
else:
169-
data = {"inherited": True}
158+
data = get_settings_data(endpoint, key, component)
170159
return Setting.load(key=key, endpoint=endpoint, data=data, component=component)
171160

172161
@classmethod
@@ -214,6 +203,8 @@ def reload(self, data: types.ApiPayload) -> None:
214203
self.multi_valued = data.get("multiValues", False)
215204
if self.key == NEW_CODE_PERIOD:
216205
self.value = new_code_to_string(data)
206+
elif self.key == MQR_ENABLED:
207+
self.value = data.get("mode", "MQR") != "STANDARD_EXPERIENCE"
217208
elif self.key == COMPONENT_VISIBILITY:
218209
self.value = data.get("visibility", None)
219210
elif self.key == "sonar.login.message":
@@ -226,6 +217,9 @@ def reload(self, data: types.ApiPayload) -> None:
226217
self.value = util.DEFAULT
227218
self.__reload_inheritance(data)
228219

220+
def refresh(self) -> None:
221+
self.reload(get_settings_data(self.endpoint, self.key, self.component))
222+
229223
def __hash__(self) -> int:
230224
"""Returns object unique ID"""
231225
return hash((self.key, self.component.key if self.component else None, self.base_url()))
@@ -241,10 +235,17 @@ def set(self, value: any) -> bool:
241235
log.debug("%s set to '%s'", str(self), str(value))
242236
if not self.is_settable():
243237
log.error("Setting '%s' does not seem to be a settable setting, trying to set anyway...", str(self))
244-
if value is None or value == "" or (self.key == "sonar.autodetect.ai.code" and value is True):
245-
return self.endpoint.reset_setting(self.key)
238+
return False
239+
if value is None or value == "" or (self.key == "sonar.autodetect.ai.code" and value is True and self.endpoint.version() < (2025, 2, 0)):
240+
return self.reset()
241+
if self.key == MQR_ENABLED:
242+
if ok := self.patch(Setting.API["MQR_MODE"], params={"mode": "STANDARD_EXPERIENCE" if not value else "MQR"}).ok:
243+
self.value = value
244+
return ok
246245
if self.key in (COMPONENT_VISIBILITY, PROJECT_DEFAULT_VISIBILITY):
247-
return set_visibility(endpoint=self.endpoint, component=self.component, visibility=value)
246+
if ok := set_visibility(endpoint=self.endpoint, component=self.component, visibility=value):
247+
self.value = value
248+
return ok
248249

249250
# Hack: Up to 9.4 cobol settings are comma separated mono-valued, in 9.5+ they are multi-valued
250251
if self.endpoint.version() > (9, 4, 0) or not self.key.startswith("sonar.cobol"):
@@ -256,34 +257,21 @@ def set(self, value: any) -> bool:
256257
return False
257258

258259
log.debug("Setting %s to value '%s'", str(self), str(value))
259-
params = {"key": self.key, "component": self.component.key if self.component else None}
260-
untransformed_value = value
261-
if isinstance(value, list):
262-
if isinstance(value[0], str):
263-
params["values"] = value
264-
else:
265-
params["fieldValues"] = [json.dumps(v) for v in value]
266-
elif isinstance(value, bool):
267-
params["value"] = str(value).lower()
268-
else:
269-
pname = "values" if self.multi_valued else "value"
270-
params[pname] = value
260+
params = {"key": self.key, "component": self.component.key if self.component else None} | encode(self, value)
271261
try:
272-
r = self.post(Setting.API[c.CREATE], params=params)
273-
self.value = untransformed_value
274-
return r.ok
262+
if ok := self.post(Setting.API[c.CREATE], params=params).ok:
263+
self.value = value
264+
return ok
275265
except (ConnectionError, RequestException) as e:
276266
util.handle_error(e, f"setting setting '{self.key}' of {str(self.component)}", catch_all=True)
277267
return False
278268

279269
def reset(self) -> bool:
280270
log.info("Resetting %s", str(self))
281-
params = {"keys": self.key}
282-
if self.component:
283-
params["component"] = self.component.key
271+
params = {"keys": self.key} | {} if not self.component else {"component": self.component.key}
284272
try:
285273
r = self.post("settings/reset", params=params)
286-
self.value = None
274+
self.refresh()
287275
return r.ok
288276
except (ConnectionError, RequestException) as e:
289277
util.handle_error(e, f"resetting setting '{self.key}' of {str(self.component)}", catch_all=True)
@@ -443,7 +431,7 @@ def get_bulk(
443431
o = get_new_code_period(endpoint, component)
444432
settings_dict[o.key] = o
445433
VALID_SETTINGS.update(set(settings_dict.keys()))
446-
VALID_SETTINGS.update({"sonar.scm.provider"})
434+
VALID_SETTINGS.update({"sonar.scm.provider", MQR_ENABLED})
447435
return settings_dict
448436

449437

@@ -562,6 +550,17 @@ def decode(setting_key: str, setting_value: any) -> any:
562550
return setting_value
563551

564552

553+
def encode(setting: Setting, setting_value: any) -> dict[str, str]:
554+
"""Encodes the params to pass to api/settings/set according to setting value type"""
555+
if isinstance(setting_value, list):
556+
params = {"values": setting_value} if isinstance(setting_value[0], str) else {"fieldValues": [json.dumps(v) for v in setting_value]}
557+
elif isinstance(setting_value, bool):
558+
params = {"value": str(setting_value).lower()}
559+
else:
560+
params = {"values" if setting.multi_valued else "value": setting_value}
561+
return params
562+
563+
565564
def reset_setting(endpoint: pf.Platform, setting_key: str, project: Optional[object] = None) -> bool:
566565
"""Resets a setting to its default"""
567566
return get_object(endpoint=endpoint, key=setting_key, component=project).reset()
@@ -575,3 +574,25 @@ def get_component_params(component: object, name: str = "component") -> types.Ap
575574
return {name: component.project.key, "branch": component.key}
576575
else:
577576
return {name: component.key}
577+
578+
579+
def get_settings_data(endpoint: pf.Platform, key: str, component: Optional[object]) -> types.ApiPayload:
580+
"""Reads a setting data with different API depending on setting key
581+
:return: The returned API data"""
582+
583+
if key == NEW_CODE_PERIOD and not endpoint.is_sonarcloud():
584+
params = get_component_params(component, name="project")
585+
data = json.loads(endpoint.get(Setting.API["NEW_CODE_GET"], params=params).text)
586+
elif key == MQR_ENABLED:
587+
data = json.loads(endpoint.get(Setting.API["MQR_MODE"]).text)
588+
else:
589+
if key == NEW_CODE_PERIOD:
590+
key = "sonar.leak.period.type"
591+
params = get_component_params(component)
592+
params.update({"keys": key})
593+
data = json.loads(endpoint.get(Setting.API[c.GET], params=params, with_organization=(component is None)).text)["settings"]
594+
if not endpoint.is_sonarcloud() and len(data) > 0:
595+
data = data[0]
596+
else:
597+
data = {"inherited": True}
598+
return data

test/unit/test_settings.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# sonar-tools tests
2+
# Copyright (C) 2025 Olivier Korach
3+
# mailto:olivier.korach AT gmail DOT com
4+
#
5+
# This program is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 3 of the License, or (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this program; if not, write to the Free Software Foundation,
17+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18+
#
19+
20+
"""Test of settings"""
21+
22+
import utilities as tutil
23+
from sonar import settings
24+
25+
26+
def test_set_standard() -> None:
27+
"""test_set_standard"""
28+
29+
o = settings.get_object(tutil.SQ, "sonar.java.file.suffixes")
30+
val = o.value
31+
new_val = [".jav", ".java", ".javacard"]
32+
assert o.set(new_val)
33+
assert sorted(o.value) == sorted(new_val)
34+
35+
new_val = [".jav", ".java", ".javacard", ".jah"]
36+
assert o.set(", ".join(new_val))
37+
assert sorted(o.value) == sorted(new_val)
38+
39+
assert o.reset()
40+
assert sorted(o.value) == sorted([".jav", ".java"])
41+
assert o.set(val)
42+
assert sorted(o.value) == sorted(val)
43+
44+
45+
def test_autodetect_ai() -> None:
46+
"""test_autodetect_ai"""
47+
48+
o = settings.get_object(tutil.SQ, "sonar.autodetect.ai.code")
49+
if tutil.SQ.version() < (2025, 1, 0):
50+
assert o is None
51+
52+
val = o.value
53+
assert o.set(True)
54+
assert o.value
55+
assert o.set(False)
56+
assert not o.value
57+
assert o.set(val)
58+
59+
60+
def test_mqr_mode() -> None:
61+
"""test_mqr_mode"""
62+
o = settings.get_object(tutil.SQ, "sonar.multi-quality-mode.enabled")
63+
if tutil.SQ.version() < (2025, 1, 0):
64+
assert o is None
65+
val = o.value
66+
assert o.set(True)
67+
assert o.value
68+
assert o.set(False)
69+
assert not o.value
70+
assert o.set(val)
71+
72+
73+
def test_unsettable() -> None:
74+
"""test_unsettable"""
75+
o = settings.get_object(tutil.SQ, "sonar.core.startTime")
76+
assert o is not None
77+
assert not o.set("2025-01-01")
78+
o = settings.get_object(tutil.SQ, "sonar.auth.github.apiUrl")
79+
assert o is not None
80+
res = True if tutil.SQ.version() < (10, 0, 0) else False
81+
assert o.set("https://api.github.com/") == res

0 commit comments

Comments
 (0)