From d58e63ea5f2c21e98887d4ef27df33f841375050 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Wed, 29 Apr 2026 00:14:53 +0800 Subject: [PATCH] docs: rebuild docs and READMEs in WebRunner-style chapter layout Reorganise the Sphinx manual into a 10-chapter structure that mirrors the WebRunner reference layout (Getting Started, Core API, Actions, User Templates, Reporting, Orchestration, Recording & Data, Tooling, Integrations, Reference) and add per-feature pages for everything shipped on the recent feature wave: - New English + Traditional Chinese pages for architecture, start_test / prepare_env, locust environment, action executor, parameter resolver, scenario modes, assertions, HTTP / WebSocket / gRPC / MQTT / socket users, metrics exporters, test record store, distributed master/worker, HAR import, SQLite persistence, MCP server, project scaffolding, exception hierarchy, and the API reference index. - Refresh installation, getting_started, cli, socket_server, generate_report, and gui pages so they cover the new extras, the CLI subcommand surface, the hardened control socket, all six report formats, and the live stats panel + JA/KO translations. - Drop the stale scheduler doc directories (LoadDensity has no scheduler; the page never matched real executor commands). - Upgrade conf.py to enable autodoc / autosummary / autosectionlabel / napoleon / mermaid, lazy-mock the optional runtime deps, and bring copyright forward; add sphinxcontrib-mermaid to docs/requirements. Rewrite the three READMEs (root, Traditional Chinese, Simplified Chinese) into the WebRunner-style layout: badge row, language switcher, full Table of Contents, Highlights, Installation (with extras matrix), Architecture diagram, Quick Start, Core API, Action Executor, User Templates per protocol, Parameter Resolver, Scenario Modes, Assertions & Extractors, Reports, Observability, Distributed runners, HAR record/replay, SQLite persistence, MCP server, Hardened Control Socket, GUI, CLI Usage, Test Record, Exception Handling, Logging, Supported Platforms, License. --- README.md | 746 ++++++++++++------ README/README_zh-CN.md | 724 +++++++++++------ README/README_zh-TW.md | 734 +++++++++++------ docs/requirements.txt | 3 +- .../action_executor/action_executor_doc.rst | 176 +++++ .../En/doc/api_reference/api_reference.rst | 37 + .../En/doc/architecture/architecture_doc.rst | 95 +++ .../En/doc/assertions/assertions_doc.rst | 76 ++ docs/source/En/doc/cli/cli_doc.rst | 136 ++-- .../doc/create_project/create_project_doc.rst | 42 + .../En/doc/distributed/distributed_doc.rst | 68 ++ .../source/En/doc/exception/exception_doc.rst | 42 + .../generate_report/generate_report_doc.rst | 179 ++--- .../getting_started/getting_started_doc.rst | 258 ++---- .../source/En/doc/grpc_user/grpc_user_doc.rst | 66 ++ docs/source/En/doc/gui/gui_doc.rst | 91 ++- .../En/doc/har_import/har_import_doc.rst | 68 ++ .../En/doc/http_users/http_users_doc.rst | 84 ++ .../En/doc/installation/installation_doc.rst | 102 ++- .../En/doc/locust_env/locust_env_doc.rst | 67 ++ .../En/doc/mcp_claude/mcp_claude_doc.rst | 72 ++ docs/source/En/doc/metrics/metrics_doc.rst | 94 +++ .../source/En/doc/mqtt_user/mqtt_user_doc.rst | 60 ++ .../parameter_resolver_doc.rst | 109 +++ .../source/En/doc/scenarios/scenarios_doc.rst | 96 +++ .../source/En/doc/scheduler/scheduler_doc.rst | 117 --- .../doc/socket_server/socket_server_doc.rst | 175 ++-- .../En/doc/socket_user/socket_user_doc.rst | 57 ++ .../sqlite_persistence_doc.rst | 58 ++ .../En/doc/start_test/start_test_doc.rst | 112 +++ .../En/doc/test_record/test_record_doc.rst | 44 ++ .../doc/websocket_user/websocket_user_doc.rst | 53 ++ docs/source/En/en_index.rst | 174 +++- .../action_executor/action_executor_doc.rst | 71 ++ .../Zh/doc/api_reference/api_reference.rst | 36 + .../Zh/doc/architecture/architecture_doc.rst | 77 ++ .../Zh/doc/assertions/assertions_doc.rst | 60 ++ docs/source/Zh/doc/cli/cli_doc.rst | 133 ++-- .../doc/create_project/create_project_doc.rst | 39 + .../Zh/doc/distributed/distributed_doc.rst | 58 ++ .../source/Zh/doc/exception/exception_doc.rst | 32 + .../generate_report/generate_report_doc.rst | 170 ++-- .../getting_started/getting_started_doc.rst | 253 ++---- .../source/Zh/doc/grpc_user/grpc_user_doc.rst | 61 ++ docs/source/Zh/doc/gui/gui_doc.rst | 85 +- .../Zh/doc/har_import/har_import_doc.rst | 56 ++ .../Zh/doc/http_users/http_users_doc.rst | 79 ++ .../Zh/doc/installation/installation_doc.rst | 103 +-- .../Zh/doc/locust_env/locust_env_doc.rst | 57 ++ .../Zh/doc/mcp_claude/mcp_claude_doc.rst | 66 ++ docs/source/Zh/doc/metrics/metrics_doc.rst | 84 ++ .../source/Zh/doc/mqtt_user/mqtt_user_doc.rst | 58 ++ .../parameter_resolver_doc.rst | 98 +++ .../source/Zh/doc/scenarios/scenarios_doc.rst | 89 +++ .../source/Zh/doc/scheduler/scheduler_doc.rst | 116 --- .../doc/socket_server/socket_server_doc.rst | 148 ++-- .../Zh/doc/socket_user/socket_user_doc.rst | 54 ++ .../sqlite_persistence_doc.rst | 54 ++ .../Zh/doc/start_test/start_test_doc.rst | 106 +++ .../Zh/doc/test_record/test_record_doc.rst | 38 + .../doc/websocket_user/websocket_user_doc.rst | 50 ++ docs/source/Zh/zh_index.rst | 161 +++- docs/source/conf.py | 53 +- docs/source/index.rst | 59 +- 64 files changed, 5445 insertions(+), 2174 deletions(-) create mode 100644 docs/source/En/doc/action_executor/action_executor_doc.rst create mode 100644 docs/source/En/doc/api_reference/api_reference.rst create mode 100644 docs/source/En/doc/architecture/architecture_doc.rst create mode 100644 docs/source/En/doc/assertions/assertions_doc.rst create mode 100644 docs/source/En/doc/create_project/create_project_doc.rst create mode 100644 docs/source/En/doc/distributed/distributed_doc.rst create mode 100644 docs/source/En/doc/exception/exception_doc.rst create mode 100644 docs/source/En/doc/grpc_user/grpc_user_doc.rst create mode 100644 docs/source/En/doc/har_import/har_import_doc.rst create mode 100644 docs/source/En/doc/http_users/http_users_doc.rst create mode 100644 docs/source/En/doc/locust_env/locust_env_doc.rst create mode 100644 docs/source/En/doc/mcp_claude/mcp_claude_doc.rst create mode 100644 docs/source/En/doc/metrics/metrics_doc.rst create mode 100644 docs/source/En/doc/mqtt_user/mqtt_user_doc.rst create mode 100644 docs/source/En/doc/parameter_resolver/parameter_resolver_doc.rst create mode 100644 docs/source/En/doc/scenarios/scenarios_doc.rst delete mode 100644 docs/source/En/doc/scheduler/scheduler_doc.rst create mode 100644 docs/source/En/doc/socket_user/socket_user_doc.rst create mode 100644 docs/source/En/doc/sqlite_persistence/sqlite_persistence_doc.rst create mode 100644 docs/source/En/doc/start_test/start_test_doc.rst create mode 100644 docs/source/En/doc/test_record/test_record_doc.rst create mode 100644 docs/source/En/doc/websocket_user/websocket_user_doc.rst create mode 100644 docs/source/Zh/doc/action_executor/action_executor_doc.rst create mode 100644 docs/source/Zh/doc/api_reference/api_reference.rst create mode 100644 docs/source/Zh/doc/architecture/architecture_doc.rst create mode 100644 docs/source/Zh/doc/assertions/assertions_doc.rst create mode 100644 docs/source/Zh/doc/create_project/create_project_doc.rst create mode 100644 docs/source/Zh/doc/distributed/distributed_doc.rst create mode 100644 docs/source/Zh/doc/exception/exception_doc.rst create mode 100644 docs/source/Zh/doc/grpc_user/grpc_user_doc.rst create mode 100644 docs/source/Zh/doc/har_import/har_import_doc.rst create mode 100644 docs/source/Zh/doc/http_users/http_users_doc.rst create mode 100644 docs/source/Zh/doc/locust_env/locust_env_doc.rst create mode 100644 docs/source/Zh/doc/mcp_claude/mcp_claude_doc.rst create mode 100644 docs/source/Zh/doc/metrics/metrics_doc.rst create mode 100644 docs/source/Zh/doc/mqtt_user/mqtt_user_doc.rst create mode 100644 docs/source/Zh/doc/parameter_resolver/parameter_resolver_doc.rst create mode 100644 docs/source/Zh/doc/scenarios/scenarios_doc.rst delete mode 100644 docs/source/Zh/doc/scheduler/scheduler_doc.rst create mode 100644 docs/source/Zh/doc/socket_user/socket_user_doc.rst create mode 100644 docs/source/Zh/doc/sqlite_persistence/sqlite_persistence_doc.rst create mode 100644 docs/source/Zh/doc/start_test/start_test_doc.rst create mode 100644 docs/source/Zh/doc/test_record/test_record_doc.rst create mode 100644 docs/source/Zh/doc/websocket_user/websocket_user_doc.rst diff --git a/README.md b/README.md index 51325e7..871abab 100644 --- a/README.md +++ b/README.md @@ -1,358 +1,622 @@ # LoadDensity -[![Python](https://img.shields.io/pypi/pyversions/je_load_density)](https://pypi.org/project/je_load_density/) -[![PyPI](https://img.shields.io/pypi/v/je_load_density)](https://pypi.org/project/je_load_density/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Documentation](https://readthedocs.org/projects/loaddensity/badge/?version=latest)](https://loaddensity.readthedocs.io/en/latest/) - -**LoadDensity** is a high-performance load & stress testing automation framework built on top of [Locust](https://locust.io/). It provides a simplified wrapper around Locust's core functionality, enabling fast user spawning, flexible test configuration via templates and JSON-driven scripts, report generation in multiple formats (HTML / JSON / XML), a built-in GUI, remote execution via TCP socket server, and a callback mechanism for post-test workflows. - -**[繁體中文](README/README_zh-TW.md)** | **[简体中文](README/README_zh-CN.md)** +

+ Multi-protocol load and stress automation: Locust + WebSocket + gRPC + MQTT + raw sockets, plus a JSON-driven action executor with batteries included. +

+ +

+ PyPI Version + Python Version + License + Documentation Status +

+ +

+ 繁體中文 | + 简体中文 +

--- -## Features - -- **Simplified Locust Wrapper** — Abstracts Locust's `Environment`, `Runner`, and `User` classes behind a clean, high-level API. -- **Two User Types** — Supports both `HttpUser` and `FastHttpUser` (geventhttpclient-based, higher throughput). -- **Fast User Spawning** — Scale to thousands of concurrent users with configurable spawn rate. -- **JSON-Driven Test Scripts** — Define test scenarios as JSON files and execute them without writing Python code. -- **Action Executor** — A built-in event-driven executor that maps action names to functions. Supports batch execution and file-driven execution. -- **Report Generation** — Export test results in three formats: - - **HTML** — Styled tables with success/failure records - - **JSON** — Structured data for programmatic consumption - - **XML** — Standard XML output for CI/CD integration -- **Request Hook** — Automatically records every request (success and failure) with method, URL, status code, response body, headers, and errors. -- **Callback Executor** — Chain a trigger function with a callback function for post-test workflows (e.g., run test then generate report). -- **TCP Socket Server** — Remote execution server based on gevent. Accepts JSON commands over TCP to execute tests remotely. -- **Project Scaffolding** — Auto-generate project directory structure with keyword templates and executor scripts. -- **Package Manager** — Dynamically load external Python packages and register their functions into the executor at runtime. -- **GUI (Optional)** — PySide6-based graphical interface with real-time log display, supporting English and Traditional Chinese. -- **CLI Support** — Run tests, execute scripts, or scaffold projects directly from the command line. -- **Cross-Platform** — Works on Windows, macOS, and Linux. +LoadDensity (`je_load_density`) started as a Locust wrapper and grew into a full multi-protocol load framework: HTTP, FastHttp, WebSocket, gRPC, MQTT, and raw TCP/UDP user templates behind one JSON-driven action executor, plus modules for parameterised data, scenario flow, reports, observability, distributed runners, recording, persistent storage, and an MCP control surface so Claude can drive load tests end-to-end. Every executor command has a deterministic name (`LD_*`) and a single dispatch point, so an action JSON can mix protocols, exporters, and reports in the same script. + +> **Optional dependencies, opt-in install** — every protocol driver and exporter ships behind a `pip install je_load_density[]` extra. The base install footprint is unchanged for users who only need HTTP load testing. + +## Table of Contents + +- [Highlights](#highlights) +- [Installation](#installation) +- [Architecture](#architecture) +- [Quick Start](#quick-start) +- [Core API](#core-api) +- [Action Executor](#action-executor) +- [User Templates](#user-templates) + - [HTTP / FastHttp](#http--fasthttp) + - [WebSocket](#websocket) + - [gRPC](#grpc) + - [MQTT](#mqtt) + - [Raw TCP / UDP](#raw-tcp--udp) +- [Parameter Resolver](#parameter-resolver) +- [Scenario Modes](#scenario-modes) +- [Assertions & Extractors](#assertions--extractors) +- [Reports](#reports) +- [Observability](#observability) +- [Distributed Master / Worker](#distributed-master--worker) +- [HAR Record / Replay](#har-record--replay) +- [Persistent Records (SQLite)](#persistent-records-sqlite) +- [MCP Server (for Claude)](#mcp-server-for-claude) +- [Hardened Control Socket](#hardened-control-socket) +- [GUI](#gui) +- [CLI Usage](#cli-usage) +- [Test Record](#test-record) +- [Exception Handling](#exception-handling) +- [Logging](#logging) +- [Supported Platforms](#supported-platforms) +- [License](#license) + +## Highlights + +- **One executor, six protocols** — HTTP, FastHttp, WebSocket, gRPC, MQTT, raw TCP/UDP — all dispatched from the same `LD_start_test` command via a `user` key. +- **JSON-driven** — Every test is an action JSON list; the same script can be hand-authored, generated by HAR import, scheduled by an MCP tool, or sent over the control socket. +- **Parameter resolver** — `${var.x}`, `${env.X}`, `${csv.source.col}`, `${faker.method}`, plus built-in `${uuid()}`, `${now()}`, `${randint(min,max)}` helpers; values can also be extracted from responses and reused downstream. +- **Scenario flow** — Declare tasks as `sequence` (default), `weighted`, or `conditional` (`run_if` / `skip_if` predicates) without touching Python. +- **Six report formats** — HTML, JSON, XML, CSV, JUnit XML, and a percentile-summary JSON. The summary covers totals, failure rate, and per-name p50 / p90 / p95 / p99 latencies for trend tracking. +- **Three exporters** — Prometheus HTTP endpoint, InfluxDB line-protocol UDP/HTTP sink, and OpenTelemetry OTLP gRPC exporter. +- **Distributed runners** — `runner_mode="master"` / `"worker"` for cross-machine load with the same start_test API. +- **HAR record / replay** — Convert real browser traffic into a runnable action JSON with regex include/exclude filters. +- **Persistent records** — Optional SQLite sink with run / record / metadata schema for cross-run regression checks. +- **MCP server** — `python -m je_load_density.mcp_server` exposes 11 tools so Claude (Desktop, Code, any MCP client) can drive LoadDensity end-to-end. +- **Hardened control socket** — Length-prefixed framing, optional TLS, shared-secret token (env or arg), with backwards-compatible legacy mode for existing IDE integrations such as PyBreeze. +- **Live GUI** — Optional PySide6 GUI with a live stats panel (RPS / avg / p95 / failures), translated to English, Traditional Chinese, Japanese, and Korean. +- **CLI subcommands** — `run` / `run-dir` / `run-str` / `init` / `serve`. Legacy `-e/-d/-c/--execute_str` flags remain for downstream tools. ## Installation -### Basic (CLI & Library) - ```bash pip install je_load_density ``` -### With GUI Support +Pulls in [Locust](https://locust.io/) and `defusedxml` — nothing else. + +### Optional extras + +| Extra | Adds | +|-------|------| +| `gui` | PySide6 + qt-material (graphical front-end) | +| `websocket` | `websocket-client` (WebSocket user template) | +| `grpc` | `grpcio` + `protobuf` (gRPC user template) | +| `mqtt` | `paho-mqtt` (MQTT user template) | +| `prometheus` | `prometheus-client` (Prometheus exporter) | +| `opentelemetry` | OpenTelemetry SDK + OTLP gRPC exporter | +| `metrics` | `prometheus` + `opentelemetry` bundle | +| `faker` | `Faker` (powers `${faker.method}` placeholders) | +| `mcp` | `mcp` SDK (drives the MCP server) | +| `all` | Everything above | + +```bash +pip install "je_load_density[gui]" +pip install "je_load_density[mqtt,grpc,websocket]" +pip install "je_load_density[metrics]" +pip install "je_load_density[mcp]" +pip install "je_load_density[all]" +``` + +### Development install ```bash -pip install je_load_density[gui] +git clone https://github.com/Integration-Automation/LoadDensity.git +cd LoadDensity +pip install -e ".[all]" +pip install -r requirements.txt ``` -This installs [PySide6](https://doc.qt.io/qtforpython/) and [qt-material](https://github.com/UN-GCPDS/qt-material) for the graphical interface. +## Architecture -## Requirements +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CLI / MCP / GUI / Control Socket │ +└──────────────────┬──────────────────────────────────────────────┘ + │ action JSON +┌──────────────────▼──────────────────────────────────────────────┐ +│ Action Executor (LD_* dispatch + safe builtins) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ start_test +┌──────────────────▼──────────────────────────────────────────────┐ +│ locust_wrapper_proxy (per-protocol task store) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┬──────────────┬──────────────┐ + ▼ ▼ ▼ ▼ +HTTP / FastHttp WebSocket gRPC MQTT Raw TCP / UDP + │ │ │ │ + └───────────────┬───────────────┴──────────────┴──────────────┘ + │ Locust events + ┌───────────────┴───────────────┐ + ▼ ▼ +test_record_instance Prometheus / InfluxDB / OTel + │ + ├── HTML / JSON / XML / CSV / JUnit / Summary reports + └── SQLite persistence (cross-run comparison) +``` -- Python **3.10** or later -- [Locust](https://locust.io/) (installed automatically as a dependency) +The dependency direction always points from the action layer down to Locust, never the other way around. ## Quick Start -### 1. Using the Python API +### HTTP load test in Python ```python from je_load_density import start_test -# Define user configuration and tasks -result = start_test( +start_test( user_detail_dict={"user": "fast_http_user"}, user_count=50, spawn_rate=10, - test_time=10, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"}, - } + test_time=30, + variables={"base": "https://httpbin.org"}, + tasks=[ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], ) ``` -**Parameters:** -| Parameter | Type | Default | Description | -|---|---|---|---| -| `user_detail_dict` | `dict` | — | User type configuration. `{"user": "fast_http_user"}` or `{"user": "http_user"}` | -| `user_count` | `int` | `50` | Total number of simulated users | -| `spawn_rate` | `int` | `10` | Number of users spawned per second | -| `test_time` | `int` | `60` | Test duration in seconds. `None` for unlimited | -| `web_ui_dict` | `dict` | `None` | Enable Locust Web UI, e.g. `{"host": "127.0.0.1", "port": 8089}` | -| `tasks` | `dict` | — | HTTP method to request URL mapping | +### Action JSON -### 2. Using JSON Script Files +```json +{"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://httpbin.org"}}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}} + ] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] +]} +``` -Create a JSON file (`test_scenario.json`): +Run via the CLI: -```json -[ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] -] -``` - -Execute from Python: +```bash +python -m je_load_density run smoke.json +``` + +## Core API ```python -from je_load_density import execute_action, read_action_json +from je_load_density import ( + start_test, prepare_env, create_env, + execute_action, execute_files, executor, add_command_to_executor, + test_record_instance, locust_wrapper_proxy, + register_variable, register_variables, + register_csv_source, register_csv_sources, + parameter_resolver, resolve, + har_to_action_json, har_to_tasks, load_har, + persist_records, list_runs, fetch_run_records, + start_prometheus_exporter, stop_prometheus_exporter, + start_influxdb_sink, stop_influxdb_sink, + start_opentelemetry_exporter, stop_opentelemetry_exporter, + start_load_density_socket_server, + generate_html_report, generate_json_report, generate_xml_report, + generate_csv_report, generate_junit_report, generate_summary_report, + build_summary, + create_project_dir, callback_executor, read_action_json, +) +``` + +`__all__` documents the full public surface in `je_load_density/__init__.py`. + +## Action Executor -execute_action(read_action_json("test_scenario.json")) +The action executor maps command strings to callable functions. Every action is a list: + +```python +["command_name"] # No parameters +["command_name", {"key": "value"}] # Keyword arguments +["command_name", [arg1, arg2]] # Positional arguments ``` -### 3. Using the CLI +The top-level document is either a bare list or `{"load_density": [...]}`. -```bash -# Execute a single JSON script file -python -m je_load_density -e test_scenario.json +### Built-in `LD_*` commands + +| Group | Commands | +|-------|----------| +| Core | `LD_start_test`, `LD_execute_action`, `LD_execute_files`, `LD_add_package_to_executor`, `LD_start_socket_server` | +| Reports | `LD_generate_html(_report)`, `LD_generate_json(_report)`, `LD_generate_xml(_report)`, `LD_generate_csv_report`, `LD_generate_junit_report`, `LD_generate_summary_report`, `LD_summary` | +| Persistence | `LD_persist_records`, `LD_list_runs`, `LD_fetch_run_records`, `LD_clear_records` | +| Parameters | `LD_register_variable(s)`, `LD_register_csv_source(s)`, `LD_clear_resolver` | +| Recording | `LD_load_har`, `LD_har_to_tasks`, `LD_har_to_action_json` | +| Metrics | `LD_start/stop_prometheus_exporter`, `LD_start/stop_influxdb_sink`, `LD_start/stop_opentelemetry_exporter` | -# Execute all JSON files in a directory -python -m je_load_density -d ./test_scripts/ +Safe Python built-ins (`print`, `len`, `range`, …) are also accepted; `eval`, `exec`, `compile`, `__import__`, `breakpoint`, `open`, and `input` are explicitly blocked. + +### Custom commands + +```python +from je_load_density import add_command_to_executor -# Execute an inline JSON string -python -m je_load_density --execute_str '[["LD_start_test", {"user_detail_dict": {"user": "fast_http_user"}, "user_count": 10, "spawn_rate": 5, "test_time": 5, "tasks": {"get": {"request_url": "http://httpbin.org/get"}}}]]' +def slack_notify(message: str) -> None: + ... -# Scaffold a new project with templates -python -m je_load_density -c MyProject +add_command_to_executor({"LD_slack_notify": slack_notify}) ``` -### 4. Using the GUI +## User Templates -```python -from je_load_density.gui.main_window import LoadDensityUI -from PySide6.QtWidgets import QApplication -import sys +Every template registers under `start_test` via `user_detail_dict={"user": ""}`. Tasks share the same shape across HTTP, WebSocket, gRPC, MQTT, and raw socket users; only the protocol-specific fields differ. -app = QApplication(sys.argv) -window = LoadDensityUI() -window.show() -sys.exit(app.exec()) +### HTTP / FastHttp + +```python +start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, spawn_rate=10, test_time=60, + variables={"base": "https://api.example.com"}, + tasks=[ + {"method": "post", "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [{"var": "auth", "from": "json_path", "path": "data.token"}]}, + {"method": "get", "request_url": "${var.base}/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], +) ``` -## Report Generation +### WebSocket -After running a test, generate reports from the recorded data: +`pip install je_load_density[websocket]` ```python -from je_load_density import ( - generate_html_report, - generate_json_report, - generate_xml_report, +start_test( + user_detail_dict={"user": "websocket_user"}, + user_count=10, spawn_rate=5, test_time=60, + tasks=[ + {"method": "connect", "request_url": "wss://echo.example.com/socket"}, + {"method": "sendrecv", "payload": '{"ping": 1}', "expect": "pong"}, + {"method": "close"}, + ], ) +``` -# HTML report — creates "my_report.html" -generate_html_report("my_report") +### gRPC -# JSON report — creates "my_report_success.json" and "my_report_failure.json" -generate_json_report("my_report") +`pip install je_load_density[grpc]` -# XML report — creates "my_report_success.xml" and "my_report_failure.xml" -generate_xml_report("my_report") +```python +start_test( + user_detail_dict={"user": "grpc_user"}, + user_count=20, spawn_rate=5, test_time=60, + tasks=[{ + "name": "say_hello", + "target": "localhost:50051", + "stub_path": "pkg.greeter_pb2_grpc.GreeterStub", + "request_path": "pkg.greeter_pb2.HelloRequest", + "method": "SayHello", + "payload": {"name": "world"}, + "metadata": [["x-token", "abc"]], + "timeout": 5, + }], +) ``` -## Advanced Usage +`stub_path` and `request_path` are validated against a strict identifier regex before `importlib.import_module`, so traversal-style attacks are rejected. -### Action Executor +### MQTT -The executor maps string action names to callable functions. All built-in Python functions are also available. +`pip install je_load_density[mqtt]` ```python -from je_load_density import executor, add_command_to_executor +start_test( + user_detail_dict={"user": "mqtt_user"}, + user_count=10, spawn_rate=5, test_time=60, + tasks=[ + {"method": "connect", "broker": "127.0.0.1:1883"}, + {"method": "subscribe", "topic": "telemetry/in", "qos": 1}, + {"method": "publish", "topic": "telemetry/out", "payload": "ping", "qos": 1}, + {"method": "disconnect"}, + ], +) +``` -# Register a custom function -def my_custom_action(message): - print(f"Custom: {message}") +### Raw TCP / UDP -add_command_to_executor({"my_action": my_custom_action}) +Stdlib only; nothing to install. -# Execute actions programmatically -executor.execute_action([ - ["my_action", ["Hello World"]], - ["print", ["Test complete"]], -]) +```python +start_test( + user_detail_dict={"user": "socket_user"}, + user_count=20, spawn_rate=5, test_time=60, + tasks=[ + {"protocol": "tcp", "target": "127.0.0.1:9000", + "payload": "PING\n", "expect_bytes": 64, + "expect_substring": "PONG"}, + {"protocol": "udp", "target": "127.0.0.1:9000", + "payload": "hex:DEADBEEF", "expect_bytes": 4}, + ], +) ``` -**Built-in executor actions:** -| Action Name | Description | -|---|---| -| `LD_start_test` | Start a load test | -| `LD_generate_html` | Generate HTML fragments | -| `LD_generate_html_report` | Generate full HTML report file | -| `LD_generate_json` | Generate JSON data structure | -| `LD_generate_json_report` | Generate JSON report files | -| `LD_generate_xml` | Generate XML strings | -| `LD_generate_xml_report` | Generate XML report files | -| `LD_execute_action` | Execute a list of actions | -| `LD_execute_files` | Execute actions from multiple files | -| `LD_add_package_to_executor` | Dynamically load a package into the executor | +## Parameter Resolver -### Callback Executor +Placeholders are expanded automatically on every task: -Chain a trigger function with a callback: +| Placeholder | Resolves to | +|-------------|-------------| +| `${var.NAME}` | Value passed to `register_variable(s)` | +| `${env.NAME}` | Environment variable `NAME` | +| `${csv.SOURCE.COL}` | Next row from CSV source `SOURCE` (cycles by default) | +| `${faker.METHOD}` | `Faker().METHOD()` (lazy import) | +| `${uuid()}` | New UUID 4 string | +| `${now()}` | Local ISO-8601 timestamp (seconds) | +| `${randint(min, max)}` | Cryptographically-strong random int | ```python -from je_load_density import callback_executor +from je_load_density import register_variable, register_csv_source -def after_test(): - print("Test finished, generating report...") +register_variable("base", "https://api.example.com") +register_csv_source("users", "users.csv") +``` -callback_executor.callback_function( - trigger_function_name="user_test", - callback_function=after_test, - user_detail_dict={"user": "fast_http_user"}, - user_count=10, - spawn_rate=5, - test_time=5, - tasks={"get": {"request_url": "http://httpbin.org/get"}}, -) +Or from action JSON: + +```json +["LD_register_variables", {"variables": {"base": "https://api.example.com"}}] +["LD_register_csv_sources", {"sources": [{"name": "users", "file_path": "users.csv"}]}] ``` -### TCP Socket Server (Remote Execution) +Unknown placeholders are left in place so missing data is visible during a dry run. -Start a TCP server that accepts JSON commands: +## Scenario Modes -```python -from je_load_density import start_load_density_socket_server +```json +{ + "mode": "weighted", + "tasks": [ + {"method": "get", "request_url": "/products", "weight": 3}, + {"method": "get", "request_url": "/expensive", "weight": 1} + ] +} +``` + +| Mode | Behaviour | +|------|-----------| +| `sequence` | Run every task in order each tick (default) | +| `weighted` | Pick one task per tick by `weight` | +| `conditional` | Use `run_if` / `skip_if` predicates evaluated against the parameter resolver | + +Predicates: `bool`, `"${var.x}"`, `{"equals": [a,b]}`, `{"not_equals": [a,b]}`, `{"in": [needle, haystack]}`, `{"truthy": value}`. -# Start server (blocking) -start_load_density_socket_server(host="localhost", port=9940) +## Assertions & Extractors + +Both run under Locust's `catch_response`; failed assertions surface in every report. + +```json +{ + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "assertions": [ + {"type": "status_code", "value": 200}, + {"type": "json_path", "path": "data.role", "value": "admin"} + ], + "extract": [ + {"var": "auth_token", "from": "json_path", "path": "data.token"}, + {"var": "request_id", "from": "header", "name": "X-Request-Id"} + ] +} ``` -Send commands from a client: +Assertion types: `status_code`, `contains`, `not_contains`, `json_path`, `header`. Extractor sources: `json_path`, `header`, `status_code`. + +## Reports + +Six formats consumed from `test_record_instance`: ```python -import socket, json +from je_load_density import ( + generate_html_report, generate_json_report, generate_xml_report, + generate_csv_report, generate_junit_report, generate_summary_report, +) -sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -sock.connect(("localhost", 9940)) +generate_html_report("report") # report.html +generate_json_report("report") # report_success.json + report_failure.json +generate_xml_report("report") # report_success.xml + report_failure.xml +generate_csv_report("report") # report.csv +generate_junit_report("report-junit") # report-junit.xml (CI) +generate_summary_report("report-sum") # totals + per-name p50/p90/p95/p99 +``` -command = json.dumps([ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, "spawn_rate": 5, "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] -]) -sock.send(command.encode("utf-8")) -response = sock.recv(8192) -print(response.decode("utf-8")) -sock.close() +## Observability + +```python +from je_load_density import ( + start_prometheus_exporter, start_influxdb_sink, start_opentelemetry_exporter, +) + +start_prometheus_exporter(port=9646, addr="127.0.0.1") +start_influxdb_sink(transport="udp", host="influxdb", port=8089) +start_opentelemetry_exporter(endpoint="http://otel-collector:4317", + service_name="loaddensity") ``` -Send `"quit_server"` to gracefully shut down the server. +| Sink | Metrics | +|------|---------| +| Prometheus | `loaddensity_requests_total`, `loaddensity_request_latency_ms`, `loaddensity_response_bytes` | +| InfluxDB | `loaddensity_request` line-protocol points (UDP or HTTP) | +| OTel | `loaddensity.requests`, `loaddensity.request.latency`, `loaddensity.response.size` | -### Project Scaffolding +All three are loaded lazily and gated by the matching install extra. -Generate a project with keyword templates and executor scripts: +## Distributed Master / Worker ```python -from je_load_density import create_project_dir +# master +start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="master", + master_bind_host="0.0.0.0", master_bind_port=5557, + expected_workers=4, + web_ui_dict={"host": "0.0.0.0", "port": 8089}, + user_count=400, spawn_rate=40, test_time=600, + tasks=[...], +) -create_project_dir(project_path="./my_tests", parent_name="LoadDensity") +# worker +start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", master_port=5557, + tasks=[...], +) ``` -This creates: -``` -my_tests/ -└── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json # FastHttpUser test template - │ └── keyword2.json # HttpUser test template - └── executor/ - ├── executor_one_file.py # Execute single keyword file - └── executor_folder.py # Execute all files in keyword/ +The master waits up to 60 s for `expected_workers` workers to register before starting the load ramp. + +## HAR Record / Replay + +```python +from je_load_density import load_har, har_to_action_json + +har = load_har("recording.har") +action_json = har_to_action_json( + har, + user="fast_http_user", + user_count=20, spawn_rate=10, test_time=120, + include=[r"api\.example\.com"], + exclude=[r"\.svg$"], +) ``` -### Dynamic Package Loading +Captures from Chrome / Firefox DevTools, mitmproxy, Charles, etc. all work. Status codes flow through as `status_code` assertions on every generated task. -Load external packages and register their functions into the executor: +## Persistent Records (SQLite) ```python -from je_load_density import executor +from je_load_density import persist_records, list_runs, fetch_run_records + +run_id = persist_records( + "loadtests.db", + label="checkout-2026-04-28", + metadata={"branch": "dev", "commit": "abc1234"}, +) +for row in list_runs("loadtests.db", limit=10): + print(row) +``` + +Schema is created lazily; an empty file is fine. Indexes on `run_id` and `name` keep cross-run queries fast. + +## MCP Server (for Claude) + +```bash +pip install "je_load_density[mcp]" +python -m je_load_density.mcp_server +``` -# Load a package and make its functions available as executor actions -executor.execute_action([ - ["LD_add_package_to_executor", ["my_custom_package"]] -]) +Wire it into Claude Desktop / Code: + +```json +{ + "mcpServers": { + "loaddensity": { + "command": "python", + "args": ["-m", "je_load_density.mcp_server"] + } + } +} +``` + +Eleven tools are exposed: `run_test`, `run_action_json`, `create_project`, `list_executor_commands`, `import_har`, `generate_reports`, `summary`, `persist_records`, `list_runs`, `fetch_run`, `clear_records`. + +## Hardened Control Socket + +```bash +python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 --framed \ + --token "$LOAD_DENSITY_SOCKET_TOKEN" \ + --tls-cert /etc/loaddensity/server.crt \ + --tls-key /etc/loaddensity/server.key ``` -### Test Records +- 4-byte big-endian length-prefixed frames (1 MiB cap) +- Optional TLS (cert/key on disk; `ssl.create_default_context`, TLS 1.2+ minimum) +- Shared-secret token compared with `hmac.compare_digest`; once configured, all payloads must use `{"token": "...", "command": [...]}` and may set `"op": "quit"` to stop the server +- Token also reads from `LOAD_DENSITY_SOCKET_TOKEN` env var +- Legacy unauthenticated mode preserved for backwards compatibility -Access raw test records programmatically: +## GUI + +```bash +pip install "je_load_density[gui]" +``` ```python -from je_load_density import test_record_instance +import sys +from PySide6.QtWidgets import QApplication +from je_load_density.gui.main_window import LoadDensityUI + +app = QApplication(sys.argv) +window = LoadDensityUI() +window.show() +sys.exit(app.exec()) +``` -# After running a test -for record in test_record_instance.test_record_list: - print(record["Method"], record["test_url"], record["status_code"]) +The GUI ships English, Traditional Chinese, Japanese, and Korean translations and a live stats panel that polls `test_record_instance` once a second (RPS, average / p95 latency, failure count). -for error in test_record_instance.error_record_list: - print(error["Method"], error["test_url"], error["error"]) +## CLI Usage -# Clear records -test_record_instance.clear_records() +``` +python -m je_load_density run FILE # execute one action JSON file +python -m je_load_density run-dir DIR # execute every .json in DIR +python -m je_load_density run-str JSON # execute an inline JSON string +python -m je_load_density init PATH # scaffold a project skeleton +python -m je_load_density serve [--host ...] # start the control socket ``` -## Architecture +Legacy single-flag form (`-e/-d/-c/--execute_str`) is still accepted for backwards compatibility with downstream tools. + +## Test Record + +`test_record_instance.test_record_list` and `error_record_list` collect every request with `Method`, `test_url`, `name`, `status_code`, `response_time_ms`, `response_length`, and (for failures) `error`. Reports and the SQLite sink read directly from these lists. + +## Exception Handling ``` -je_load_density/ -├── __init__.py # Public API exports -├── __main__.py # CLI entry point -├── gui/ # PySide6 GUI (optional dependency) -│ ├── main_window.py # Main window (QMainWindow) -│ ├── main_widget.py # Test parameter form & log panel -│ ├── load_density_gui_thread.py # Background thread for tests -│ ├── log_to_ui_filter.py # Log interceptor for GUI display -│ └── language_wrapper/ # i18n (English, Traditional Chinese) -├── wrapper/ -│ ├── create_locust_env/ # Locust Environment & Runner setup -│ ├── start_wrapper/ # High-level start_test() entry point -│ ├── user_template/ # HttpUser & FastHttpUser wrappers -│ ├── proxy/ # User proxy container & configuration -│ └── event/ # Request hook (records all requests) -└── utils/ - ├── executor/ # Action executor (event-driven) - ├── generate_report/ # HTML, JSON, XML report generators - ├── test_record/ # Test record storage - ├── socket_server/ # TCP server for remote execution - ├── callback/ # Callback function executor - ├── project/ # Project scaffolding & templates - ├── package_manager/ # Dynamic package loading - ├── json/ # JSON file read/write utilities - ├── xml/ # XML structure utilities - ├── file_process/ # Directory file listing - ├── logging/ # Logger instance - └── exception/ # Custom exceptions & error tags -``` - -## Tested Platforms - -- Windows 10 / 11 -- macOS 10.15 ~ 11 (Big Sur) -- Ubuntu 20.04 -- Raspberry Pi 3B+ +LoadDensityTestException +├── LoadDensityTestJsonException +├── LoadDensityGenerateJsonReportException +├── LoadDensityTestExecuteException +├── LoadDensityAssertException +├── LoadDensityHTMLException +├── LoadDensityAddCommandException +├── XMLException → XMLTypeException +└── CallbackExecutorException +``` -## License +All custom exceptions inherit from `LoadDensityTestException`; catching that one class covers the public surface. -This project is licensed under the [MIT License](LICENSE). +## Logging -## Contributing +LoadDensity exposes a single configured logger (`load_density_logger`) under `je_load_density.utils.logging.loggin_instance`. Hook it into your existing log infrastructure with the standard `logging` module APIs. -See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. +## Supported Platforms -## Links +| Platform | Status | +|----------|--------| +| Windows 10 / 11 | Fully supported | +| macOS | Fully supported | +| Ubuntu / Linux | Fully supported | +| Raspberry Pi | Tested on 3B+ and later | + +Python 3.10+ required. + +## License -- **PyPI**: https://pypi.org/project/je_load_density/ -- **Documentation**: https://loaddensity.readthedocs.io/en/latest/ -- **Source Code**: https://github.com/Intergration-Automation-Testing/LoadDensity +MIT — see [LICENSE](LICENSE). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index fcd92f3..0301d51 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -1,358 +1,590 @@ # LoadDensity -[![Python](https://img.shields.io/pypi/pyversions/je_load_density)](https://pypi.org/project/je_load_density/) -[![PyPI](https://img.shields.io/pypi/v/je_load_density)](https://pypi.org/project/je_load_density/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Documentation](https://readthedocs.org/projects/loaddensity/badge/?version=latest)](https://loaddensity.readthedocs.io/en/latest/) - -**LoadDensity** 是一个基于 [Locust](https://locust.io/) 构建的高性能负载与压力测试自动化框架。它对 Locust 的核心功能进行了简化封装,提供快速用户生成、通过模板与 JSON 脚本进行灵活测试配置、多格式报告生成(HTML / JSON / XML)、内置 GUI 图形界面、通过 TCP Socket 服务器进行远程执行,以及测试后工作流程的回调机制。 - -**[English](../README.md)** | **[繁體中文](README_zh-TW.md)** +

