Skip to content

Commit c042e45

Browse files
authored
Merge pull request #97 from Integration-Automation/dev
Dev
2 parents 8f53c8b + 824dda8 commit c042e45

37 files changed

+1252
-1167
lines changed

README.md

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,35 @@
1-
## LoadDensity
2-
[![Downloads](https://static.pepy.tech/badge/je-load-density)](https://pepy.tech/project/je-load-density)
3-
4-
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/b3f05488c16a44959cbf0ec28d4c977c)](https://www.codacy.com/gh/JE-Chen/LoadDensity/dashboard?utm_source=github.com&utm_medium=referral&utm_content=JE-Chen/LoadDensity&utm_campaign=Badge_Grade)
5-
6-
[![LoadDensity Stable Python3.8](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_8.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_8.yml)
7-
8-
[![LoadDensity Stable Python3.9](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_9.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_9.yml)
9-
10-
[![LoadDensity Stable Python3.10](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_10.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_10.yml)
11-
12-
[![LoadDensity Stable Python3.11](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_11.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/LoadDensity/actions/workflows/stable_python3_11.yml)
13-
14-
### Documentation
15-
16-
[![Documentation Status](https://readthedocs.org/projects/loaddensity/badge/?version=latest)](https://loaddensity.readthedocs.io/en/latest/?badge=latest)
17-
18-
[LoadDensity Doc Click Here!](https://loaddensity.readthedocs.io/en/latest/)
19-
20-
---
21-
> Project Kanban \
22-
> https://github.com/orgs/Integration-Automation/projects/2/views/1
23-
> * Load automation.
24-
> * Easy setup user template.
25-
> * Load Density script.
26-
> * Generate JSON/HTML/XML report.
27-
> * 1 sec / thousands requests.
28-
> * Fast spawn users.
29-
> * Multi test on one task.
30-
> * Specify test time.
31-
> * OS Independent.
32-
> * Remote automation support.
33-
> * Project & Template support.
34-
---
35-
36-
## install
1+
# LoadDensity
2+
A high‑performance load testing and automation tool.
3+
It supports fast user spawning, flexible templates, and generates reports in multiple formats.
4+
Designed to be cross‑platform and easy to integrate into your projects.
5+
6+
- Load automation: Quickly set up and run load tests
7+
- User templates: Simple configuration for reusable test users
8+
- Load Density scripts: Define and execute repeatable scenarios
9+
- Report generation: Export results in JSON, HTML, or XML
10+
- High throughput: Thousands of requests per second
11+
- Fast user spawning: Scale up test users instantly
12+
- Multi‑test support: Run multiple tests on a single task
13+
- Configurable test duration: Specify how long tests should run
14+
- OS independent: Works across major operating systems
15+
- Remote automation: Execute tests remotely
16+
- Project & template support: Organize and reuse test setup
17+
18+
## Installation
3719

3820
```
3921
pip install je_locust_wrapper
4022
```
4123

42-
## require
24+
## Require
4325

4426
```
45-
python 3.8 or later
27+
python 3.9 or later
4628
```
4729

48-
>* Test on
49-
>> * windows 10 ~ 11
50-
>> * osx 10.5 ~ 11 big sur
51-
>> * ubuntu 20.0.4
52-
>> * raspberry pi 3B+
53-
>> * All test in test dir
54-
>> *
55-
### Architecture Diagram
56-
![Architecture Diagram](architecture_diagram/LoadDnesity_Archirecture.drawio.png)
30+
## Tested Platforms
31+
- Windows 10 ~ 11
32+
- macOS 10.15 ~ 11 Big Sur
33+
- Ubuntu 20.04
34+
- Raspberry Pi 3B+
35+
- All test cases are located in the test directory

architecture_diagram/LoadDnesity_Archirecture.drawio

Lines changed: 0 additions & 406 deletions
This file was deleted.
-434 KB
Binary file not shown.

dev.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66

77
[project]
88
name = "je_load_density_dev"
9-
version = "0.0.77"
9+
version = "0.0.78"
1010
authors = [
1111
{ name = "JE-Chen", email = "[email protected]" },
1212
]

dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ sphinx
33
twine
44
sphinx-rtd-theme
55
build
6+
pytest
Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,59 @@
11
from PySide6.QtCore import QThread
22
from je_load_density.wrapper.start_wrapper.start_test import start_test
33

4+
# 定義常數,避免硬編碼字串
5+
# Define constant to avoid hard-coded string
6+
DEFAULT_USER_TYPE = "fast_http_user"
7+
48

59
class LoadDensityGUIThread(QThread):
10+
"""
11+
GUI 測試執行緒
12+
GUI Test Thread
13+
14+
用於在背景執行負載測試,避免阻塞主介面。
15+
Used to run load tests in the background without blocking the main GUI.
16+
"""
617

7-
def __init__(self):
18+
def __init__(self,
19+
request_url: str = None,
20+
test_duration: int = None,
21+
user_count: int = None,
22+
spawn_rate: int = None,
23+
http_method: str = None):
24+
"""
25+
初始化執行緒參數
26+
Initialize thread parameters
27+
28+
:param request_url: 測試目標 URL (Target request URL)
29+
:param test_duration: 測試持續時間 (Test duration in seconds)
30+
:param user_count: 使用者數量 (Number of simulated users)
31+
:param spawn_rate: 使用者生成速率 (User spawn rate)
32+
:param http_method: HTTP 方法 (HTTP method, e.g., "GET", "POST")
33+
"""
834
super().__init__()
9-
self.url = None
10-
self.test_time = None
11-
self.user_count = None
12-
self.spawn_rate = None
13-
self.method = None
35+
self.request_url = request_url
36+
self.test_duration = test_duration
37+
self.user_count = user_count
38+
self.spawn_rate = spawn_rate
39+
self.http_method = http_method
1440

1541
def run(self):
42+
"""
43+
執行負載測試
44+
Run the load test
45+
"""
46+
if not self.request_url or not self.http_method:
47+
# 基本檢查,避免傳入 None
48+
# Basic validation to avoid None values
49+
raise ValueError("Request URL and HTTP method must be provided.")
50+
1651
start_test(
17-
{
18-
"user": "fast_http_user",
19-
},
20-
self.user_count, self.spawn_rate, self.test_time,
21-
**{
22-
"tasks": {
23-
self.method: {"request_url": self.url},
24-
}
52+
{"user": DEFAULT_USER_TYPE}, # 使用者類型 (User type)
53+
self.user_count,
54+
self.spawn_rate,
55+
self.test_duration,
56+
tasks={
57+
self.http_method: {"request_url": self.request_url}
2558
}
2659
)
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
import logging
22
import queue
33

4-
locust_log_queue = queue.Queue()
4+
# 建立一個佇列,用來存放攔截到的日誌訊息
5+
# Create a queue to store intercepted log messages
6+
log_message_queue: queue.Queue[str] = queue.Queue()
7+
58

69
class InterceptAllFilter(logging.Filter):
10+
"""
11+
攔截所有日誌訊息並存入佇列
12+
Intercept all log messages and store them into a queue
13+
14+
此 Filter 可用於將 logging 模組的輸出導向 GUI 或其他處理流程。
15+
This filter can be used to redirect logging outputs to a GUI or other processing pipelines.
16+
"""
17+
18+
def filter(self, record: logging.LogRecord) -> bool:
19+
"""
20+
攔截日誌紀錄並存入佇列
21+
Intercept log record and put it into the queue
722
8-
def filter(self, record):
9-
locust_log_queue.put(record.getMessage())
10-
return False
23+
:param record: logging.LogRecord 物件 (Log record object)
24+
:return: False → 阻止訊息繼續傳遞到其他 Handler
25+
False → Prevents the message from propagating to other handlers
26+
"""
27+
# 只存放訊息文字,也可以改成存整個 record 以保留更多資訊
28+
# Only store the message text; alternatively, store the whole record for more details
29+
log_message_queue.put(record.getMessage())
30+
return False

je_load_density/gui/main_widget.py

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,52 @@
11
import logging
2+
import queue
3+
from typing import Optional
24

35
from PySide6.QtCore import QTimer
46
from PySide6.QtGui import QIntValidator
5-
from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QComboBox, QPushButton, QTextEdit, QVBoxLayout, QLabel
7+
from PySide6.QtWidgets import (
8+
QWidget, QFormLayout, QLineEdit, QComboBox,
9+
QPushButton, QTextEdit, QVBoxLayout, QLabel, QMessageBox
10+
)
611

712
from je_load_density.gui.load_density_gui_thread import LoadDensityGUIThread
813
from je_load_density.gui.language_wrapper.multi_language_wrapper import language_wrapper
9-
from je_load_density.gui.log_to_ui_filter import InterceptAllFilter, locust_log_queue
14+
from je_load_density.gui.log_to_ui_filter import InterceptAllFilter, log_message_queue
1015

1116

1217
class LoadDensityWidget(QWidget):
18+
"""
19+
負載測試 GUI 控制元件
20+
Load Test GUI Widget
1321
14-
def __init__(self, parent=None):
22+
提供使用者輸入測試參數並啟動負載測試,
23+
並將日誌訊息即時顯示在 GUI 中。
24+
Provides input fields for test parameters, starts load tests,
25+
and displays log messages in real-time.
26+
"""
27+
28+
def __init__(self, parent: Optional[QWidget] = None):
1529
super().__init__(parent)
16-
# from
30+
31+
# === 表單區域 (Form Section) ===
1732
form_layout = QFormLayout()
33+
34+
# URL 輸入框 (Target URL input)
1835
self.url_input = QLineEdit()
36+
37+
# 測試時間 (Test duration, must be int)
1938
self.test_time_input = QLineEdit()
2039
self.test_time_input.setValidator(QIntValidator())
40+
41+
# 使用者數量 (User count)
2142
self.user_count_input = QLineEdit()
2243
self.user_count_input.setValidator(QIntValidator())
44+
45+
# 生成速率 (Spawn rate)
2346
self.spawn_rate_input = QLineEdit()
2447
self.spawn_rate_input.setValidator(QIntValidator())
48+
49+
# HTTP 方法選擇 (HTTP method selection)
2550
self.method_combobox = QComboBox()
2651
self.method_combobox.addItems([
2752
language_wrapper.language_word_dict.get("get"),
@@ -32,50 +57,75 @@ def __init__(self, parent=None):
3257
language_wrapper.language_word_dict.get("head"),
3358
language_wrapper.language_word_dict.get("options"),
3459
])
60+
61+
# 將元件加入表單 (Add widgets to form layout)
3562
form_layout.addRow(language_wrapper.language_word_dict.get("url"), self.url_input)
3663
form_layout.addRow(language_wrapper.language_word_dict.get("test_time"), self.test_time_input)
3764
form_layout.addRow(language_wrapper.language_word_dict.get("user_count"), self.user_count_input)
3865
form_layout.addRow(language_wrapper.language_word_dict.get("spawn_rate"), self.spawn_rate_input)
3966
form_layout.addRow(language_wrapper.language_word_dict.get("test_method"), self.method_combobox)
4067

68+
# === 啟動按鈕 (Start button) ===
4169
self.start_button = QPushButton(language_wrapper.language_word_dict.get("start_button"))
4270
self.start_button.clicked.connect(self.run_load_density)
4371

44-
# Log panel
72+
# === 日誌面板 (Log panel) ===
4573
self.log_panel = QTextEdit()
4674
self.log_panel.setReadOnly(True)
4775

48-
# Add widget to vertical layout
76+
# === 主版面配置 (Main layout) ===
4977
main_layout = QVBoxLayout()
5078
main_layout.addLayout(form_layout)
5179
main_layout.addWidget(self.start_button)
5280
main_layout.addWidget(QLabel(language_wrapper.language_word_dict.get("log")))
5381
main_layout.addWidget(self.log_panel)
5482

55-
# Param
56-
self.run_load_density_thread = None
83+
# === 執行緒與計時器 (Thread & Timer) ===
84+
self.run_load_density_thread: Optional[LoadDensityGUIThread] = None
5785
self.pull_log_timer = QTimer()
58-
self.pull_log_timer.setInterval(20)
86+
self.pull_log_timer.setInterval(50) # 稍微放大間隔,避免 UI 卡頓
5987
self.pull_log_timer.timeout.connect(self.add_text_to_log)
6088

6189
self.setLayout(main_layout)
6290

63-
def run_load_density(self):
91+
def run_load_density(self) -> None:
92+
"""
93+
啟動負載測試
94+
Start the load test
95+
"""
96+
try:
97+
test_time = int(self.test_time_input.text())
98+
user_count = int(self.user_count_input.text())
99+
spawn_rate = int(self.spawn_rate_input.text())
100+
except ValueError:
101+
QMessageBox.warning(self, "Invalid Input", "請輸入有效的數字\nPlease enter valid numbers")
102+
return
103+
64104
self.run_load_density_thread = LoadDensityGUIThread()
65-
self.run_load_density_thread.url = self.url_input.text()
66-
self.run_load_density_thread.test_time = int(self.test_time_input.text())
67-
self.run_load_density_thread.user_count = int(self.user_count_input.text())
68-
self.run_load_density_thread.spawn_rate = int(self.spawn_rate_input.text())
69-
self.run_load_density_thread.method = self.method_combobox.currentText().lower()
70-
log_handler_list = [handler for handler in logging.getLogger("root").handlers if handler.name == "log_reader"]
105+
self.run_load_density_thread.request_url = self.url_input.text()
106+
self.run_load_density_thread.test_duration = test_time
107+
self.run_load_density_thread.user_count = user_count
108+
self.run_load_density_thread.spawn_rate = spawn_rate
109+
self.run_load_density_thread.http_method = self.method_combobox.currentText().lower()
110+
111+
# 設定日誌攔截器 (Attach log filter)
112+
root_logger = logging.getLogger("root")
113+
log_handler_list = [handler for handler in root_logger.handlers if handler.name == "log_reader"]
71114
if log_handler_list:
72115
log_handler = log_handler_list[0]
73-
log_handler.addFilter(InterceptAllFilter())
116+
# 避免重複新增 Filter (Prevent duplicate filters)
117+
if not any(isinstance(f, InterceptAllFilter) for f in log_handler.filters):
118+
log_handler.addFilter(InterceptAllFilter())
119+
120+
# 啟動執行緒與計時器 (Start thread & timer)
74121
self.run_load_density_thread.start()
75-
self.pull_log_timer.stop()
76122
self.pull_log_timer.start()
77123
self.log_panel.clear()
78124

79-
def add_text_to_log(self):
80-
if not locust_log_queue.empty():
81-
self.log_panel.append(locust_log_queue.get_nowait())
125+
def add_text_to_log(self) -> None:
126+
"""
127+
將日誌訊息加入到 GUI 面板
128+
Append log messages to GUI panel
129+
"""
130+
while not log_message_queue.empty():
131+
self.log_panel.append(log_message_queue.get_nowait())

0 commit comments

Comments
 (0)