Skip to content

Commit 417dbd0

Browse files
committed
fix: address SonarCloud quality issues on PR #105
Cognitive complexity (python:S3776) — extract helpers so each function stays at or below 15: - request_executor._check_assertions: split per assertion type into _ASSERTION_HANDLERS lookup with one helper per kind. - scenario_runner._eval_condition: dispatch operators via the new _CONDITION_OPS table. - mqtt/websocket _do_step: split into _dispatch_step plus per-method helpers (_publish, _subscribe, _send_only, _recv_only, etc.). - har_importer._entry_to_task: factor out _extract_request_headers and _attach_post_body. - socket_server.handle: factor out _handle_legacy_quit, _authorise_payload, and _dispatch_command. - influxdb_sink.start_influxdb_sink: factor _validate_transport and _build_listener. Other findings: - python:S3516 on start_influxdb_sink and __main__.main: stop always-returning the same value (drop the redundant True from the sink, replace the unreachable raise in main with a print + exit code 2 so the return is honest). - python:S4423 on the TLS server context: pin minimum_version to TLSv1_2 so older suites cannot be negotiated. - python:S1192 on the response terminator: extract _RESPONSE_TERMINATOR (and _AUTH_FAILED sentinel) instead of duplicating the byte string. - python:S1172 on execute_task/execute_tasks/run_scenario: drop the unused 'client' parameter; HTTP and FastHttp callers updated. - python:S6353 on the dotted-path and function regexes: collapse [A-Za-z0-9_] to \w in parameter_resolver and grpc_user_template. - python:S117 + python:S1481 in mcp_server: rename the Server local to server_cls in build_server, and drop the unused Server binding from run_stdio. bandit clean on the full package; pytest test/ — 84 passed.
1 parent 732eca8 commit 417dbd0

13 files changed

Lines changed: 316 additions & 204 deletions

File tree

je_load_density/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import List, Optional
55

66
from je_load_density.utils.exception.exception_tags import argparse_get_wrong_data
7-
from je_load_density.utils.exception.exceptions import LoadDensityTestExecuteException
87
from je_load_density.utils.executor.action_executor import execute_action, execute_files
98
from je_load_density.utils.file_process.get_dir_file_list import get_dir_files_as_list
109
from je_load_density.utils.json.json_file.json_file import read_action_json
@@ -114,7 +113,8 @@ def main(argv: Optional[List[str]] = None) -> int:
114113
if _dispatch_legacy(args):
115114
return 0
116115

117-
raise LoadDensityTestExecuteException(argparse_get_wrong_data)
116+
print(argparse_get_wrong_data, file=sys.stderr)
117+
return 2
118118

119119

120120
if __name__ == "__main__":

je_load_density/mcp_server/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ def build_server():
254254
"""
255255
Build the MCP Server instance with the LoadDensity tool surface.
256256
"""
257-
Server, _, mcp_types = _ensure_mcp()
258-
server = Server("loaddensity")
257+
server_cls, _, mcp_types = _ensure_mcp()
258+
server = server_cls("loaddensity")
259259

260260
@server.list_tools()
261261
async def _list_tools():
@@ -283,7 +283,7 @@ def run_stdio() -> None:
283283
"""
284284
Run the MCP server over stdio (the standard transport for Claude).
285285
"""
286-
Server, stdio_server, _ = _ensure_mcp()
286+
_, stdio_server, _ = _ensure_mcp()
287287
server = build_server()
288288

289289
import asyncio