+ 多协议压力与负载自动化框架:Locust + WebSocket + gRPC + MQTT + 原生 socket,搭配内建电池的 JSON 动作执行器。 +

+ +

+ PyPI Version + Python Version + License + Documentation Status +

+ +

+ English | + 繁體中文 +

--- -## 功能特性 - -- **简化的 Locust 封装** — 将 Locust 的 `Environment`、`Runner` 和 `User` 类抽象化为简洁的高层 API。 -- **两种用户类型** — 同时支持 `HttpUser` 和 `FastHttpUser`(基于 geventhttpclient,吞吐量更高)。 -- **快速用户生成** — 可配置生成速率,轻松扩展至数千名并发用户。 -- **JSON 驱动的测试脚本** — 将测试场景定义为 JSON 文件,无需编写 Python 代码即可执行。 -- **动作执行器** — 内置的事件驱动执行器,将动作名称映射到函数。支持批量执行与文件驱动执行。 -- **报告生成** — 导出三种格式的测试结果: - - **HTML** — 包含成功/失败记录的样式化表格 - - **JSON** — 适合程序化处理的结构化数据 - - **XML** — 标准 XML 输出,适合 CI/CD 集成 -- **请求钩子** — 自动记录每个请求(成功与失败),包含方法、URL、状态码、响应内容、头部与错误信息。 -- **回调执行器** — 将触发函数与回调函数串联,用于测试后工作流程(例如:执行测试后自动生成报告)。 -- **TCP Socket 服务器** — 基于 gevent 的远程执行服务器。通过 TCP 接收 JSON 命令以远程执行测试。 -- **项目脚手架** — 自动生成项目目录结构,包含关键字模板与执行器脚本。 -- **包管理器** — 在运行时动态加载外部 Python 包,并将其函数注册到执行器中。 -- **GUI 图形界面(可选)** — 基于 PySide6 的图形界面,支持实时日志显示,提供英文与繁体中文界面。 -- **CLI 命令行支持** — 直接从命令行执行测试、运行脚本或创建项目结构。 -- **跨平台** — 支持 Windows、macOS 和 Linux。 +LoadDensity(`je_load_density`)从 Locust 封装起家,逐步扩展为完整的多协议负载框架:HTTP、FastHttp、WebSocket、gRPC、MQTT 与原生 TCP/UDP 等使用者模板,皆通过同一个 JSON 驱动的动作执行器;另含数据参数化、情境流程、报告、可观测性、分布式 runner、录制、持久化存储,以及让 Claude 端到端驱动测试的 MCP 控制接口。每个 executor 指令以 `LD_*` 命名、使用单一派发点,因此一份动作 JSON 可同时混用协议、exporter 与报告。 + +> **可选依赖、可选安装** — 每个协议驱动与 exporter 都以 `pip install je_load_density[]` 提供。仅做 HTTP 压测者运行期不受影响。 + +## 目录 + +- [亮点](#亮点) +- [安装](#安装) +- [架构](#架构) +- [Quick Start](#quick-start) +- [核心 API](#核心-api) +- [动作 Executor](#动作-executor) +- [使用者模板](#使用者模板) +- [参数解析器](#参数解析器) +- [情境模式](#情境模式) +- [断言与提取](#断言与提取) +- [报告](#报告) +- [可观测性](#可观测性) +- [分布式 Master / Worker](#分布式-master--worker) +- [HAR 录制/重放](#har-录制重放) +- [持久化记录(SQLite)](#持久化记录sqlite) +- [MCP Server(给 Claude)](#mcp-server给-claude) +- [硬化控制 Socket](#硬化控制-socket) +- [GUI](#gui) +- [CLI 用法](#cli-用法) +- [测试记录](#测试记录) +- [异常处理](#异常处理) +- [日志](#日志) +- [支持平台](#支持平台) +- [许可证](#许可证) + +## 亮点 + +- **一个 executor,六种协议** — HTTP、FastHttp、WebSocket、gRPC、MQTT、原生 TCP/UDP,全部通过 `LD_start_test` 以 `user` 切换派发。 +- **JSON 驱动** — 每支测试皆为动作 JSON 列表;可手写、由 HAR 导入产生、由 MCP 工具调度,或经控制 socket 传送。 +- **参数解析器** — `${var.x}`、`${env.X}`、`${csv.source.col}`、`${faker.method}`,以及内置 `${uuid()}`、`${now()}`、`${randint(min,max)}` 等 helper;可从响应提取值,后续 task 再用。 +- **情境流程** — 以 `sequence`(默认)/`weighted`/`conditional`(`run_if`、`skip_if`)声明 task 流程,无需动到 Python。 +- **六种报告格式** — HTML、JSON、XML、CSV、JUnit XML,以及百分位摘要 JSON(总计、失败率、per-name p50/p90/p95/p99)。 +- **三种 exporter** — Prometheus HTTP 端点、InfluxDB line-protocol UDP/HTTP sink、OpenTelemetry OTLP gRPC。 +- **分布式 runner** — `runner_mode="master"` / `"worker"`,跨机压测使用同一份 start_test API。 +- **HAR 录制/重放** — 将真实浏览流量转成可执行动作 JSON,含 regex include/exclude 过滤。 +- **持久化记录** — 可选 SQLite sink,含 run/record/metadata schema,便于跨次回归检查。 +- **MCP server** — `python -m je_load_density.mcp_server` 对外开 11 个工具,让 Claude 端到端驱动 LoadDensity。 +- **硬化控制 socket** — Length-prefix framing、可选 TLS、共享密钥 token(环境变数或参数),同时保留与 PyBreeze 等工具兼容的 legacy 模式。 +- **实时 GUI** — 可选的 PySide6 GUI 含实时统计面板(RPS、平均、p95、失败),翻译为英文、繁体中文、日文、韩文。 +- **CLI 子命令** — `run` / `run-dir` / `run-str` / `init` / `serve`。旧式 `-e/-d/-c/--execute_str` 标志保留以维持下游工具兼容。 ## 安装 -### 基本安装(CLI 与库) - ```bash pip install je_load_density ``` -### 包含 GUI 支持 +引入 [Locust](https://locust.io/) 与 `defusedxml`,仅此而已。 + +### 可选 extras + +| Extra | 加入 | +|-------|------| +| `gui` | PySide6 + qt-material(图形界面) | +| `websocket` | `websocket-client`(WebSocket user 模板) | +| `grpc` | `grpcio` + `protobuf`(gRPC user 模板) | +| `mqtt` | `paho-mqtt`(MQTT user 模板) | +| `prometheus` | `prometheus-client`(Prometheus exporter) | +| `opentelemetry` | OpenTelemetry SDK + OTLP gRPC exporter | +| `metrics` | `prometheus` + `opentelemetry` 一次装 | +| `faker` | `Faker`(驱动 `${faker.method}` 占位符) | +| `mcp` | `mcp` SDK(驱动 MCP server) | +| `all` | 上列全部 | + +```bash +pip install "je_load_density[gui]" +pip install "je_load_density[mqtt,grpc,websocket]" +pip install "je_load_density[metrics]" +pip install "je_load_density[mcp]" +pip install "je_load_density[all]" +``` + +### 开发安装 ```bash -pip install je_load_density[gui] +git clone https://github.com/Integration-Automation/LoadDensity.git +cd LoadDensity +pip install -e ".[all]" +pip install -r requirements.txt ``` -这会安装 [PySide6](https://doc.qt.io/qtforpython/) 和 [qt-material](https://github.com/UN-GCPDS/qt-material) 以提供图形界面。 +## 架构 -## 系统要求 +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CLI / MCP / GUI / 控制 Socket │ +└──────────────────┬──────────────────────────────────────────────┘ + │ 动作 JSON +┌──────────────────▼──────────────────────────────────────────────┐ +│ 动作 Executor(LD_* 派发 + 安全 builtin) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ start_test +┌──────────────────▼──────────────────────────────────────────────┐ +│ locust_wrapper_proxy(每协议 task store) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┬──────────────┬──────────────┐ + ▼ ▼ ▼ ▼ +HTTP / FastHttp WebSocket gRPC MQTT 原生 TCP / UDP + │ │ │ │ + └───────────────┬───────────────┴──────────────┴──────────────┘ + │ Locust 事件 + ┌───────────────┴───────────────┐ + ▼ ▼ +test_record_instance Prometheus / InfluxDB / OTel + │ + ├── HTML / JSON / XML / CSV / JUnit / Summary 报告 + └── SQLite 持久化(跨次比对) +``` -- Python **3.10** 或更高版本 -- [Locust](https://locust.io/)(会作为依赖项自动安装) +依赖方向永远是动作层 → Locust。 -## 快速上手 +## Quick Start -### 1. 使用 Python API +### Python API ```python from je_load_density import start_test -# 定义用户配置与任务 -result = start_test( +start_test( user_detail_dict={"user": "fast_http_user"}, - user_count=50, - spawn_rate=10, - test_time=10, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"}, - } + user_count=50, spawn_rate=10, test_time=30, + variables={"base": "https://httpbin.org"}, + tasks=[ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], ) ``` -**参数说明:** -| 参数 | 类型 | 默认值 | 说明 | -|---|---|---|---| -| `user_detail_dict` | `dict` | — | 用户类型配置。`{"user": "fast_http_user"}` 或 `{"user": "http_user"}` | -| `user_count` | `int` | `50` | 模拟用户总数 | -| `spawn_rate` | `int` | `10` | 每秒生成的用户数量 | -| `test_time` | `int` | `60` | 测试持续时间(秒)。设为 `None` 则无限执行 | -| `web_ui_dict` | `dict` | `None` | 启用 Locust Web UI,例如 `{"host": "127.0.0.1", "port": 8089}` | -| `tasks` | `dict` | — | HTTP 方法对应请求 URL 的映射 | - -### 2. 使用 JSON 脚本文件 - -创建 JSON 文件(`test_scenario.json`): +### 动作 JSON ```json -[ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] -] -``` - -从 Python 执行: +{"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://httpbin.org"}}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}} + ] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] +]} +``` -```python -from je_load_density import execute_action, read_action_json +CLI 执行: -execute_action(read_action_json("test_scenario.json")) +```bash +python -m je_load_density run smoke.json ``` -### 3. 使用 CLI 命令行 +## 核心 API -```bash -# 执行单个 JSON 脚本文件 -python -m je_load_density -e test_scenario.json +完整公开接口见 `je_load_density/__init__.py` 的 `__all__`。 + +```python +from je_load_density import ( + start_test, prepare_env, create_env, + execute_action, execute_files, executor, add_command_to_executor, + test_record_instance, locust_wrapper_proxy, + register_variable, register_variables, + register_csv_source, register_csv_sources, + parameter_resolver, resolve, + har_to_action_json, har_to_tasks, load_har, + persist_records, list_runs, fetch_run_records, + start_prometheus_exporter, stop_prometheus_exporter, + start_influxdb_sink, stop_influxdb_sink, + start_opentelemetry_exporter, stop_opentelemetry_exporter, + start_load_density_socket_server, + generate_html_report, generate_json_report, generate_xml_report, + generate_csv_report, generate_junit_report, generate_summary_report, + build_summary, + create_project_dir, callback_executor, read_action_json, +) +``` -# 执行目录中所有 JSON 文件 -python -m je_load_density -d ./test_scripts/ +## 动作 Executor -# 执行内联 JSON 字符串 -python -m je_load_density --execute_str '[["LD_start_test", {"user_detail_dict": {"user": "fast_http_user"}, "user_count": 10, "spawn_rate": 5, "test_time": 5, "tasks": {"get": {"request_url": "http://httpbin.org/get"}}}]]' +每个动作为列表: -# 使用模板创建新项目 -python -m je_load_density -c MyProject +```python +["command_name"] # 无参数 +["command_name", {"key": "value"}] # 关键字参数 +["command_name", [arg1, arg2]] # 位置参数 ``` -### 4. 使用 GUI 图形界面 +最上层为裸列表,或 `{"load_density": [...]}` 包装。 + +### 内置 `LD_*` 指令 + +| 群组 | 指令 | +|------|------| +| 核心 | `LD_start_test`、`LD_execute_action`、`LD_execute_files`、`LD_add_package_to_executor`、`LD_start_socket_server` | +| 报告 | `LD_generate_html(_report)`、`LD_generate_json(_report)`、`LD_generate_xml(_report)`、`LD_generate_csv_report`、`LD_generate_junit_report`、`LD_generate_summary_report`、`LD_summary` | +| 持久化 | `LD_persist_records`、`LD_list_runs`、`LD_fetch_run_records`、`LD_clear_records` | +| 参数 | `LD_register_variable(s)`、`LD_register_csv_source(s)`、`LD_clear_resolver` | +| 录制 | `LD_load_har`、`LD_har_to_tasks`、`LD_har_to_action_json` | +| 指标 | `LD_start/stop_prometheus_exporter`、`LD_start/stop_influxdb_sink`、`LD_start/stop_opentelemetry_exporter` | + +安全的 Python builtin(`print`、`len`、`range` 等)也可使用;`eval`、`exec`、`compile`、`__import__`、`breakpoint`、`open`、`input` 已被明确封锁。 + +### 自定义指令 ```python -from je_load_density.gui.main_window import LoadDensityUI -from PySide6.QtWidgets import QApplication -import sys +from je_load_density import add_command_to_executor -app = QApplication(sys.argv) -window = LoadDensityUI() -window.show() -sys.exit(app.exec()) +def slack_notify(message: str) -> None: + ... + +add_command_to_executor({"LD_slack_notify": slack_notify}) ``` -## 报告生成 +## 使用者模板 -执行测试后,从记录的数据生成报告: +所有模板皆通过 `start_test` 的 `user_detail_dict={"user": ""}` 注册;HTTP / WebSocket / gRPC / MQTT / raw socket 共用相同 task 结构,仅协议相关字段不同。 + +### HTTP / FastHttp ```python -from je_load_density import ( - generate_html_report, - generate_json_report, - generate_xml_report, +start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, spawn_rate=10, test_time=60, + variables={"base": "https://api.example.com"}, + tasks=[ + {"method": "post", "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [{"var": "auth", "from": "json_path", "path": "data.token"}]}, + {"method": "get", "request_url": "${var.base}/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], ) +``` + +### WebSocket -# HTML 报告 — 创建 "my_report.html" -generate_html_report("my_report") +```python +start_test( + user_detail_dict={"user": "websocket_user"}, + user_count=10, spawn_rate=5, test_time=60, + tasks=[ + {"method": "connect", "request_url": "wss://echo.example.com/socket"}, + {"method": "sendrecv", "payload": '{"ping": 1}', "expect": "pong"}, + {"method": "close"}, + ], +) +``` -# JSON 报告 — 创建 "my_report_success.json" 和 "my_report_failure.json" -generate_json_report("my_report") +### gRPC -# XML 报告 — 创建 "my_report_success.xml" 和 "my_report_failure.xml" -generate_xml_report("my_report") +```python +start_test( + user_detail_dict={"user": "grpc_user"}, + user_count=20, spawn_rate=5, test_time=60, + tasks=[{ + "name": "say_hello", + "target": "localhost:50051", + "stub_path": "pkg.greeter_pb2_grpc.GreeterStub", + "request_path": "pkg.greeter_pb2.HelloRequest", + "method": "SayHello", + "payload": {"name": "world"}, + "metadata": [["x-token", "abc"]], + "timeout": 5, + }], +) ``` -## 高级用法 +`stub_path` 与 `request_path` 在 `importlib.import_module` 之前皆通过严格标识符 regex 验证,traversal 攻击将被拒绝。 + +### MQTT + +```python +start_test( + user_detail_dict={"user": "mqtt_user"}, + user_count=10, spawn_rate=5, test_time=60, + tasks=[ + {"method": "connect", "broker": "127.0.0.1:1883"}, + {"method": "subscribe", "topic": "telemetry/in", "qos": 1}, + {"method": "publish", "topic": "telemetry/out", "payload": "ping", "qos": 1}, + {"method": "disconnect"}, + ], +) +``` -### 动作执行器 +### 原生 TCP / UDP -执行器将字符串动作名称映射到可调用的函数。所有 Python 内置函数也可使用。 +仅用标准库,无需安装。 ```python -from je_load_density import executor, add_command_to_executor +start_test( + user_detail_dict={"user": "socket_user"}, + user_count=20, spawn_rate=5, test_time=60, + tasks=[ + {"protocol": "tcp", "target": "127.0.0.1:9000", + "payload": "PING\n", "expect_bytes": 64, + "expect_substring": "PONG"}, + {"protocol": "udp", "target": "127.0.0.1:9000", + "payload": "hex:DEADBEEF", "expect_bytes": 4}, + ], +) +``` + +## 参数解析器 + +| 占位符 | 解析为 | +|--------|--------| +| `${var.NAME}` | `register_variable(s)` 设定的值 | +| `${env.NAME}` | 环境变量 `NAME` | +| `${csv.SOURCE.COL}` | CSV 来源 `SOURCE` 的下一行(默认循环) | +| `${faker.METHOD}` | `Faker().METHOD()`(lazy import) | +| `${uuid()}` | 新 UUID 4 字串 | +| `${now()}` | 本地 ISO-8601 时间(秒) | +| `${randint(min, max)}` | 密码学强度随机整数 | -# 注册自定义函数 -def my_custom_action(message): - print(f"自定义动作: {message}") +未知占位符原样保留,便于 dry run 调试。 -add_command_to_executor({"my_action": my_custom_action}) +## 情境模式 -# 程序化执行动作 -executor.execute_action([ - ["my_action", ["Hello World"]], - ["print", ["测试完成"]], -]) +```json +{ + "mode": "weighted", + "tasks": [ + {"method": "get", "request_url": "/products", "weight": 3}, + {"method": "get", "request_url": "/expensive", "weight": 1} + ] +} ``` -**内置执行器动作:** -| 动作名称 | 说明 | -|---|---| -| `LD_start_test` | 启动负载测试 | -| `LD_generate_html` | 生成 HTML 片段 | -| `LD_generate_html_report` | 生成完整 HTML 报告文件 | -| `LD_generate_json` | 生成 JSON 数据结构 | -| `LD_generate_json_report` | 生成 JSON 报告文件 | -| `LD_generate_xml` | 生成 XML 字符串 | -| `LD_generate_xml_report` | 生成 XML 报告文件 | -| `LD_execute_action` | 执行动作列表 | -| `LD_execute_files` | 从多个文件执行动作 | -| `LD_add_package_to_executor` | 动态加载包到执行器 | +| 模式 | 行为 | +|------|------| +| `sequence` | 依序执行所有 task(默认) | +| `weighted` | 每 tick 依 `weight` 加权挑一个 | +| `conditional` | 以 `run_if` / `skip_if` 谓词控制 | + +谓词:`bool`、`"${var.x}"`、`{"equals": [a,b]}`、`{"not_equals": [a,b]}`、`{"in": [needle, haystack]}`、`{"truthy": value}`。 -### 回调执行器 +## 断言与提取 -将触发函数与回调串联: +```json +{ + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "assertions": [ + {"type": "status_code", "value": 200}, + {"type": "json_path", "path": "data.role", "value": "admin"} + ], + "extract": [ + {"var": "auth_token", "from": "json_path", "path": "data.token"}, + {"var": "request_id", "from": "header", "name": "X-Request-Id"} + ] +} +``` + +断言类型:`status_code`、`contains`、`not_contains`、`json_path`、`header`。 +提取来源:`json_path`、`header`、`status_code`。 + +## 报告 ```python -from je_load_density import callback_executor +from je_load_density import ( + generate_html_report, generate_json_report, generate_xml_report, + generate_csv_report, generate_junit_report, generate_summary_report, +) + +generate_html_report("report") # report.html +generate_json_report("report") # report_success.json + report_failure.json +generate_xml_report("report") # report_success.xml + report_failure.xml +generate_csv_report("report") # report.csv +generate_junit_report("report-junit") # report-junit.xml(CI) +generate_summary_report("report-sum") # 总计 + per-name p50/p90/p95/p99 +``` -def after_test(): - print("测试完成,正在生成报告...") +## 可观测性 -callback_executor.callback_function( - trigger_function_name="user_test", - callback_function=after_test, - user_detail_dict={"user": "fast_http_user"}, - user_count=10, - spawn_rate=5, - test_time=5, - tasks={"get": {"request_url": "http://httpbin.org/get"}}, +```python +from je_load_density import ( + start_prometheus_exporter, start_influxdb_sink, start_opentelemetry_exporter, ) + +start_prometheus_exporter(port=9646, addr="127.0.0.1") +start_influxdb_sink(transport="udp", host="influxdb", port=8089) +start_opentelemetry_exporter(endpoint="http://otel-collector:4317", + service_name="loaddensity") ``` -### TCP Socket 服务器(远程执行) +| Sink | 指标 | +|------|------| +| Prometheus | `loaddensity_requests_total`、`loaddensity_request_latency_ms`、`loaddensity_response_bytes` | +| InfluxDB | `loaddensity_request` line-protocol(UDP 或 HTTP) | +| OTel | `loaddensity.requests`、`loaddensity.request.latency`、`loaddensity.response.size` | -启动接收 JSON 命令的 TCP 服务器: +三者皆 lazy load,由对应 install extra 控管依赖。 + +## 分布式 Master / Worker ```python -from je_load_density import start_load_density_socket_server +# master +start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="master", + master_bind_host="0.0.0.0", master_bind_port=5557, + expected_workers=4, + web_ui_dict={"host": "0.0.0.0", "port": 8089}, + user_count=400, spawn_rate=40, test_time=600, + tasks=[...], +) -# 启动服务器(阻塞式) -start_load_density_socket_server(host="localhost", port=9940) +# worker +start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", master_port=5557, + tasks=[...], +) ``` -从客户端发送命令: +Master 在开始 ramp 前最多等 60 秒,等待 `expected_workers` 个 worker 加入。 + +## HAR 录制/重放 ```python -import socket, json +from je_load_density import load_har, har_to_action_json + +har = load_har("recording.har") +action_json = har_to_action_json( + har, + user="fast_http_user", + user_count=20, spawn_rate=10, test_time=120, + include=[r"api\.example\.com"], + exclude=[r"\.svg$"], +) +``` -sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -sock.connect(("localhost", 9940)) +可吃 Chrome / Firefox DevTools、mitmproxy、Charles 等录制。状态码会转成 `status_code` 断言。 -command = json.dumps([ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, "spawn_rate": 5, "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] -]) -sock.send(command.encode("utf-8")) -response = sock.recv(8192) -print(response.decode("utf-8")) -sock.close() -``` +## 持久化记录(SQLite) -发送 `"quit_server"` 可优雅地关闭服务器。 +```python +from je_load_density import persist_records, list_runs, fetch_run_records -### 项目脚手架 +run_id = persist_records( + "loadtests.db", + label="checkout-2026-04-28", + metadata={"branch": "dev", "commit": "abc1234"}, +) +for row in list_runs("loadtests.db", limit=10): + print(row) +``` -生成包含关键字模板与执行器脚本的项目: +Schema 采延迟建立。`run_id` 与 `name` 上有索引,跨次查询快速。 -```python -from je_load_density import create_project_dir +## MCP Server(给 Claude) -create_project_dir(project_path="./my_tests", parent_name="LoadDensity") +```bash +pip install "je_load_density[mcp]" +python -m je_load_density.mcp_server ``` -这会创建以下结构: +接到 Claude Desktop / Code: + +```json +{ + "mcpServers": { + "loaddensity": { + "command": "python", + "args": ["-m", "je_load_density.mcp_server"] + } + } +} ``` -my_tests/ -└── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json # FastHttpUser 测试模板 - │ └── keyword2.json # HttpUser 测试模板 - └── executor/ - ├── executor_one_file.py # 执行单个关键字文件 - └── executor_folder.py # 执行 keyword/ 目录中所有文件 + +对外开 11 个工具:`run_test`、`run_action_json`、`create_project`、`list_executor_commands`、`import_har`、`generate_reports`、`summary`、`persist_records`、`list_runs`、`fetch_run`、`clear_records`。 + +## 硬化控制 Socket + +```bash +python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 --framed \ + --token "$LOAD_DENSITY_SOCKET_TOKEN" \ + --tls-cert /etc/loaddensity/server.crt \ + --tls-key /etc/loaddensity/server.key ``` -### 动态包加载 +- 4-byte big-endian 长度前缀框架(1 MiB 上限) +- 可选 TLS(`ssl.create_default_context`,TLS 1.2+ minimum) +- 共享密钥 token,以 `hmac.compare_digest` 比对;一旦设定,所有 payload 须使用 `{"token": "...", "command": [...]}` 信封,可以 `"op": "quit"` 停机 +- Token 也可由 `LOAD_DENSITY_SOCKET_TOKEN` 环境变数读取 +- 保留未验证 legacy 模式以维持兼容 -加载外部包并将其函数注册到执行器中: +## GUI + +```bash +pip install "je_load_density[gui]" +``` ```python -from je_load_density import executor +import sys +from PySide6.QtWidgets import QApplication +from je_load_density.gui.main_window import LoadDensityUI -# 加载包并使其函数可作为执行器动作使用 -executor.execute_action([ - ["LD_add_package_to_executor", ["my_custom_package"]] -]) +app = QApplication(sys.argv) +window = LoadDensityUI() +window.show() +sys.exit(app.exec()) ``` -### 测试记录 +GUI 提供英文、繁体中文、日文、韩文翻译,以及每秒轮询 `test_record_instance` 的实时统计面板(RPS、平均、p95、失败)。 -以程序化方式访问原始测试记录: +## CLI 用法 -```python -from je_load_density import test_record_instance +``` +python -m je_load_density run FILE # 执行单一动作 JSON 档 +python -m je_load_density run-dir DIR # 执行 DIR 下所有 .json +python -m je_load_density run-str JSON # 执行 inline JSON +python -m je_load_density init PATH # 建立专案骨架 +python -m je_load_density serve [--host ...] # 启动控制 socket +``` -# 执行测试后 -for record in test_record_instance.test_record_list: - print(record["Method"], record["test_url"], record["status_code"]) +旧式 `-e/-d/-c/--execute_str` 仍接受,兼容下游工具。 -for error in test_record_instance.error_record_list: - print(error["Method"], error["test_url"], error["error"]) +## 测试记录 -# 清除记录 -test_record_instance.clear_records() -``` +`test_record_instance.test_record_list` 与 `error_record_list` 收集每笔请求:`Method`、`test_url`、`name`、`status_code`、`response_time_ms`、`response_length`,失败则含 `error`。报告与 SQLite sink 直接读取此处。 -## 架构 +## 异常处理 ``` -je_load_density/ -├── __init__.py # 公开 API 导出 -├── __main__.py # CLI 入口点 -├── gui/ # PySide6 GUI(可选依赖) -│ ├── main_window.py # 主窗口(QMainWindow) -│ ├── main_widget.py # 测试参数表单与日志面板 -│ ├── load_density_gui_thread.py # 测试后台线程 -│ ├── log_to_ui_filter.py # GUI 显示的日志拦截器 -│ └── language_wrapper/ # 国际化(英文、繁体中文) -├── wrapper/ -│ ├── create_locust_env/ # Locust Environment 与 Runner 设置 -│ ├── start_wrapper/ # 高层 start_test() 入口点 -│ ├── user_template/ # HttpUser 与 FastHttpUser 封装 -│ ├── proxy/ # 用户代理容器与配置 -│ └── event/ # 请求钩子(记录所有请求) -└── utils/ - ├── executor/ # 动作执行器(事件驱动) - ├── generate_report/ # HTML、JSON、XML 报告生成器 - ├── test_record/ # 测试记录存储 - ├── socket_server/ # 远程执行 TCP 服务器 - ├── callback/ # 回调函数执行器 - ├── project/ # 项目脚手架与模板 - ├── package_manager/ # 动态包加载 - ├── json/ # JSON 文件读写工具 - ├── xml/ # XML 结构工具 - ├── file_process/ # 目录文件列表 - ├── logging/ # Logger 实例 - └── exception/ # 自定义异常与错误标签 -``` - -## 已测试平台 - -- Windows 10 / 11 -- macOS 10.15 ~ 11(Big Sur) -- Ubuntu 20.04 -- Raspberry Pi 3B+ +LoadDensityTestException +├── LoadDensityTestJsonException +├── LoadDensityGenerateJsonReportException +├── LoadDensityTestExecuteException +├── LoadDensityAssertException +├── LoadDensityHTMLException +├── LoadDensityAddCommandException +├── XMLException → XMLTypeException +└── CallbackExecutorException +``` -## 许可证 +所有自定义异常皆继承 `LoadDensityTestException`;拦该类别即可全面处理。 -本项目采用 [MIT 许可证](../LICENSE)。 +## 日志 -## 贡献指南 +LoadDensity 提供已配置好的 logger(`load_density_logger`,位于 `je_load_density.utils.logging.loggin_instance`)。以标准 `logging` 模组 API 即可整合现有日志系统。 -请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解贡献规范。 +## 支持平台 -## 相关链接 +| 平台 | 状态 | +|------|------| +| Windows 10 / 11 | 完整支持 | +| macOS | 完整支持 | +| Ubuntu / Linux | 完整支持 | +| Raspberry Pi | 已测 3B+ 以上 | + +需 Python 3.10+。 + +## 许可证 -- **PyPI**:https://pypi.org/project/je_load_density/ -- **文档**:https://loaddensity.readthedocs.io/en/latest/ -- **源代码**:https://github.com/Intergration-Automation-Testing/LoadDensity +MIT — 见 [LICENSE](../LICENSE)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 0245400..0bf0bd6 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -1,358 +1,590 @@ # LoadDensity -[![Python](https://img.shields.io/pypi/pyversions/je_load_density)](https://pypi.org/project/je_load_density/) -[![PyPI](https://img.shields.io/pypi/v/je_load_density)](https://pypi.org/project/je_load_density/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Documentation](https://readthedocs.org/projects/loaddensity/badge/?version=latest)](https://loaddensity.readthedocs.io/en/latest/) - -**LoadDensity** 是一個基於 [Locust](https://locust.io/) 建構的高效能負載與壓力測試自動化框架。它對 Locust 的核心功能進行了簡化封裝,提供快速使用者生成、透過模板與 JSON 腳本進行彈性測試配置、多格式報告生成(HTML / JSON / XML)、內建 GUI 圖形介面、透過 TCP Socket 伺服器進行遠端執行,以及測試後工作流程的回呼機制。 - -**[English](../README.md)** | **[简体中文](README_zh-CN.md)** +

