Skip to content

Commit 9e33088

Browse files
authored
Merge pull request #102 from Integration-Automation/dev
docs: add SonarQube/Codacy linter compliance rules
2 parents 4f16814 + 81c7ef9 commit 9e33088

6 files changed

Lines changed: 185 additions & 39 deletions

File tree

CLAUDE.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# LoadDensity
2+
3+
Load & Stress Automation Framework built on top of Locust.
4+
5+
## Tech Stack
6+
7+
- Python 3.10+
8+
- Locust (load testing engine)
9+
- PySide6 + qt-material (optional GUI)
10+
- setuptools (build system)
11+
12+
## Project Structure
13+
14+
- `je_load_density/` - main package
15+
- `gui/` - PySide6 GUI with multi-language support
16+
- `utils/` - utilities (executor, file I/O, reports, logging, JSON/XML, socket server, test records)
17+
- `wrapper/` - Locust wrappers (env creation, event hooks, proxy users, start/stop)
18+
- `load_density_driver/` - driver generation
19+
- `test/` - pytest test suite
20+
- `docs/` - Sphinx documentation
21+
22+
## Development Commands
23+
24+
```bash
25+
# Install
26+
pip install -e .
27+
pip install -e ".[gui]"
28+
29+
# Test
30+
pytest test/
31+
32+
# Build
33+
python -m build
34+
```
35+
36+
## Coding Standards
37+
38+
### Design Patterns & Software Engineering
39+
40+
- Apply appropriate design patterns (Strategy, Factory, Observer, etc.) where they reduce complexity
41+
- Follow SOLID principles: single responsibility, open-closed, Liskov substitution, interface segregation, dependency inversion
42+
- Prefer composition over inheritance
43+
- Keep functions small and focused on a single task
44+
- Use meaningful, descriptive names for variables, functions, classes, and modules
45+
46+
### Performance
47+
48+
- Avoid unnecessary object creation in hot paths
49+
- Prefer generators over lists for large data iteration
50+
- Use appropriate data structures (set for membership checks, dict for lookups)
51+
- Minimize I/O operations; batch when possible
52+
- Profile before optimizing - measure, don't guess
53+
54+
### Code Hygiene
55+
56+
- Remove all unused imports, variables, functions, classes, and dead code blocks
57+
- No commented-out code in commits
58+
- No placeholder or stub code left behind
59+
- Every import must be used; every function must be called or exported
60+
61+
### Security
62+
63+
- Never hardcode secrets, tokens, passwords, or API keys
64+
- Validate and sanitize all external input (user input, file content, network data)
65+
- Use parameterized queries for any database operations
66+
- Avoid `eval()`, `exec()`, and `__import__()` with untrusted input
67+
- Use `subprocess` with argument lists, never shell=True with user input
68+
- Set restrictive file permissions on sensitive files
69+
- Escape output to prevent injection (HTML, XML, JSON)
70+
- Pin dependency versions to avoid supply chain attacks
71+
72+
### Linter Compliance (SonarQube / Codacy / Pylint / Flake8)
73+
74+
Code must pass static analysis with no new issues introduced. Follow these rules proactively so SonarQube, Codacy, Pylint, Flake8, Bandit, and Radon do not flag regressions.
75+
76+
#### Complexity & Structure
77+
78+
- Cognitive complexity per function: ≤ 15 (SonarQube rule `python:S3776`)
79+
- Cyclomatic complexity per function: ≤ 10 (Radon grade A–B)
80+
- Function length: ≤ 80 lines; file length: ≤ 1000 lines
81+
- Max parameters per function: ≤ 7 (SonarQube `python:S107`)
82+
- Max nesting depth: ≤ 4 levels (SonarQube `python:S134`)
83+
- Avoid deeply nested `if/for/try` — extract helpers or use early returns
84+
- No duplicated code blocks ≥ 10 lines (SonarQube `common-py:DuplicatedBlocks`)
85+
- Keep boolean expressions simple: ≤ 3 operators (SonarQube `python:S1067`)
86+
87+
#### Naming & Style (PEP 8 + Pylint)
88+
89+
- `snake_case` for functions, methods, variables, modules
90+
- `PascalCase` for classes; `UPPER_SNAKE_CASE` for module-level constants
91+
- Private members prefixed with single underscore `_name`
92+
- Line length: ≤ 120 characters (soft limit), hard max 160
93+
- No single-letter names except loop counters (`i`, `j`, `k`) and comprehensions
94+
- Avoid shadowing built-ins (`id`, `list`, `type`, `dict`, `file`, etc.)
95+
- No unused function/method parameters — prefix with `_` if required by signature
96+
97+
#### Bug-Prone Patterns
98+
99+
- Never use mutable default arguments (`def f(x=[])`) — use `None` sentinel (SonarQube `python:S5644`)
100+
- Do not compare with `==` / `!=` to `None`, `True`, `False` — use `is` / `is not`
101+
- Do not catch bare `except:` — catch specific exceptions; never swallow silently
102+
- Always re-raise with `raise` or `raise X from e`, preserving context
103+
- Close resources with `with` context managers (files, sockets, locks)
104+
- Do not modify a collection while iterating over it
105+
- Avoid `assert` for runtime validation (stripped by `python -O`); raise explicit exceptions
106+
- No `TODO` / `FIXME` / `XXX` comments without a tracked issue reference
107+
- Remove unreachable code after `return`, `raise`, `break`, `continue`
108+
109+
#### Type Safety & API Design
110+
111+
- Public functions and methods should have type hints (parameters + return)
112+
- Avoid `Any` unless truly dynamic; prefer `Optional[T]`, `Union[...]`, protocols
113+
- Do not return inconsistent types from one function (e.g. `str` or `None` or `int`)
114+
- Prefer `@dataclass` or `TypedDict` over ad-hoc dict payloads
115+
- Use `enum.Enum` instead of string/int constants for closed sets
116+
117+
#### Security (Bandit + SonarQube Security Hotspots)
118+
119+
- No `eval`, `exec`, `pickle.loads`, `yaml.load` (use `yaml.safe_load`) on untrusted input
120+
- No `hashlib.md5` / `sha1` for security purposes — use `sha256` or `blake2b`
121+
- No `random` module for tokens/secrets — use `secrets` module
122+
- No `tempfile.mktemp` — use `mkstemp` / `NamedTemporaryFile`
123+
- Never log secrets, tokens, or raw request bodies containing credentials
124+
- Validate file paths against traversal (`..`, absolute paths, symlinks)
125+
- Set explicit timeouts on `requests.*` and socket operations
126+
127+
#### Testing Hygiene
128+
129+
- Tests must be deterministic — no reliance on wall-clock, network, or ordering
130+
- Each test asserts something; no test without an `assert`
131+
- Mock external side effects (filesystem writes, HTTP, subprocess)
132+
- Test names describe behavior: `test_<unit>_<condition>_<expected>`
133+
134+
### Git Commit Rules
135+
136+
- Commit messages must NOT reference any AI tool, assistant, or model name
137+
- No `Co-Authored-By` lines referencing AI
138+
- Write commit messages as if authored solely by the developer
139+
- Use conventional commit style: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`
140+
- Keep subject line under 72 characters
141+
- Use imperative mood ("add feature" not "added feature")

je_load_density/utils/executor/action_executor.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
from je_load_density.utils.package_manager.package_manager_class import package_manager
2727
from je_load_density.wrapper.start_wrapper.start_test import start_test
2828

29+
_UNSAFE_BUILTINS = frozenset({
30+
"eval", "exec", "compile", "__import__",
31+
"breakpoint", "open", "input",
32+
})
33+
2934

3035
class Executor:
3136
"""
@@ -54,9 +59,11 @@ def __init__(self) -> None:
5459
"LD_add_package_to_executor": package_manager.add_package_to_executor,
5560
}
5661

57-
# 將所有 Python 內建函式加入事件字典
58-
# Add all Python built-in functions to event_dict
62+
# 將安全的 Python 內建函式加入事件字典,排除可執行任意程式碼者
63+
# Add safe Python built-in functions; exclude those allowing arbitrary code execution
5964
for name, func in getmembers(builtins, isbuiltin):
65+
if name in _UNSAFE_BUILTINS:
66+
continue
6067
self.event_dict[name] = func
6168

6269
def _execute_event(self, action: list) -> Any:
@@ -100,11 +107,8 @@ def execute_action(self, action_list: Union[list, dict]) -> dict[str, Any]:
100107

101108
execute_record_dict: dict[str, Any] = {}
102109

103-
try:
104-
if not isinstance(action_list, list) or len(action_list) == 0:
105-
raise LoadDensityTestExecuteException(executor_list_error)
106-
except Exception as error:
107-
print(repr(error), file=sys.stderr)
110+
if not isinstance(action_list, list) or len(action_list) == 0:
111+
raise LoadDensityTestExecuteException(executor_list_error)
108112

109113
for action in action_list:
110114
try:

je_load_density/utils/generate_report/generate_html_report.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import sys
2-
from threading import Lock
2+
from html import escape
33
from typing import List, Tuple
44

55
from je_load_density.utils.exception.exceptions import LoadDensityHTMLException
66
from je_load_density.utils.exception.exception_tags import html_generate_no_data_tag
77
from je_load_density.utils.test_record.test_record_class import test_record_instance
88

9+
10+
def _safe(value: object) -> str:
11+
return escape(str(value), quote=True)
12+
913
# HTML 標頭 (HTML head)
1014
_HTML_STRING_HEAD = """<!DOCTYPE html>
1115
<html lang="en">
@@ -79,24 +83,24 @@ def generate_html() -> Tuple[List[str], List[str]]:
7983

8084
success_list: List[str] = [
8185
_SUCCESS_TABLE.format(
82-
Method=record.get("Method"),
83-
test_url=record.get("test_url"),
84-
name=record.get("name"),
85-
status_code=record.get("status_code"),
86-
text=record.get("text"),
87-
content=record.get("content"),
88-
headers=record.get("headers"),
86+
Method=_safe(record.get("Method")),
87+
test_url=_safe(record.get("test_url")),
88+
name=_safe(record.get("name")),
89+
status_code=_safe(record.get("status_code")),
90+
text=_safe(record.get("text")),
91+
content=_safe(record.get("content")),
92+
headers=_safe(record.get("headers")),
8993
)
9094
for record in test_record_instance.test_record_list
9195
]
9296

9397
failure_list: List[str] = [
9498
_FAILURE_TABLE.format(
95-
http_method=record.get("Method"),
96-
test_url=record.get("test_url"),
97-
name=record.get("name"),
98-
status_code=record.get("status_code"),
99-
error=record.get("error"),
99+
http_method=_safe(record.get("Method")),
100+
test_url=_safe(record.get("test_url")),
101+
name=_safe(record.get("name")),
102+
status_code=_safe(record.get("status_code")),
103+
error=_safe(record.get("error")),
100104
)
101105
for record in test_record_instance.error_record_list
102106
]
@@ -112,18 +116,16 @@ def generate_html_report(html_name: str = "default_name") -> str:
112116
:param html_name: 輸出檔案名稱 (Output file name, without extension)
113117
:return: HTML 字串 (HTML string)
114118
"""
115-
_lock = Lock()
116119
success_list, failure_list = generate_html()
117120

118121
try:
119-
with _lock: # 使用 with 確保自動 acquire/release
120-
html_path = f"{html_name}.html"
121-
with open(html_path, "w+", encoding="utf-8") as file_to_write:
122-
file_to_write.write(_HTML_STRING_HEAD)
123-
file_to_write.writelines(success_list)
124-
file_to_write.writelines(failure_list)
125-
file_to_write.write(_HTML_STRING_BOTTOM)
126-
return html_path
122+
html_path = f"{html_name}.html"
123+
with open(html_path, "w+", encoding="utf-8") as file_to_write:
124+
file_to_write.write(_HTML_STRING_HEAD)
125+
file_to_write.writelines(success_list)
126+
file_to_write.writelines(failure_list)
127+
file_to_write.write(_HTML_STRING_BOTTOM)
128+
return html_path
127129
except Exception as error:
128130
print(repr(error), file=sys.stderr)
129131
return ""

je_load_density/utils/generate_report/generate_json_report.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from je_load_density.utils.exception.exceptions import LoadDensityGenerateJsonReportException
88
from je_load_density.utils.test_record.test_record_class import test_record_instance
99

10+
_json_report_lock = Lock()
11+
1012

1113
def generate_json() -> Tuple[Dict[str, dict], Dict[str, dict]]:
1214
"""
@@ -55,14 +57,13 @@ def generate_json_report(json_file_name: str = "default_name") -> Tuple[str, str
5557
:param json_file_name: 輸出檔案名稱前綴 (Output file name prefix)
5658
:return: (成功檔案路徑, 失敗檔案路徑)
5759
"""
58-
lock = Lock()
5960
success_dict, failure_dict = generate_json()
6061

6162
success_path = f"{json_file_name}_success.json"
6263
failure_path = f"{json_file_name}_failure.json"
6364

6465
try:
65-
with lock: # 使用 with 確保自動 acquire/release
66+
with _json_report_lock:
6667
with open(success_path, "w+", encoding="utf-8") as file_to_write:
6768
json.dump(success_dict, file_to_write, indent=4, ensure_ascii=False)
6869

je_load_density/utils/json/json_file/json_file.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
22
from pathlib import Path
33
from threading import Lock
4-
from typing import Any, Union
4+
from typing import Union
55

66
from je_load_density.utils.exception.exceptions import LoadDensityTestJsonException
77
from je_load_density.utils.exception.exception_tags import cant_find_json_error, cant_save_json_error
88

9+
_json_file_lock = Lock()
10+
911

1012
def read_action_json(json_file_path: str) -> Union[dict, list]:
1113
"""
@@ -16,18 +18,15 @@ def read_action_json(json_file_path: str) -> Union[dict, list]:
1618
:return: JSON 內容 (dict or list)
1719
:raises LoadDensityTestJsonException: 當檔案不存在或無法讀取時 (if file not found or cannot be read)
1820
"""
19-
lock = Lock()
2021
try:
21-
with lock:
22+
with _json_file_lock:
2223
file_path = Path(json_file_path)
2324
if file_path.exists() and file_path.is_file():
2425
with open(json_file_path, "r", encoding="utf-8") as read_file:
2526
return json.load(read_file)
2627
else:
2728
raise LoadDensityTestJsonException(cant_find_json_error)
2829
except Exception as error:
29-
# 捕捉所有錯誤並轉換成自訂例外
30-
# Catch all errors and raise custom exception
3130
raise LoadDensityTestJsonException(f"{cant_find_json_error}: {error}")
3231

3332

@@ -40,9 +39,8 @@ def write_action_json(json_save_path: str, action_json: Union[dict, list]) -> No
4039
:param action_json: 要寫入的資料 (data to write, dict or list)
4140
:raises LoadDensityTestJsonException: 當檔案無法寫入時 (if file cannot be saved)
4241
"""
43-
lock = Lock()
4442
try:
45-
with lock:
43+
with _json_file_lock:
4644
with open(json_save_path, "w+", encoding="utf-8") as file_to_write:
4745
json.dump(action_json, file_to_write, indent=4, ensure_ascii=False)
4846
except Exception as error:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from je_load_density.utils.socket_server.load_density_socket_server import start_load_density_socket_server
22

33
try:
4-
server = start_load_density_socket_server()
4+
start_load_density_socket_server()
55
except Exception as error:
66
print(repr(error))

0 commit comments

Comments
 (0)