Skip to content
Merged

Dev #97

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 26 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,35 @@
## LoadDensity
[![Downloads](https://static.pepy.tech/badge/je-load-density)](https://pepy.tech/project/je-load-density)

[![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)

[![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)

[![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)

[![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)

[![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)

### Documentation

[![Documentation Status](https://readthedocs.org/projects/loaddensity/badge/?version=latest)](https://loaddensity.readthedocs.io/en/latest/?badge=latest)

[LoadDensity Doc Click Here!](https://loaddensity.readthedocs.io/en/latest/)

---
> Project Kanban \
> https://github.com/orgs/Integration-Automation/projects/2/views/1
> * Load automation.
> * Easy setup user template.
> * Load Density script.
> * Generate JSON/HTML/XML report.
> * 1 sec / thousands requests.
> * Fast spawn users.
> * Multi test on one task.
> * Specify test time.
> * OS Independent.
> * Remote automation support.
> * Project & Template support.
---

## install
# LoadDensity
A high‑performance load testing and automation tool.
It supports fast user spawning, flexible templates, and generates reports in multiple formats.
Designed to be cross‑platform and easy to integrate into your projects.

- Load automation: Quickly set up and run load tests
- User templates: Simple configuration for reusable test users
- Load Density scripts: Define and execute repeatable scenarios
- Report generation: Export results in JSON, HTML, or XML
- High throughput: Thousands of requests per second
- Fast user spawning: Scale up test users instantly
- Multi‑test support: Run multiple tests on a single task
- Configurable test duration: Specify how long tests should run
- OS independent: Works across major operating systems
- Remote automation: Execute tests remotely
- Project & template support: Organize and reuse test setup

## Installation

```
pip install je_locust_wrapper
```

## require
## Require

```
python 3.8 or later
python 3.9 or later
```

>* Test on
>> * windows 10 ~ 11
>> * osx 10.5 ~ 11 big sur
>> * ubuntu 20.0.4
>> * raspberry pi 3B+
>> * All test in test dir
>> *
### Architecture Diagram
![Architecture Diagram](architecture_diagram/LoadDnesity_Archirecture.drawio.png)
## Tested Platforms
- Windows 10 ~ 11
- macOS 10.15 ~ 11 Big Sur
- Ubuntu 20.04
- Raspberry Pi 3B+
- All test cases are located in the test directory
406 changes: 0 additions & 406 deletions architecture_diagram/LoadDnesity_Archirecture.drawio

This file was deleted.

Binary file not shown.
2 changes: 1 addition & 1 deletion dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "je_load_density_dev"
version = "0.0.77"
version = "0.0.78"
authors = [
{ name = "JE-Chen", email = "jechenmailman@gmail.com" },
]
Expand Down
1 change: 1 addition & 0 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ sphinx
twine
sphinx-rtd-theme
build
pytest
61 changes: 47 additions & 14 deletions je_load_density/gui/load_density_gui_thread.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
from PySide6.QtCore import QThread
from je_load_density.wrapper.start_wrapper.start_test import start_test

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


class LoadDensityGUIThread(QThread):
"""
GUI 測試執行緒
GUI Test Thread

用於在背景執行負載測試,避免阻塞主介面。
Used to run load tests in the background without blocking the main GUI.
"""

def __init__(self):
def __init__(self,
request_url: str = None,
test_duration: int = None,
user_count: int = None,
spawn_rate: int = None,
http_method: str = None):
"""
初始化執行緒參數
Initialize thread parameters

:param request_url: 測試目標 URL (Target request URL)
:param test_duration: 測試持續時間 (Test duration in seconds)
:param user_count: 使用者數量 (Number of simulated users)
:param spawn_rate: 使用者生成速率 (User spawn rate)
:param http_method: HTTP 方法 (HTTP method, e.g., "GET", "POST")
"""
super().__init__()
self.url = None
self.test_time = None
self.user_count = None
self.spawn_rate = None
self.method = None
self.request_url = request_url
self.test_duration = test_duration
self.user_count = user_count
self.spawn_rate = spawn_rate
self.http_method = http_method

def run(self):
"""
執行負載測試
Run the load test
"""
if not self.request_url or not self.http_method:
# 基本檢查,避免傳入 None
# Basic validation to avoid None values
raise ValueError("Request URL and HTTP method must be provided.")

start_test(
{
"user": "fast_http_user",
},
self.user_count, self.spawn_rate, self.test_time,
**{
"tasks": {
self.method: {"request_url": self.url},
}
{"user": DEFAULT_USER_TYPE}, # 使用者類型 (User type)
self.user_count,
self.spawn_rate,
self.test_duration,
tasks={
self.http_method: {"request_url": self.request_url}
}
)
28 changes: 24 additions & 4 deletions je_load_density/gui/log_to_ui_filter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import logging
import queue

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


class InterceptAllFilter(logging.Filter):
"""
攔截所有日誌訊息並存入佇列
Intercept all log messages and store them into a queue

此 Filter 可用於將 logging 模組的輸出導向 GUI 或其他處理流程。
This filter can be used to redirect logging outputs to a GUI or other processing pipelines.
"""

def filter(self, record: logging.LogRecord) -> bool:
"""
攔截日誌紀錄並存入佇列
Intercept log record and put it into the queue

def filter(self, record):
locust_log_queue.put(record.getMessage())
return False
:param record: logging.LogRecord 物件 (Log record object)
:return: False → 阻止訊息繼續傳遞到其他 Handler
False → Prevents the message from propagating to other handlers
"""
# 只存放訊息文字,也可以改成存整個 record 以保留更多資訊
# Only store the message text; alternatively, store the whole record for more details
log_message_queue.put(record.getMessage())
return False
92 changes: 71 additions & 21 deletions je_load_density/gui/main_widget.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
import logging
import queue

Check warning on line 2 in je_load_density/gui/main_widget.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

je_load_density/gui/main_widget.py#L2

'queue' imported but unused (F401)
from typing import Optional

from PySide6.QtCore import QTimer
from PySide6.QtGui import QIntValidator
from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QComboBox, QPushButton, QTextEdit, QVBoxLayout, QLabel
from PySide6.QtWidgets import (
QWidget, QFormLayout, QLineEdit, QComboBox,
QPushButton, QTextEdit, QVBoxLayout, QLabel, QMessageBox
)

from je_load_density.gui.load_density_gui_thread import LoadDensityGUIThread
from je_load_density.gui.language_wrapper.multi_language_wrapper import language_wrapper
from je_load_density.gui.log_to_ui_filter import InterceptAllFilter, locust_log_queue
from je_load_density.gui.log_to_ui_filter import InterceptAllFilter, log_message_queue


class LoadDensityWidget(QWidget):
"""
負載測試 GUI 控制元件
Load Test GUI Widget

def __init__(self, parent=None):
提供使用者輸入測試參數並啟動負載測試,
並將日誌訊息即時顯示在 GUI 中。
Provides input fields for test parameters, starts load tests,
and displays log messages in real-time.
"""

def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent)
# from

# === 表單區域 (Form Section) ===
form_layout = QFormLayout()

# URL 輸入框 (Target URL input)
self.url_input = QLineEdit()

# 測試時間 (Test duration, must be int)
self.test_time_input = QLineEdit()
self.test_time_input.setValidator(QIntValidator())

# 使用者數量 (User count)
self.user_count_input = QLineEdit()
self.user_count_input.setValidator(QIntValidator())

# 生成速率 (Spawn rate)
self.spawn_rate_input = QLineEdit()
self.spawn_rate_input.setValidator(QIntValidator())

# HTTP 方法選擇 (HTTP method selection)
self.method_combobox = QComboBox()
self.method_combobox.addItems([
language_wrapper.language_word_dict.get("get"),
Expand All @@ -32,50 +57,75 @@
language_wrapper.language_word_dict.get("head"),
language_wrapper.language_word_dict.get("options"),
])

# 將元件加入表單 (Add widgets to form layout)
form_layout.addRow(language_wrapper.language_word_dict.get("url"), self.url_input)
form_layout.addRow(language_wrapper.language_word_dict.get("test_time"), self.test_time_input)
form_layout.addRow(language_wrapper.language_word_dict.get("user_count"), self.user_count_input)
form_layout.addRow(language_wrapper.language_word_dict.get("spawn_rate"), self.spawn_rate_input)
form_layout.addRow(language_wrapper.language_word_dict.get("test_method"), self.method_combobox)

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

# Log panel
# === 日誌面板 (Log panel) ===
self.log_panel = QTextEdit()
self.log_panel.setReadOnly(True)

# Add widget to vertical layout
# === 主版面配置 (Main layout) ===
main_layout = QVBoxLayout()
main_layout.addLayout(form_layout)
main_layout.addWidget(self.start_button)
main_layout.addWidget(QLabel(language_wrapper.language_word_dict.get("log")))
main_layout.addWidget(self.log_panel)

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

self.setLayout(main_layout)

def run_load_density(self):
def run_load_density(self) -> None:
"""
啟動負載測試
Start the load test
"""
try:
test_time = int(self.test_time_input.text())
user_count = int(self.user_count_input.text())
spawn_rate = int(self.spawn_rate_input.text())
except ValueError:
QMessageBox.warning(self, "Invalid Input", "請輸入有效的數字\nPlease enter valid numbers")
return

self.run_load_density_thread = LoadDensityGUIThread()
self.run_load_density_thread.url = self.url_input.text()
self.run_load_density_thread.test_time = int(self.test_time_input.text())
self.run_load_density_thread.user_count = int(self.user_count_input.text())
self.run_load_density_thread.spawn_rate = int(self.spawn_rate_input.text())
self.run_load_density_thread.method = self.method_combobox.currentText().lower()
log_handler_list = [handler for handler in logging.getLogger("root").handlers if handler.name == "log_reader"]
self.run_load_density_thread.request_url = self.url_input.text()
self.run_load_density_thread.test_duration = test_time
self.run_load_density_thread.user_count = user_count
self.run_load_density_thread.spawn_rate = spawn_rate
self.run_load_density_thread.http_method = self.method_combobox.currentText().lower()

# 設定日誌攔截器 (Attach log filter)
root_logger = logging.getLogger("root")
log_handler_list = [handler for handler in root_logger.handlers if handler.name == "log_reader"]
if log_handler_list:
log_handler = log_handler_list[0]
log_handler.addFilter(InterceptAllFilter())
# 避免重複新增 Filter (Prevent duplicate filters)
if not any(isinstance(f, InterceptAllFilter) for f in log_handler.filters):
log_handler.addFilter(InterceptAllFilter())

# 啟動執行緒與計時器 (Start thread & timer)
self.run_load_density_thread.start()
self.pull_log_timer.stop()
self.pull_log_timer.start()
self.log_panel.clear()

def add_text_to_log(self):
if not locust_log_queue.empty():
self.log_panel.append(locust_log_queue.get_nowait())
def add_text_to_log(self) -> None:
"""
將日誌訊息加入到 GUI 面板
Append log messages to GUI panel
"""
while not log_message_queue.empty():
self.log_panel.append(log_message_queue.get_nowait())
Loading