+ 多協定壓力與負載自動化框架:Locust + WebSocket + gRPC + MQTT + 原生 socket,搭配內建電池的 JSON 動作執行器。 +

+ +

+ PyPI Version + Python Version + License + Documentation Status +

+ +

+ English | + 简体中文 +

--- -## 功能特色 - -- **簡化的 Locust 封裝** — 將 Locust 的 `Environment`、`Runner` 和 `User` 類別抽象化為簡潔的高階 API。 -- **兩種使用者類型** — 同時支援 `HttpUser` 和 `FastHttpUser`(基於 geventhttpclient,吞吐量更高)。 -- **快速使用者生成** — 可配置生成速率,輕鬆擴展至數千名並行使用者。 -- **JSON 驅動的測試腳本** — 將測試場景定義為 JSON 檔案,無需撰寫 Python 程式碼即可執行。 -- **動作執行器** — 內建的事件驅動執行器,將動作名稱映射到函式。支援批次執行與檔案驅動執行。 -- **報告生成** — 匯出三種格式的測試結果: - - **HTML** — 包含成功/失敗記錄的樣式化表格 - - **JSON** — 適合程式化處理的結構化資料 - - **XML** — 標準 XML 輸出,適合 CI/CD 整合 -- **請求鉤子** — 自動記錄每個請求(成功與失敗),包含方法、URL、狀態碼、回應內容、標頭與錯誤資訊。 -- **回呼執行器** — 將觸發函式與回呼函式串聯,用於測試後工作流程(例如:執行測試後自動生成報告)。 -- **TCP Socket 伺服器** — 基於 gevent 的遠端執行伺服器。透過 TCP 接收 JSON 指令以遠端執行測試。 -- **專案腳手架** — 自動生成專案目錄結構,包含關鍵字模板與執行器腳本。 -- **套件管理器** — 在執行期間動態載入外部 Python 套件,並將其函式註冊到執行器中。 -- **GUI 圖形介面(選用)** — 基於 PySide6 的圖形介面,支援即時日誌顯示,提供英文與繁體中文介面。 -- **CLI 命令列支援** — 直接從命令列執行測試、運行腳本或建立專案結構。 -- **跨平台** — 支援 Windows、macOS 和 Linux。 +LoadDensity(`je_load_density`)從 Locust 封裝起家,逐步擴展為完整的多協定負載框架:HTTP、FastHttp、WebSocket、gRPC、MQTT 與原生 TCP/UDP 等使用者模板,皆透過同一個 JSON 驅動的動作執行器;另含資料參數化、情境流程、報告、可觀測性、分散式 runner、錄製、持久化儲存,以及讓 Claude 端對端驅動測試的 MCP 控制介面。每個 executor 指令以 `LD_*` 命名、使用單一派發點,因此一份動作 JSON 可同時混用協定、exporter 與報告。 + +> **選用相依、可選安裝** — 每個協定驅動與 exporter 都以 `pip install je_load_density[]` 提供。僅做 HTTP 壓測者執行期不受影響。 + +## 目次 + +- [亮點](#亮點) +- [安裝](#安裝) +- [架構](#架構) +- [Quick Start](#quick-start) +- [核心 API](#核心-api) +- [動作 Executor](#動作-executor) +- [使用者模板](#使用者模板) +- [參數解析器](#參數解析器) +- [情境模式](#情境模式) +- [斷言與擷取](#斷言與擷取) +- [報告](#報告) +- [可觀測性](#可觀測性) +- [分散式 Master / Worker](#分散式-master--worker) +- [HAR 錄製/重放](#har-錄製重放) +- [持久化紀錄(SQLite)](#持久化紀錄sqlite) +- [MCP Server(給 Claude)](#mcp-server給-claude) +- [硬化控制 Socket](#硬化控制-socket) +- [GUI](#gui) +- [CLI 用法](#cli-用法) +- [測試紀錄](#測試紀錄) +- [例外處理](#例外處理) +- [日誌](#日誌) +- [支援平台](#支援平台) +- [授權](#授權) + +## 亮點 + +- **一個 executor,六種協定** — HTTP、FastHttp、WebSocket、gRPC、MQTT、原生 TCP/UDP,全部透過 `LD_start_test` 以 `user` 切換派發。 +- **JSON 驅動** — 每支測試皆為動作 JSON 列表;可手寫、由 HAR 匯入產生、由 MCP 工具排程,或經控制 socket 傳送。 +- **參數解析器** — `${var.x}`、`${env.X}`、`${csv.source.col}`、`${faker.method}`,以及內建 `${uuid()}`、`${now()}`、`${randint(min,max)}` 等 helper;可從回應擷取值,後續 task 再用。 +- **情境流程** — 以 `sequence`(預設)/`weighted`/`conditional`(`run_if`、`skip_if`)宣告 task 流程,無需動到 Python。 +- **六種報告格式** — HTML、JSON、XML、CSV、JUnit XML,以及百分位摘要 JSON(總計、失敗率、per-name p50/p90/p95/p99)。 +- **三種 exporter** — Prometheus HTTP 端點、InfluxDB line-protocol UDP/HTTP sink、OpenTelemetry OTLP gRPC。 +- **分散式 runner** — `runner_mode="master"` / `"worker"`,跨機壓測使用同一份 start_test API。 +- **HAR 錄製/重放** — 將真實瀏覽流量轉成可執行動作 JSON,含 regex include/exclude 過濾。 +- **持久化紀錄** — 選用 SQLite sink,含 run/record/metadata schema,便於跨次回歸檢查。 +- **MCP server** — `python -m je_load_density.mcp_server` 對外開 11 個工具,讓 Claude 端對端驅動 LoadDensity。 +- **硬化控制 socket** — Length-prefix framing、選用 TLS、共享密鑰 token(環境變數或參數),同時保留與 PyBreeze 等工具相容的 legacy 模式。 +- **即時 GUI** — 選用的 PySide6 GUI 含即時統計面板(RPS、平均、p95、失敗),翻譯為英文、繁體中文、日文、韓文。 +- **CLI 子指令** — `run` / `run-dir` / `run-str` / `init` / `serve`。舊式 `-e/-d/-c/--execute_str` 旗標保留以維持下游工具相容。 ## 安裝 -### 基本安裝(CLI 與函式庫) - ```bash pip install je_load_density ``` -### 包含 GUI 支援 +引入 [Locust](https://locust.io/) 與 `defusedxml`,僅此而已。 + +### 選用 extras + +| Extra | 加入 | +|-------|------| +| `gui` | PySide6 + qt-material(圖形介面) | +| `websocket` | `websocket-client`(WebSocket user 模板) | +| `grpc` | `grpcio` + `protobuf`(gRPC user 模板) | +| `mqtt` | `paho-mqtt`(MQTT user 模板) | +| `prometheus` | `prometheus-client`(Prometheus exporter) | +| `opentelemetry` | OpenTelemetry SDK + OTLP gRPC exporter | +| `metrics` | `prometheus` + `opentelemetry` 一次裝 | +| `faker` | `Faker`(驅動 `${faker.method}` 占位符) | +| `mcp` | `mcp` SDK(驅動 MCP server) | +| `all` | 上列全部 | ```bash -pip install je_load_density[gui] +pip install "je_load_density[gui]" +pip install "je_load_density[mqtt,grpc,websocket]" +pip install "je_load_density[metrics]" +pip install "je_load_density[mcp]" +pip install "je_load_density[all]" ``` -這會安裝 [PySide6](https://doc.qt.io/qtforpython/) 和 [qt-material](https://github.com/UN-GCPDS/qt-material) 以提供圖形介面。 +### 開發安裝 -## 系統需求 +```bash +git clone https://github.com/Integration-Automation/LoadDensity.git +cd LoadDensity +pip install -e ".[all]" +pip install -r requirements.txt +``` -- Python **3.10** 或更高版本 -- [Locust](https://locust.io/)(會作為依賴項自動安裝) +## 架構 -## 快速上手 +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CLI / MCP / GUI / 控制 Socket │ +└──────────────────┬──────────────────────────────────────────────┘ + │ 動作 JSON +┌──────────────────▼──────────────────────────────────────────────┐ +│ 動作 Executor(LD_* 派發 + 安全 builtin) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ start_test +┌──────────────────▼──────────────────────────────────────────────┐ +│ locust_wrapper_proxy(每協定 task store) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┬──────────────┬──────────────┐ + ▼ ▼ ▼ ▼ +HTTP / FastHttp WebSocket gRPC MQTT 原生 TCP / UDP + │ │ │ │ + └───────────────┬───────────────┴──────────────┴──────────────┘ + │ Locust 事件 + ┌───────────────┴───────────────┐ + ▼ ▼ +test_record_instance Prometheus / InfluxDB / OTel + │ + ├── HTML / JSON / XML / CSV / JUnit / Summary 報告 + └── SQLite 持久化(跨次比對) +``` + +依賴方向永遠是動作層 → Locust。 + +## Quick Start -### 1. 使用 Python API +### Python API ```python from je_load_density import start_test -# 定義使用者配置與任務 -result = start_test( +start_test( user_detail_dict={"user": "fast_http_user"}, - user_count=50, - spawn_rate=10, - test_time=10, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"}, - } + user_count=50, spawn_rate=10, test_time=30, + variables={"base": "https://httpbin.org"}, + tasks=[ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], ) ``` -**參數說明:** -| 參數 | 類型 | 預設值 | 說明 | -|---|---|---|---| -| `user_detail_dict` | `dict` | — | 使用者類型配置。`{"user": "fast_http_user"}` 或 `{"user": "http_user"}` | -| `user_count` | `int` | `50` | 模擬使用者總數 | -| `spawn_rate` | `int` | `10` | 每秒生成的使用者數量 | -| `test_time` | `int` | `60` | 測試持續時間(秒)。設為 `None` 則無限執行 | -| `web_ui_dict` | `dict` | `None` | 啟用 Locust Web UI,例如 `{"host": "127.0.0.1", "port": 8089}` | -| `tasks` | `dict` | — | HTTP 方法對應請求 URL 的映射 | +### 動作 JSON -### 2. 使用 JSON 腳本檔案 +```json +{"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://httpbin.org"}}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}} + ] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] +]} +``` -建立 JSON 檔案(`test_scenario.json`): +CLI 執行: -```json -[ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] -] -``` - -從 Python 執行: +```bash +python -m je_load_density run smoke.json +``` + +## 核心 API + +完整公開介面見 `je_load_density/__init__.py` 的 `__all__`。 ```python -from je_load_density import execute_action, read_action_json +from je_load_density import ( + start_test, prepare_env, create_env, + execute_action, execute_files, executor, add_command_to_executor, + test_record_instance, locust_wrapper_proxy, + register_variable, register_variables, + register_csv_source, register_csv_sources, + parameter_resolver, resolve, + har_to_action_json, har_to_tasks, load_har, + persist_records, list_runs, fetch_run_records, + start_prometheus_exporter, stop_prometheus_exporter, + start_influxdb_sink, stop_influxdb_sink, + start_opentelemetry_exporter, stop_opentelemetry_exporter, + start_load_density_socket_server, + generate_html_report, generate_json_report, generate_xml_report, + generate_csv_report, generate_junit_report, generate_summary_report, + build_summary, + create_project_dir, callback_executor, read_action_json, +) +``` + +## 動作 Executor -execute_action(read_action_json("test_scenario.json")) +每個動作為列表: + +```python +["command_name"] # 無參數 +["command_name", {"key": "value"}] # 關鍵字參數 +["command_name", [arg1, arg2]] # 位置參數 ``` -### 3. 使用 CLI 命令列 +最上層為裸列表,或 `{"load_density": [...]}` 包裝。 -```bash -# 執行單一 JSON 腳本檔案 -python -m je_load_density -e test_scenario.json +### 內建 `LD_*` 指令 + +| 群組 | 指令 | +|------|------| +| 核心 | `LD_start_test`、`LD_execute_action`、`LD_execute_files`、`LD_add_package_to_executor`、`LD_start_socket_server` | +| 報告 | `LD_generate_html(_report)`、`LD_generate_json(_report)`、`LD_generate_xml(_report)`、`LD_generate_csv_report`、`LD_generate_junit_report`、`LD_generate_summary_report`、`LD_summary` | +| 持久化 | `LD_persist_records`、`LD_list_runs`、`LD_fetch_run_records`、`LD_clear_records` | +| 參數 | `LD_register_variable(s)`、`LD_register_csv_source(s)`、`LD_clear_resolver` | +| 錄製 | `LD_load_har`、`LD_har_to_tasks`、`LD_har_to_action_json` | +| 指標 | `LD_start/stop_prometheus_exporter`、`LD_start/stop_influxdb_sink`、`LD_start/stop_opentelemetry_exporter` | -# 執行目錄中所有 JSON 檔案 -python -m je_load_density -d ./test_scripts/ +安全的 Python builtin(`print`、`len`、`range` 等)也可使用;`eval`、`exec`、`compile`、`__import__`、`breakpoint`、`open`、`input` 已被明確封鎖。 -# 執行內嵌 JSON 字串 -python -m je_load_density --execute_str '[["LD_start_test", {"user_detail_dict": {"user": "fast_http_user"}, "user_count": 10, "spawn_rate": 5, "test_time": 5, "tasks": {"get": {"request_url": "http://httpbin.org/get"}}}]]' +### 自訂指令 -# 使用模板建立新專案 -python -m je_load_density -c MyProject +```python +from je_load_density import add_command_to_executor + +def slack_notify(message: str) -> None: + ... + +add_command_to_executor({"LD_slack_notify": slack_notify}) ``` -### 4. 使用 GUI 圖形介面 +## 使用者模板 + +所有模板皆透過 `start_test` 的 `user_detail_dict={"user": ""}` 註冊;HTTP / WebSocket / gRPC / MQTT / raw socket 共用相同 task 結構,僅協定相關欄位不同。 + +### HTTP / FastHttp ```python -from je_load_density.gui.main_window import LoadDensityUI -from PySide6.QtWidgets import QApplication -import sys +start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, spawn_rate=10, test_time=60, + variables={"base": "https://api.example.com"}, + tasks=[ + {"method": "post", "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [{"var": "auth", "from": "json_path", "path": "data.token"}]}, + {"method": "get", "request_url": "${var.base}/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], +) +``` -app = QApplication(sys.argv) -window = LoadDensityUI() -window.show() -sys.exit(app.exec()) +### WebSocket + +```python +start_test( + user_detail_dict={"user": "websocket_user"}, + user_count=10, spawn_rate=5, test_time=60, + tasks=[ + {"method": "connect", "request_url": "wss://echo.example.com/socket"}, + {"method": "sendrecv", "payload": '{"ping": 1}', "expect": "pong"}, + {"method": "close"}, + ], +) +``` + +### gRPC + +```python +start_test( + user_detail_dict={"user": "grpc_user"}, + user_count=20, spawn_rate=5, test_time=60, + tasks=[{ + "name": "say_hello", + "target": "localhost:50051", + "stub_path": "pkg.greeter_pb2_grpc.GreeterStub", + "request_path": "pkg.greeter_pb2.HelloRequest", + "method": "SayHello", + "payload": {"name": "world"}, + "metadata": [["x-token", "abc"]], + "timeout": 5, + }], +) ``` -## 報告生成 +`stub_path` 與 `request_path` 在 `importlib.import_module` 之前皆通過嚴格識別符 regex 驗證,traversal 攻擊將被拒絕。 -執行測試後,從記錄的資料生成報告: +### MQTT ```python -from je_load_density import ( - generate_html_report, - generate_json_report, - generate_xml_report, +start_test( + user_detail_dict={"user": "mqtt_user"}, + user_count=10, spawn_rate=5, test_time=60, + tasks=[ + {"method": "connect", "broker": "127.0.0.1:1883"}, + {"method": "subscribe", "topic": "telemetry/in", "qos": 1}, + {"method": "publish", "topic": "telemetry/out", "payload": "ping", "qos": 1}, + {"method": "disconnect"}, + ], ) +``` -# HTML 報告 — 建立 "my_report.html" -generate_html_report("my_report") +### 原生 TCP / UDP -# JSON 報告 — 建立 "my_report_success.json" 和 "my_report_failure.json" -generate_json_report("my_report") +僅用標準函式庫,無需安裝。 -# XML 報告 — 建立 "my_report_success.xml" 和 "my_report_failure.xml" -generate_xml_report("my_report") +```python +start_test( + user_detail_dict={"user": "socket_user"}, + user_count=20, spawn_rate=5, test_time=60, + tasks=[ + {"protocol": "tcp", "target": "127.0.0.1:9000", + "payload": "PING\n", "expect_bytes": 64, + "expect_substring": "PONG"}, + {"protocol": "udp", "target": "127.0.0.1:9000", + "payload": "hex:DEADBEEF", "expect_bytes": 4}, + ], +) ``` -## 進階用法 +## 參數解析器 -### 動作執行器 +| 占位符 | 解析為 | +|--------|--------| +| `${var.NAME}` | `register_variable(s)` 設定的值 | +| `${env.NAME}` | 環境變數 `NAME` | +| `${csv.SOURCE.COL}` | CSV 來源 `SOURCE` 的下一筆(預設循環) | +| `${faker.METHOD}` | `Faker().METHOD()`(lazy import) | +| `${uuid()}` | 新 UUID 4 字串 | +| `${now()}` | 本地 ISO-8601 時間(秒) | +| `${randint(min, max)}` | 密碼學強度隨機整數 | -執行器將字串動作名稱映射到可呼叫的函式。所有 Python 內建函式也可使用。 +未知占位符原樣保留,便於 dry run 偵錯。 -```python -from je_load_density import executor, add_command_to_executor +## 情境模式 -# 註冊自訂函式 -def my_custom_action(message): - print(f"自訂動作: {message}") +```json +{ + "mode": "weighted", + "tasks": [ + {"method": "get", "request_url": "/products", "weight": 3}, + {"method": "get", "request_url": "/expensive", "weight": 1} + ] +} +``` + +| 模式 | 行為 | +|------|------| +| `sequence` | 依序執行所有 task(預設) | +| `weighted` | 每 tick 依 `weight` 加權挑一個 | +| `conditional` | 以 `run_if` / `skip_if` 預測式控制 | -add_command_to_executor({"my_action": my_custom_action}) +預測式:`bool`、`"${var.x}"`、`{"equals": [a,b]}`、`{"not_equals": [a,b]}`、`{"in": [needle, haystack]}`、`{"truthy": value}`。 -# 程式化執行動作 -executor.execute_action([ - ["my_action", ["Hello World"]], - ["print", ["測試完成"]], -]) +## 斷言與擷取 + +```json +{ + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "assertions": [ + {"type": "status_code", "value": 200}, + {"type": "json_path", "path": "data.role", "value": "admin"} + ], + "extract": [ + {"var": "auth_token", "from": "json_path", "path": "data.token"}, + {"var": "request_id", "from": "header", "name": "X-Request-Id"} + ] +} ``` -**內建執行器動作:** -| 動作名稱 | 說明 | -|---|---| -| `LD_start_test` | 啟動負載測試 | -| `LD_generate_html` | 生成 HTML 片段 | -| `LD_generate_html_report` | 生成完整 HTML 報告檔案 | -| `LD_generate_json` | 生成 JSON 資料結構 | -| `LD_generate_json_report` | 生成 JSON 報告檔案 | -| `LD_generate_xml` | 生成 XML 字串 | -| `LD_generate_xml_report` | 生成 XML 報告檔案 | -| `LD_execute_action` | 執行動作列表 | -| `LD_execute_files` | 從多個檔案執行動作 | -| `LD_add_package_to_executor` | 動態載入套件到執行器 | +斷言類型:`status_code`、`contains`、`not_contains`、`json_path`、`header`。 +擷取來源:`json_path`、`header`、`status_code`。 + +## 報告 + +```python +from je_load_density import ( + generate_html_report, generate_json_report, generate_xml_report, + generate_csv_report, generate_junit_report, generate_summary_report, +) -### 回呼執行器 +generate_html_report("report") # report.html +generate_json_report("report") # report_success.json + report_failure.json +generate_xml_report("report") # report_success.xml + report_failure.xml +generate_csv_report("report") # report.csv +generate_junit_report("report-junit") # report-junit.xml(CI) +generate_summary_report("report-sum") # 總計 + per-name p50/p90/p95/p99 +``` -將觸發函式與回呼串聯: +## 可觀測性 ```python -from je_load_density import callback_executor +from je_load_density import ( + start_prometheus_exporter, start_influxdb_sink, start_opentelemetry_exporter, +) + +start_prometheus_exporter(port=9646, addr="127.0.0.1") +start_influxdb_sink(transport="udp", host="influxdb", port=8089) +start_opentelemetry_exporter(endpoint="http://otel-collector:4317", + service_name="loaddensity") +``` -def after_test(): - print("測試完成,正在生成報告...") +| Sink | 指標 | +|------|------| +| Prometheus | `loaddensity_requests_total`、`loaddensity_request_latency_ms`、`loaddensity_response_bytes` | +| InfluxDB | `loaddensity_request` line-protocol(UDP 或 HTTP) | +| OTel | `loaddensity.requests`、`loaddensity.request.latency`、`loaddensity.response.size` | -callback_executor.callback_function( - trigger_function_name="user_test", - callback_function=after_test, +三者皆 lazy load,由對應 install extra 控管相依。 + +## 分散式 Master / Worker + +```python +# master +start_test( user_detail_dict={"user": "fast_http_user"}, - user_count=10, - spawn_rate=5, - test_time=5, - tasks={"get": {"request_url": "http://httpbin.org/get"}}, + runner_mode="master", + master_bind_host="0.0.0.0", master_bind_port=5557, + expected_workers=4, + web_ui_dict={"host": "0.0.0.0", "port": 8089}, + user_count=400, spawn_rate=40, test_time=600, + tasks=[...], +) + +# worker +start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", master_port=5557, + tasks=[...], ) ``` -### TCP Socket 伺服器(遠端執行) +Master 在開始 ramp 前最多等 60 秒,等待 `expected_workers` 個 worker 加入。 -啟動接收 JSON 指令的 TCP 伺服器: +## HAR 錄製/重放 ```python -from je_load_density import start_load_density_socket_server - -# 啟動伺服器(阻塞式) -start_load_density_socket_server(host="localhost", port=9940) +from je_load_density import load_har, har_to_action_json + +har = load_har("recording.har") +action_json = har_to_action_json( + har, + user="fast_http_user", + user_count=20, spawn_rate=10, test_time=120, + include=[r"api\.example\.com"], + exclude=[r"\.svg$"], +) ``` -從客戶端發送指令: +可吃 Chrome / Firefox DevTools、mitmproxy、Charles 等錄製。狀態碼會轉成 `status_code` 斷言。 + +## 持久化紀錄(SQLite) ```python -import socket, json +from je_load_density import persist_records, list_runs, fetch_run_records + +run_id = persist_records( + "loadtests.db", + label="checkout-2026-04-28", + metadata={"branch": "dev", "commit": "abc1234"}, +) +for row in list_runs("loadtests.db", limit=10): + print(row) +``` + +Schema 採延遲建立。`run_id` 與 `name` 上有索引,跨次查詢快速。 + +## MCP Server(給 Claude) -sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -sock.connect(("localhost", 9940)) +```bash +pip install "je_load_density[mcp]" +python -m je_load_density.mcp_server +``` + +接到 Claude Desktop / Code: + +```json +{ + "mcpServers": { + "loaddensity": { + "command": "python", + "args": ["-m", "je_load_density.mcp_server"] + } + } +} +``` -command = json.dumps([ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, "spawn_rate": 5, "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] -]) -sock.send(command.encode("utf-8")) -response = sock.recv(8192) -print(response.decode("utf-8")) -sock.close() +對外開 11 個工具:`run_test`、`run_action_json`、`create_project`、`list_executor_commands`、`import_har`、`generate_reports`、`summary`、`persist_records`、`list_runs`、`fetch_run`、`clear_records`。 + +## 硬化控制 Socket + +```bash +python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 --framed \ + --token "$LOAD_DENSITY_SOCKET_TOKEN" \ + --tls-cert /etc/loaddensity/server.crt \ + --tls-key /etc/loaddensity/server.key ``` -發送 `"quit_server"` 可優雅地關閉伺服器。 +- 4-byte big-endian 長度前綴框架(1 MiB 上限) +- 選用 TLS(`ssl.create_default_context`,TLS 1.2+ minimum) +- 共享密鑰 token,以 `hmac.compare_digest` 比對;一旦設定,所有 payload 須使用 `{"token": "...", "command": [...]}` 信封,可以 `"op": "quit"` 停機 +- Token 也可由 `LOAD_DENSITY_SOCKET_TOKEN` 環境變數讀取 +- 保留未驗證 legacy 模式以維持相容 -### 專案腳手架 +## GUI -生成包含關鍵字模板與執行器腳本的專案: +```bash +pip install "je_load_density[gui]" +``` ```python -from je_load_density import create_project_dir +import sys +from PySide6.QtWidgets import QApplication +from je_load_density.gui.main_window import LoadDensityUI -create_project_dir(project_path="./my_tests", parent_name="LoadDensity") +app = QApplication(sys.argv) +window = LoadDensityUI() +window.show() +sys.exit(app.exec()) ``` -這會建立以下結構: +GUI 提供英文、繁體中文、日文、韓文翻譯,以及每秒輪詢 `test_record_instance` 的即時統計面板(RPS、平均、p95、失敗)。 + +## CLI 用法 + ``` -my_tests/ -└── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json # FastHttpUser 測試模板 - │ └── keyword2.json # HttpUser 測試模板 - └── executor/ - ├── executor_one_file.py # 執行單一關鍵字檔案 - └── executor_folder.py # 執行 keyword/ 目錄中所有檔案 +python -m je_load_density run FILE # 執行單一動作 JSON 檔 +python -m je_load_density run-dir DIR # 執行 DIR 下所有 .json +python -m je_load_density run-str JSON # 執行 inline JSON +python -m je_load_density init PATH # 建立專案骨架 +python -m je_load_density serve [--host ...] # 啟動控制 socket ``` -### 動態套件載入 +舊式 `-e/-d/-c/--execute_str` 仍接受,相容下游工具。 -載入外部套件並將其函式註冊到執行器中: +## 測試紀錄 -```python -from je_load_density import executor +`test_record_instance.test_record_list` 與 `error_record_list` 收集每筆請求:`Method`、`test_url`、`name`、`status_code`、`response_time_ms`、`response_length`,失敗則含 `error`。報告與 SQLite sink 直接讀取此處。 + +## 例外處理 -# 載入套件並使其函式可作為執行器動作使用 -executor.execute_action([ - ["LD_add_package_to_executor", ["my_custom_package"]] -]) +``` +LoadDensityTestException +├── LoadDensityTestJsonException +├── LoadDensityGenerateJsonReportException +├── LoadDensityTestExecuteException +├── LoadDensityAssertException +├── LoadDensityHTMLException +├── LoadDensityAddCommandException +├── XMLException → XMLTypeException +└── CallbackExecutorException ``` -### 測試記錄 +所有自訂例外皆繼承 `LoadDensityTestException`;攔該類別即可全面處理。 -以程式化方式存取原始測試記錄: +## 日誌 -```python -from je_load_density import test_record_instance +LoadDensity 提供已配置好的 logger(`load_density_logger`,位於 `je_load_density.utils.logging.loggin_instance`)。以標準 `logging` 模組 API 即可整合既有日誌系統。 -# 執行測試後 -for record in test_record_instance.test_record_list: - print(record["Method"], record["test_url"], record["status_code"]) +## 支援平台 -for error in test_record_instance.error_record_list: - print(error["Method"], error["test_url"], error["error"]) +| 平台 | 狀態 | +|------|------| +| Windows 10 / 11 | 完整支援 | +| macOS | 完整支援 | +| Ubuntu / Linux | 完整支援 | +| Raspberry Pi | 已測 3B+ 以上 | -# 清除記錄 -test_record_instance.clear_records() -``` +需 Python 3.10+。 -## 架構 +## 授權 -``` -je_load_density/ -├── __init__.py # 公開 API 匯出 -├── __main__.py # CLI 進入點 -├── gui/ # PySide6 GUI(選用依賴) -│ ├── main_window.py # 主視窗(QMainWindow) -│ ├── main_widget.py # 測試參數表單與日誌面板 -│ ├── load_density_gui_thread.py # 測試背景執行緒 -│ ├── log_to_ui_filter.py # GUI 顯示的日誌攔截器 -│ └── language_wrapper/ # 國際化(英文、繁體中文) -├── wrapper/ -│ ├── create_locust_env/ # Locust Environment 與 Runner 設定 -│ ├── start_wrapper/ # 高階 start_test() 進入點 -│ ├── user_template/ # HttpUser 與 FastHttpUser 封裝 -│ ├── proxy/ # 使用者代理容器與配置 -│ └── event/ # 請求鉤子(記錄所有請求) -└── utils/ - ├── executor/ # 動作執行器(事件驅動) - ├── generate_report/ # HTML、JSON、XML 報告生成器 - ├── test_record/ # 測試記錄儲存 - ├── socket_server/ # 遠端執行 TCP 伺服器 - ├── callback/ # 回呼函式執行器 - ├── project/ # 專案腳手架與模板 - ├── package_manager/ # 動態套件載入 - ├── json/ # JSON 檔案讀寫工具 - ├── xml/ # XML 結構工具 - ├── file_process/ # 目錄檔案列表 - ├── logging/ # Logger 實例 - └── exception/ # 自訂例外與錯誤標籤 -``` - -## 已測試平台 - -- Windows 10 / 11 -- macOS 10.15 ~ 11(Big Sur) -- Ubuntu 20.04 -- Raspberry Pi 3B+ - -## 授權條款 - -本專案採用 [MIT 授權條款](../LICENSE)。 - -## 貢獻指南 - -請參閱 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解貢獻規範。 - -## 相關連結 - -- **PyPI**:https://pypi.org/project/je_load_density/ -- **文件**:https://loaddensity.readthedocs.io/en/latest/ -- **原始碼**:https://github.com/Intergration-Automation-Testing/LoadDensity +MIT — 見 [LICENSE](../LICENSE)。 diff --git a/docs/requirements.txt b/docs/requirements.txt index 4170c03..ece0968 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ -sphinx-rtd-theme \ No newline at end of file +sphinx-rtd-theme +sphinxcontrib-mermaid diff --git a/docs/source/En/doc/action_executor/action_executor_doc.rst b/docs/source/En/doc/action_executor/action_executor_doc.rst new file mode 100644 index 0000000..8edab93 --- /dev/null +++ b/docs/source/En/doc/action_executor/action_executor_doc.rst @@ -0,0 +1,176 @@ +Action Executor +=============== + +Overview +-------- + +The action executor maps command strings to callable functions. Action +scripts are JSON lists, so the same script can be hand-authored, +generated by HAR import, scheduled by an MCP tool, or sent over the +control socket. + +Every shipped command starts with the ``LD_`` prefix; safe Python +built-ins (``print``, ``len``, ``range``…) are also available, but +``eval``, ``exec``, ``compile``, ``__import__``, ``breakpoint``, +``open``, and ``input`` are explicitly blocked. + +Action format +------------- + +.. code-block:: python + + ["command_name"] # No parameters + ["command_name", {"key": "value"}] # Keyword arguments + ["command_name", [arg1, arg2]] # Positional arguments + +The top-level document is either: + +.. code-block:: json + + {"load_density": [["LD_start_test", {...}], ...]} + +or a bare list of actions. + +Quick example +------------- + +.. code-block:: python + + from je_load_density import execute_action + + execute_action({"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://api.example.com"}}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, + "spawn_rate": 10, + "test_time": 30, + "tasks": [{"method": "get", "request_url": "${var.base}/health"}], + }], + ["LD_generate_summary_report", {"report_name": "smoke"}], + ]}) + +LD_* commands +------------- + +The executor exposes the following commands. Each is implemented in the +matching module under ``je_load_density``. + +**Core:** + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Command + - Summary + * - ``LD_start_test`` + - Run a Locust load test (HTTP / FastHttp / WebSocket / gRPC / + MQTT / Socket). + * - ``LD_execute_action`` + - Execute a nested action list. + * - ``LD_execute_files`` + - Execute every action JSON file in a list. + * - ``LD_add_package_to_executor`` + - Register a Python package's functions into the executor. + * - ``LD_start_socket_server`` + - Start the hardened TCP control plane. + +**Reports:** + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Command + - Summary + * - ``LD_generate_html`` / ``LD_generate_html_report`` + - HTML report generators. + * - ``LD_generate_json`` / ``LD_generate_json_report`` + - JSON report generators. + * - ``LD_generate_xml`` / ``LD_generate_xml_report`` + - XML report generators. + * - ``LD_generate_csv_report`` + - One-row-per-request CSV export. + * - ``LD_generate_junit_report`` + - JUnit XML for CI consumers. + * - ``LD_generate_summary_report`` + - JSON summary with per-name p50/p90/p95/p99 latencies. + * - ``LD_summary`` + - In-memory dict of the same summary. + +**Test record persistence:** + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Command + - Summary + * - ``LD_persist_records`` + - Save the in-memory records to a SQLite database. + * - ``LD_list_runs`` + - List recent runs in a database. + * - ``LD_fetch_run_records`` + - Load every record for one run. + * - ``LD_clear_records`` + - Drop the in-memory record list. + +**Parameter resolver:** + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Command + - Summary + * - ``LD_register_variable`` / ``LD_register_variables`` + - Register one or many ``${var.x}`` values. + * - ``LD_register_csv_source`` / ``LD_register_csv_sources`` + - Bind a CSV file to a ``${csv.name.col}`` source. + * - ``LD_clear_resolver`` + - Reset every registered variable / source. + +**Recording / replay:** + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Command + - Summary + * - ``LD_load_har`` + - Read a HAR JSON file from disk. + * - ``LD_har_to_tasks`` + - Convert a HAR document into a list of LoadDensity tasks. + * - ``LD_har_to_action_json`` + - Convert a HAR document into a runnable action JSON. + +**Metrics exporters:** + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Command + - Summary + * - ``LD_start_prometheus_exporter`` / ``LD_stop_prometheus_exporter`` + - Toggle the Prometheus HTTP endpoint. + * - ``LD_start_influxdb_sink`` / ``LD_stop_influxdb_sink`` + - Toggle the InfluxDB UDP / HTTP sink. + * - ``LD_start_opentelemetry_exporter`` / ``LD_stop_opentelemetry_exporter`` + - Toggle the OTLP gRPC exporter. + +Adding custom commands +---------------------- + +.. code-block:: python + + from je_load_density import add_command_to_executor + + def slack_notify(message: str) -> None: + ... + + add_command_to_executor({"LD_slack_notify": slack_notify}) + +Once registered, the new command is callable from any action JSON. diff --git a/docs/source/En/doc/api_reference/api_reference.rst b/docs/source/En/doc/api_reference/api_reference.rst new file mode 100644 index 0000000..8e9d0c6 --- /dev/null +++ b/docs/source/En/doc/api_reference/api_reference.rst @@ -0,0 +1,37 @@ +API Reference +============= + +The auto-generated Python reference is regenerated by Sphinx +``autosummary`` on every build. + +.. autosummary:: + :toctree: _autosummary + :recursive: + + je_load_density + je_load_density.utils.executor.action_executor + je_load_density.utils.parameterization.parameter_resolver + je_load_density.utils.recording.har_importer + je_load_density.utils.metrics.prometheus_exporter + je_load_density.utils.metrics.influxdb_sink + je_load_density.utils.metrics.opentelemetry_exporter + je_load_density.utils.test_record.test_record_class + je_load_density.utils.test_record.sqlite_persistence + je_load_density.utils.generate_report.generate_html_report + je_load_density.utils.generate_report.generate_json_report + je_load_density.utils.generate_report.generate_xml_report + je_load_density.utils.generate_report.generate_csv_report + je_load_density.utils.generate_report.generate_junit_report + je_load_density.utils.generate_report.generate_summary_report + je_load_density.utils.socket_server.load_density_socket_server + je_load_density.wrapper.create_locust_env.create_locust_env + je_load_density.wrapper.start_wrapper.start_test + je_load_density.wrapper.user_template.request_executor + je_load_density.wrapper.user_template.scenario_runner + je_load_density.wrapper.user_template.http_user_template + je_load_density.wrapper.user_template.fast_http_user_template + je_load_density.wrapper.user_template.websocket_user_template + je_load_density.wrapper.user_template.grpc_user_template + je_load_density.wrapper.user_template.mqtt_user_template + je_load_density.wrapper.user_template.socket_user_template + je_load_density.mcp_server.server diff --git a/docs/source/En/doc/architecture/architecture_doc.rst b/docs/source/En/doc/architecture/architecture_doc.rst new file mode 100644 index 0000000..7f4ee0e --- /dev/null +++ b/docs/source/En/doc/architecture/architecture_doc.rst @@ -0,0 +1,95 @@ +Architecture +============ + +Overview +-------- + +LoadDensity is a thin facade over Locust that adds a JSON-driven action +executor, a multi-protocol user template registry, scenario flow, data +parameterisation, observability sinks, and an MCP control surface. + +The dependency direction always points from the action layer down to +Locust, never the other way around — your action JSON defines what to +do, the executor maps each command to a Python callable, and Locust +runs the resulting load. + +Layered view +------------ + +.. mermaid:: + + flowchart TB + cli[CLI / MCP / GUI / Socket Server] --> exec[Action Executor] + exec --> start[start_test] + start --> proxy[locust_wrapper_proxy] + proxy --> userhttp[HTTP / FastHttp Wrapper] + proxy --> userws[WebSocket Wrapper] + proxy --> usergrpc[gRPC Wrapper] + proxy --> usermqtt[MQTT Wrapper] + proxy --> usersock[Raw TCP/UDP Wrapper] + userhttp & userws & usergrpc & usermqtt & usersock --> hooks[Locust events] + hooks --> records[test_record_instance] + hooks --> exporters[Prometheus / Influx / OTel] + records --> reports[HTML / JSON / XML / CSV / JUnit / Summary] + records --> sqlite[SQLite persistence] + +Module map +---------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Module + - Purpose + * - ``je_load_density.utils.executor`` + - ``Executor`` class, dispatch table, ``execute_action`` / + ``execute_files`` entrypoints. + * - ``je_load_density.utils.parameterization`` + - ``ParameterResolver`` for ``${var.x}`` / ``${env.X}`` / + ``${csv.s.col}`` / ``${faker.method}`` / built-in helpers. + * - ``je_load_density.utils.recording`` + - HAR ingestion → action JSON. + * - ``je_load_density.utils.metrics`` + - Prometheus exporter, InfluxDB sink, OpenTelemetry exporter. + * - ``je_load_density.utils.test_record`` + - In-memory record list plus optional SQLite sink. + * - ``je_load_density.utils.generate_report`` + - HTML / JSON / XML / CSV / JUnit / summary generators. + * - ``je_load_density.utils.socket_server`` + - Length-framed TCP control plane with optional TLS and token. + * - ``je_load_density.wrapper.proxy`` + - Per-protocol proxy holding the configured tasks for each user + template. + * - ``je_load_density.wrapper.user_template`` + - Locust user classes for HTTP, FastHttp, WebSocket, gRPC, MQTT, + and raw socket. + * - ``je_load_density.wrapper.start_wrapper`` + - ``start_test`` dispatcher that picks a user template and forwards + to ``prepare_env``. + * - ``je_load_density.wrapper.create_locust_env`` + - ``prepare_env`` / ``create_env`` building a Locust environment in + local, master, or worker mode. + * - ``je_load_density.mcp_server`` + - MCP server exposing 11 tools so Claude can drive LoadDensity. + * - ``je_load_density.gui`` + - Optional PySide6 widgets (form controls + live stats panel). + +Action lifecycle +---------------- + +#. Caller submits an action JSON via the CLI, MCP tool, socket server, + or direct ``execute_action(...)`` call. +#. ``Executor.execute_action`` dispatches each step against + ``event_dict`` (``LD_*`` commands plus safe builtins). +#. When the step is ``LD_start_test``, the dispatcher selects a user + template (``http_user``, ``fast_http_user``, ``websocket_user``, + ``grpc_user``, ``mqtt_user``, ``socket_user``), seeds the parameter + resolver from any ``variables`` / ``csv_sources``, and calls + ``prepare_env``. +#. ``prepare_env`` builds a Locust ``Environment`` in local, master, or + worker mode and starts the run. +#. Each user runs ``run_scenario`` (or the protocol equivalent) per + tick, fires Locust events, and feeds ``test_record_instance``. +#. Reports, metrics exporters, and SQLite persistence consume the + accumulated records. diff --git a/docs/source/En/doc/assertions/assertions_doc.rst b/docs/source/En/doc/assertions/assertions_doc.rst new file mode 100644 index 0000000..660281a --- /dev/null +++ b/docs/source/En/doc/assertions/assertions_doc.rst @@ -0,0 +1,76 @@ +Assertions & Extractors +======================= + +Overview +-------- + +HTTP and FastHttp tasks accept ``assertions`` and ``extract`` blocks +that run under Locust's ``catch_response``. Failed assertions mark the +request as a Locust failure and surface in every report. + +Assertions +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - ``type`` + - Behaviour + * - ``status_code`` + - ``int(response.status_code) == int(value)``. + * - ``contains`` + - ``str(value) in response.text``. + * - ``not_contains`` + - ``str(value) not in response.text``. + * - ``json_path`` + - Resolves ``response.json()`` along ``path`` (dot-separated; list + indices supported) and compares to ``value``. + * - ``header`` + - ``response.headers[name] == value``. + +Example +~~~~~~~ + +.. code-block:: json + + { + "method": "get", + "request_url": "${var.base}/health", + "assertions": [ + {"type": "status_code", "value": 200}, + {"type": "json_path", "path": "status", "value": "ok"}, + {"type": "header", "name": "X-Service", "value": "checkout"} + ] + } + +Extractors +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - ``from`` + - Source + * - ``json_path`` + - Same dotted path syntax as the ``json_path`` assertion. + * - ``header`` + - ``response.headers[name]``. + * - ``status_code`` + - ``response.status_code``. + +Extracted values are written into the parameter resolver under the +chosen ``var`` name; subsequent tasks reference them as ``${var.NAME}``. + +.. code-block:: json + + { + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [ + {"var": "auth_token", "from": "json_path", "path": "data.token"}, + {"var": "request_id", "from": "header", "name": "X-Request-Id"} + ] + } diff --git a/docs/source/En/doc/cli/cli_doc.rst b/docs/source/En/doc/cli/cli_doc.rst index ba7af9d..8ff6d3e 100644 --- a/docs/source/En/doc/cli/cli_doc.rst +++ b/docs/source/En/doc/cli/cli_doc.rst @@ -1,106 +1,86 @@ CLI (Command Line Interface) ============================ -LoadDensity provides a full command-line interface via ``python -m je_load_density``. +LoadDensity ships a subcommand-style CLI. Run +``python -m je_load_density --help`` for the full surface. -CLI Arguments -------------- +Subcommands +----------- .. list-table:: :header-rows: 1 - :widths: 25 10 65 - - * - Argument - - Short - - Description - * - ``--execute_file`` - - ``-e`` - - Execute a single JSON script file - * - ``--execute_dir`` - - ``-d`` - - Execute all JSON files in a directory - * - ``--execute_str`` - - — - - Execute an inline JSON string - * - ``--create_project`` - - ``-c`` - - Scaffold a new project with templates - -Execute a Single JSON File --------------------------- - -Run a test defined in a single JSON keyword file: + :widths: 25 75 + + * - Subcommand + - Purpose + * - ``run FILE`` + - Execute one action JSON file. + * - ``run-dir DIR`` + - Execute every ``.json`` in a directory. + * - ``run-str JSON`` + - Execute an inline JSON string (Windows double-encoding handled + transparently). + * - ``init PATH`` + - Scaffold a new project skeleton. + * - ``serve`` + - Start the hardened TCP control socket server. + +``run`` +------- .. code-block:: bash - python -m je_load_density -e test_scenario.json + python -m je_load_density run smoke.json -The JSON file should follow the action list format: +Where ``smoke.json`` is:: -.. code-block:: json + {"load_density": [ + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [{"method": "get", "request_url": "https://httpbin.org/get"}] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] + ]} - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] - ] +``run-dir`` +----------- -Execute All JSON Files in a Directory -------------------------------------- +Run every ``.json`` action file in a directory tree:: -Run all JSON keyword files in a specified directory recursively: + python -m je_load_density run-dir ./scenarios -.. code-block:: bash - - python -m je_load_density -d ./test_scripts/ - -This scans the directory for all ``.json`` files and executes each one sequentially. - -Execute an Inline JSON String ------------------------------ +``run-str`` +----------- -Execute a JSON action list directly as a string: +Inline JSON (handy for CI scripts):: -.. code-block:: bash + python -m je_load_density run-str '{"load_density":[["LD_summary",{}]]}' - python -m je_load_density --execute_str '[["LD_start_test", {"user_detail_dict": {"user": "fast_http_user"}, "user_count": 10, "spawn_rate": 5, "test_time": 5, "tasks": {"get": {"request_url": "http://httpbin.org/get"}}}]]' +``init`` +-------- -.. note:: +Scaffold a project at PATH:: - On **Windows**, inline JSON strings are automatically double-parsed due to shell - escaping differences. The CLI handles this transparently. + python -m je_load_density init ./my_load_test -Create a Project ----------------- +``serve`` +--------- -Scaffold a new project with keyword templates and executor scripts: +Start the control socket server. See +:doc:`../socket_server/socket_server_doc` for protocol details. .. code-block:: bash - python -m je_load_density -c MyProject - -This generates a project directory structure: - -.. code-block:: text - - MyProject/ - └── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json - │ └── keyword2.json - └── executor/ - ├── executor_one_file.py - └── executor_folder.py + python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 \ + --framed --token "$LOAD_DENSITY_SOCKET_TOKEN" \ + --tls-cert /etc/loaddensity/server.crt \ + --tls-key /etc/loaddensity/server.key -Error Handling --------------- +Legacy flags +------------ -If no valid argument is provided, the CLI raises a ``LoadDensityTestExecuteException`` -and exits with code 1. All errors are printed to stderr. +The flat ``-e/-d/-c/--execute_str`` flags from previous releases are +still accepted (suppressed in ``--help``) for backwards compatibility +with tools such as PyBreeze. New scripts should use the subcommands. diff --git a/docs/source/En/doc/create_project/create_project_doc.rst b/docs/source/En/doc/create_project/create_project_doc.rst new file mode 100644 index 0000000..d36200a --- /dev/null +++ b/docs/source/En/doc/create_project/create_project_doc.rst @@ -0,0 +1,42 @@ +Create Project +============== + +Overview +-------- + +``create_project_dir`` (CLI: ``je_load_density init``) scaffolds a +LoadDensity project skeleton at a chosen path. The skeleton contains +a sample action JSON, a runner script, and a placeholder for assets. + +Python API +---------- + +.. code-block:: python + + from je_load_density import create_project_dir + create_project_dir("./my_load_test") + +CLI +--- + +.. code-block:: bash + + python -m je_load_density init ./my_load_test + +Layout +------ + +:: + + my_load_test/ + ├── run.py # tiny runner that reads the action JSON + └── action.json # sample action JSON + +After scaffolding, edit ``action.json`` (see +:doc:`../action_executor/action_executor_doc`) and run:: + + python run.py + +or:: + + python -m je_load_density run action.json diff --git a/docs/source/En/doc/distributed/distributed_doc.rst b/docs/source/En/doc/distributed/distributed_doc.rst new file mode 100644 index 0000000..da8edc5 --- /dev/null +++ b/docs/source/En/doc/distributed/distributed_doc.rst @@ -0,0 +1,68 @@ +Distributed Master / Worker +=========================== + +Overview +-------- + +LoadDensity exposes Locust's distributed runner via a ``runner_mode`` +parameter on ``start_test`` / ``prepare_env``. Three modes are +supported: + +* ``local`` — single process (default). +* ``master`` — coordinates a cluster of workers, optionally serves the + Locust Web UI. +* ``worker`` — joins a master and runs the requested user count. + +Master +------ + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="master", + master_bind_host="0.0.0.0", + master_bind_port=5557, + expected_workers=4, # wait for 4 workers + web_ui_dict={"host": "0.0.0.0", "port": 8089}, + user_count=400, + spawn_rate=40, + test_time=600, + tasks=[...], + ) + +The master waits up to 60 s for ``expected_workers`` workers to join +before starting the load ramp. If only N workers (N < expected) join, +it logs a warning and starts anyway. + +Worker +------ + +Run on each load-generating node: + +.. code-block:: python + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", + master_port=5557, + tasks=[...], + ) + +Workers do not start a Web UI and skip the local stats greenlets — the +master collects and publishes aggregate stats. + +Tips +---- + +* Open the master ``master_bind_port`` in your firewall. Default + Locust port is ``5557``. +* Use ``master_bind_host="0.0.0.0"`` only when the master is reachable + by the workers; bind to a private interface IP otherwise. +* Match the user template (``http_user`` / ``fast_http_user`` / ...) + on master and workers — the master broadcasts the user class name. +* If you parameterise tasks with ``${csv.X.col}``, register the same + CSV files on every worker (they don't share state). diff --git a/docs/source/En/doc/exception/exception_doc.rst b/docs/source/En/doc/exception/exception_doc.rst new file mode 100644 index 0000000..7354416 --- /dev/null +++ b/docs/source/En/doc/exception/exception_doc.rst @@ -0,0 +1,42 @@ +Exceptions +========== + +Hierarchy +--------- + +:: + + Exception + └── LocustNotFoundException + └── LoadDensityTestException + ├── LoadDensityTestJsonException + ├── LoadDensityGenerateJsonReportException + ├── LoadDensityTestExecuteException + ├── LoadDensityAssertException + ├── LoadDensityHTMLException + ├── LoadDensityAddCommandException + ├── XMLException + │ └── XMLTypeException + └── CallbackExecutorException + +When to catch what +------------------ + +* ``LoadDensityTestExecuteException`` — Action JSON shape is wrong, or + an unknown command was referenced. Catch this to surface user-input + errors without crashing on internal exceptions. +* ``LoadDensityHTMLException`` / + ``LoadDensityGenerateJsonReportException`` — Report generation ran + with no records (in-memory store empty). +* ``LoadDensityAssertException`` — Reserved for future use by the + assertions layer; HTTP assertions today fail the request via Locust + rather than raise. +* ``XMLException`` / ``XMLTypeException`` — Malformed XML or unexpected + payload shape in the XML utilities. +* ``CallbackExecutorException`` — The callback executor was given an + invalid trigger or function reference. +* ``LoadDensityAddCommandException`` — ``add_command_to_executor`` was + passed a non-callable. + +All custom exceptions inherit from ``LoadDensityTestException``, so +catching that one class is enough for blanket error handling. diff --git a/docs/source/En/doc/generate_report/generate_report_doc.rst b/docs/source/En/doc/generate_report/generate_report_doc.rst index 9e723b5..27f0d14 100644 --- a/docs/source/En/doc/generate_report/generate_report_doc.rst +++ b/docs/source/En/doc/generate_report/generate_report_doc.rst @@ -1,162 +1,91 @@ Report Generation ================= -LoadDensity can generate test reports in three formats: **HTML**, **JSON**, and **XML**. -Reports are generated from the test records collected by the request hook during test execution. +Overview +-------- -.. note:: +LoadDensity can render six report formats from +``test_record_instance``: HTML, JSON, XML, CSV, JUnit XML, and a +percentile-summary JSON. - Reports can only be generated after a test has been run. If no test records exist, - a ``LoadDensityHTMLException`` or ``LoadDensityGenerateJsonReportException`` will be raised. +.. note:: -HTML Report ------------ + Reports require at least one record; calling a generator on an + empty store raises ``LoadDensityHTMLException`` / + ``LoadDensityGenerateJsonReportException``. -Generates a styled HTML file with tables showing success and failure records. +HTML +---- .. code-block:: python from je_load_density import generate_html_report + generate_html_report("my_report") # writes my_report.html - # Generates "my_report.html" - generate_html_report("my_report") - -The HTML report includes: - -* **Success records** — displayed in tables with aqua-colored headers, showing Method, URL, - name, status_code, response text, content, and headers -* **Failure records** — displayed in tables with red-colored headers, showing Method, URL, - name, status_code, and error message - -To get raw HTML fragments without writing to a file: - -.. code-block:: python - - from je_load_density import generate_html - - success_fragments, failure_fragments = generate_html() - # success_fragments: List[str] — HTML table strings for each success record - # failure_fragments: List[str] — HTML table strings for each failure record - -JSON Report ------------ - -Generates structured JSON files for programmatic consumption. +JSON (split by outcome) +----------------------- .. code-block:: python from je_load_density import generate_json_report - - # Generates "my_report_success.json" and "my_report_failure.json" success_path, failure_path = generate_json_report("my_report") -**Success JSON format:** +XML (split by outcome) +---------------------- -.. code-block:: json - - { - "Success_Test1": { - "Method": "GET", - "test_url": "http://httpbin.org/get", - "name": "/get", - "status_code": "200", - "text": "...", - "content": "...", - "headers": "..." - }, - "Success_Test2": {} - } - -**Failure JSON format:** - -.. code-block:: json +.. code-block:: python - { - "Failure_Test1": { - "Method": "POST", - "test_url": "http://httpbin.org/status/500", - "name": "/status/500", - "status_code": "500", - "error": "..." - } - } + from je_load_density import generate_xml_report + success_path, failure_path = generate_xml_report("my_report") -To get raw JSON data structures without writing to a file: +CSV (one row per request) +------------------------- .. code-block:: python - from je_load_density import generate_json - - success_dict, failure_dict = generate_json() + from je_load_density import generate_csv_report + generate_csv_report("my_report") # writes my_report.csv -XML Report ----------- +Columns: ``outcome, Method, test_url, name, status_code, +response_time_ms, response_length, error``. -Generates XML files for CI/CD integration. +JUnit XML (CI-friendly) +----------------------- .. code-block:: python - from je_load_density import generate_xml_report - - # Generates "my_report_success.xml" and "my_report_failure.xml" - success_path, failure_path = generate_xml_report("my_report") + from je_load_density import generate_junit_report + generate_junit_report("loaddensity-junit") # writes loaddensity-junit.xml -The XML output is pretty-printed using ``xml.dom.minidom``. Each test record is wrapped -under an ```` root element. +Each request becomes a ````; failures attach ```` +nodes carrying the error message. Compatible with Jenkins, GitHub +Actions test annotations, GitLab, etc. -To get raw XML strings without writing to a file: +Summary (percentiles) +--------------------- .. code-block:: python - from je_load_density import generate_xml + from je_load_density import generate_summary_report, build_summary - success_xml_str, failure_xml_str = generate_xml() + summary = build_summary() # in-memory dict + generate_summary_report("loaddensity-summary") -Using in JSON Scripts ---------------------- +The summary contains totals, per-name counts, min / max / mean / +percentile (p50 / p90 / p95 / p99) latencies, and an overall block. +Useful for charting and regression checks across runs. + +Action JSON +----------- -Report generation can be chained with test execution in JSON scripts: - -.. code-block:: json - - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }], - ["LD_generate_html_report", {"html_name": "report"}], - ["LD_generate_json_report", {"json_file_name": "report"}], - ["LD_generate_xml_report", {"xml_file_name": "report"}] - ] - -Report Functions Summary ------------------------- - -.. list-table:: - :header-rows: 1 - :widths: 35 25 40 - - * - Function - - Returns - - Description - * - ``generate_html()`` - - ``Tuple[List[str], List[str]]`` - - HTML fragments for success and failure records - * - ``generate_html_report(html_name)`` - - ``str`` - - Write HTML report file, returns file path - * - ``generate_json()`` - - ``Tuple[Dict, Dict]`` - - JSON dicts for success and failure records - * - ``generate_json_report(json_file_name)`` - - ``Tuple[str, str]`` - - Write JSON report files, returns paths - * - ``generate_xml()`` - - ``Tuple[str, str]`` - - XML strings for success and failure records - * - ``generate_xml_report(xml_file_name)`` - - ``Tuple[str, str]`` - - Write XML report files, returns paths +Chain reports into a test:: + + {"load_density": [ + ["LD_start_test", {...}], + ["LD_generate_html_report", {"html_name": "report"}], + ["LD_generate_json_report", {"json_file_name": "report"}], + ["LD_generate_xml_report", {"xml_file_name": "report"}], + ["LD_generate_csv_report", {"csv_name": "report"}], + ["LD_generate_junit_report", {"report_name": "report-junit"}], + ["LD_generate_summary_report",{"report_name": "report-summary"}] + ]} diff --git a/docs/source/En/doc/getting_started/getting_started_doc.rst b/docs/source/En/doc/getting_started/getting_started_doc.rst index f30bcc8..4f8b1d9 100644 --- a/docs/source/En/doc/getting_started/getting_started_doc.rst +++ b/docs/source/En/doc/getting_started/getting_started_doc.rst @@ -1,240 +1,100 @@ Getting Started =============== -This guide walks you through the basics of using LoadDensity to run your first load test. +This guide walks you through the basics of running your first +LoadDensity load test. -User Types +User types ---------- -LoadDensity supports two types of Locust users: +LoadDensity ships six user templates: -.. list-table:: - :header-rows: 1 - :widths: 25 25 50 +* ``fast_http_user`` — high-throughput HTTP (``locust.FastHttpUser`` + + geventhttpclient). +* ``http_user`` — ``locust.HttpUser`` + ``requests``. +* ``websocket_user``, ``grpc_user``, ``mqtt_user``, ``socket_user`` — + see Chapter 4. - * - User Type Key - - Locust Class - - Description - * - ``fast_http_user`` - - ``FastHttpUser`` - - Uses ``geventhttpclient`` for higher throughput. Recommended for most use cases. - * - ``http_user`` - - ``HttpUser`` - - Uses Python ``requests`` library. Better compatibility, lower throughput. - -Supported HTTP Methods ----------------------- - -LoadDensity supports the following HTTP methods: - -* ``get`` -* ``post`` -* ``put`` -* ``patch`` -* ``delete`` -* ``head`` -* ``options`` - -Running a Test with Python API ------------------------------- - -The simplest way to run a load test is to call ``start_test()``: +Run a test (Python API) +----------------------- .. code-block:: python from je_load_density import start_test - result = start_test( + start_test( user_detail_dict={"user": "fast_http_user"}, user_count=50, spawn_rate=10, - test_time=10, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"}, - } + test_time=30, + variables={"base": "https://httpbin.org"}, + tasks=[ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], ) -``start_test()`` Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 20 15 10 55 - - * - Parameter - - Type - - Default - - Description - * - ``user_detail_dict`` - - ``dict`` - - (required) - - User type configuration. ``{"user": "fast_http_user"}`` or ``{"user": "http_user"}`` - * - ``user_count`` - - ``int`` - - ``50`` - - Total number of simulated users to spawn - * - ``spawn_rate`` - - ``int`` - - ``10`` - - Number of users spawned per second - * - ``test_time`` - - ``int`` or ``None`` - - ``60`` - - Test duration in seconds. Pass ``None`` for unlimited duration - * - ``web_ui_dict`` - - ``dict`` or ``None`` - - ``None`` - - Enable Locust Web UI. e.g. ``{"host": "127.0.0.1", "port": 8089}`` - -Return Value -~~~~~~~~~~~~ - -``start_test()`` returns a dictionary summarizing the test configuration: - -.. code-block:: python - - { - "user_detail": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 10, - "web_ui": None, - } - -Enabling the Locust Web UI --------------------------- - -To monitor the test in real-time through the Locust Web UI: +Launch the Locust Web UI +------------------------ .. code-block:: python - from je_load_density import start_test - - result = start_test( - user_detail_dict={"user": "http_user"}, - user_count=100, - spawn_rate=20, - test_time=30, + start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, spawn_rate=10, test_time=30, web_ui_dict={"host": "127.0.0.1", "port": 8089}, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - } + tasks=[{"method": "get", "request_url": "https://httpbin.org/get"}], ) -Then open ``http://127.0.0.1:8089`` in your browser to view real-time statistics. - -Running a Test with JSON Script Files -------------------------------------- +Then open ``http://127.0.0.1:8089`` in your browser. -You can define test scenarios as JSON files and execute them without writing Python code. +Run a JSON action script +------------------------ -Create a ``test_scenario.json`` file: +Create ``test_scenario.json``: .. code-block:: json - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] - ] - -Execute from Python: + {"load_density": [ + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [{"method": "get", "request_url": "https://httpbin.org/get"}] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] + ]} -.. code-block:: python +Execute via the CLI:: - from je_load_density import execute_action, read_action_json + python -m je_load_density run test_scenario.json - execute_action(read_action_json("test_scenario.json")) - -JSON Script Format -~~~~~~~~~~~~~~~~~~ - -Each JSON script is an array of actions. Each action is a list: - -* With keyword arguments: ``["action_name", {"param1": "value1"}]`` -* With positional arguments: ``["action_name", ["arg1", "arg2"]]`` -* With no arguments: ``["action_name"]`` - -Chaining Multiple Actions -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Multiple actions can be chained in a single JSON file. For example, run a test and -generate reports automatically: - -.. code-block:: json - - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }], - ["LD_generate_html_report", {"html_name": "my_report"}], - ["LD_generate_json_report", {"json_file_name": "my_report"}], - ["LD_generate_xml_report", {"xml_file_name": "my_report"}] - ] - -Dict-based JSON Format -~~~~~~~~~~~~~~~~~~~~~~~ - -JSON scripts can also be wrapped in a dict with a ``"load_density"`` key: - -.. code-block:: json - - { - "load_density": [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] - ] - } - -Project Scaffolding -------------------- - -LoadDensity can generate a project directory structure with keyword templates and -executor scripts: +Or from Python: .. code-block:: python - from je_load_density import create_project_dir - - create_project_dir(project_path="./my_tests", parent_name="LoadDensity") - -Or via CLI: + from je_load_density import execute_action, read_action_json + execute_action(read_action_json("test_scenario.json")) -.. code-block:: bash +JSON script format +~~~~~~~~~~~~~~~~~~ - python -m je_load_density -c ./my_tests +Each action is a list: -This creates the following structure: +* with keyword arguments: ``["action_name", {"param1": "value1"}]`` +* with positional arguments: ``["action_name", ["arg1", "arg2"]]`` +* with no arguments: ``["action_name"]`` -.. code-block:: text +The top-level document is either a bare action list or a +``{"load_density": [...]}`` wrapper. - my_tests/ - └── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json # FastHttpUser test template - │ └── keyword2.json # HttpUser test template - └── executor/ - ├── executor_one_file.py # Execute single keyword file - └── executor_folder.py # Execute all files in keyword/ +Next steps +---------- -* ``keyword1.json`` — Template using ``fast_http_user`` with sample GET/POST tasks -* ``keyword2.json`` — Template using ``http_user`` with sample GET/POST tasks -* ``executor_one_file.py`` — Python script to execute ``keyword1.json`` -* ``executor_folder.py`` — Python script to execute all JSON files in ``keyword/`` +* Parameterise scripts: see :doc:`../parameter_resolver/parameter_resolver_doc`. +* Layer scenario flow: see :doc:`../scenarios/scenarios_doc`. +* Run a distributed master/worker fleet: + see :doc:`../distributed/distributed_doc`. +* Ship metrics to Prometheus / InfluxDB / OTel: + see :doc:`../metrics/metrics_doc`. diff --git a/docs/source/En/doc/grpc_user/grpc_user_doc.rst b/docs/source/En/doc/grpc_user/grpc_user_doc.rst new file mode 100644 index 0000000..ccdfd0f --- /dev/null +++ b/docs/source/En/doc/grpc_user/grpc_user_doc.rst @@ -0,0 +1,66 @@ +gRPC User +========= + +Overview +-------- + +The gRPC user template drives unary calls against operator-supplied +stub modules. It uses ``grpcio`` (and your own ``*_pb2`` / +``*_pb2_grpc`` modules), loaded lazily — install with +``pip install je_load_density[grpc]``. + +Task fields +----------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Field + - Meaning + * - ``target`` / ``host`` + - gRPC endpoint, e.g. ``localhost:50051``. + * - ``stub_path`` + - Dotted path to the stub class (``pkg.greeter_pb2_grpc.GreeterStub``). + * - ``request_path`` + - Dotted path to the request message (``pkg.greeter_pb2.HelloRequest``). + * - ``method`` + - Method name on the stub. + * - ``payload`` + - Dict of fields used to construct the request message. + * - ``metadata`` + - List of ``[key, value]`` pairs or a flat dict. + * - ``timeout`` + - Per-call timeout in seconds (default 10). + +The dotted paths are validated against a strict identifier regex +before ``importlib.import_module`` is called, so traversal-style +attacks (``../``, ``;``, ``__import__``) are rejected. + +Example +------- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "grpc_user"}, + user_count=20, + spawn_rate=5, + test_time=60, + tasks=[ + { + "name": "say_hello", + "target": "localhost:50051", + "stub_path": "pkg.greeter_pb2_grpc.GreeterStub", + "request_path": "pkg.greeter_pb2.HelloRequest", + "method": "SayHello", + "payload": {"name": "world"}, + "metadata": [["x-token", "abc"]], + "timeout": 5, + } + ], + ) + +Each call fires a Locust event tagged ``GRPC``. diff --git a/docs/source/En/doc/gui/gui_doc.rst b/docs/source/En/doc/gui/gui_doc.rst index 75deabe..4708df3 100644 --- a/docs/source/En/doc/gui/gui_doc.rst +++ b/docs/source/En/doc/gui/gui_doc.rst @@ -1,89 +1,88 @@ GUI (Graphical User Interface) ============================== -LoadDensity includes an optional PySide6-based graphical interface for running load tests -with a visual form and real-time log display. +Overview +-------- -Requirements ------------- +LoadDensity ships an optional PySide6 graphical front-end. It carries +the form controls for kicking off a quick HTTP test, a log panel that +mirrors the framework log, and a live stats panel that polls +``test_record_instance`` once a second. -The GUI requires additional dependencies. Install with: +Install +------- .. code-block:: bash - pip install je_load_density[gui] + pip install "je_load_density[gui]" -This installs: +Pulls in: -* **PySide6** (6.10.0) — Qt for Python bindings -* **qt-material** — Material design theme +* ``PySide6`` — Qt for Python bindings. +* ``qt-material`` — Material design theme. -Launching the GUI ------------------ +Launch +------ .. code-block:: python - from je_load_density.gui.main_window import LoadDensityUI - from PySide6.QtWidgets import QApplication import sys + from PySide6.QtWidgets import QApplication + from je_load_density.gui.main_window import LoadDensityUI app = QApplication(sys.argv) window = LoadDensityUI() window.show() sys.exit(app.exec()) -GUI Features ------------- +Layout +------ -The GUI provides: +* **Test parameter form** — URL, test duration, user count, spawn rate, + HTTP method. +* **Start button** — Launches the load test in a background ``QThread``. +* **Live stats panel** — Total requests, current rate, average and p95 + latency, failure count. Refreshes every 1 s. +* **Log panel** — Real-time framework log feed. +* **Material Design theme** — ``dark_amber.xml`` from ``qt-material``. -* **Test Parameter Form** — Input fields for: +Languages +--------- - * Target URL - * Test duration (seconds) - * User count (number of simulated users) - * Spawn rate (users per second) - * HTTP method selection (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) +The GUI ships with English, Traditional Chinese, Japanese, and +Korean translations. Switch via the ``LanguageWrapper.reset_language`` +helper: -* **Start Button** — Launches the load test in a background thread (non-blocking UI) -* **Real-time Log Panel** — Displays log messages from the test execution in real-time, - updated every 50ms via a QTimer -* **Material Design Theme** — Uses the ``dark_amber.xml`` theme from qt-material - -Language Support ----------------- - -The GUI supports two languages: - -* **English** (default) -* **Traditional Chinese** (繁體中文) +.. code-block:: python -Language strings are managed via the ``language_wrapper`` module under -``je_load_density/gui/language_wrapper/``. + from je_load_density.gui.language_wrapper.multi_language_wrapper import ( + language_wrapper, + ) + language_wrapper.reset_language("Japanese") # or Korean / Traditional_Chinese / English Architecture ------------ -The GUI consists of the following components: - .. list-table:: :header-rows: 1 - :widths: 35 65 + :widths: 30 70 * - Component - Description * - ``LoadDensityUI`` - - Main window (``QMainWindow``). Applies theme and contains the widget. + - ``QMainWindow`` host. Applies theme and wires the central widget. * - ``LoadDensityWidget`` - - Central widget with form inputs, start button, and log panel. + - Form + start button + stats panel + log panel. + * - ``StatsPanel`` + - QTimer-driven panel reading ``test_record_instance``. * - ``LoadDensityGUIThread`` - - Background ``QThread`` that runs the load test without blocking the UI. + - Background ``QThread`` that runs the test without blocking the UI. * - ``InterceptAllFilter`` - - Log filter that captures log messages into a queue for GUI display. + - Captures log records into a thread-safe queue. * - ``log_message_queue`` - - Thread-safe queue bridging the logger and the GUI log panel. + - Bridges the logger and the GUI log panel. .. note:: - On Windows, the GUI sets ``AppUserModelID`` via ``ctypes`` so the taskbar correctly - identifies the application. + On Windows the main window sets ``AppUserModelID`` via ``ctypes`` so + the taskbar correctly identifies the application. diff --git a/docs/source/En/doc/har_import/har_import_doc.rst b/docs/source/En/doc/har_import/har_import_doc.rst new file mode 100644 index 0000000..638a538 --- /dev/null +++ b/docs/source/En/doc/har_import/har_import_doc.rst @@ -0,0 +1,68 @@ +HAR Record / Replay +=================== + +Overview +-------- + +The HAR importer turns recorded HTTP traffic (HAR JSON) into a list of +LoadDensity tasks or a complete runnable action JSON. Capture HAR via +Chrome / Firefox DevTools, mitmproxy, Charles, or any tool that +exports the `HAR 1.2 `_ +format. + +Python API +---------- + +.. code-block:: python + + from je_load_density import load_har, har_to_tasks, har_to_action_json + + har = load_har("recording.har") + tasks = har_to_tasks(har, include=[r"example\.com"], exclude=[r"\.svg$"]) + action_json = har_to_action_json( + har, + user="fast_http_user", + user_count=20, + spawn_rate=10, + test_time=120, + include=[r"api\.example\.com"], + ) + +Filters +------- + +* ``include`` — list of regex patterns; an entry is kept only if its + URL matches any pattern. +* ``exclude`` — list of regex patterns; an entry is dropped if its URL + matches any pattern. + +Mapping rules +------------- + +* HTTP method, URL, and request headers are copied directly. +* Hop-by-hop and HTTP/2 pseudo headers + (``host``, ``content-length``, ``connection``, ``:authority``, …) + are stripped. +* JSON request bodies (``application/json`` MIME) are parsed into the + ``json`` field; form params become ``data`` dicts; raw text bodies + fall back to ``data`` strings. +* The captured response status becomes a ``status_code`` assertion on + the generated task. + +Action JSON +----------- + +.. code-block:: json + + {"load_density": [ + ["LD_har_to_action_json", { + "har": {"log": {...}}, + "user": "fast_http_user", + "user_count": 20, + "spawn_rate": 10, + "test_time": 120 + }] + ]} + +The result of ``LD_har_to_action_json`` is itself an action JSON that +can be saved or piped into ``LD_execute_action``. diff --git a/docs/source/En/doc/http_users/http_users_doc.rst b/docs/source/En/doc/http_users/http_users_doc.rst new file mode 100644 index 0000000..ac94c93 --- /dev/null +++ b/docs/source/En/doc/http_users/http_users_doc.rst @@ -0,0 +1,84 @@ +HTTP Users +========== + +Overview +-------- + +LoadDensity ships two HTTP user templates, both wired through the same +``request_executor`` and ``scenario_runner`` modules: + +* ``http_user`` — wraps ``locust.HttpUser`` (``requests`` under the + hood). +* ``fast_http_user`` — wraps ``locust.FastHttpUser`` (geventhttpclient, + much higher RPS). + +Choose ``fast_http_user`` for high-load scenarios. Use ``http_user`` +when you need ``requests``-specific features or middleware. + +Task fields +----------- + +Every HTTP task is a dict; the runner forwards the fields below to the +underlying client. Anything else is ignored. + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Field + - Meaning + * - ``method`` + - ``get`` / ``post`` / ``put`` / ``patch`` / ``delete`` / ``head`` + / ``options`` (case-insensitive). + * - ``request_url`` / ``url`` + - Target URL (absolute or relative to ``host``). + * - ``name`` + - Locust event name; defaults to the URL. + * - ``headers`` + - Dict of request headers. + * - ``params`` + - Query string parameters (dict or list of pairs). + * - ``json`` + - Body serialised as JSON. + * - ``data`` + - Form-encoded body (dict, list, or str). + * - ``cookies`` + - Dict of cookies. + * - ``timeout`` + - Per-request timeout in seconds. + * - ``allow_redirects``, ``verify``, ``files`` + - Forwarded directly to the client. + * - ``auth`` + - ``{"type": "basic", "username": "...", "password": "..."}`` or + ``{"type": "bearer", "token": "..."}``. + * - ``assertions`` + - Response assertions (see :doc:`../assertions/assertions_doc`). + * - ``extract`` + - Response extractors (see :doc:`../parameter_resolver/parameter_resolver_doc`). + * - ``weight``, ``run_if``, ``skip_if`` + - Scenario flow controls (see :doc:`../scenarios/scenarios_doc`). + +Example +------- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, + spawn_rate=10, + test_time=60, + variables={"base": "https://api.example.com"}, + tasks=[ + {"method": "post", "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [ + {"var": "auth", "from": "json_path", "path": "data.token"} + ]}, + {"method": "get", "request_url": "${var.base}/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], + ) diff --git a/docs/source/En/doc/installation/installation_doc.rst b/docs/source/En/doc/installation/installation_doc.rst index 9def6ca..04587f7 100644 --- a/docs/source/En/doc/installation/installation_doc.rst +++ b/docs/source/En/doc/installation/installation_doc.rst @@ -15,65 +15,83 @@ Supported Platforms :widths: 30 70 * - Platform - - Version - * - Windows - - 10 / 11 + - Notes + * - Windows 10 / 11 + - Fully supported * - macOS - - 10.15 ~ 11 (Big Sur) - * - Linux - - Ubuntu 20.04 + - Fully supported + * - Ubuntu / Linux + - Fully supported * - Raspberry Pi - - 3B+ + - Tested on 3B+ and later -Basic Installation (CLI & Library) ----------------------------------- - -Install LoadDensity from PyPI: +Base install (CLI & library) +---------------------------- .. code-block:: bash pip install je_load_density -This installs the core library and CLI. `Locust `_ is automatically -installed as a dependency. - -Installation with GUI Support ------------------------------ - -To use the optional PySide6-based graphical interface: - -.. code-block:: bash - - pip install je_load_density[gui] - -This additionally installs: - -* `PySide6 `_ — Qt for Python bindings -* `qt-material `_ — Material design theme +This pulls in `Locust `_ and ``defusedxml`` — +nothing else. -Development Installation -------------------------- +Optional extras +--------------- -To install from source for development: +LoadDensity ships every protocol driver, exporter, recorder, and +control surface as an opt-in extra. The base package never imports +these modules eagerly, so the runtime footprint is unchanged for users +who only need HTTP load testing. -.. code-block:: bash - - git clone https://github.com/Intergration-Automation-Testing/LoadDensity.git - cd LoadDensity - pip install -e . - pip install -r dev_requirements.txt - -Verify Installation +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Extra + - Adds + * - ``gui`` + - PySide6 + qt-material (graphical front-end). + * - ``websocket`` + - ``websocket-client`` (WebSocket user template). + * - ``grpc`` + - ``grpcio`` + ``protobuf`` (gRPC user template). + * - ``mqtt`` + - ``paho-mqtt`` (MQTT user template). + * - ``prometheus`` + - ``prometheus-client`` (Prometheus exporter). + * - ``opentelemetry`` + - OpenTelemetry SDK + OTLP gRPC exporter. + * - ``metrics`` + - ``prometheus`` + ``opentelemetry`` bundle. + * - ``faker`` + - ``Faker`` (powers ``${faker.method}`` placeholders). + * - ``mcp`` + - ``mcp`` SDK (drives the MCP server for Claude). + * - ``all`` + - Everything above. + +Examples:: + + pip install "je_load_density[gui]" + pip install "je_load_density[mqtt,grpc,websocket]" + pip install "je_load_density[metrics]" + pip install "je_load_density[mcp]" + pip install "je_load_density[all]" + +Development install ------------------- -After installation, verify that LoadDensity is correctly installed: - .. code-block:: bash - python -c "from je_load_density import start_test; print('LoadDensity installed successfully')" + git clone https://github.com/Integration-Automation/LoadDensity.git + cd LoadDensity + pip install -e ".[all]" + pip install -r requirements.txt -You can also check the installed version: +Verify +------ .. code-block:: bash + python -c "from je_load_density import start_test; print('LoadDensity installed')" pip show je_load_density diff --git a/docs/source/En/doc/locust_env/locust_env_doc.rst b/docs/source/En/doc/locust_env/locust_env_doc.rst new file mode 100644 index 0000000..8a16f1e --- /dev/null +++ b/docs/source/En/doc/locust_env/locust_env_doc.rst @@ -0,0 +1,67 @@ +Locust Environment +================== + +Overview +-------- + +``prepare_env`` and ``create_env`` wrap ``locust.env.Environment`` and +hide the boilerplate of wiring up runners, stats printers, and the +optional Web UI. + +create_env +---------- + +Builds an ``Environment`` and a runner without starting any users: + +.. code-block:: python + + from je_load_density import create_env + from je_load_density.wrapper.user_template.fast_http_user_template import ( + FastHttpUserWrapper, + ) + + env = create_env( + FastHttpUserWrapper, + runner_mode="local", # "local" | "master" | "worker" + master_bind_host="*", + master_bind_port=5557, + master_host="127.0.0.1", + master_port=5557, + ) + +Use ``create_env`` when you want to attach extra event listeners +before the runner is started. + +prepare_env +----------- + +A complete lifecycle helper: create environment → start runner → +optionally launch the Locust Web UI → schedule a stop after +``test_time`` → join. + +.. code-block:: python + + from je_load_density import prepare_env + + prepare_env( + user_class=FastHttpUserWrapper, + user_count=50, + spawn_rate=10, + test_time=60, + web_ui_dict={"host": "127.0.0.1", "port": 8089}, + ) + +Web UI +------ + +Pass ``web_ui_dict`` to ``prepare_env`` (or to ``start_test``) to enable +the Locust web UI on the configured host/port. The UI is only started +in local and master modes; workers never start a UI. + +Stats greenlets +--------------- + +In local and master modes, ``create_env`` spawns the standard Locust +``stats_printer`` and ``stats_history`` greenlets so the console keeps +streaming aggregate stats and the in-memory history is updated for +charting. Workers skip both because the master collects and prints. diff --git a/docs/source/En/doc/mcp_claude/mcp_claude_doc.rst b/docs/source/En/doc/mcp_claude/mcp_claude_doc.rst new file mode 100644 index 0000000..317d751 --- /dev/null +++ b/docs/source/En/doc/mcp_claude/mcp_claude_doc.rst @@ -0,0 +1,72 @@ +MCP Server (for Claude) +======================= + +Overview +-------- + +LoadDensity ships a `Model Context Protocol +`_ server that exposes the framework +as a set of MCP tools. With it, Claude (Desktop, Code, or any MCP +client) can drive load tests, generate reports, import HAR files, and +inspect persisted runs without leaving the chat. + +Install +------- + +.. code-block:: bash + + pip install "je_load_density[mcp]" + +Run the server +-------------- + +.. code-block:: bash + + python -m je_load_density.mcp_server + +The server speaks MCP over stdio. Wire it into the client of your +choice (Claude Desktop ``claude_desktop_config.json``, Claude Code, +etc.): + +.. code-block:: json + + { + "mcpServers": { + "loaddensity": { + "command": "python", + "args": ["-m", "je_load_density.mcp_server"] + } + } + } + +Exposed tools +------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Tool + - Purpose + * - ``load_density.run_test`` + - Run a Locust-backed load test (HTTP / WS / gRPC / MQTT / Socket). + * - ``load_density.run_action_json`` + - Execute an action JSON document. + * - ``load_density.create_project`` + - Scaffold a project skeleton at PATH. + * - ``load_density.list_executor_commands`` + - List every ``LD_*`` command registered in the executor. + * - ``load_density.import_har`` + - Convert a HAR file into a runnable action JSON. + * - ``load_density.generate_reports`` + - Emit any combination of HTML / JSON / XML / CSV / JUnit / summary. + * - ``load_density.summary`` + - Return aggregated stats (totals, per-name p50/p90/p95/p99). + * - ``load_density.persist_records`` + - Save the current records into a SQLite database. + * - ``load_density.list_runs`` + - List recent persisted runs. + * - ``load_density.fetch_run`` + - Fetch records belonging to a saved run. + * - ``load_density.clear_records`` + - Drop in-memory records before a new run. diff --git a/docs/source/En/doc/metrics/metrics_doc.rst b/docs/source/En/doc/metrics/metrics_doc.rst new file mode 100644 index 0000000..9a7e07b --- /dev/null +++ b/docs/source/En/doc/metrics/metrics_doc.rst @@ -0,0 +1,94 @@ +Metrics Exporters +================= + +Overview +-------- + +LoadDensity ships three observability sinks that hook into Locust's +``request`` event and emit per-request metrics. All three are loaded +lazily and ship as optional extras. + +Prometheus +---------- + +Install: ``pip install je_load_density[prometheus]``. + +.. code-block:: python + + from je_load_density import start_prometheus_exporter + start_prometheus_exporter(port=9646, addr="127.0.0.1") + +Metrics: + +* ``loaddensity_requests_total{request_type, name, outcome}`` — counter +* ``loaddensity_request_latency_ms{request_type, name}`` — histogram +* ``loaddensity_response_bytes{request_type, name}`` — histogram + +The default bind address is loopback. Pass ``addr="0.0.0.0"`` to expose +the endpoint to a Docker / Kubernetes scrape target. + +InfluxDB +-------- + +Stdlib only — no extra package needed. Pick UDP for fire-and-forget or +HTTP for an authenticated cloud endpoint. + +.. code-block:: python + + from je_load_density import start_influxdb_sink + + # UDP listener on the InfluxDB box + start_influxdb_sink(transport="udp", host="127.0.0.1", port=8089) + + # HTTPS write API + start_influxdb_sink( + transport="http", + url="https://eu-central-1-1.aws.cloud2.influxdata.com/api/v2/write?org=...&bucket=...", + token="...", + ) + +The HTTP transport rejects URLs that aren't ``http://`` or ``https://``. + +OpenTelemetry +------------- + +Install: ``pip install je_load_density[opentelemetry]``. + +.. code-block:: python + + from je_load_density import start_opentelemetry_exporter + start_opentelemetry_exporter( + endpoint="http://otel-collector:4317", + service_name="loaddensity", + export_interval_ms=5000, + ) + +Instruments emitted: + +* ``loaddensity.requests`` — counter +* ``loaddensity.request.latency`` — histogram (ms) +* ``loaddensity.response.size`` — histogram (bytes) + +Each instrument carries ``request_type``, ``name``, and ``outcome`` +attributes. + +Stop helpers +------------ + +Each ``start_*`` has a paired ``stop_*`` that detaches the listener +(and shuts down the OTel provider). The Prometheus HTTP server itself +keeps running because ``prometheus_client`` does not expose a stop +hook. + +Action JSON +----------- + +The same exporters are reachable from action JSON: + +.. code-block:: json + + {"load_density": [ + ["LD_start_prometheus_exporter", {"port": 9646, "addr": "127.0.0.1"}], + ["LD_start_test", {...}], + ["LD_stop_prometheus_exporter", {}] + ]} diff --git a/docs/source/En/doc/mqtt_user/mqtt_user_doc.rst b/docs/source/En/doc/mqtt_user/mqtt_user_doc.rst new file mode 100644 index 0000000..ab2b09b --- /dev/null +++ b/docs/source/En/doc/mqtt_user/mqtt_user_doc.rst @@ -0,0 +1,60 @@ +MQTT User +========= + +Overview +-------- + +The MQTT user template drives ``connect`` / ``publish`` / ``subscribe`` +/ ``disconnect`` against an MQTT broker. It uses ``paho-mqtt``, loaded +lazily — install with ``pip install je_load_density[mqtt]``. + +Task fields +----------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Field + - Meaning + * - ``method`` + - ``connect`` / ``publish`` / ``subscribe`` / ``disconnect``. + * - ``broker`` / ``host`` + - ``host:port`` of the MQTT broker. + * - ``topic`` + - Topic for publish / subscribe. + * - ``payload`` + - Body for publish (``str`` or ``bytes``). + * - ``qos`` + - 0 / 1 / 2. + * - ``retain`` + - Boolean. + * - ``username`` / ``password`` + - Credentials. + * - ``client_id`` + - Optional client id (defaults to a random hex token). + * - ``timeout`` + - Publish wait timeout (default 5 seconds). + +Example +------- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "mqtt_user"}, + user_count=10, + spawn_rate=5, + test_time=60, + tasks=[ + {"method": "connect", "broker": "127.0.0.1:1883"}, + {"method": "subscribe", "topic": "telemetry/in", "qos": 1}, + {"method": "publish", "topic": "telemetry/out", + "payload": "ping", "qos": 1}, + {"method": "disconnect"}, + ], + ) + +Each step fires a Locust event tagged ``MQTT``. diff --git a/docs/source/En/doc/parameter_resolver/parameter_resolver_doc.rst b/docs/source/En/doc/parameter_resolver/parameter_resolver_doc.rst new file mode 100644 index 0000000..5a87389 --- /dev/null +++ b/docs/source/En/doc/parameter_resolver/parameter_resolver_doc.rst @@ -0,0 +1,109 @@ +Parameter Resolver +================== + +Overview +-------- + +The parameter resolver expands ``${...}`` placeholders inside any +nested string / list / dict structure. It is invoked automatically on +every task before the user template touches it, so values flow +seamlessly between actions. + +Supported placeholders +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Placeholder + - Resolves to + * - ``${var.NAME}`` + - The value passed to ``register_variable`` / ``register_variables``. + * - ``${env.NAME}`` + - Environment variable ``NAME``. + * - ``${csv.SOURCE.COLUMN}`` + - The next row from CSV source ``SOURCE`` (cycles by default). + * - ``${faker.METHOD}`` + - Calls ``Faker().METHOD()`` (lazy import, optional dependency). + * - ``${uuid()}`` + - A new UUID 4 string. + * - ``${now()}`` + - Local time in ISO-8601 (seconds resolution). + * - ``${randint(min, max)}`` + - Cryptographically-strong random int in ``[min, max]``. + +Unknown placeholders are left in place so missing data is visible +during a dry run. + +Registering data +---------------- + +.. code-block:: python + + from je_load_density import ( + register_variable, register_variables, + register_csv_source, register_csv_sources, + ) + + register_variable("base", "https://api.example.com") + register_variables({"token": "abc", "tenant": "acme"}) + + register_csv_source("users", "users.csv") # cycles + register_csv_sources([ + {"name": "products", "file_path": "products.csv", "cycle": False}, + ]) + +CSV files must have a header row. Each call to ``${csv.name.col}`` +returns the value at column ``col`` from the next row. + +Action-JSON usage +----------------- + +The same APIs are available through the executor so an entire run can +be parameterised from JSON: + +.. code-block:: json + + {"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://api.example.com"}}], + ["LD_register_csv_sources", {"sources": [ + {"name": "users", "file_path": "users.csv"} + ]}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "tasks": [{ + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "${csv.users.email}", "password": "${csv.users.password}"} + }] + }] + ]} + +Extracting values from responses +-------------------------------- + +HTTP tasks may declare ``extract`` rules; matching values are written +back into the resolver under the chosen variable name: + +.. code-block:: json + + { + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [ + {"var": "auth_token", "from": "json_path", "path": "data.token"}, + {"var": "request_id", "from": "header", "name": "X-Request-Id"}, + {"var": "status", "from": "status_code"} + ] + } + +Subsequent tasks can read ``${var.auth_token}`` straight from the +resolver. + +Clearing +-------- + +Call ``parameter_resolver.clear()`` (or ``LD_clear_resolver``) between +runs to discard accumulated state. diff --git a/docs/source/En/doc/scenarios/scenarios_doc.rst b/docs/source/En/doc/scenarios/scenarios_doc.rst new file mode 100644 index 0000000..ab772b6 --- /dev/null +++ b/docs/source/En/doc/scenarios/scenarios_doc.rst @@ -0,0 +1,96 @@ +Scenario Modes +============== + +Overview +-------- + +Tasks for HTTP, FastHttp, and WebSocket users can be bundled into a +scenario object that controls *which* tasks run on each tick. Three +modes are supported: + +* ``sequence`` — every task runs in order (default). +* ``weighted`` — one task is picked per tick, weighted by ``weight``. +* ``conditional`` — each task is gated by ``run_if`` / ``skip_if`` + predicates evaluated against the parameter resolver. + +Shape +----- + +.. code-block:: json + + { + "mode": "sequence", + "tasks": [ + {"method": "get", "request_url": "${var.base}/products"}, + {"method": "post", "request_url": "${var.base}/cart", + "json": {"product_id": 1}} + ] + } + +The legacy ``{"get": {...}}`` map and a bare list also still work; the +runner normalises them to ``{"mode": "sequence", "tasks": [...]}``. + +Weighted picks +-------------- + +Each task may carry a positive integer ``weight``; the runner picks one +task per tick with probability proportional to weight. Tasks without a +``weight`` default to 1. + +.. code-block:: json + + { + "mode": "weighted", + "tasks": [ + {"method": "get", "request_url": "/", "weight": 3}, + {"method": "get", "request_url": "/expensive", "weight": 1} + ] + } + +Conditional flow +---------------- + +``run_if`` and ``skip_if`` accept the same predicate language; ``run_if`` +must be truthy for the task to run, ``skip_if`` must be falsy. + +Predicates +~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Form + - Meaning + * - ``true`` / ``false`` / int + - Direct truthy check. + * - ``"${var.x}"`` + - Resolve the placeholder, then truthy check. + * - ``{"equals": [a, b]}`` + - ``a == b`` after resolution. + * - ``{"not_equals": [a, b]}`` + - ``a != b`` after resolution. + * - ``{"in": [needle, haystack]}`` + - ``needle in haystack``. + * - ``{"truthy": value}`` + - Truthy check after resolution. + +Example +~~~~~~~ + +.. code-block:: json + + { + "mode": "sequence", + "tasks": [ + {"method": "post", "request_url": "/login", + "json": {"email": "${var.email}"}, + "extract": [{"var": "auth", "from": "json_path", "path": "token"}]}, + {"method": "get", "request_url": "/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "run_if": {"truthy": "${var.auth}"}}, + {"method": "post", "request_url": "/cart", + "json": {"product_id": 1}, + "skip_if": {"equals": ["${var.tenant}", "internal"]}} + ] + } diff --git a/docs/source/En/doc/scheduler/scheduler_doc.rst b/docs/source/En/doc/scheduler/scheduler_doc.rst deleted file mode 100644 index 97eff2a..0000000 --- a/docs/source/En/doc/scheduler/scheduler_doc.rst +++ /dev/null @@ -1,117 +0,0 @@ -Scheduler -========= - -LoadDensity includes a built-in scheduler that allows you to schedule recurring test -execution at defined intervals. The scheduler supports both blocking and non-blocking modes. - -Basic Usage ------------ - -.. code-block:: python - - from je_load_density.utils.scheduler.scheduler_manager import SchedulerManager - - scheduler = SchedulerManager() - - def my_task(): - print("Scheduled task executed") - - # Add a job that runs every 5 seconds (blocking mode) - scheduler.add_interval_blocking_secondly(my_task, seconds=5) - - # Start the blocking scheduler - scheduler.start_block_scheduler() - -Blocking vs Non-blocking -------------------------- - -The scheduler has two modes: - -* **Blocking mode** — ``start_block_scheduler()`` blocks the current thread. Use this for - standalone scheduler scripts. -* **Non-blocking mode** — ``start_nonblocking_scheduler()`` runs the scheduler in a background - thread. Use this when you need to continue executing other code. - -Interval Methods (Blocking) ----------------------------- - -.. list-table:: - :header-rows: 1 - :widths: 50 50 - - * - Method - - Description - * - ``add_interval_blocking_secondly(func, seconds)`` - - Run every N seconds - * - ``add_interval_blocking_minutely(func, minutes)`` - - Run every N minutes - * - ``add_interval_blocking_hourly(func, hours)`` - - Run every N hours - * - ``add_interval_blocking_daily(func, days)`` - - Run every N days - * - ``add_interval_blocking_weekly(func, weeks)`` - - Run every N weeks - -Interval Methods (Non-blocking) --------------------------------- - -.. list-table:: - :header-rows: 1 - :widths: 50 50 - - * - Method - - Description - * - ``add_interval_nonblocking_secondly(func, seconds)`` - - Run every N seconds (non-blocking) - * - ``add_interval_nonblocking_minutely(func, minutes)`` - - Run every N minutes (non-blocking) - * - ``add_interval_nonblocking_hourly(func, hours)`` - - Run every N hours (non-blocking) - * - ``add_interval_nonblocking_daily(func, days)`` - - Run every N days (non-blocking) - * - ``add_interval_nonblocking_weekly(func, weeks)`` - - Run every N weeks (non-blocking) - -Cron Methods ------------- - -For cron-like scheduling: - -* ``add_cron_blocking(func, **cron_args)`` — Add a cron job in blocking mode -* ``add_cron_nonblocking(func, **cron_args)`` — Add a cron job in non-blocking mode - -Job Management --------------- - -* ``remove_blocking_job(job_id)`` — Remove a job from the blocking scheduler -* ``remove_nonblocking_job(job_id)`` — Remove a job from the non-blocking scheduler - -Starting Schedulers -------------------- - -* ``start_block_scheduler()`` — Start the blocking scheduler (blocks current thread) -* ``start_nonblocking_scheduler()`` — Start the non-blocking scheduler (background) -* ``start_all_scheduler()`` — Start both schedulers - -Example: Scheduled Load Test ------------------------------ - -.. code-block:: python - - from je_load_density import start_test - from je_load_density.utils.scheduler.scheduler_manager import SchedulerManager - - scheduler = SchedulerManager() - - def run_test(): - start_test( - user_detail_dict={"user": "fast_http_user"}, - user_count=10, - spawn_rate=5, - test_time=5, - tasks={"get": {"request_url": "http://httpbin.org/get"}}, - ) - - # Run the test every 60 seconds - scheduler.add_interval_blocking_secondly(run_test, seconds=60) - scheduler.start_block_scheduler() diff --git a/docs/source/En/doc/socket_server/socket_server_doc.rst b/docs/source/En/doc/socket_server/socket_server_doc.rst index 58f5a82..3b2a430 100644 --- a/docs/source/En/doc/socket_server/socket_server_doc.rst +++ b/docs/source/En/doc/socket_server/socket_server_doc.rst @@ -1,110 +1,107 @@ -TCP Socket Server (Remote Execution) -===================================== +TCP Control Socket Server +========================= -LoadDensity includes a TCP server based on ``gevent`` that accepts JSON commands over the -network, enabling remote test execution. +Overview +-------- -Starting the Server -------------------- - -.. code-block:: python - - from je_load_density import start_load_density_socket_server +The control socket server is a gevent-based TCP listener that runs +LoadDensity action JSON sent over the wire. The hardened protocol adds +length-prefix framing, optional TLS, and a shared-secret token; the +legacy unauthenticated mode is preserved for backwards compatibility. - # Start server (blocking call) - start_load_density_socket_server(host="localhost", port=9940) +Modes +----- .. list-table:: :header-rows: 1 - :widths: 20 15 15 50 - - * - Parameter - - Type - - Default - - Description - * - ``host`` - - ``str`` - - ``"localhost"`` - - Server bind address - * - ``port`` - - ``int`` - - ``9940`` - - Server bind port - -The server starts listening and prints ``Server started on {host}:{port}``. Each incoming -connection is handled in a separate ``gevent`` greenlet for concurrent request handling. - -Sending Commands from a Client -------------------------------- - -Commands are sent as JSON-encoded action lists — the same format used in JSON script files. + :widths: 25 75 + + * - Mode + - Notes + * - ``legacy`` + - Single ``recv(8192)``, raw JSON, no auth. Default to keep older + clients (e.g. PyBreeze) working. + * - ``framed`` + - 4-byte big-endian length prefix + JSON body. Safer against + partial reads and oversized payloads (1 MiB cap). + * - ``framed + TLS`` + - Wrap the connection with ``ssl.create_default_context`` (TLS + 1.2+ minimum) using a cert/key on disk. + +Auth +---- + +Pass ``token=`` (or set ``LOAD_DENSITY_SOCKET_TOKEN``) to require a +shared secret. Once configured: + +* ``quit_server`` is rejected without a valid token. +* All command payloads must use the envelope + ``{"token": "...", "command": [...action JSON...]}`` and may set + ``"op": "quit"`` to signal a shutdown. + +Tokens are compared with ``hmac.compare_digest`` to avoid timing +oracles. + +Starting the server +------------------- -.. code-block:: python +Python:: - import socket - import json - - # Connect to server - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(("localhost", 9940)) - - # Send a test command - command = json.dumps([ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] - ]) - sock.send(command.encode("utf-8")) - - # Receive response - response = sock.recv(8192) - print(response.decode("utf-8")) - sock.close() + from je_load_density import start_load_density_socket_server -Server Protocol ---------------- + start_load_density_socket_server( + host="0.0.0.0", + port=9940, + framed=True, + token="ROTATE_ME", + certfile="/etc/loaddensity/server.crt", + keyfile="/etc/loaddensity/server.key", + ) -* **Command format**: JSON-encoded action list (same format as JSON script files) -* **Response**: Each action's return value is sent back as a line, terminated by - ``Return_Data_Over_JE\n`` -* **Error handling**: If an error occurs during execution, the error message is sent back - followed by ``Return_Data_Over_JE\n`` -* **Buffer size**: 8192 bytes per receive +CLI:: -Shutting Down the Server ------------------------- + python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 --framed \ + --token "$LOAD_DENSITY_SOCKET_TOKEN" -Send the string ``"quit_server"`` to gracefully shut down the server: +Sending commands (framed mode) +------------------------------ .. code-block:: python - import socket - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(("localhost", 9940)) - sock.send(b"quit_server") - response = sock.recv(8192) - print(response.decode("utf-8")) # "Server shutting down" + import json, socket, struct + + payload = json.dumps({ + "token": "ROTATE_ME", + "command": {"load_density": [["LD_summary", {}]]} + }).encode("utf-8") + + sock = socket.create_connection(("127.0.0.1", 9940)) + sock.sendall(struct.pack("!I", len(payload)) + payload) + while True: + header = sock.recv(4) + if not header: + break + (length,) = struct.unpack("!I", header) + chunk = sock.recv(length) + if chunk == b"Return_Data_Over_JE\n": + break + print(chunk.decode("utf-8")) sock.close() -The server will close all connections and print ``Server shutdown complete``. - -Architecture ------------- +Shutdown +-------- -The TCP server consists of two components: +* Legacy mode: send the literal string ``quit_server``. +* Framed mode (with token): send + ``{"token": "...", "op": "quit"}``. -* **TCPServer** — Main server class based on ``gevent.socket``. Listens for connections - and spawns greenlets for each client. -* **start_load_density_socket_server()** — Convenience function that patches the process - with ``gevent.monkey.patch_all()`` and starts the server. +The server prints ``Server shutdown complete`` and exits. -.. note:: +Notes +----- - ``gevent.monkey.patch_all()`` is called when starting the socket server. This patches - standard library modules (socket, threading, etc.) to be gevent-compatible. Be aware - of this if integrating the socket server into a larger application. +* ``gevent.monkey.patch_all()`` is invoked on start-up. Plan + integration accordingly. +* The token may be read from the ``LOAD_DENSITY_SOCKET_TOKEN`` + environment variable so CI secrets stay out of process arguments. diff --git a/docs/source/En/doc/socket_user/socket_user_doc.rst b/docs/source/En/doc/socket_user/socket_user_doc.rst new file mode 100644 index 0000000..39999f2 --- /dev/null +++ b/docs/source/En/doc/socket_user/socket_user_doc.rst @@ -0,0 +1,57 @@ +Raw TCP / UDP User +================== + +Overview +-------- + +The raw socket user template sends arbitrary bytes over TCP or UDP and +optionally reads back a bounded response. It uses Python's stdlib +``socket`` module, so no extra dependency is required. + +Task fields +----------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Field + - Meaning + * - ``protocol`` + - ``tcp`` or ``udp``. + * - ``target`` / ``host`` + - ``host:port``. + * - ``payload`` + - Bytes to send. Strings are encoded as UTF-8. Use a + ``hex:DEADBEEF`` prefix to send raw bytes from a hex string. + * - ``expect_bytes`` + - Read at most N bytes from the response (0 to skip read). + * - ``expect_substring`` + - Substring assertion on the decoded response. + * - ``timeout`` + - Connect / read timeout in seconds (default 5). + * - ``name`` + - Event name; defaults to ``protocol:target``. + +Example +------- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "socket_user"}, + user_count=20, + spawn_rate=5, + test_time=60, + tasks=[ + {"protocol": "tcp", "target": "127.0.0.1:9000", + "payload": "PING\\n", "expect_bytes": 64, + "expect_substring": "PONG"}, + {"protocol": "udp", "target": "127.0.0.1:9000", + "payload": "hex:DEADBEEF", "expect_bytes": 4}, + ], + ) + +Each step fires a Locust event tagged ``TCP`` or ``UDP``. diff --git a/docs/source/En/doc/sqlite_persistence/sqlite_persistence_doc.rst b/docs/source/En/doc/sqlite_persistence/sqlite_persistence_doc.rst new file mode 100644 index 0000000..602bb68 --- /dev/null +++ b/docs/source/En/doc/sqlite_persistence/sqlite_persistence_doc.rst @@ -0,0 +1,58 @@ +SQLite Persistence +================== + +Overview +-------- + +The SQLite sink writes the in-memory ``test_record_instance`` to a +SQLite database so runs can be compared, regression-checked, or shipped +to another tool. The schema is created lazily; an empty file is fine. + +Python API +---------- + +.. code-block:: python + + from je_load_density import ( + persist_records, list_runs, fetch_run_records, + ) + + run_id = persist_records( + "loadtests.db", + label="checkout-2026-04-28", + metadata={"branch": "dev", "commit": "abc1234"}, + ) + + for row in list_runs("loadtests.db", limit=10): + print(row) + + for record in fetch_run_records("loadtests.db", run_id): + print(record) + +Schema +------ + +* ``load_density_runs(id, started_at, label, metadata_json)`` +* ``load_density_records(id, run_id, outcome, method, test_url, name, + status_code, response_time_ms, response_length, error)`` + +Indexes are created on ``run_id`` and ``name`` to keep cross-run +queries fast. + +Action JSON +----------- + +.. code-block:: json + + {"load_density": [ + ["LD_clear_records", {}], + ["LD_start_test", {...}], + ["LD_persist_records", { + "database_path": "loadtests.db", + "label": "checkout", + "metadata": {"branch": "dev"} + }] + ]} + +Use ``LD_list_runs`` and ``LD_fetch_run_records`` from later scripts to +read back the data. diff --git a/docs/source/En/doc/start_test/start_test_doc.rst b/docs/source/En/doc/start_test/start_test_doc.rst new file mode 100644 index 0000000..fd7a5b9 --- /dev/null +++ b/docs/source/En/doc/start_test/start_test_doc.rst @@ -0,0 +1,112 @@ +start_test & prepare_env +======================== + +Overview +-------- + +``start_test`` is the high-level entrypoint that picks a user template, +seeds the parameter resolver, and asks ``prepare_env`` to build a Locust +environment in the requested mode (local / master / worker). + +Signature +--------- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, + spawn_rate=10, + test_time=60, + web_ui_dict=None, # {"host": "...", "port": ...} + runner_mode="local", # "local" | "master" | "worker" + master_bind_host="*", + master_bind_port=5557, + master_host="127.0.0.1", + master_port=5557, + expected_workers=0, + tasks=..., + variables={"host": "https://api.example.com"}, + csv_sources=[{"name": "users", "file_path": "users.csv"}], + ) + +Supported user types +-------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - ``user`` + - Template + * - ``http_user`` + - ``locust.HttpUser`` wrapper backed by ``requests``. + * - ``fast_http_user`` + - ``locust.FastHttpUser`` wrapper backed by ``geventhttpclient``. + * - ``websocket_user`` + - WebSocket frame loop (lazy ``websocket-client`` import). + * - ``grpc_user`` + - Unary gRPC calls against operator-supplied stubs. + * - ``mqtt_user`` + - MQTT publish / subscribe loop. + * - ``socket_user`` + - Raw TCP / UDP send-recv. + +prepare_env +----------- + +``prepare_env`` is the lower-level layer behind ``start_test``. It is +useful when you want to build a Locust environment manually, for +example to integrate with another runner. + +.. code-block:: python + + from je_load_density import prepare_env + from je_load_density.wrapper.user_template.fast_http_user_template import ( + FastHttpUserWrapper, set_wrapper_fasthttp_user, + ) + + set_wrapper_fasthttp_user( + {"user": "fast_http_user"}, + tasks=[{"method": "get", "request_url": "https://example.com/"}], + ) + prepare_env( + user_class=FastHttpUserWrapper, + user_count=50, + spawn_rate=10, + test_time=60, + runner_mode="local", + ) + +Distributed mode +---------------- + +Master:: + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="master", + master_bind_host="0.0.0.0", + master_bind_port=5557, + expected_workers=4, + user_count=200, + spawn_rate=20, + test_time=300, + tasks=[...], + ) + +Worker (run on each node, on the same network as master):: + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", + master_port=5557, + tasks=[...], + ) + +The master waits for ``expected_workers`` workers to register before +ramping up. Workers join the master and run the requested user count +proportional to the cluster size. diff --git a/docs/source/En/doc/test_record/test_record_doc.rst b/docs/source/En/doc/test_record/test_record_doc.rst new file mode 100644 index 0000000..64ed245 --- /dev/null +++ b/docs/source/En/doc/test_record/test_record_doc.rst @@ -0,0 +1,44 @@ +Test Record +=========== + +Overview +-------- + +``test_record_instance`` is the in-memory store that the Locust +``request`` hook feeds. Every report generator (HTML / JSON / XML / CSV +/ JUnit / summary) reads from this object, and the SQLite persistence +helpers write it to disk. + +Record fields +------------- + +Each entry is a dict with the following keys: + +* ``Method`` — HTTP method or protocol tag (``GET``, ``POST``, ``WS``, + ``GRPC``, ``MQTT``, ``TCP``, ``UDP``). +* ``test_url`` — Target URL or address. +* ``name`` — Locust event name (``request_url`` if not overridden). +* ``status_code`` — Response status (string) or ``None``. +* ``response_time_ms`` — Locust-reported response time in ms. +* ``response_length`` — Response size in bytes. +* ``error`` — ``None`` for success rows; the exception string for + failures. +* ``text``, ``content``, ``headers`` — Optional, only present on HTTP + successes. + +Clearing between runs +--------------------- + +.. code-block:: python + + from je_load_density import test_record_instance + test_record_instance.clear_records() + +or via the executor:: + + ["LD_clear_records", {}] + +SQLite persistence +------------------ + +See :doc:`../../../En/doc/sqlite_persistence/sqlite_persistence_doc`. diff --git a/docs/source/En/doc/websocket_user/websocket_user_doc.rst b/docs/source/En/doc/websocket_user/websocket_user_doc.rst new file mode 100644 index 0000000..e10803c --- /dev/null +++ b/docs/source/En/doc/websocket_user/websocket_user_doc.rst @@ -0,0 +1,53 @@ +WebSocket User +============== + +Overview +-------- + +The WebSocket user template drives a connect / send / recv loop against +a configured ``ws://`` or ``wss://`` URL. It uses the +``websocket-client`` package, which is loaded lazily — install it with +``pip install je_load_density[websocket]``. + +Task fields +----------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Field + - Meaning + * - ``method`` + - ``connect`` / ``send`` / ``recv`` / ``sendrecv`` / ``close``. + * - ``request_url`` / ``url`` + - WebSocket URL (required for ``connect``; reused otherwise). + * - ``name`` + - Event name; defaults to URL or method. + * - ``payload`` + - String / bytes to send. + * - ``expect`` + - Substring assertion on the received frame. + * - ``timeout`` + - Recv timeout in seconds (default 5). + +Example +------- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "websocket_user"}, + user_count=10, + spawn_rate=5, + test_time=60, + tasks=[ + {"method": "connect", "request_url": "wss://echo.example.com/socket"}, + {"method": "sendrecv", "payload": '{"ping": 1}', "expect": "pong"}, + {"method": "close"}, + ], + ) + +Each step fires a Locust event tagged ``WS`` for stat aggregation. diff --git a/docs/source/En/en_index.rst b/docs/source/En/en_index.rst index 9416629..a65b74f 100644 --- a/docs/source/En/en_index.rst +++ b/docs/source/En/en_index.rst @@ -1,20 +1,172 @@ -English Documentation -============================================= +==================================== +LoadDensity English Documentation +==================================== -Welcome to the LoadDensity English documentation. LoadDensity is a load & stress testing -automation framework built on top of Locust, providing a simplified API, JSON-driven test -scripts, multi-format report generation, an optional GUI, and remote execution capabilities. +The English manual is split into chapters that follow a typical reader +journey: install → run a load test → author actions → scale → integrate. +Use the table of contents on the left, or jump straight to a chapter +below. + +.. contents:: On this page + :local: + :depth: 1 + +---- + +.. _en-getting-started: + +Chapter 1 — Getting Started +=========================== + +Install LoadDensity, run your first load test, and scaffold a project. .. toctree:: - :maxdepth: 4 - :caption: User Guide + :maxdepth: 2 + :caption: Getting Started doc/installation/installation_doc doc/getting_started/getting_started_doc - doc/cli/cli_doc - doc/generate_report/generate_report_doc - doc/scheduler/scheduler_doc - doc/socket_server/socket_server_doc + doc/create_project/create_project_doc + +.. _en-core-api: + +Chapter 2 — Core API +==================== + +The Locust-facing facade: environments, runners, and user proxies. +Read this once and the rest of the framework stops feeling magical. + +.. toctree:: + :maxdepth: 2 + :caption: Core API + + doc/architecture/architecture_doc + doc/start_test/start_test_doc + doc/locust_env/locust_env_doc + +.. _en-actions: + +Chapter 3 — Action Authoring & Execution +======================================== + +Compose JSON-driven action scripts, parameterise data, build scenarios, +and chain post-test callbacks. + +.. toctree:: + :maxdepth: 2 + :caption: Actions + + doc/action_executor/action_executor_doc + doc/parameter_resolver/parameter_resolver_doc + doc/scenarios/scenarios_doc + doc/assertions/assertions_doc doc/callback/callback_doc doc/package_manager/package_manager_doc + +.. _en-user-templates: + +Chapter 4 — User Templates +========================== + +The protocol drivers: HTTP, FastHttp, WebSocket, gRPC, MQTT, and raw +TCP/UDP. Each template registers as a Locust user with the same task +contract. + +.. toctree:: + :maxdepth: 2 + :caption: User Templates + + doc/http_users/http_users_doc + doc/websocket_user/websocket_user_doc + doc/grpc_user/grpc_user_doc + doc/mqtt_user/mqtt_user_doc + doc/socket_user/socket_user_doc + +.. _en-reporting: + +Chapter 5 — Reporting & Observability +===================================== + +Generate HTML / JSON / XML / CSV / JUnit / percentile-summary reports, +ship metrics to Prometheus, InfluxDB, or any OTLP backend. + +.. toctree:: + :maxdepth: 2 + :caption: Reporting + + doc/generate_report/generate_report_doc + doc/metrics/metrics_doc + doc/test_record/test_record_doc + +.. _en-orchestration: + +Chapter 6 — Orchestration & Scale +================================= + +Run distributed master/worker fleets, share state through the parameter +resolver, and gate execution on extracted variables. + +.. toctree:: + :maxdepth: 2 + :caption: Orchestration + + doc/distributed/distributed_doc + +.. _en-recording-data: + +Chapter 7 — Recording & Data +============================ + +Convert real browser traffic (HAR) into runnable action JSON, persist +test records to SQLite, and compare runs over time. + +.. toctree:: + :maxdepth: 2 + :caption: Recording & Data + + doc/har_import/har_import_doc + doc/sqlite_persistence/sqlite_persistence_doc + +.. _en-tooling: + +Chapter 8 — Tooling, CLI & Diagnostics +====================================== + +Command-line subcommands, the hardened control socket server, and the +exception hierarchy you will see in tracebacks. + +.. toctree:: + :maxdepth: 2 + :caption: Tooling + + doc/cli/cli_doc + doc/socket_server/socket_server_doc + doc/exception/exception_doc + +.. _en-integrations: + +Chapter 9 — Integrations +======================== + +The optional GUI, the **Model Context Protocol (MCP)** server that lets +Claude drive LoadDensity, and the downstream PyBreeze IDE integration. + +.. toctree:: + :maxdepth: 2 + :caption: Integrations + doc/gui/gui_doc + doc/mcp_claude/mcp_claude_doc + +.. _en-reference: + +Chapter 10 — API Reference +========================== + +Auto-generated Python API reference. + +.. toctree:: + :maxdepth: 2 + :caption: Reference + + doc/api_reference/api_reference diff --git a/docs/source/Zh/doc/action_executor/action_executor_doc.rst b/docs/source/Zh/doc/action_executor/action_executor_doc.rst new file mode 100644 index 0000000..894279f --- /dev/null +++ b/docs/source/Zh/doc/action_executor/action_executor_doc.rst @@ -0,0 +1,71 @@ +動作 Executor +============= + +概觀 +---- + +動作 executor 將指令字串對應到 callable。動作腳本是 JSON 列表,所以同一個腳本可以手寫、由 HAR 匯入產生、由 MCP 工具排程,或經由控制 socket 傳送。 + +所有內建指令以 ``LD_`` 為字首;安全的 Python builtin(``print``、``len``、``range`` 等)也可使用,但 ``eval``、``exec``、``compile``、``__import__``、``breakpoint``、``open``、``input`` 已被明確封鎖。 + +動作格式 +-------- + +.. code-block:: python + + ["command_name"] # 無參數 + ["command_name", {"key": "value"}] # 關鍵字參數 + ["command_name", [arg1, arg2]] # 位置參數 + +最上層文件可為: + +.. code-block:: json + + {"load_density": [["LD_start_test", {...}], ...]} + +或裸列表。 + +範例 +---- + +.. code-block:: python + + from je_load_density import execute_action + + execute_action({"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://api.example.com"}}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, + "spawn_rate": 10, + "test_time": 30, + "tasks": [{"method": "get", "request_url": "${var.base}/health"}], + }], + ["LD_generate_summary_report", {"report_name": "smoke"}], + ]}) + +LD_* 指令 +--------- + +下列指令於 executor 註冊。每個對應到 ``je_load_density`` 之下對應模組的實作。詳見 *Reference*。 + +* **核心**:``LD_start_test``、``LD_execute_action``、``LD_execute_files``、``LD_add_package_to_executor``、``LD_start_socket_server``。 +* **報告**:``LD_generate_html(_report)``、``LD_generate_json(_report)``、``LD_generate_xml(_report)``、``LD_generate_csv_report``、``LD_generate_junit_report``、``LD_generate_summary_report``、``LD_summary``。 +* **持久化**:``LD_persist_records``、``LD_list_runs``、``LD_fetch_run_records``、``LD_clear_records``。 +* **參數解析器**:``LD_register_variable(s)``、``LD_register_csv_source(s)``、``LD_clear_resolver``。 +* **錄製/重放**:``LD_load_har``、``LD_har_to_tasks``、``LD_har_to_action_json``。 +* **指標 exporter**:``LD_start/stop_prometheus_exporter``、``LD_start/stop_influxdb_sink``、``LD_start/stop_opentelemetry_exporter``。 + +新增自訂指令 +------------ + +.. code-block:: python + + from je_load_density import add_command_to_executor + + def slack_notify(message: str) -> None: + ... + + add_command_to_executor({"LD_slack_notify": slack_notify}) + +註冊後,新指令即可在任何動作 JSON 中呼叫。 diff --git a/docs/source/Zh/doc/api_reference/api_reference.rst b/docs/source/Zh/doc/api_reference/api_reference.rst new file mode 100644 index 0000000..a215bc4 --- /dev/null +++ b/docs/source/Zh/doc/api_reference/api_reference.rst @@ -0,0 +1,36 @@ +API Reference +============= + +由 Sphinx ``autosummary`` 在每次 build 時自動產生。 + +.. autosummary:: + :toctree: _autosummary + :recursive: + + je_load_density + je_load_density.utils.executor.action_executor + je_load_density.utils.parameterization.parameter_resolver + je_load_density.utils.recording.har_importer + je_load_density.utils.metrics.prometheus_exporter + je_load_density.utils.metrics.influxdb_sink + je_load_density.utils.metrics.opentelemetry_exporter + je_load_density.utils.test_record.test_record_class + je_load_density.utils.test_record.sqlite_persistence + je_load_density.utils.generate_report.generate_html_report + je_load_density.utils.generate_report.generate_json_report + je_load_density.utils.generate_report.generate_xml_report + je_load_density.utils.generate_report.generate_csv_report + je_load_density.utils.generate_report.generate_junit_report + je_load_density.utils.generate_report.generate_summary_report + je_load_density.utils.socket_server.load_density_socket_server + je_load_density.wrapper.create_locust_env.create_locust_env + je_load_density.wrapper.start_wrapper.start_test + je_load_density.wrapper.user_template.request_executor + je_load_density.wrapper.user_template.scenario_runner + je_load_density.wrapper.user_template.http_user_template + je_load_density.wrapper.user_template.fast_http_user_template + je_load_density.wrapper.user_template.websocket_user_template + je_load_density.wrapper.user_template.grpc_user_template + je_load_density.wrapper.user_template.mqtt_user_template + je_load_density.wrapper.user_template.socket_user_template + je_load_density.mcp_server.server diff --git a/docs/source/Zh/doc/architecture/architecture_doc.rst b/docs/source/Zh/doc/architecture/architecture_doc.rst new file mode 100644 index 0000000..8620bfd --- /dev/null +++ b/docs/source/Zh/doc/architecture/architecture_doc.rst @@ -0,0 +1,77 @@ +架構 +==== + +概觀 +---- + +LoadDensity 是 Locust 之上的薄層封裝,加入 JSON 動作執行器、多協定 user 模板註冊、情境流程、資料參數化、可觀測性 sink,以及 MCP 控制介面。 + +依賴方向永遠是動作層 → Locust,反之不行:你的動作 JSON 描述要做什麼,executor 把每個指令對應到 Python callable,Locust 負責跑壓測。 + +分層 +---- + +.. mermaid:: + + flowchart TB + cli[CLI / MCP / GUI / Socket Server] --> exec[動作 Executor] + exec --> start[start_test] + start --> proxy[locust_wrapper_proxy] + proxy --> userhttp[HTTP / FastHttp] + proxy --> userws[WebSocket] + proxy --> usergrpc[gRPC] + proxy --> usermqtt[MQTT] + proxy --> usersock[Raw TCP/UDP] + userhttp & userws & usergrpc & usermqtt & usersock --> hooks[Locust 事件] + hooks --> records[test_record_instance] + hooks --> exporters[Prometheus / Influx / OTel] + records --> reports[HTML / JSON / XML / CSV / JUnit / Summary] + records --> sqlite[SQLite 持久化] + +模組對照表 +---------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - 模組 + - 用途 + * - ``je_load_density.utils.executor`` + - ``Executor`` 類別、dispatch table、``execute_action`` / + ``execute_files`` 進入點。 + * - ``je_load_density.utils.parameterization`` + - ``ParameterResolver``,處理 ``${var.x}`` / ``${env.X}`` / + ``${csv.s.col}`` / ``${faker.method}`` 與內建 helpers。 + * - ``je_load_density.utils.recording`` + - HAR 匯入 → 動作 JSON。 + * - ``je_load_density.utils.metrics`` + - Prometheus exporter、InfluxDB sink、OpenTelemetry exporter。 + * - ``je_load_density.utils.test_record`` + - 記憶體紀錄清單與選用 SQLite sink。 + * - ``je_load_density.utils.generate_report`` + - HTML / JSON / XML / CSV / JUnit / summary 產生器。 + * - ``je_load_density.utils.socket_server`` + - 含 framing、選用 TLS 與 token 的 TCP 控制平面。 + * - ``je_load_density.wrapper.proxy`` + - 各協定的 proxy,保存對應 user 模板的 task 設定。 + * - ``je_load_density.wrapper.user_template`` + - HTTP、FastHttp、WebSocket、gRPC、MQTT、raw socket 的 Locust user 類別。 + * - ``je_load_density.wrapper.start_wrapper`` + - ``start_test`` 分派器,挑選 user 模板並轉發 ``prepare_env``。 + * - ``je_load_density.wrapper.create_locust_env`` + - ``prepare_env`` / ``create_env`` 建立 local / master / worker 模式的 Locust 環境。 + * - ``je_load_density.mcp_server`` + - 提供 11 個工具的 MCP server,可讓 Claude 驅動 LoadDensity。 + * - ``je_load_density.gui`` + - 選用的 PySide6 widget(表單 + 即時統計面板)。 + +動作生命週期 +------------ + +#. 呼叫端透過 CLI、MCP 工具、socket server 或直接呼叫 ``execute_action(...)`` 提交動作 JSON。 +#. ``Executor.execute_action`` 對 ``event_dict`` 派發每個步驟(``LD_*`` 指令與安全的 builtin)。 +#. 當步驟為 ``LD_start_test`` 時,分派器會挑選 user 模板(``http_user``、``fast_http_user``、``websocket_user``、``grpc_user``、``mqtt_user``、``socket_user``),由 ``variables`` / ``csv_sources`` 種入參數解析器後呼叫 ``prepare_env``。 +#. ``prepare_env`` 以指定模式(local / master / worker)建立 Locust ``Environment`` 並啟動。 +#. 每個 user 每個 tick 跑 ``run_scenario``(或對應的協定執行函式),觸發 Locust 事件並寫入 ``test_record_instance``。 +#. 報告、metrics exporter、SQLite 持久化會吸收累積的紀錄。 diff --git a/docs/source/Zh/doc/assertions/assertions_doc.rst b/docs/source/Zh/doc/assertions/assertions_doc.rst new file mode 100644 index 0000000..142a082 --- /dev/null +++ b/docs/source/Zh/doc/assertions/assertions_doc.rst @@ -0,0 +1,60 @@ +斷言與擷取 +========== + +概觀 +---- + +HTTP / FastHttp task 可附 ``assertions`` 與 ``extract``,會在 Locust 的 ``catch_response`` 之下執行。失敗的斷言會被 Locust 標為 failure,並出現在所有報告中。 + +斷言 +---- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - ``type`` + - 行為 + * - ``status_code`` + - ``int(response.status_code) == int(value)``。 + * - ``contains`` + - ``str(value) in response.text``。 + * - ``not_contains`` + - ``str(value) not in response.text``。 + * - ``json_path`` + - 沿著 ``path``(點分隔,支援 list 索引)解析 ``response.json()`` 後與 ``value`` 比對。 + * - ``header`` + - ``response.headers[name] == value``。 + +範例 +~~~~ + +.. code-block:: json + + { + "method": "get", + "request_url": "${var.base}/health", + "assertions": [ + {"type": "status_code", "value": 200}, + {"type": "json_path", "path": "status", "value": "ok"}, + {"type": "header", "name": "X-Service", "value": "checkout"} + ] + } + +擷取 +---- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - ``from`` + - 來源 + * - ``json_path`` + - 與 ``json_path`` 斷言相同的點分語法。 + * - ``header`` + - ``response.headers[name]``。 + * - ``status_code`` + - ``response.status_code``。 + +擷取值會以指定 ``var`` 名寫入參數解析器;後續 task 以 ``${var.NAME}`` 引用。 diff --git a/docs/source/Zh/doc/cli/cli_doc.rst b/docs/source/Zh/doc/cli/cli_doc.rst index e982be5..f5f6ab7 100644 --- a/docs/source/Zh/doc/cli/cli_doc.rst +++ b/docs/source/Zh/doc/cli/cli_doc.rst @@ -1,106 +1,81 @@ -命令列介面(CLI) -================== +CLI 命令列介面 +============== -LoadDensity 提供完整的命令列介面,透過 ``python -m je_load_density`` 使用。 +LoadDensity 採子指令式 CLI。執行 ``python -m je_load_density --help`` 可查看完整介面。 -CLI 參數 --------- +子指令 +------ .. list-table:: :header-rows: 1 - :widths: 25 10 65 - - * - 參數 - - 簡寫 - - 說明 - * - ``--execute_file`` - - ``-e`` - - 執行單一 JSON 腳本檔案 - * - ``--execute_dir`` - - ``-d`` - - 執行目錄下所有 JSON 檔案 - * - ``--execute_str`` - - — - - 執行行內 JSON 字串 - * - ``--create_project`` - - ``-c`` - - 建置新專案(包含模板) - -執行單一 JSON 檔案 --------------------- - -執行定義在單一 JSON 關鍵字檔案中的測試: + :widths: 25 75 + + * - 子指令 + - 用途 + * - ``run FILE`` + - 執行單一動作 JSON 檔。 + * - ``run-dir DIR`` + - 執行目錄下所有 ``.json``。 + * - ``run-str JSON`` + - 直接執行 inline JSON 字串(Windows 雙重編碼自動處理)。 + * - ``init PATH`` + - 建立新的專案骨架。 + * - ``serve`` + - 啟動硬化的 TCP 控制 socket server。 + +``run`` +------- .. code-block:: bash - python -m je_load_density -e test_scenario.json - -JSON 檔案應遵循動作列表格式: - -.. code-block:: json + python -m je_load_density run smoke.json - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] - ] +``smoke.json`` 內容:: -執行目錄下所有 JSON 檔案 --------------------------- + {"load_density": [ + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [{"method": "get", "request_url": "https://httpbin.org/get"}] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] + ]} -遞迴執行指定目錄下所有 JSON 關鍵字檔案: +``run-dir`` +----------- -.. code-block:: bash - - python -m je_load_density -d ./test_scripts/ +對目錄樹下所有 ``.json`` 動作檔執行:: -此命令會掃描目錄中所有 ``.json`` 檔案,依序執行。 + python -m je_load_density run-dir ./scenarios -執行行內 JSON 字串 --------------------- +``run-str`` +----------- -直接以字串形式執行 JSON 動作列表: +Inline JSON(CI script 友善):: -.. code-block:: bash + python -m je_load_density run-str '{"load_density":[["LD_summary",{}]]}' - python -m je_load_density --execute_str '[["LD_start_test", {"user_detail_dict": {"user": "fast_http_user"}, "user_count": 10, "spawn_rate": 5, "test_time": 5, "tasks": {"get": {"request_url": "http://httpbin.org/get"}}}]]' - -.. note:: - - 在 **Windows** 平台上,行內 JSON 字串會因為 shell 跳脫字元差異而自動進行雙重解析。 - CLI 會自動處理此差異。 - -建立專案 +``init`` -------- -建置包含關鍵字模板與執行器腳本的新專案: +於 PATH 建立專案骨架:: -.. code-block:: bash + python -m je_load_density init ./my_load_test - python -m je_load_density -c MyProject +``serve`` +--------- -產生的專案目錄結構: +啟動控制 socket server。詳見 :doc:`../socket_server/socket_server_doc`。 -.. code-block:: text +.. code-block:: bash - MyProject/ - └── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json - │ └── keyword2.json - └── executor/ - ├── executor_one_file.py - └── executor_folder.py + python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 \ + --framed --token "$LOAD_DENSITY_SOCKET_TOKEN" \ + --tls-cert /etc/loaddensity/server.crt \ + --tls-key /etc/loaddensity/server.key -錯誤處理 +舊式旗標 -------- -若未提供有效參數,CLI 會拋出 ``LoadDensityTestExecuteException`` 並以結束碼 1 退出。 -所有錯誤訊息會輸出至 stderr。 +之前版本的扁平旗標 ``-e/-d/-c/--execute_str`` 仍接受(在 ``--help`` 中隱藏),維持與 PyBreeze 等下游工具相容。新腳本應使用子指令。 diff --git a/docs/source/Zh/doc/create_project/create_project_doc.rst b/docs/source/Zh/doc/create_project/create_project_doc.rst new file mode 100644 index 0000000..3841a39 --- /dev/null +++ b/docs/source/Zh/doc/create_project/create_project_doc.rst @@ -0,0 +1,39 @@ +建立專案 +======== + +概觀 +---- + +``create_project_dir``(CLI: ``je_load_density init``)在指定路徑建立 LoadDensity 專案骨架。骨架包含一份範例動作 JSON、執行腳本,以及資源放置目錄。 + +Python API +---------- + +.. code-block:: python + + from je_load_density import create_project_dir + create_project_dir("./my_load_test") + +CLI +--- + +.. code-block:: bash + + python -m je_load_density init ./my_load_test + +結構 +---- + +:: + + my_load_test/ + ├── run.py # 讀取動作 JSON 的小執行腳本 + └── action.json # 範例動作 JSON + +建立後,編輯 ``action.json``(見 :doc:`../action_executor/action_executor_doc`)並執行:: + + python run.py + +或:: + + python -m je_load_density run action.json diff --git a/docs/source/Zh/doc/distributed/distributed_doc.rst b/docs/source/Zh/doc/distributed/distributed_doc.rst new file mode 100644 index 0000000..9795b46 --- /dev/null +++ b/docs/source/Zh/doc/distributed/distributed_doc.rst @@ -0,0 +1,58 @@ +分散式 Master / Worker +====================== + +概觀 +---- + +LoadDensity 透過 ``start_test`` / ``prepare_env`` 的 ``runner_mode`` 參數開放 Locust 的分散式 runner。三種模式: + +* ``local`` — 單一程序(預設)。 +* ``master`` — 協調 worker 群,可選擇啟動 Locust Web UI。 +* ``worker`` — 加入 master 並執行指定的 user count。 + +Master +------ + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="master", + master_bind_host="0.0.0.0", + master_bind_port=5557, + expected_workers=4, # 等待 4 個 worker + web_ui_dict={"host": "0.0.0.0", "port": 8089}, + user_count=400, + spawn_rate=40, + test_time=600, + tasks=[...], + ) + +Master 在開始 ramp 前最多等待 60 秒,等待 ``expected_workers`` 個 worker 加入。若僅 N(N < expected)人加入,會記錄警告並照常啟動。 + +Worker +------ + +於每個壓測節點執行: + +.. code-block:: python + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", + master_port=5557, + tasks=[...], + ) + +Worker 不啟動 Web UI 並跳過本地 stats greenlet — 由 master 集中收集與發佈整體統計。 + +提示 +---- + +* 在防火牆開啟 master 的 ``master_bind_port``。Locust 預設埠 ``5557``。 +* 僅在 master 對 worker 可達時用 ``master_bind_host="0.0.0.0"``;否則綁定私網 IP。 +* Master 與 worker 的 user 模板(``http_user`` / ``fast_http_user`` / ...)需一致 — master 廣播 user class 名稱。 +* 若用 ``${csv.X.col}`` 參數化 task,每個 worker 都需註冊相同 CSV 檔(不共享狀態)。 diff --git a/docs/source/Zh/doc/exception/exception_doc.rst b/docs/source/Zh/doc/exception/exception_doc.rst new file mode 100644 index 0000000..d59f83d --- /dev/null +++ b/docs/source/Zh/doc/exception/exception_doc.rst @@ -0,0 +1,32 @@ +例外 +==== + +階層 +---- + +:: + + Exception + └── LocustNotFoundException + └── LoadDensityTestException + ├── LoadDensityTestJsonException + ├── LoadDensityGenerateJsonReportException + ├── LoadDensityTestExecuteException + ├── LoadDensityAssertException + ├── LoadDensityHTMLException + ├── LoadDensityAddCommandException + ├── XMLException + │ └── XMLTypeException + └── CallbackExecutorException + +何時該攔截何者 +-------------- + +* ``LoadDensityTestExecuteException`` — 動作 JSON 結構錯誤,或引用不存在的指令。攔截以呈現使用者輸入錯誤,不致掩蓋內部錯誤。 +* ``LoadDensityHTMLException`` / ``LoadDensityGenerateJsonReportException`` — 在沒有紀錄時呼叫報告產生器(記憶體 store 為空)。 +* ``LoadDensityAssertException`` — 預留給斷言層;目前 HTTP 斷言會經由 Locust 將 request 標為 fail 而非拋出。 +* ``XMLException`` / ``XMLTypeException`` — XML 格式錯誤或未預期 payload 結構。 +* ``CallbackExecutorException`` — callback executor 收到錯誤的 trigger 或 function。 +* ``LoadDensityAddCommandException`` — ``add_command_to_executor`` 收到非 callable。 + +所有自訂例外皆繼承自 ``LoadDensityTestException``,攔截該類別即可達成全面錯誤處理。 diff --git a/docs/source/Zh/doc/generate_report/generate_report_doc.rst b/docs/source/Zh/doc/generate_report/generate_report_doc.rst index d663dc4..4f9f536 100644 --- a/docs/source/Zh/doc/generate_report/generate_report_doc.rst +++ b/docs/source/Zh/doc/generate_report/generate_report_doc.rst @@ -1,160 +1,82 @@ -報告產生 +產生報告 ======== -LoadDensity 可以產生三種格式的測試報告:**HTML**、**JSON** 和 **XML**。 -報告是根據測試執行期間由 request hook 收集的測試紀錄產生的。 +概觀 +---- -.. note:: +LoadDensity 可從 ``test_record_instance`` 產生六種報告:HTML、JSON、XML、CSV、JUnit XML、百分位摘要 JSON。 - 報告只能在測試執行後產生。若不存在測試紀錄,會拋出 - ``LoadDensityHTMLException`` 或 ``LoadDensityGenerateJsonReportException``。 +.. note:: -HTML 報告 ---------- + 報告需要至少一筆紀錄;對空 store 呼叫產生器會拋出 ``LoadDensityHTMLException`` / ``LoadDensityGenerateJsonReportException``。 -產生帶有樣式的 HTML 檔案,以表格顯示成功與失敗紀錄。 +HTML +---- .. code-block:: python from je_load_density import generate_html_report + generate_html_report("my_report") # 寫出 my_report.html - # 產生 "my_report.html" - generate_html_report("my_report") - -HTML 報告包含: - -* **成功紀錄** — 以青色表頭的表格顯示,包含 Method、URL、name、status_code、 - 回應內文、content 和 headers -* **失敗紀錄** — 以紅色表頭的表格顯示,包含 Method、URL、name、status_code 和錯誤訊息 - -若要取得原始 HTML 片段而不寫入檔案: - -.. code-block:: python - - from je_load_density import generate_html - - success_fragments, failure_fragments = generate_html() - # success_fragments: List[str] — 每筆成功紀錄的 HTML 表格字串 - # failure_fragments: List[str] — 每筆失敗紀錄的 HTML 表格字串 - -JSON 報告 ---------- - -產生結構化的 JSON 檔案,供程式化使用。 +JSON(依結果分檔) +------------------ .. code-block:: python from je_load_density import generate_json_report - - # 產生 "my_report_success.json" 和 "my_report_failure.json" success_path, failure_path = generate_json_report("my_report") -**成功 JSON 格式:** +XML(依結果分檔) +----------------- -.. code-block:: json - - { - "Success_Test1": { - "Method": "GET", - "test_url": "http://httpbin.org/get", - "name": "/get", - "status_code": "200", - "text": "...", - "content": "...", - "headers": "..." - }, - "Success_Test2": {} - } - -**失敗 JSON 格式:** - -.. code-block:: json +.. code-block:: python - { - "Failure_Test1": { - "Method": "POST", - "test_url": "http://httpbin.org/status/500", - "name": "/status/500", - "status_code": "500", - "error": "..." - } - } + from je_load_density import generate_xml_report + success_path, failure_path = generate_xml_report("my_report") -若要取得原始 JSON 資料結構而不寫入檔案: +CSV(每筆請求一列) +------------------- .. code-block:: python - from je_load_density import generate_json - - success_dict, failure_dict = generate_json() + from je_load_density import generate_csv_report + generate_csv_report("my_report") # 寫出 my_report.csv -XML 報告 --------- +欄位:``outcome, Method, test_url, name, status_code, response_time_ms, response_length, error``。 -產生 XML 檔案,適用於 CI/CD 整合。 +JUnit XML(CI 友善) +-------------------- .. code-block:: python - from je_load_density import generate_xml_report - - # 產生 "my_report_success.xml" 和 "my_report_failure.xml" - success_path, failure_path = generate_xml_report("my_report") + from je_load_density import generate_junit_report + generate_junit_report("loaddensity-junit") # 寫出 loaddensity-junit.xml -XML 輸出使用 ``xml.dom.minidom`` 進行格式化。每筆測試紀錄包裝在 ```` 根節點下。 +每筆請求變成 ````;失敗附上 ```` 節點。可餵 Jenkins、GitHub Actions、GitLab 等。 -若要取得原始 XML 字串而不寫入檔案: +Summary(百分位) +----------------- .. code-block:: python - from je_load_density import generate_xml + from je_load_density import generate_summary_report, build_summary - success_xml_str, failure_xml_str = generate_xml() + summary = build_summary() # 記憶體 dict + generate_summary_report("loaddensity-summary") -在 JSON 腳本中使用 --------------------- +包含 totals、per-name 計數、min / max / mean / 百分位(p50 / p90 / p95 / p99)延遲與整體區塊。便於繪圖與跨次回歸檢查。 + +動作 JSON +--------- -報告產生可以與測試執行在 JSON 腳本中串連: - -.. code-block:: json - - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }], - ["LD_generate_html_report", {"html_name": "report"}], - ["LD_generate_json_report", {"json_file_name": "report"}], - ["LD_generate_xml_report", {"xml_file_name": "report"}] - ] - -報告函式總覽 ------------- - -.. list-table:: - :header-rows: 1 - :widths: 35 25 40 - - * - 函式 - - 回傳值 - - 說明 - * - ``generate_html()`` - - ``Tuple[List[str], List[str]]`` - - 成功與失敗紀錄的 HTML 片段 - * - ``generate_html_report(html_name)`` - - ``str`` - - 寫入 HTML 報告檔案,回傳檔案路徑 - * - ``generate_json()`` - - ``Tuple[Dict, Dict]`` - - 成功與失敗紀錄的 JSON 字典 - * - ``generate_json_report(json_file_name)`` - - ``Tuple[str, str]`` - - 寫入 JSON 報告檔案,回傳路徑 - * - ``generate_xml()`` - - ``Tuple[str, str]`` - - 成功與失敗紀錄的 XML 字串 - * - ``generate_xml_report(xml_file_name)`` - - ``Tuple[str, str]`` - - 寫入 XML 報告檔案,回傳路徑 +把報告串入測試:: + + {"load_density": [ + ["LD_start_test", {...}], + ["LD_generate_html_report", {"html_name": "report"}], + ["LD_generate_json_report", {"json_file_name": "report"}], + ["LD_generate_xml_report", {"xml_file_name": "report"}], + ["LD_generate_csv_report", {"csv_name": "report"}], + ["LD_generate_junit_report", {"report_name": "report-junit"}], + ["LD_generate_summary_report",{"report_name": "report-summary"}] + ]} diff --git a/docs/source/Zh/doc/getting_started/getting_started_doc.rst b/docs/source/Zh/doc/getting_started/getting_started_doc.rst index c4066dd..f40dd92 100644 --- a/docs/source/Zh/doc/getting_started/getting_started_doc.rst +++ b/docs/source/Zh/doc/getting_started/getting_started_doc.rst @@ -1,238 +1,83 @@ -開始使用 -======== +入門 +==== -本指南將帶您了解如何使用 LoadDensity 執行第一個負載測試。 +本指南帶你跑出第一支 LoadDensity 壓測。 -使用者類型 ----------- +User 類型 +--------- -LoadDensity 支援兩種 Locust 使用者類型: +LoadDensity 提供六種 user 類型: -.. list-table:: - :header-rows: 1 - :widths: 25 25 50 +* ``fast_http_user`` — 高吞吐 HTTP(``locust.FastHttpUser`` + geventhttpclient)。 +* ``http_user`` — ``locust.HttpUser`` + ``requests``。 +* ``websocket_user``、``grpc_user``、``mqtt_user``、``socket_user`` — 詳見第 4 章。 - * - 使用者類型鍵值 - - Locust 類別 - - 說明 - * - ``fast_http_user`` - - ``FastHttpUser`` - - 使用 ``geventhttpclient``,效能較高。建議大多數情況使用。 - * - ``http_user`` - - ``HttpUser`` - - 使用 Python ``requests`` 函式庫。相容性較佳,效能較低。 - -支援的 HTTP 方法 ------------------ - -LoadDensity 支援以下 HTTP 方法: - -* ``get`` -* ``post`` -* ``put`` -* ``patch`` -* ``delete`` -* ``head`` -* ``options`` - -使用 Python API 執行測試 -------------------------- - -最簡單的方式是呼叫 ``start_test()``: +以 Python API 執行 +------------------ .. code-block:: python from je_load_density import start_test - result = start_test( + start_test( user_detail_dict={"user": "fast_http_user"}, user_count=50, spawn_rate=10, - test_time=10, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"}, - } + test_time=30, + variables={"base": "https://httpbin.org"}, + tasks=[ + {"method": "get", "request_url": "${var.base}/get"}, + {"method": "post", "request_url": "${var.base}/post", + "json": {"hello": "world"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], ) -``start_test()`` 參數說明 -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 20 15 10 55 - - * - 參數 - - 類型 - - 預設值 - - 說明 - * - ``user_detail_dict`` - - ``dict`` - - (必填) - - 使用者類型設定。``{"user": "fast_http_user"}`` 或 ``{"user": "http_user"}`` - * - ``user_count`` - - ``int`` - - ``50`` - - 模擬使用者總數 - * - ``spawn_rate`` - - ``int`` - - ``10`` - - 每秒生成使用者數量 - * - ``test_time`` - - ``int`` 或 ``None`` - - ``60`` - - 測試持續時間(秒)。傳入 ``None`` 則無限制 - * - ``web_ui_dict`` - - ``dict`` 或 ``None`` - - ``None`` - - 啟用 Locust Web UI。例如 ``{"host": "127.0.0.1", "port": 8089}`` - -回傳值 -~~~~~~ - -``start_test()`` 回傳一個測試設定摘要字典: - -.. code-block:: python - - { - "user_detail": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 10, - "web_ui": None, - } - -啟用 Locust Web UI -------------------- - -若要透過 Locust Web UI 即時監控測試: +啟動 Locust Web UI +------------------ .. code-block:: python - from je_load_density import start_test - - result = start_test( - user_detail_dict={"user": "http_user"}, - user_count=100, - spawn_rate=20, - test_time=30, + start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, spawn_rate=10, test_time=30, web_ui_dict={"host": "127.0.0.1", "port": 8089}, - tasks={ - "get": {"request_url": "http://httpbin.org/get"}, - } + tasks=[{"method": "get", "request_url": "https://httpbin.org/get"}], ) -然後在瀏覽器開啟 ``http://127.0.0.1:8089`` 即可查看即時統計資料。 - -使用 JSON 腳本檔案執行測試 ----------------------------- - -可以將測試情境定義為 JSON 檔案,無需撰寫 Python 程式碼即可執行。 - -建立 ``test_scenario.json`` 檔案: - -.. code-block:: json - - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 50, - "spawn_rate": 10, - "test_time": 5, - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - }] - ] - -從 Python 執行: +之後在瀏覽器開啟 ``http://127.0.0.1:8089``。 -.. code-block:: python - - from je_load_density import execute_action, read_action_json - - execute_action(read_action_json("test_scenario.json")) - -JSON 腳本格式 -~~~~~~~~~~~~~~ +以 JSON 動作腳本執行 +-------------------- -每個 JSON 腳本是一個動作陣列。每個動作是一個列表: - -* 使用關鍵字參數:``["action_name", {"param1": "value1"}]`` -* 使用位置參數:``["action_name", ["arg1", "arg2"]]`` -* 無參數:``["action_name"]`` - -串連多個動作 -~~~~~~~~~~~~ - -多個動作可以在單一 JSON 檔案中串連。例如,執行測試並自動產生報告: +建立 ``test_scenario.json``: .. code-block:: json - [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }], - ["LD_generate_html_report", {"html_name": "my_report"}], - ["LD_generate_json_report", {"json_file_name": "my_report"}], - ["LD_generate_xml_report", {"xml_file_name": "my_report"}] - ] + {"load_density": [ + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "user_count": 20, "spawn_rate": 10, "test_time": 30, + "tasks": [{"method": "get", "request_url": "https://httpbin.org/get"}] + }], + ["LD_generate_summary_report", {"report_name": "smoke"}] + ]} -字典格式 JSON -~~~~~~~~~~~~~~ +執行:: -JSON 腳本也可以用字典包裝,使用 ``"load_density"`` 鍵值: - -.. code-block:: json + python -m je_load_density run test_scenario.json - { - "load_density": [ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] - ] - } - -專案建置 --------- - -LoadDensity 可以自動產生專案目錄結構,包含關鍵字模板與執行器腳本: +或於 Python: .. code-block:: python - from je_load_density import create_project_dir - - create_project_dir(project_path="./my_tests", parent_name="LoadDensity") - -或透過 CLI: - -.. code-block:: bash - - python -m je_load_density -c ./my_tests - -產生的結構如下: - -.. code-block:: text + from je_load_density import execute_action, read_action_json + execute_action(read_action_json("test_scenario.json")) - my_tests/ - └── LoadDensity/ - ├── keyword/ - │ ├── keyword1.json # FastHttpUser 測試模板 - │ └── keyword2.json # HttpUser 測試模板 - └── executor/ - ├── executor_one_file.py # 執行單一關鍵字檔案 - └── executor_folder.py # 執行 keyword/ 下所有檔案 +下一步 +------ -* ``keyword1.json`` — 使用 ``fast_http_user`` 的模板,包含範例 GET/POST 任務 -* ``keyword2.json`` — 使用 ``http_user`` 的模板,包含範例 GET/POST 任務 -* ``executor_one_file.py`` — 執行 ``keyword1.json`` 的 Python 腳本 -* ``executor_folder.py`` — 執行 ``keyword/`` 目錄下所有 JSON 檔案的 Python 腳本 +* 為動作腳本參數化:見 :doc:`../parameter_resolver/parameter_resolver_doc`。 +* 改用情境流程:見 :doc:`../scenarios/scenarios_doc`。 +* 升級到分散式 master/worker:見 :doc:`../distributed/distributed_doc`。 +* 啟動 Prometheus / InfluxDB / OTel 指標 sink:見 :doc:`../metrics/metrics_doc`。 diff --git a/docs/source/Zh/doc/grpc_user/grpc_user_doc.rst b/docs/source/Zh/doc/grpc_user/grpc_user_doc.rst new file mode 100644 index 0000000..6a1a45f --- /dev/null +++ b/docs/source/Zh/doc/grpc_user/grpc_user_doc.rst @@ -0,0 +1,61 @@ +gRPC 使用者 +=========== + +概觀 +---- + +gRPC user 模板對 operator 提供的 stub 進行 unary 呼叫。底層使用 ``grpcio`` 與你自己的 ``*_pb2`` / ``*_pb2_grpc``,皆 lazy import — 以 ``pip install je_load_density[grpc]`` 安裝。 + +Task 欄位 +--------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - 欄位 + - 意義 + * - ``target`` / ``host`` + - gRPC 端點,如 ``localhost:50051``。 + * - ``stub_path`` + - Stub 類別的 dotted path(``pkg.greeter_pb2_grpc.GreeterStub``)。 + * - ``request_path`` + - 請求訊息的 dotted path(``pkg.greeter_pb2.HelloRequest``)。 + * - ``method`` + - Stub 上的 method 名。 + * - ``payload`` + - 用以建構 request message 的 dict。 + * - ``metadata`` + - ``[key, value]`` pair 列表或扁平 dict。 + * - ``timeout`` + - 單呼叫 timeout(秒),預設 10。 + +dotted path 在 ``importlib.import_module`` 之前會通過嚴格識別符 regex 驗證;traversal 攻擊(``../``、``;``、``__import__``)皆被拒絕。 + +範例 +---- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "grpc_user"}, + user_count=20, + spawn_rate=5, + test_time=60, + tasks=[ + { + "name": "say_hello", + "target": "localhost:50051", + "stub_path": "pkg.greeter_pb2_grpc.GreeterStub", + "request_path": "pkg.greeter_pb2.HelloRequest", + "method": "SayHello", + "payload": {"name": "world"}, + "metadata": [["x-token", "abc"]], + "timeout": 5, + } + ], + ) + +每次呼叫會觸發標記為 ``GRPC`` 的 Locust 事件。 diff --git a/docs/source/Zh/doc/gui/gui_doc.rst b/docs/source/Zh/doc/gui/gui_doc.rst index 04e4c37..32033ce 100644 --- a/docs/source/Zh/doc/gui/gui_doc.rst +++ b/docs/source/Zh/doc/gui/gui_doc.rst @@ -1,87 +1,80 @@ -GUI(圖形化使用者介面) -====================== +GUI 圖形介面 +============ -LoadDensity 包含一個可選的 PySide6 圖形化介面,可透過視覺化表單執行負載測試, -並即時顯示日誌。 +概觀 +---- -安裝需求 --------- +LoadDensity 內含選用的 PySide6 圖形前端。提供啟動快速 HTTP 測試的表單控制元件、鏡像框架日誌的 log panel,以及每秒輪詢 ``test_record_instance`` 的即時統計面板。 -GUI 需要額外的相依套件。請使用以下方式安裝: +安裝 +---- .. code-block:: bash - pip install je_load_density[gui] + pip install "je_load_density[gui]" -這會安裝: +引入: -* **PySide6** (6.10.0) — Qt for Python 綁定 -* **qt-material** — Material Design 主題 +* ``PySide6`` — Qt for Python bindings。 +* ``qt-material`` — Material design 主題。 -啟動 GUI --------- +啟動 +---- .. code-block:: python - from je_load_density.gui.main_window import LoadDensityUI - from PySide6.QtWidgets import QApplication import sys + from PySide6.QtWidgets import QApplication + from je_load_density.gui.main_window import LoadDensityUI app = QApplication(sys.argv) window = LoadDensityUI() window.show() sys.exit(app.exec()) -GUI 功能 --------- - -GUI 提供以下功能: - -* **測試參數表單** — 輸入欄位包含: - - * 目標 URL - * 測試持續時間(秒) - * 使用者數量(模擬使用者總數) - * 生成速率(每秒生成使用者數量) - * HTTP 方法選擇(GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS) +版面 +---- -* **啟動按鈕** — 在背景執行緒中啟動負載測試(不會阻塞 UI) -* **即時日誌面板** — 每 50 毫秒更新一次,即時顯示測試執行的日誌訊息 -* **Material Design 主題** — 使用 qt-material 的 ``dark_amber.xml`` 主題 +* **測試參數表單** — URL、測試時間、user 數、spawn rate、HTTP method。 +* **開始按鈕** — 在背景 ``QThread`` 啟動壓測。 +* **即時統計面板** — 總請求、目前速率、平均與 p95 延遲、失敗數。每 1 秒重新整理。 +* **Log panel** — 即時框架日誌。 +* **Material Design 主題** — ``qt-material`` 的 ``dark_amber.xml``。 -語言支援 --------- +語言 +---- -GUI 支援兩種語言: +GUI 內含英文、繁體中文、日文、韓文翻譯。透過 ``LanguageWrapper.reset_language`` 切換: -* **英文**(預設) -* **繁體中文** +.. code-block:: python -語言字串由 ``je_load_density/gui/language_wrapper/`` 下的 ``language_wrapper`` 模組管理。 + from je_load_density.gui.language_wrapper.multi_language_wrapper import ( + language_wrapper, + ) + language_wrapper.reset_language("Japanese") # 或 Korean / Traditional_Chinese / English 架構 ---- -GUI 由以下元件組成: - .. list-table:: :header-rows: 1 - :widths: 35 65 + :widths: 30 70 * - 元件 - 說明 * - ``LoadDensityUI`` - - 主視窗(``QMainWindow``)。套用主題並包含 widget。 + - ``QMainWindow`` 主機。套用主題並嵌入中央 widget。 * - ``LoadDensityWidget`` - - 中央 widget,包含表單輸入、啟動按鈕和日誌面板。 + - 表單 + 開始按鈕 + 統計面板 + log panel。 + * - ``StatsPanel`` + - 由 QTimer 驅動、讀取 ``test_record_instance`` 的面板。 * - ``LoadDensityGUIThread`` - - 背景 ``QThread``,在不阻塞 UI 的情況下執行負載測試。 + - 在背景跑測試的 ``QThread``,避免阻擋 UI。 * - ``InterceptAllFilter`` - - 日誌過濾器,將日誌訊息擷取到佇列中供 GUI 顯示。 + - 將 log records 攔截至 thread-safe queue。 * - ``log_message_queue`` - - 執行緒安全的佇列,連接日誌系統與 GUI 日誌面板。 + - 連接 logger 與 GUI log panel 的橋接 queue。 .. note:: - 在 Windows 平台上,GUI 會透過 ``ctypes`` 設定 ``AppUserModelID``, - 讓工作列能正確識別應用程式。 + 在 Windows 上,主視窗會以 ``ctypes`` 設定 ``AppUserModelID``,工作列才會顯示正確的應用名稱。 diff --git a/docs/source/Zh/doc/har_import/har_import_doc.rst b/docs/source/Zh/doc/har_import/har_import_doc.rst new file mode 100644 index 0000000..a0de5e3 --- /dev/null +++ b/docs/source/Zh/doc/har_import/har_import_doc.rst @@ -0,0 +1,56 @@ +HAR 錄製/重放 +============== + +概觀 +---- + +HAR 匯入器把錄製的 HTTP 流量(HAR JSON)轉換為 LoadDensity tasks 列表或完整可執行的動作 JSON。可從 Chrome / Firefox DevTools、mitmproxy、Charles 等工具匯出 `HAR 1.2 `_ 格式。 + +Python API +---------- + +.. code-block:: python + + from je_load_density import load_har, har_to_tasks, har_to_action_json + + har = load_har("recording.har") + tasks = har_to_tasks(har, include=[r"example\.com"], exclude=[r"\.svg$"]) + action_json = har_to_action_json( + har, + user="fast_http_user", + user_count=20, + spawn_rate=10, + test_time=120, + include=[r"api\.example\.com"], + ) + +過濾 +---- + +* ``include`` — regex 列表;URL 必須命中其一才保留。 +* ``exclude`` — regex 列表;URL 命中其一即丟棄。 + +對應規則 +-------- + +* HTTP method、URL、請求 headers 直接複製。 +* 移除 hop-by-hop 與 HTTP/2 pseudo header(``host``、``content-length``、``connection``、``:authority`` 等)。 +* JSON 請求 body(``application/json`` MIME)解析為 ``json`` 欄位;form params 變成 ``data`` dict;純文字 body 退回 ``data`` 字串。 +* 擷取的 response status 變成生成 task 上的 ``status_code`` 斷言。 + +動作 JSON +--------- + +.. code-block:: json + + {"load_density": [ + ["LD_har_to_action_json", { + "har": {"log": {...}}, + "user": "fast_http_user", + "user_count": 20, + "spawn_rate": 10, + "test_time": 120 + }] + ]} + +``LD_har_to_action_json`` 的結果本身是動作 JSON,可儲存或餵給 ``LD_execute_action``。 diff --git a/docs/source/Zh/doc/http_users/http_users_doc.rst b/docs/source/Zh/doc/http_users/http_users_doc.rst new file mode 100644 index 0000000..ce74266 --- /dev/null +++ b/docs/source/Zh/doc/http_users/http_users_doc.rst @@ -0,0 +1,79 @@ +HTTP 使用者 +=========== + +概觀 +---- + +LoadDensity 內含兩個 HTTP user 模板,皆透過 ``request_executor`` 與 ``scenario_runner`` 連線: + +* ``http_user`` — 封裝 ``locust.HttpUser``(底層 ``requests``)。 +* ``fast_http_user`` — 封裝 ``locust.FastHttpUser``(底層 geventhttpclient,吞吐高得多)。 + +高負載情境選 ``fast_http_user``。需要 ``requests`` 特性或 middleware 時用 ``http_user``。 + +Task 欄位 +--------- + +每個 HTTP task 是 dict;runner 將下列欄位轉送底層 client,其餘欄位忽略。 + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - 欄位 + - 意義 + * - ``method`` + - ``get`` / ``post`` / ``put`` / ``patch`` / ``delete`` / ``head`` / + ``options``(不分大小寫)。 + * - ``request_url`` / ``url`` + - 目標 URL(絕對或相對 ``host``)。 + * - ``name`` + - Locust 事件名;預設等同 URL。 + * - ``headers`` + - 請求 headers dict。 + * - ``params`` + - Query string 參數(dict 或 list of pairs)。 + * - ``json`` + - 以 JSON 序列化的 body。 + * - ``data`` + - Form-encoded body(dict / list / str)。 + * - ``cookies`` + - cookies dict。 + * - ``timeout`` + - 單請求 timeout(秒)。 + * - ``allow_redirects``、``verify``、``files`` + - 直接轉送 client。 + * - ``auth`` + - ``{"type": "basic", "username": "...", "password": "..."}`` 或 + ``{"type": "bearer", "token": "..."}``。 + * - ``assertions`` + - 回應斷言(見 :doc:`../assertions/assertions_doc`)。 + * - ``extract`` + - 回應擷取(見 :doc:`../parameter_resolver/parameter_resolver_doc`)。 + * - ``weight``、``run_if``、``skip_if`` + - 情境流程控制(見 :doc:`../scenarios/scenarios_doc`)。 + +範例 +---- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, + spawn_rate=10, + test_time=60, + variables={"base": "https://api.example.com"}, + tasks=[ + {"method": "post", "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [ + {"var": "auth", "from": "json_path", "path": "data.token"} + ]}, + {"method": "get", "request_url": "${var.base}/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "assertions": [{"type": "status_code", "value": 200}]}, + ], + ) diff --git a/docs/source/Zh/doc/installation/installation_doc.rst b/docs/source/Zh/doc/installation/installation_doc.rst index ebf5b6f..07f7c84 100644 --- a/docs/source/Zh/doc/installation/installation_doc.rst +++ b/docs/source/Zh/doc/installation/installation_doc.rst @@ -1,11 +1,11 @@ 安裝 ==== -系統需求 --------- +需求 +---- -* Python **3.10** 或更新版本 -* pip 19.3 或更新版本 +* Python **3.10** 以上 +* pip 19.3 以上 支援平台 ~~~~~~~~ @@ -15,64 +15,79 @@ :widths: 30 70 * - 平台 - - 版本 - * - Windows - - 10 / 11 + - 註記 + * - Windows 10 / 11 + - 完整支援 * - macOS - - 10.15 ~ 11 (Big Sur) - * - Linux - - Ubuntu 20.04 + - 完整支援 + * - Ubuntu / Linux + - 完整支援 * - Raspberry Pi - - 3B+ + - 已測 3B+ 以上 基本安裝(CLI 與函式庫) --------------------------- - -從 PyPI 安裝 LoadDensity: +------------------------ .. code-block:: bash pip install je_load_density -這會安裝核心函式庫與 CLI 工具。`Locust `_ 會作為相依套件自動安裝。 - -安裝 GUI 支援 --------------- - -若要使用可選的 PySide6 圖形化介面: - -.. code-block:: bash - - pip install je_load_density[gui] - -這會額外安裝: - -* `PySide6 `_ — Qt for Python 綁定 -* `qt-material `_ — Material Design 主題 +僅引入 `Locust `_ 與 ``defusedxml`` — 其餘皆為選用。 -開發者安裝 ----------- +選用 extras +----------- -從原始碼安裝進行開發: +LoadDensity 將每個協定驅動、exporter、錄製器與控制介面都拆成可選 extras。基礎套件不會 eager import 這些模組,僅做 HTTP 壓測者執行期不受影響。 -.. code-block:: bash - - git clone https://github.com/Intergration-Automation-Testing/LoadDensity.git - cd LoadDensity - pip install -e . - pip install -r dev_requirements.txt - -驗證安裝 +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Extra + - 加入 + * - ``gui`` + - PySide6 + qt-material(圖形介面)。 + * - ``websocket`` + - ``websocket-client``(WebSocket user 模板)。 + * - ``grpc`` + - ``grpcio`` + ``protobuf``(gRPC user 模板)。 + * - ``mqtt`` + - ``paho-mqtt``(MQTT user 模板)。 + * - ``prometheus`` + - ``prometheus-client``(Prometheus exporter)。 + * - ``opentelemetry`` + - OpenTelemetry SDK + OTLP gRPC exporter。 + * - ``metrics`` + - 結合 ``prometheus`` 與 ``opentelemetry``。 + * - ``faker`` + - ``Faker``(驅動 ``${faker.method}`` 占位符)。 + * - ``mcp`` + - ``mcp`` SDK(驅動 Claude 用的 MCP server)。 + * - ``all`` + - 上述全部。 + +範例:: + + pip install "je_load_density[gui]" + pip install "je_load_density[mqtt,grpc,websocket]" + pip install "je_load_density[metrics]" + pip install "je_load_density[mcp]" + pip install "je_load_density[all]" + +開發安裝 -------- -安裝後,驗證 LoadDensity 是否正確安裝: - .. code-block:: bash - python -c "from je_load_density import start_test; print('LoadDensity 安裝成功')" + git clone https://github.com/Integration-Automation/LoadDensity.git + cd LoadDensity + pip install -e ".[all]" + pip install -r requirements.txt -也可以檢查已安裝的版本: +驗證 +---- .. code-block:: bash + python -c "from je_load_density import start_test; print('LoadDensity installed')" pip show je_load_density diff --git a/docs/source/Zh/doc/locust_env/locust_env_doc.rst b/docs/source/Zh/doc/locust_env/locust_env_doc.rst new file mode 100644 index 0000000..5f7aec7 --- /dev/null +++ b/docs/source/Zh/doc/locust_env/locust_env_doc.rst @@ -0,0 +1,57 @@ +Locust 環境 +=========== + +概觀 +---- + +``prepare_env`` 與 ``create_env`` 封裝 ``locust.env.Environment``,把 wiring runner、stats printer、可選 Web UI 的樣板程式都隱藏起來。 + +create_env +---------- + +建立 ``Environment`` 與 runner,但不啟動任何 user: + +.. code-block:: python + + from je_load_density import create_env + from je_load_density.wrapper.user_template.fast_http_user_template import ( + FastHttpUserWrapper, + ) + + env = create_env( + FastHttpUserWrapper, + runner_mode="local", # "local" | "master" | "worker" + master_bind_host="*", + master_bind_port=5557, + master_host="127.0.0.1", + master_port=5557, + ) + +當你需要在 runner 啟動前掛上額外事件監聽器時使用。 + +prepare_env +----------- + +完整生命週期 helper:建立 environment → 啟動 runner → 視情況啟動 Locust Web UI → 在 ``test_time`` 後停止 → join。 + +.. code-block:: python + + from je_load_density import prepare_env + + prepare_env( + user_class=FastHttpUserWrapper, + user_count=50, + spawn_rate=10, + test_time=60, + web_ui_dict={"host": "127.0.0.1", "port": 8089}, + ) + +Web UI +------ + +傳入 ``web_ui_dict`` 即可啟動 Locust web UI。只有 local 與 master 模式會啟動 UI;workers 永遠不啟動。 + +Stats greenlets +--------------- + +local 與 master 模式下,``create_env`` 會 spawn Locust 標準 ``stats_printer`` 與 ``stats_history`` greenlet。Workers 兩者皆跳過,因為由 master 收集並列印。 diff --git a/docs/source/Zh/doc/mcp_claude/mcp_claude_doc.rst b/docs/source/Zh/doc/mcp_claude/mcp_claude_doc.rst new file mode 100644 index 0000000..f4c00b7 --- /dev/null +++ b/docs/source/Zh/doc/mcp_claude/mcp_claude_doc.rst @@ -0,0 +1,66 @@ +MCP Server(給 Claude) +======================= + +概觀 +---- + +LoadDensity 內含一個 `Model Context Protocol `_ server,將框架功能以 MCP 工具暴露。Claude(Desktop、Code 或任何 MCP 客戶端)可藉此驅動壓力測試、產生報告、匯入 HAR、檢視持久化資料,無需離開對話。 + +安裝 +---- + +.. code-block:: bash + + pip install "je_load_density[mcp]" + +啟動 +---- + +.. code-block:: bash + + python -m je_load_density.mcp_server + +Server 透過 stdio 講 MCP。請接到你選用的客戶端(Claude Desktop ``claude_desktop_config.json``、Claude Code 等): + +.. code-block:: json + + { + "mcpServers": { + "loaddensity": { + "command": "python", + "args": ["-m", "je_load_density.mcp_server"] + } + } + } + +提供的工具 +---------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Tool + - 用途 + * - ``load_density.run_test`` + - 跑一個 Locust 壓測(HTTP / WS / gRPC / MQTT / Socket)。 + * - ``load_density.run_action_json`` + - 執行動作 JSON 文件。 + * - ``load_density.create_project`` + - 在 PATH 建立專案骨架。 + * - ``load_density.list_executor_commands`` + - 列出 executor 註冊的所有 ``LD_*`` 指令。 + * - ``load_density.import_har`` + - 將 HAR 檔轉成可執行的動作 JSON。 + * - ``load_density.generate_reports`` + - 產生 HTML / JSON / XML / CSV / JUnit / summary 任意組合。 + * - ``load_density.summary`` + - 回傳彙整統計(totals、per-name p50/p90/p95/p99)。 + * - ``load_density.persist_records`` + - 將目前紀錄寫入 SQLite 資料庫。 + * - ``load_density.list_runs`` + - 列出近期持久化的 runs。 + * - ``load_density.fetch_run`` + - 取出某次 run 的所有紀錄。 + * - ``load_density.clear_records`` + - 開始新一輪前清除記憶體中的紀錄。 diff --git a/docs/source/Zh/doc/metrics/metrics_doc.rst b/docs/source/Zh/doc/metrics/metrics_doc.rst new file mode 100644 index 0000000..de7a217 --- /dev/null +++ b/docs/source/Zh/doc/metrics/metrics_doc.rst @@ -0,0 +1,84 @@ +指標 Exporter +============= + +概觀 +---- + +LoadDensity 內含三個可觀測性 sink,掛上 Locust 的 ``request`` 事件,逐請求送出指標。三者皆 lazy load,並以選用 extras 提供。 + +Prometheus +---------- + +安裝:``pip install je_load_density[prometheus]``。 + +.. code-block:: python + + from je_load_density import start_prometheus_exporter + start_prometheus_exporter(port=9646, addr="127.0.0.1") + +指標: + +* ``loaddensity_requests_total{request_type, name, outcome}`` — counter +* ``loaddensity_request_latency_ms{request_type, name}`` — histogram +* ``loaddensity_response_bytes{request_type, name}`` — histogram + +預設僅綁 loopback。要對 Docker / Kubernetes scraping target 開放,請改傳 ``addr="0.0.0.0"``。 + +InfluxDB +-------- + +僅用標準函式庫,無需額外套件。可選 UDP(fire-and-forget)或 HTTP(含 token)。 + +.. code-block:: python + + from je_load_density import start_influxdb_sink + + # InfluxDB UDP listener + start_influxdb_sink(transport="udp", host="127.0.0.1", port=8089) + + # HTTPS write API + start_influxdb_sink( + transport="http", + url="https://eu-central-1-1.aws.cloud2.influxdata.com/api/v2/write?org=...&bucket=...", + token="...", + ) + +HTTP transport 會拒絕非 ``http://`` / ``https://`` 的 URL。 + +OpenTelemetry +------------- + +安裝:``pip install je_load_density[opentelemetry]``。 + +.. code-block:: python + + from je_load_density import start_opentelemetry_exporter + start_opentelemetry_exporter( + endpoint="http://otel-collector:4317", + service_name="loaddensity", + export_interval_ms=5000, + ) + +送出的儀器: + +* ``loaddensity.requests`` — counter +* ``loaddensity.request.latency`` — histogram(ms) +* ``loaddensity.response.size`` — histogram(bytes) + +每個儀器皆攜帶 ``request_type``、``name``、``outcome`` 屬性。 + +Stop helpers +------------ + +每個 ``start_*`` 都有對應的 ``stop_*``,會卸下 listener(並 shutdown OTel provider)。Prometheus HTTP server 因 ``prometheus_client`` 沒提供 stop hook,只能繼續執行。 + +動作 JSON +--------- + +.. code-block:: json + + {"load_density": [ + ["LD_start_prometheus_exporter", {"port": 9646, "addr": "127.0.0.1"}], + ["LD_start_test", {...}], + ["LD_stop_prometheus_exporter", {}] + ]} diff --git a/docs/source/Zh/doc/mqtt_user/mqtt_user_doc.rst b/docs/source/Zh/doc/mqtt_user/mqtt_user_doc.rst new file mode 100644 index 0000000..18bead3 --- /dev/null +++ b/docs/source/Zh/doc/mqtt_user/mqtt_user_doc.rst @@ -0,0 +1,58 @@ +MQTT 使用者 +=========== + +概觀 +---- + +MQTT user 模板對 MQTT broker 進行 ``connect`` / ``publish`` / ``subscribe`` / ``disconnect``。底層使用 ``paho-mqtt``,lazy import — 以 ``pip install je_load_density[mqtt]`` 安裝。 + +Task 欄位 +--------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - 欄位 + - 意義 + * - ``method`` + - ``connect`` / ``publish`` / ``subscribe`` / ``disconnect``。 + * - ``broker`` / ``host`` + - MQTT broker 的 ``host:port``。 + * - ``topic`` + - 發佈/訂閱主題。 + * - ``payload`` + - publish body(``str`` 或 ``bytes``)。 + * - ``qos`` + - 0 / 1 / 2。 + * - ``retain`` + - 布林。 + * - ``username`` / ``password`` + - 憑證。 + * - ``client_id`` + - 選用 client id(預設為隨機十六進位字串)。 + * - ``timeout`` + - publish 等待 timeout(預設 5 秒)。 + +範例 +---- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "mqtt_user"}, + user_count=10, + spawn_rate=5, + test_time=60, + tasks=[ + {"method": "connect", "broker": "127.0.0.1:1883"}, + {"method": "subscribe", "topic": "telemetry/in", "qos": 1}, + {"method": "publish", "topic": "telemetry/out", + "payload": "ping", "qos": 1}, + {"method": "disconnect"}, + ], + ) + +每個步驟會觸發標記為 ``MQTT`` 的 Locust 事件。 diff --git a/docs/source/Zh/doc/parameter_resolver/parameter_resolver_doc.rst b/docs/source/Zh/doc/parameter_resolver/parameter_resolver_doc.rst new file mode 100644 index 0000000..c4454bb --- /dev/null +++ b/docs/source/Zh/doc/parameter_resolver/parameter_resolver_doc.rst @@ -0,0 +1,98 @@ +參數解析器 +========== + +概觀 +---- + +參數解析器會展開任何巢狀 string / list / dict 結構中的 ``${...}`` 占位符。在每個 task 被 user 模板處理之前自動套用,讓資料能在動作之間順暢流動。 + +支援的占位符 +------------ + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - 占位符 + - 解析為 + * - ``${var.NAME}`` + - 由 ``register_variable`` / ``register_variables`` 設定的值。 + * - ``${env.NAME}`` + - 環境變數 ``NAME``。 + * - ``${csv.SOURCE.COLUMN}`` + - CSV 來源 ``SOURCE`` 的下一筆資料中欄位 ``COLUMN`` 的值(預設循環)。 + * - ``${faker.METHOD}`` + - 呼叫 ``Faker().METHOD()``(lazy import,選用相依)。 + * - ``${uuid()}`` + - 新的 UUID 4 字串。 + * - ``${now()}`` + - 本地 ISO-8601 時間(秒)。 + * - ``${randint(min, max)}`` + - 介於 ``[min, max]`` 之間、密碼學強度的隨機整數。 + +未知占位符會原樣保留,便於 dry run 時偵測缺值。 + +註冊資料 +-------- + +.. code-block:: python + + from je_load_density import ( + register_variable, register_variables, + register_csv_source, register_csv_sources, + ) + + register_variable("base", "https://api.example.com") + register_variables({"token": "abc", "tenant": "acme"}) + + register_csv_source("users", "users.csv") # 循環 + register_csv_sources([ + {"name": "products", "file_path": "products.csv", "cycle": False}, + ]) + +CSV 必須有 header;每次呼叫 ``${csv.name.col}`` 取下一行對應欄位。 + +動作 JSON 用法 +-------------- + +.. code-block:: json + + {"load_density": [ + ["LD_register_variables", {"variables": {"base": "https://api.example.com"}}], + ["LD_register_csv_sources", {"sources": [ + {"name": "users", "file_path": "users.csv"} + ]}], + ["LD_start_test", { + "user_detail_dict": {"user": "fast_http_user"}, + "tasks": [{ + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "${csv.users.email}", "password": "${csv.users.password}"} + }] + }] + ]} + +從回應擷取值 +------------ + +HTTP task 可以宣告 ``extract`` 規則;命中的值會寫回解析器: + +.. code-block:: json + + { + "method": "post", + "request_url": "${var.base}/login", + "json": {"email": "u@example.com", "password": "secret"}, + "extract": [ + {"var": "auth_token", "from": "json_path", "path": "data.token"}, + {"var": "request_id", "from": "header", "name": "X-Request-Id"}, + {"var": "status", "from": "status_code"} + ] + } + +後續 task 即可用 ``${var.auth_token}`` 取用。 + +清除 +---- + +呼叫 ``parameter_resolver.clear()``(或 ``LD_clear_resolver``)以清除累積狀態。 diff --git a/docs/source/Zh/doc/scenarios/scenarios_doc.rst b/docs/source/Zh/doc/scenarios/scenarios_doc.rst new file mode 100644 index 0000000..617e4ea --- /dev/null +++ b/docs/source/Zh/doc/scenarios/scenarios_doc.rst @@ -0,0 +1,89 @@ +情境模式 +======== + +概觀 +---- + +HTTP / FastHttp / WebSocket user 的 tasks 可包成情境物件,控制每個 tick *要跑哪些 task*。三種模式: + +* ``sequence`` — 每個 task 依序執行(預設)。 +* ``weighted`` — 每 tick 依 ``weight`` 加權挑一個。 +* ``conditional`` — 以 ``run_if`` / ``skip_if`` 預測式(透過參數解析器評估)控制。 + +格式 +---- + +.. code-block:: json + + { + "mode": "sequence", + "tasks": [ + {"method": "get", "request_url": "${var.base}/products"}, + {"method": "post", "request_url": "${var.base}/cart", + "json": {"product_id": 1}} + ] + } + +舊式 ``{"get": {...}}`` map 與裸列表也仍可用,runner 會 normalise 成 ``{"mode": "sequence", "tasks": [...]}``。 + +加權挑選 +-------- + +每個 task 可帶正整數 ``weight``;runner 每 tick 挑一個,機率與 weight 成正比。未提供 ``weight`` 預設 1。 + +.. code-block:: json + + { + "mode": "weighted", + "tasks": [ + {"method": "get", "request_url": "/", "weight": 3}, + {"method": "get", "request_url": "/expensive", "weight": 1} + ] + } + +條件流程 +-------- + +``run_if`` 與 ``skip_if`` 皆使用相同預測式語言;``run_if`` 必須為真才執行該 task,``skip_if`` 必須為假才執行。 + +預測式 +~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 形式 + - 意義 + * - ``true`` / ``false`` / int + - 直接 truthy 檢查。 + * - ``"${var.x}"`` + - 解析占位符後 truthy 檢查。 + * - ``{"equals": [a, b]}`` + - 解析後 ``a == b``。 + * - ``{"not_equals": [a, b]}`` + - 解析後 ``a != b``。 + * - ``{"in": [needle, haystack]}`` + - ``needle in haystack``。 + * - ``{"truthy": value}`` + - 解析後 truthy 檢查。 + +範例 +~~~~ + +.. code-block:: json + + { + "mode": "sequence", + "tasks": [ + {"method": "post", "request_url": "/login", + "json": {"email": "${var.email}"}, + "extract": [{"var": "auth", "from": "json_path", "path": "token"}]}, + {"method": "get", "request_url": "/profile", + "headers": {"Authorization": "Bearer ${var.auth}"}, + "run_if": {"truthy": "${var.auth}"}}, + {"method": "post", "request_url": "/cart", + "json": {"product_id": 1}, + "skip_if": {"equals": ["${var.tenant}", "internal"]}} + ] + } diff --git a/docs/source/Zh/doc/scheduler/scheduler_doc.rst b/docs/source/Zh/doc/scheduler/scheduler_doc.rst deleted file mode 100644 index ac6c0f9..0000000 --- a/docs/source/Zh/doc/scheduler/scheduler_doc.rst +++ /dev/null @@ -1,116 +0,0 @@ -排程器 -====== - -LoadDensity 內建排程器,可讓您在指定的時間間隔排程重複執行測試。 -排程器支援阻塞(blocking)與非阻塞(non-blocking)兩種模式。 - -基本用法 --------- - -.. code-block:: python - - from je_load_density.utils.scheduler.scheduler_manager import SchedulerManager - - scheduler = SchedulerManager() - - def my_task(): - print("排程任務已執行") - - # 新增每 5 秒執行一次的工作(阻塞模式) - scheduler.add_interval_blocking_secondly(my_task, seconds=5) - - # 啟動阻塞排程器 - scheduler.start_block_scheduler() - -阻塞 vs 非阻塞 ----------------- - -排程器有兩種模式: - -* **阻塞模式** — ``start_block_scheduler()`` 會阻塞當前執行緒。適用於獨立的排程腳本。 -* **非阻塞模式** — ``start_nonblocking_scheduler()`` 在背景執行緒中執行排程器。 - 適用於需要繼續執行其他程式碼的情境。 - -間隔方法(阻塞) -~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 50 50 - - * - 方法 - - 說明 - * - ``add_interval_blocking_secondly(func, seconds)`` - - 每 N 秒執行一次 - * - ``add_interval_blocking_minutely(func, minutes)`` - - 每 N 分鐘執行一次 - * - ``add_interval_blocking_hourly(func, hours)`` - - 每 N 小時執行一次 - * - ``add_interval_blocking_daily(func, days)`` - - 每 N 天執行一次 - * - ``add_interval_blocking_weekly(func, weeks)`` - - 每 N 週執行一次 - -間隔方法(非阻塞) -~~~~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 50 50 - - * - 方法 - - 說明 - * - ``add_interval_nonblocking_secondly(func, seconds)`` - - 每 N 秒執行一次(非阻塞) - * - ``add_interval_nonblocking_minutely(func, minutes)`` - - 每 N 分鐘執行一次(非阻塞) - * - ``add_interval_nonblocking_hourly(func, hours)`` - - 每 N 小時執行一次(非阻塞) - * - ``add_interval_nonblocking_daily(func, days)`` - - 每 N 天執行一次(非阻塞) - * - ``add_interval_nonblocking_weekly(func, weeks)`` - - 每 N 週執行一次(非阻塞) - -Cron 方法 ---------- - -用於類似 cron 的排程: - -* ``add_cron_blocking(func, **cron_args)`` — 以阻塞模式新增 cron 工作 -* ``add_cron_nonblocking(func, **cron_args)`` — 以非阻塞模式新增 cron 工作 - -工作管理 --------- - -* ``remove_blocking_job(job_id)`` — 從阻塞排程器移除工作 -* ``remove_nonblocking_job(job_id)`` — 從非阻塞排程器移除工作 - -啟動排程器 ----------- - -* ``start_block_scheduler()`` — 啟動阻塞排程器(阻塞當前執行緒) -* ``start_nonblocking_scheduler()`` — 啟動非阻塞排程器(背景執行) -* ``start_all_scheduler()`` — 啟動兩種排程器 - -範例:排程負載測試 -------------------- - -.. code-block:: python - - from je_load_density import start_test - from je_load_density.utils.scheduler.scheduler_manager import SchedulerManager - - scheduler = SchedulerManager() - - def run_test(): - start_test( - user_detail_dict={"user": "fast_http_user"}, - user_count=10, - spawn_rate=5, - test_time=5, - tasks={"get": {"request_url": "http://httpbin.org/get"}}, - ) - - # 每 60 秒執行一次測試 - scheduler.add_interval_blocking_secondly(run_test, seconds=60) - scheduler.start_block_scheduler() diff --git a/docs/source/Zh/doc/socket_server/socket_server_doc.rst b/docs/source/Zh/doc/socket_server/socket_server_doc.rst index 5cc1011..f9bd67b 100644 --- a/docs/source/Zh/doc/socket_server/socket_server_doc.rst +++ b/docs/source/Zh/doc/socket_server/socket_server_doc.rst @@ -1,106 +1,94 @@ -TCP Socket 伺服器(遠端執行) -============================== +TCP 控制 Socket Server +====================== -LoadDensity 內建基於 ``gevent`` 的 TCP 伺服器,可透過網路接收 JSON 指令,實現遠端測試執行。 - -啟動伺服器 ----------- - -.. code-block:: python +概觀 +---- - from je_load_density import start_load_density_socket_server +控制 socket server 是 gevent 為基礎的 TCP listener,將收到的 LoadDensity 動作 JSON 透過網路執行。硬化版協定加入 length-prefix framing、選用 TLS,以及共享密鑰 token;舊版未驗證模式仍保留以維持相容。 - # 啟動伺服器(阻塞呼叫) - start_load_density_socket_server(host="localhost", port=9940) +模式 +---- .. list-table:: :header-rows: 1 - :widths: 20 15 15 50 + :widths: 25 75 + + * - 模式 + - 註記 + * - ``legacy`` + - 單次 ``recv(8192)``、純 JSON、無驗證。預設模式以維持舊客戶端(如 PyBreeze)相容。 + * - ``framed`` + - 4-byte big-endian 長度前綴 + JSON body。對 partial read 與超大 payload 較安全(1 MiB 上限)。 + * - ``framed + TLS`` + - 以 ``ssl.create_default_context``(TLS 1.2+)包裝連線,需 cert/key 檔案。 + +驗證 +---- - * - 參數 - - 類型 - - 預設值 - - 說明 - * - ``host`` - - ``str`` - - ``"localhost"`` - - 伺服器綁定位址 - * - ``port`` - - ``int`` - - ``9940`` - - 伺服器綁定埠號 +傳入 ``token=``(或設定 ``LOAD_DENSITY_SOCKET_TOKEN``)即可要求共享密鑰。一旦設定: -伺服器啟動後會輸出 ``Server started on {host}:{port}``。每個連入的連線會在獨立的 -``gevent`` greenlet 中處理,支援並行請求。 +* ``quit_server`` 沒有正確 token 將被拒絕。 +* 所有指令 payload 必須使用 envelope ``{"token": "...", "command": [...action JSON...]}``,可以 ``"op": "quit"`` 表示停機。 -從客戶端發送指令 ------------------ +Token 以 ``hmac.compare_digest`` 比對,避免 timing oracle。 -指令以 JSON 編碼的動作列表發送 — 與 JSON 腳本檔案使用相同的格式。 +啟動 server +----------- -.. code-block:: python +Python:: - import socket - import json - - # 連接到伺服器 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(("localhost", 9940)) - - # 發送測試指令 - command = json.dumps([ - ["LD_start_test", { - "user_detail_dict": {"user": "fast_http_user"}, - "user_count": 10, - "spawn_rate": 5, - "test_time": 5, - "tasks": {"get": {"request_url": "http://httpbin.org/get"}} - }] - ]) - sock.send(command.encode("utf-8")) - - # 接收回應 - response = sock.recv(8192) - print(response.decode("utf-8")) - sock.close() + from je_load_density import start_load_density_socket_server -伺服器協定 ----------- + start_load_density_socket_server( + host="0.0.0.0", + port=9940, + framed=True, + token="ROTATE_ME", + certfile="/etc/loaddensity/server.crt", + keyfile="/etc/loaddensity/server.key", + ) -* **指令格式**:JSON 編碼的動作列表(與 JSON 腳本檔案格式相同) -* **回應**:每個動作的回傳值以一行傳回,最後以 ``Return_Data_Over_JE\n`` 結尾 -* **錯誤處理**:若執行過程中發生錯誤,錯誤訊息會傳回,後接 ``Return_Data_Over_JE\n`` -* **緩衝區大小**:每次接收 8192 bytes +CLI:: -關閉伺服器 ----------- + python -m je_load_density serve \ + --host 0.0.0.0 --port 9940 --framed \ + --token "$LOAD_DENSITY_SOCKET_TOKEN" -發送字串 ``"quit_server"`` 即可優雅地關閉伺服器: +傳送指令(framed 模式) +----------------------- .. code-block:: python - import socket - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(("localhost", 9940)) - sock.send(b"quit_server") - response = sock.recv(8192) - print(response.decode("utf-8")) # "Server shutting down" + import json, socket, struct + + payload = json.dumps({ + "token": "ROTATE_ME", + "command": {"load_density": [["LD_summary", {}]]} + }).encode("utf-8") + + sock = socket.create_connection(("127.0.0.1", 9940)) + sock.sendall(struct.pack("!I", len(payload)) + payload) + while True: + header = sock.recv(4) + if not header: + break + (length,) = struct.unpack("!I", header) + chunk = sock.recv(length) + if chunk == b"Return_Data_Over_JE\n": + break + print(chunk.decode("utf-8")) sock.close() -伺服器會關閉所有連線並輸出 ``Server shutdown complete``。 - -架構 +關閉 ---- -TCP 伺服器由兩個元件組成: +* Legacy 模式:傳送字面字串 ``quit_server``。 +* Framed 模式(含 token):傳送 ``{"token": "...", "op": "quit"}``。 -* **TCPServer** — 基於 ``gevent.socket`` 的主伺服器類別。監聽連線並為每個客戶端產生 greenlet。 -* **start_load_density_socket_server()** — 便利函式,呼叫 - ``gevent.monkey.patch_all()`` 並啟動伺服器。 +Server 列印 ``Server shutdown complete`` 後結束。 -.. note:: +注意事項 +-------- - 啟動 socket 伺服器時會呼叫 ``gevent.monkey.patch_all()``。這會修補標準函式庫模組 - (socket、threading 等)以相容 gevent。若將 socket 伺服器整合到較大的應用程式中, - 請注意此行為。 +* 啟動時會呼叫 ``gevent.monkey.patch_all()``,整合時請留意。 +* token 可由環境變數 ``LOAD_DENSITY_SOCKET_TOKEN`` 讀取,避免將密鑰寫進指令參數。 diff --git a/docs/source/Zh/doc/socket_user/socket_user_doc.rst b/docs/source/Zh/doc/socket_user/socket_user_doc.rst new file mode 100644 index 0000000..67a3c67 --- /dev/null +++ b/docs/source/Zh/doc/socket_user/socket_user_doc.rst @@ -0,0 +1,54 @@ +原生 TCP / UDP 使用者 +===================== + +概觀 +---- + +原生 socket user 模板透過 TCP 或 UDP 收送任意 bytes,並可選擇讀取有限長度的回應。使用 Python 內建 ``socket`` 模組,無需額外相依。 + +Task 欄位 +--------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - 欄位 + - 意義 + * - ``protocol`` + - ``tcp`` 或 ``udp``。 + * - ``target`` / ``host`` + - ``host:port``。 + * - ``payload`` + - 要送出的 bytes。字串會以 UTF-8 編碼;以 ``hex:DEADBEEF`` 字首傳入十六進位字串可送出原始 bytes。 + * - ``expect_bytes`` + - 至多讀取 N bytes 的回應(0 表示略過讀取)。 + * - ``expect_substring`` + - 對解碼後回應的 substring 斷言。 + * - ``timeout`` + - 連線/讀取 timeout(秒),預設 5。 + * - ``name`` + - 事件名;預設 ``protocol:target``。 + +範例 +---- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "socket_user"}, + user_count=20, + spawn_rate=5, + test_time=60, + tasks=[ + {"protocol": "tcp", "target": "127.0.0.1:9000", + "payload": "PING\\n", "expect_bytes": 64, + "expect_substring": "PONG"}, + {"protocol": "udp", "target": "127.0.0.1:9000", + "payload": "hex:DEADBEEF", "expect_bytes": 4}, + ], + ) + +每個步驟會觸發標記為 ``TCP`` 或 ``UDP`` 的 Locust 事件。 diff --git a/docs/source/Zh/doc/sqlite_persistence/sqlite_persistence_doc.rst b/docs/source/Zh/doc/sqlite_persistence/sqlite_persistence_doc.rst new file mode 100644 index 0000000..86ee90d --- /dev/null +++ b/docs/source/Zh/doc/sqlite_persistence/sqlite_persistence_doc.rst @@ -0,0 +1,54 @@ +SQLite 持久化 +============= + +概觀 +---- + +SQLite sink 將記憶體 ``test_record_instance`` 寫入 SQLite 資料庫,便於跨次比對、回歸檢查或匯出至其他工具。Schema 採延遲建立;空檔案即可使用。 + +Python API +---------- + +.. code-block:: python + + from je_load_density import ( + persist_records, list_runs, fetch_run_records, + ) + + run_id = persist_records( + "loadtests.db", + label="checkout-2026-04-28", + metadata={"branch": "dev", "commit": "abc1234"}, + ) + + for row in list_runs("loadtests.db", limit=10): + print(row) + + for record in fetch_run_records("loadtests.db", run_id): + print(record) + +Schema +------ + +* ``load_density_runs(id, started_at, label, metadata_json)`` +* ``load_density_records(id, run_id, outcome, method, test_url, name, + status_code, response_time_ms, response_length, error)`` + +``run_id`` 與 ``name`` 上建立索引以加速跨次查詢。 + +動作 JSON +--------- + +.. code-block:: json + + {"load_density": [ + ["LD_clear_records", {}], + ["LD_start_test", {...}], + ["LD_persist_records", { + "database_path": "loadtests.db", + "label": "checkout", + "metadata": {"branch": "dev"} + }] + ]} + +之後的腳本以 ``LD_list_runs`` 與 ``LD_fetch_run_records`` 讀取資料。 diff --git a/docs/source/Zh/doc/start_test/start_test_doc.rst b/docs/source/Zh/doc/start_test/start_test_doc.rst new file mode 100644 index 0000000..63f16d6 --- /dev/null +++ b/docs/source/Zh/doc/start_test/start_test_doc.rst @@ -0,0 +1,106 @@ +start_test 與 prepare_env +========================= + +概觀 +---- + +``start_test`` 是高層進入點,挑選 user 模板、種入參數解析器、請 ``prepare_env`` 以指定模式(local / master / worker)建立 Locust 環境。 + +簽章 +---- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "fast_http_user"}, + user_count=50, + spawn_rate=10, + test_time=60, + web_ui_dict=None, # {"host": "...", "port": ...} + runner_mode="local", # "local" | "master" | "worker" + master_bind_host="*", + master_bind_port=5557, + master_host="127.0.0.1", + master_port=5557, + expected_workers=0, + tasks=..., + variables={"host": "https://api.example.com"}, + csv_sources=[{"name": "users", "file_path": "users.csv"}], + ) + +支援的 user 類型 +---------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - ``user`` + - 模板 + * - ``http_user`` + - ``locust.HttpUser`` 封裝(以 ``requests`` 為底)。 + * - ``fast_http_user`` + - ``locust.FastHttpUser`` 封裝(以 ``geventhttpclient`` 為底)。 + * - ``websocket_user`` + - WebSocket 框架收送迴圈(lazy import ``websocket-client``)。 + * - ``grpc_user`` + - 對 operator 提供的 stub 進行 unary gRPC 呼叫。 + * - ``mqtt_user`` + - MQTT 發佈/訂閱迴圈。 + * - ``socket_user`` + - 原生 TCP / UDP 收送。 + +prepare_env +----------- + +``prepare_env`` 是 ``start_test`` 之下的較低階 API。當你想自行整合至其他 runner 時較有用。 + +.. code-block:: python + + from je_load_density import prepare_env + from je_load_density.wrapper.user_template.fast_http_user_template import ( + FastHttpUserWrapper, set_wrapper_fasthttp_user, + ) + + set_wrapper_fasthttp_user( + {"user": "fast_http_user"}, + tasks=[{"method": "get", "request_url": "https://example.com/"}], + ) + prepare_env( + user_class=FastHttpUserWrapper, + user_count=50, + spawn_rate=10, + test_time=60, + runner_mode="local", + ) + +分散式模式 +---------- + +Master:: + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="master", + master_bind_host="0.0.0.0", + master_bind_port=5557, + expected_workers=4, + user_count=200, + spawn_rate=20, + test_time=300, + tasks=[...], + ) + +Worker(在每個壓測節點執行,與 master 同網段):: + + start_test( + user_detail_dict={"user": "fast_http_user"}, + runner_mode="worker", + master_host="10.0.0.10", + master_port=5557, + tasks=[...], + ) + +Master 在開始 ramp 前會等待 ``expected_workers`` 個 worker 註冊完成。Workers 加入 master 後,會依群集規模分擔 user count。 diff --git a/docs/source/Zh/doc/test_record/test_record_doc.rst b/docs/source/Zh/doc/test_record/test_record_doc.rst new file mode 100644 index 0000000..33d85b0 --- /dev/null +++ b/docs/source/Zh/doc/test_record/test_record_doc.rst @@ -0,0 +1,38 @@ +測試紀錄 +======== + +概觀 +---- + +``test_record_instance`` 為 Locust ``request`` hook 寫入的記憶體紀錄。所有報告產生器(HTML / JSON / XML / CSV / JUnit / summary)都從此物件讀取,SQLite 持久化 helper 將其寫入磁碟。 + +紀錄欄位 +-------- + +每筆紀錄為 dict,欄位如下: + +* ``Method`` — HTTP method 或協定標籤(``GET``、``POST``、``WS``、``GRPC``、``MQTT``、``TCP``、``UDP``)。 +* ``test_url`` — 目標 URL 或位址。 +* ``name`` — Locust 事件名(未指定時為 ``request_url``)。 +* ``status_code`` — 回應 status(字串)或 ``None``。 +* ``response_time_ms`` — Locust 回報的回應時間(ms)。 +* ``response_length`` — 回應大小(bytes)。 +* ``error`` — 成功為 ``None``;失敗為 exception 字串。 +* ``text``、``content``、``headers`` — 選用,僅 HTTP 成功才有。 + +清除 +---- + +.. code-block:: python + + from je_load_density import test_record_instance + test_record_instance.clear_records() + +或透過 executor:: + + ["LD_clear_records", {}] + +SQLite 持久化 +------------- + +見 :doc:`../sqlite_persistence/sqlite_persistence_doc`。 diff --git a/docs/source/Zh/doc/websocket_user/websocket_user_doc.rst b/docs/source/Zh/doc/websocket_user/websocket_user_doc.rst new file mode 100644 index 0000000..3b7d494 --- /dev/null +++ b/docs/source/Zh/doc/websocket_user/websocket_user_doc.rst @@ -0,0 +1,50 @@ +WebSocket 使用者 +================ + +概觀 +---- + +WebSocket user 模板對指定的 ``ws://`` / ``wss://`` URL 做 connect / send / recv 迴圈。底層使用 ``websocket-client``,採 lazy import — 以 ``pip install je_load_density[websocket]`` 安裝。 + +Task 欄位 +--------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - 欄位 + - 意義 + * - ``method`` + - ``connect`` / ``send`` / ``recv`` / ``sendrecv`` / ``close``。 + * - ``request_url`` / ``url`` + - WebSocket URL(``connect`` 必填,其餘步驟可重用上次連線)。 + * - ``name`` + - 事件名;預設為 URL 或 method。 + * - ``payload`` + - 要送出的字串 / bytes。 + * - ``expect`` + - 對接收到的 frame 做 substring 斷言。 + * - ``timeout`` + - 接收 timeout(秒),預設 5。 + +範例 +---- + +.. code-block:: python + + from je_load_density import start_test + + start_test( + user_detail_dict={"user": "websocket_user"}, + user_count=10, + spawn_rate=5, + test_time=60, + tasks=[ + {"method": "connect", "request_url": "wss://echo.example.com/socket"}, + {"method": "sendrecv", "payload": '{"ping": 1}', "expect": "pong"}, + {"method": "close"}, + ], + ) + +每個步驟會觸發標記為 ``WS`` 的 Locust 事件,供統計彙整。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index f15f817..bece33e 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -1,19 +1,160 @@ -繁體中文文件 -============================================= +==================================== +LoadDensity 繁體中文手冊 +==================================== -歡迎來到 LoadDensity 繁體中文文件。LoadDensity 是一個建構於 Locust 之上的負載與壓力測試自動化框架, -提供簡化的 API、JSON 驅動的測試腳本、多格式報告產生、可選的 GUI 介面,以及遠端執行功能。 +繁體中文手冊依照讀者使用順序分為十章:安裝 → 執行壓測 → 撰寫動作腳本 → 擴展 → 整合。可使用左側目次,或直接跳到下方章節。 + +.. contents:: 本頁目次 + :local: + :depth: 1 + +---- + +.. _zh-getting-started: + +第 1 章 — 入門 +============== + +安裝 LoadDensity、執行第一次壓測,並建立專案骨架。 .. toctree:: - :maxdepth: 4 - :caption: 使用指南 + :maxdepth: 2 + :caption: 入門 doc/installation/installation_doc doc/getting_started/getting_started_doc - doc/cli/cli_doc - doc/generate_report/generate_report_doc - doc/scheduler/scheduler_doc - doc/socket_server/socket_server_doc + doc/create_project/create_project_doc + +.. _zh-core-api: + +第 2 章 — 核心 API +================== + +面向 Locust 的封裝:環境、Runner、使用者代理。讀過這章後,整個框架就不再神秘。 + +.. toctree:: + :maxdepth: 2 + :caption: 核心 API + + doc/architecture/architecture_doc + doc/start_test/start_test_doc + doc/locust_env/locust_env_doc + +.. _zh-actions: + +第 3 章 — 動作撰寫與執行 +======================== + +組合 JSON 動作腳本、參數化資料、建立情境流程,串接測試後 callback。 + +.. toctree:: + :maxdepth: 2 + :caption: 動作 + + doc/action_executor/action_executor_doc + doc/parameter_resolver/parameter_resolver_doc + doc/scenarios/scenarios_doc + doc/assertions/assertions_doc doc/callback/callback_doc doc/package_manager/package_manager_doc + +.. _zh-user-templates: + +第 4 章 — 使用者模板 +==================== + +協定驅動程式:HTTP、FastHttp、WebSocket、gRPC、MQTT,以及原生 TCP/UDP。每個模板皆以 Locust 使用者註冊,採用相同的 task 契約。 + +.. toctree:: + :maxdepth: 2 + :caption: 使用者模板 + + doc/http_users/http_users_doc + doc/websocket_user/websocket_user_doc + doc/grpc_user/grpc_user_doc + doc/mqtt_user/mqtt_user_doc + doc/socket_user/socket_user_doc + +.. _zh-reporting: + +第 5 章 — 報告與可觀測性 +======================== + +產生 HTML / JSON / XML / CSV / JUnit / 百分位摘要報告,將指標送至 Prometheus、InfluxDB,或任何 OTLP 後端。 + +.. toctree:: + :maxdepth: 2 + :caption: 報告 + + doc/generate_report/generate_report_doc + doc/metrics/metrics_doc + doc/test_record/test_record_doc + +.. _zh-orchestration: + +第 6 章 — 編排與擴展 +==================== + +執行分散式 master/worker 群集、透過參數解析器共享狀態、依擷取變數控制執行流程。 + +.. toctree:: + :maxdepth: 2 + :caption: 編排 + + doc/distributed/distributed_doc + +.. _zh-recording-data: + +第 7 章 — 錄製與資料 +==================== + +將真實瀏覽流量(HAR)轉換為可執行的動作 JSON,將測試紀錄持久化到 SQLite,並比對歷次執行結果。 + +.. toctree:: + :maxdepth: 2 + :caption: 錄製與資料 + + doc/har_import/har_import_doc + doc/sqlite_persistence/sqlite_persistence_doc + +.. _zh-tooling: + +第 8 章 — 工具、CLI 與診斷 +========================== + +命令列子指令、硬化的控制 socket server,以及 traceback 中可能出現的例外階層。 + +.. toctree:: + :maxdepth: 2 + :caption: 工具 + + doc/cli/cli_doc + doc/socket_server/socket_server_doc + doc/exception/exception_doc + +.. _zh-integrations: + +第 9 章 — 整合 +============== + +選用的 GUI、可讓 Claude 驅動 LoadDensity 的 **Model Context Protocol (MCP)** server,以及下游 PyBreeze IDE 整合。 + +.. toctree:: + :maxdepth: 2 + :caption: 整合 + doc/gui/gui_doc + doc/mcp_claude/mcp_claude_doc + +.. _zh-reference: + +第 10 章 — API Reference +======================== + +自動產生的 Python API reference。 + +.. toctree:: + :maxdepth: 2 + :caption: 參考 + + doc/api_reference/api_reference diff --git a/docs/source/conf.py b/docs/source/conf.py index da1537c..e5bef3d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,30 +6,65 @@ import os import sys -# -- Path setup -------------------------------------------------------------- -sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath('.')) +# Reach the repo root so ``import je_load_density`` works inside autodoc. +sys.path.insert(0, os.path.abspath(os.path.join(os.pardir, os.pardir))) # -- Project information ----------------------------------------------------- + project = 'LoadDensity' -project_copyright = '2022, JE-Chen' +project_copyright = '2022 ~ 2025, JE-Chen' author = 'JE-Chen' - -# The full version, including alpha/beta/rc tags release = '0.0.65' # -- General configuration --------------------------------------------------- -extensions = [] -templates_path = ['_templates'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.autosectionlabel", + "sphinx.ext.napoleon", + "sphinxcontrib.mermaid", +] + +# Autosummary writes per-module reference pages on every build. +autosummary_generate = True +# autosectionlabel collides on common section titles repeated across +# language manuals; prefix every label with the document path so +# duplicates become unique. +autosectionlabel_prefix_document = True +autodoc_default_options = { + "members": True, + "undoc-members": False, + "show-inheritance": True, +} +# Autodoc imports the modules it documents; some carry soft deps +# that aren't installed in the docs build environment, so silence them. +autodoc_mock_imports = [ + "locust", + "gevent", + "PySide6", + "qt_material", + "defusedxml", + "websocket", + "grpc", + "paho", + "prometheus_client", + "opentelemetry", + "faker", + "mcp", +] +mermaid_version = "10.9.0" + +templates_path = ['_templates'] exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- -html_theme = 'sphinx_rtd_theme' +html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] -# -- Options for HTML theme -------------------------------------------------- html_theme_options = { 'navigation_depth': 4, 'collapse_navigation': False, diff --git a/docs/source/index.rst b/docs/source/index.rst index 5ffc108..dccf857 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,22 +1,49 @@ -Welcome to LoadDensity's documentation! -============================================= +================== +LoadDensity +================== -**LoadDensity** is a high-performance load & stress testing automation framework built on top of -`Locust `_. It provides a simplified wrapper around Locust's core functionality, -enabling fast user spawning, flexible test configuration via templates and JSON-driven scripts, -report generation in multiple formats (HTML / JSON / XML), a built-in GUI, remote execution -via TCP socket server, and a callback mechanism for post-test workflows. +**A multi-protocol load and stress automation framework built on Locust** -.. note:: +LoadDensity (``je_load_density``) wraps Locust with a JSON-driven action +executor and adds first-class support for HTTP, WebSocket, gRPC, MQTT, +and raw TCP/UDP user templates. Every executor command has a +deterministic name (``LD_*``) and a single dispatch point, so an action +JSON can mix protocols, parameterised data, scenario flow, reports, and +metrics exporters in the same script. - - **PyPI**: https://pypi.org/project/je_load_density/ - - **Source Code**: https://github.com/Intergration-Automation-Testing/LoadDensity - - **License**: MIT +* **PyPI**: https://pypi.org/project/je_load_density/ +* **GitHub**: https://github.com/Integration-Automation/LoadDensity +* **License**: MIT + +---- + +The documentation is split by language and by content type. Each +language manual is organised into chapters (Getting Started, Core API, +Actions, User Templates, Reporting, Orchestration, Recording & Data, +Tooling, Integrations, Reference); the API book holds the +auto-generated Python reference. + +.. toctree:: + :maxdepth: 2 + :caption: English manual + + En/en_index.rst .. toctree:: - :maxdepth: 4 - :caption: Contents + :maxdepth: 2 + :caption: 繁體中文手冊 + + Zh/zh_index.rst + +.. toctree:: + :maxdepth: 2 + :caption: API reference + + api/api_index.rst + +---- + +RoadMap +------- - En/en_index - Zh/zh_index - api/api_index +* Project Kanban: https://github.com/orgs/Integration-Automation/projects