je_load_density/utils/metrics/influxdb_sink.py

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,46 @@ def _post_line_protocol(line: str, url: str, token: Optional[str], timeout: floa
7676
load_density_logger.warning(f"InfluxDB write failed: {error}")
7777

7878

79+
def _validate_transport(transport: str, url: Optional[str]) -> None:
80+
if transport not in {"udp", "http"}:
81+
raise ValueError(f"unsupported transport: {transport}")
82+
if transport == "http":
83+
if not url:
84+
raise ValueError("url required when transport=http")
85+
if not url.lower().startswith(_ALLOWED_URL_SCHEMES):
86+
raise ValueError("InfluxDB URL must use http:// or https://")
87+
88+
89+
def _build_listener(config: Dict[str, Any]):
90+
transport = config["transport"]
91+
host = config["host"]
92+
port = config["port"]
93+
url = config["url"]
94+
token = config["token"]
95+
measurement = config["measurement"]
96+
timeout = config["timeout"]
97+
98+
def _listener(request_type, name, response_time, response_length, exception=None, **_kwargs):
99+
tags = {"request_type": str(request_type), "name": str(name)}
100+
fields: Dict[str, Any] = {
101+
"latency_ms": float(response_time or 0),
102+
"response_bytes": int(response_length or 0),
103+
"success": exception is None,
104+
}
105+
if exception is not None:
106+
fields["error"] = repr(exception)[:512]
107+
line = _build_line(measurement, tags, fields, time.time_ns())
108+
try:
109+
if transport == "udp":
110+
_send_udp(line, host, port)
111+
else:
112+
_post_line_protocol(line, url, token, timeout)
113+
except Exception as error:
114+
load_density_logger.debug(f"InfluxDB write failed: {error}")
115+
116+
return _listener
117+
118+
79119
def start_influxdb_sink(
80120
transport: str = "udp",
81121
host: str = "127.0.0.1",
@@ -84,7 +124,7 @@ def start_influxdb_sink(
84124
token: Optional[str] = None,
85125
measurement: str = "loaddensity_request",
86126
timeout: float = 2.0,
87-
) -> bool:
127+
) -> None:
88128
"""
89129
啟動 InfluxDB sink,將每個 request 寫入 line protocol。
90130
Start an InfluxDB sink that writes each request as a line-protocol
@@ -94,17 +134,11 @@ def start_influxdb_sink(
94134
InfluxDB endpoint; not a hard-coded destination.
95135
"""
96136
transport = transport.lower()
97-
if transport not in {"udp", "http"}:
98-
raise ValueError(f"unsupported transport: {transport}")
99-
if transport == "http":
100-
if not url:
101-
raise ValueError("url required when transport=http")
102-
if not url.lower().startswith(_ALLOWED_URL_SCHEMES):
103-
raise ValueError("InfluxDB URL must use http:// or https://")
137+
_validate_transport(transport, url)
104138

105139
with _lock:
106140
if _state["started"]:
107-
return True
141+
return
108142

109143
config = {
110144
"transport": transport,
@@ -115,31 +149,13 @@ def start_influxdb_sink(
115149
"measurement": measurement,
116150
"timeout": timeout,
117151
}
152+
listener = _build_listener(config)
118153

119-
def _listener(request_type, name, response_time, response_length, exception=None, **_kwargs):
120-
tags = {"request_type": str(request_type), "name": str(name)}
121-
fields: Dict[str, Any] = {
122-
"latency_ms": float(response_time or 0),
123-
"response_bytes": int(response_length or 0),
124-
"success": exception is None,
125-
}
126-
if exception is not None:
127-
fields["error"] = repr(exception)[:512]
128-
line = _build_line(measurement, tags, fields, time.time_ns())
129-
try:
130-
if transport == "udp":
131-
_send_udp(line, host, port)
132-
else:
133-
_post_line_protocol(line, url, token, timeout)
134-
except Exception as error:
135-
load_density_logger.debug(f"InfluxDB write failed: {error}")
136-
137-
events.request.add_listener(_listener)
154+
events.request.add_listener(listener)
138155
_state["started"] = True
139-
_state["listener"] = _listener
156+
_state["listener"] = listener
140157
_state["config"] = config
141158
load_density_logger.info(f"InfluxDB sink started transport={transport}")
142-
return True
143159

144160

145161
def stop_influxdb_sink() -> None:

je_load_density/utils/parameterization/parameter_resolver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any, Dict, Iterable, Iterator, List, Optional
77

88
_PLACEHOLDER_PATTERN = re.compile(r"\$\{([^}]+)\}")
9-
_FUNCTION_PATTERN = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)$")
9+
_FUNCTION_PATTERN = re.compile(r"^([a-zA-Z_]\w*)\((.*)\)$")
1010

1111

1212
class ParameterResolver:

je_load_density/utils/recording/har_importer.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,17 @@ def load_har(file_path: str) -> Dict[str, Any]:
1717
return json.load(fh)
1818

1919

20-
def _entry_to_task(entry: Dict[str, Any]) -> Optional[Dict[str, Any]]:
21-
request = entry.get("request") or {}
22-
method = str(request.get("method", "")).lower()
23-
url = request.get("url")
24-
if not method or not url:
25-
return None
26-
27-
headers = {}
28-
for header in request.get("headers") or []:
20+
def _extract_request_headers(raw_headers: Any) -> Dict[str, str]:
21+
headers: Dict[str, str] = {}
22+
for header in raw_headers or []:
2923
name = str(header.get("name", "")).strip()
3024
value = header.get("value", "")
3125
if name and name.lower() not in _NON_REQUEST_HEADERS:
3226
headers[name] = value
27+
return headers
3328

34-
task: Dict[str, Any] = {
35-
"method": method,
36-
"request_url": url,
37-
"name": f"{method.upper()} {_path_only(url)}",
38-
}
39-
if headers:
40-
task["headers"] = headers
4129

42-
post_data = request.get("postData") or {}
30+
def _attach_post_body(task: Dict[str, Any], post_data: Dict[str, Any]) -> None:
4331
mime = str(post_data.get("mimeType", "")).lower()
4432
text = post_data.get("text")
4533
params = post_data.get("params")
@@ -49,11 +37,32 @@ def _entry_to_task(entry: Dict[str, Any]) -> Optional[Dict[str, Any]]:
4937
task["json"] = json.loads(text)
5038
except json.JSONDecodeError:
5139
task["data"] = text
52-
elif params:
40+
return
41+
if params:
5342
task["data"] = {p.get("name"): p.get("value") for p in params if p.get("name")}
54-
elif text:
43+
return
44+
if text:
5545
task["data"] = text
5646

47+
48+
def _entry_to_task(entry: Dict[str, Any]) -> Optional[Dict[str, Any]]:
49+
request = entry.get("request") or {}
50+
method = str(request.get("method", "")).lower()
51+
url = request.get("url")
52+
if not method or not url:
53+
return None
54+
55+
task: Dict[str, Any] = {
56+
"method": method,
57+
"request_url": url,
58+
"name": f"{method.upper()} {_path_only(url)}",
59+
}
60+
headers = _extract_request_headers(request.get("headers"))
61+
if headers:
62+
task["headers"] = headers
63+
64+
_attach_post_body(task, request.get("postData") or {})
65+
5766
expected_status = (entry.get("response") or {}).get("status")
5867
if isinstance(expected_status, int) and expected_status:
5968
task["assertions"] = [{"type": "status_code", "value": expected_status}]

je_load_density/utils/socket_server/load_density_socket_server.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
_MAX_PAYLOAD_BYTES = 1 << 20 # 1 MiB
1717
_FRAME_HEADER = struct.Struct("!I")
18+
_RESPONSE_TERMINATOR = b"Return_Data_Over_JE\n"
19+
_AUTH_FAILED = object()
1820

1921

2022
class TCPServer:
@@ -49,6 +51,9 @@ def __init__(
4951
self._tls_context: Optional[ssl.SSLContext] = None
5052
if certfile and keyfile:
5153
self._tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
54+
# Pin minimum TLS version so older insecure suites cannot be
55+
# negotiated; PROTOCOL_TLS_SERVER alone permits TLS 1.0/1.1.
56+
self._tls_context.minimum_version = ssl.TLSVersion.TLSv1_2
5257
self._tls_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
5358

5459
def socket_server(self, host: str, port: int) -> None:
@@ -119,49 +124,66 @@ def handle(self, connection) -> None:
119124
print(f"Command received: {len(command_string)} bytes", flush=True)
120125

121126
if command_string == "quit_server":
122-
if self.token is not None:
123-
self._send_frame(connection, b"Error: token required\n")
124-
return
125-
self.close_flag = True
126-
self._send_frame(connection, b"Server shutting down\n")
127-
print("Now quit server", flush=True)
128-
return
129-
130-
try:
131-
payload = json.loads(command_string)
132-
except json.JSONDecodeError as error:
133-
self._send_frame(connection, f"Error: {error}\n".encode("utf-8"))
134-
self._send_frame(connection, b"Return_Data_Over_JE\n")
127+
self._handle_legacy_quit(connection)
135128
return
136129

137-
command = payload
138-
if isinstance(payload, dict) and ("token" in payload or "command" in payload):
139-
if not self._check_token(payload.get("token")):
140-
self._send_frame(connection, b"Error: unauthorised\n")
141-
return
142-
command = payload.get("command")
143-
if payload.get("op") == "quit":
144-
self.close_flag = True
145-
self._send_frame(connection, b"Server shutting down\n")
146-
return
147-
elif self.token is not None:
148-
self._send_frame(connection, b"Error: token required\n")
130+
command = self._authorise_payload(connection, command_string)
131+
if command is _AUTH_FAILED:
149132
return
150-
151133
if command is None:
152-
self._send_frame(connection, b"Return_Data_Over_JE\n")
134+
self._send_frame(connection, _RESPONSE_TERMINATOR)
153135
return
154136

155-
try:
156-
for execute_return in execute_action(command).values():
157-
self._send_frame(connection, f"{execute_return}\n".encode("utf-8"))
158-
self._send_frame(connection, b"Return_Data_Over_JE\n")
159-
except Exception as error:
160-
self._send_frame(connection, f"Error: {error}\n".encode("utf-8"))
161-
self._send_frame(connection, b"Return_Data_Over_JE\n")
137+
self._dispatch_command(connection, command)
162138
finally:
163139
connection.close()
164140

141+
def _handle_legacy_quit(self, connection) -> None:
142+
if self.token is not None:
143+
self._send_frame(connection, b"Error: token required\n")
144+
return
145+
self.close_flag = True
146+
self._send_frame(connection, b"Server shutting down\n")
147+
print("Now quit server", flush=True)
148+
149+
def _authorise_payload(self, connection, command_string: str):
150+
"""
151+
Decode the JSON envelope, enforce the token, and return the
152+
actual command to execute. Returns ``_AUTH_FAILED`` when the
153+
client has already been answered (bad JSON, missing/bad token,
154+
or a quit op was honoured).
155+
"""
156+
try:
157+
payload = json.loads(command_string)
158+
except json.JSONDecodeError as error:
159+
self._send_frame(connection, f"Error: {error}\n".encode("utf-8"))
160+
self._send_frame(connection, _RESPONSE_TERMINATOR)
161+
return _AUTH_FAILED
162+
163+
if isinstance(payload, dict) and ("token" in payload or "command" in payload):
164+
if not self._check_token(payload.get("token")):
165+
self._send_frame(connection, b"Error: unauthorised\n")
166+
return _AUTH_FAILED
167+
if payload.get("op") == "quit":
168+
self.close_flag = True
169+
self._send_frame(connection, b"Server shutting down\n")
170+
return _AUTH_FAILED
171+
return payload.get("command")
172+
173+
if self.token is not None:
174+
self._send_frame(connection, b"Error: token required\n")
175+
return _AUTH_FAILED
176+
177+
return payload
178+
179+
def _dispatch_command(self, connection, command) -> None:
180+
try:
181+
for execute_return in execute_action(command).values():
182+
self._send_frame(connection, f"{execute_return}\n".encode("utf-8"))
183+
except Exception as error:
184+
self._send_frame(connection, f"Error: {error}\n".encode("utf-8"))
185+
self._send_frame(connection, _RESPONSE_TERMINATOR)
186+
165187

166188
def start_load_density_socket_server(
167189
host: str = "localhost",

je_load_density/wrapper/user_template/fast_http_user_template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@ def test(self) -> None:
5050
proxy_user = locust_wrapper_proxy.user_dict.get("fast_http_user")
5151
if not proxy_user or not proxy_user.tasks:
5252
return
53-
run_scenario(self.client, self.method, proxy_user.tasks)
53+
run_scenario(self.method, proxy_user.tasks)

je_load_density/wrapper/user_template/grpc_user_template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def set_wrapper_grpc_user(user_detail_dict: Dict[str, Any], **kwargs) -> type:
2828
return GrpcUserWrapper
2929

3030

31-
_SAFE_DOTTED_PATH = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$")
31+
_SAFE_DOTTED_PATH = re.compile(r"^[A-Za-z_]\w*(\.[A-Za-z_]\w*)*$")
3232

3333

3434
def _import_dotted(path: str) -> Any:

je_load_density/wrapper/user_template/http_user_template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@ def test(self) -> None:
5050
proxy_user = locust_wrapper_proxy.user_dict.get("http_user")
5151
if not proxy_user or not proxy_user.tasks:
5252
return
53-
run_scenario(self.client, self.method, proxy_user.tasks)
53+
run_scenario(self.method, proxy_user.tasks)

0 commit comments

Comments
 (0)