From 1bbe73fb7b57343493d8e3fe237a50f4ad599aeb Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 4 Apr 2026 22:36:01 +0800 Subject: [PATCH] Update docs Update docs --- docs/source/En/doc/callback/callback_doc.rst | 133 +++++++++ docs/source/En/doc/cli/cli_doc.rst | 107 ++++++- .../generate_report/generate_report_doc.rst | 196 +++++++++---- .../getting_started/getting_started_doc.rst | 251 ++++++++++++++-- docs/source/En/doc/gui/gui_doc.rst | 89 ++++++ .../En/doc/installation/installation_doc.rst | 82 +++++- .../package_manager/package_manager_doc.rst | 76 +++++ .../source/En/doc/scheduler/scheduler_doc.rst | 114 +++++++- .../doc/socket_server/socket_server_doc.rst | 110 +++++++ docs/source/En/en_index.rst | 22 +- docs/source/Zh/doc/callback/callback_doc.rst | 132 +++++++++ docs/source/Zh/doc/cli/cli_doc.rst | 107 ++++++- .../generate_report/generate_report_doc.rst | 190 ++++++++---- .../getting_started/getting_started_doc.rst | 243 ++++++++++++++-- docs/source/Zh/doc/gui/gui_doc.rst | 87 ++++++ .../Zh/doc/installation/installation_doc.rst | 81 +++++- .../package_manager/package_manager_doc.rst | 75 +++++ .../source/Zh/doc/scheduler/scheduler_doc.rst | 115 +++++++- .../doc/socket_server/socket_server_doc.rst | 106 +++++++ docs/source/Zh/zh_index.rst | 20 +- docs/source/api/api_index.rst | 25 +- docs/source/api/loaddensity/loaddensity.rst | 274 ++++++++++++++++-- docs/source/api/utils/callback.rst | 88 +++++- docs/source/api/utils/executor.rst | 130 ++++++++- docs/source/api/utils/file.rst | 88 +++++- docs/source/api/utils/generate_report.rst | 114 ++++++-- docs/source/api/utils/package_manager.rst | 146 ++++------ docs/source/api/utils/scheduler.rst | 254 +++++++--------- docs/source/api/utils/socket_server.rst | 129 +++++---- docs/source/conf.py | 44 +-- docs/source/index.rst | 19 +- 31 files changed, 2974 insertions(+), 673 deletions(-) create mode 100644 docs/source/En/doc/callback/callback_doc.rst create mode 100644 docs/source/En/doc/gui/gui_doc.rst create mode 100644 docs/source/En/doc/package_manager/package_manager_doc.rst create mode 100644 docs/source/En/doc/socket_server/socket_server_doc.rst create mode 100644 docs/source/Zh/doc/callback/callback_doc.rst create mode 100644 docs/source/Zh/doc/gui/gui_doc.rst create mode 100644 docs/source/Zh/doc/package_manager/package_manager_doc.rst create mode 100644 docs/source/Zh/doc/socket_server/socket_server_doc.rst diff --git a/docs/source/En/doc/callback/callback_doc.rst b/docs/source/En/doc/callback/callback_doc.rst new file mode 100644 index 0000000..bb5a601 --- /dev/null +++ b/docs/source/En/doc/callback/callback_doc.rst @@ -0,0 +1,133 @@ +Callback Executor +================= + +The ``CallbackFunctionExecutor`` allows chaining a trigger function with a callback function. +This is useful for post-test workflows — for example, run a test, then automatically +generate a report. + +Basic Usage +----------- + +.. code-block:: python + + from je_load_density import callback_executor + + def after_test(): + print("Test finished, generating report...") + + 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"}}, + ) + +How It Works +------------ + +1. The ``trigger_function_name`` is looked up in the executor's ``event_dict`` +2. The trigger function is executed with the provided ``**kwargs`` +3. After the trigger function completes, the ``callback_function`` is called +4. The return value of the trigger function is returned + +Available Trigger Functions +--------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Trigger Name + - Function + * - ``user_test`` + - ``start_test()`` — Run a load test + * - ``LD_generate_html`` + - ``generate_html()`` — Generate HTML fragments + * - ``LD_generate_html_report`` + - ``generate_html_report()`` — Generate HTML report file + * - ``LD_generate_json`` + - ``generate_json()`` — Generate JSON data + * - ``LD_generate_json_report`` + - ``generate_json_report()`` — Generate JSON report files + * - ``LD_generate_xml`` + - ``generate_xml()`` — Generate XML strings + * - ``LD_generate_xml_report`` + - ``generate_xml_report()`` — Generate XML report files + +Passing Parameters to Callbacks +--------------------------------- + +With keyword arguments (default): + +.. code-block:: python + + def my_callback(report_name, format_type): + print(f"Generating {format_type} report: {report_name}") + + callback_executor.callback_function( + trigger_function_name="user_test", + callback_function=my_callback, + callback_function_param={"report_name": "final", "format_type": "html"}, + callback_param_method="kwargs", + user_detail_dict={"user": "fast_http_user"}, + user_count=10, + spawn_rate=5, + test_time=5, + tasks={"get": {"request_url": "http://httpbin.org/get"}}, + ) + +With positional arguments: + +.. code-block:: python + + def my_callback(arg1, arg2): + print(f"Args: {arg1}, {arg2}") + + callback_executor.callback_function( + trigger_function_name="user_test", + callback_function=my_callback, + callback_function_param=["value1", "value2"], + callback_param_method="args", + user_detail_dict={"user": "fast_http_user"}, + user_count=10, + spawn_rate=5, + test_time=5, + tasks={"get": {"request_url": "http://httpbin.org/get"}}, + ) + +Parameters +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Parameter + - Type + - Description + * - ``trigger_function_name`` + - ``str`` + - Name of function in ``event_dict`` to trigger + * - ``callback_function`` + - ``Callable`` + - Callback function to execute after the trigger + * - ``callback_function_param`` + - ``dict`` or ``list`` or ``None`` + - Parameters for the callback (dict for kwargs, list for args) + * - ``callback_param_method`` + - ``str`` + - ``"kwargs"`` (default) or ``"args"`` + * - ``**kwargs`` + - — + - Parameters passed to the trigger function + +Error Handling +-------------- + +* ``CallbackExecutorException`` is raised if: + + * ``trigger_function_name`` is not found in ``event_dict`` + * ``callback_param_method`` is not ``"kwargs"`` or ``"args"`` diff --git a/docs/source/En/doc/cli/cli_doc.rst b/docs/source/En/doc/cli/cli_doc.rst index ff1fcb3..ba7af9d 100644 --- a/docs/source/En/doc/cli/cli_doc.rst +++ b/docs/source/En/doc/cli/cli_doc.rst @@ -1,19 +1,106 @@ -CLI ----- +CLI (Command Line Interface) +============================ -We can use the CLI mode to execute the keyword. -json file or execute the folder containing the Keyword.json files. +LoadDensity provides a full command-line interface via ``python -m je_load_density``. -The following example is to execute the specified path of the keyword JSON file. +CLI Arguments +------------- -.. code-block:: +.. list-table:: + :header-rows: 1 + :widths: 25 10 65 - python je_load_density --execute_file "your_file_path" + * - 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: -The following example is to run all keyword JSON files in a specified folder: +.. code-block:: bash -.. code-block:: + python -m je_load_density -e test_scenario.json - python je_load_density --execute_dir "your_dir_path" \ No newline at end of file +The JSON file should follow the action list format: + +.. 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 All JSON Files in a Directory +------------------------------------- + +Run all JSON keyword files in a specified directory recursively: + +.. 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 +----------------------------- + +Execute a JSON action list directly as a string: + +.. code-block:: bash + + 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:: + + On **Windows**, inline JSON strings are automatically double-parsed due to shell + escaping differences. The CLI handles this transparently. + +Create a Project +---------------- + +Scaffold a new project with keyword templates and executor scripts: + +.. 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 + +Error Handling +-------------- + +If no valid argument is provided, the CLI raises a ``LoadDensityTestExecuteException`` +and exits with code 1. All errors are printed to stderr. 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 0000956..9e723b5 100644 --- a/docs/source/En/doc/generate_report/generate_report_doc.rst +++ b/docs/source/En/doc/generate_report/generate_report_doc.rst @@ -1,68 +1,162 @@ -Generate Report ----- +Report Generation +================= -* Generate Report can generate reports in the following formats: - * HTML - * JSON - * XML +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. -* Generate Report is mainly used to record and confirm which steps were executed and whether they were successful or not. -* The following example is used with keywords and an executor. If you don't understand, please first take a look at the executor. +.. note:: -Here's an example of generating an HTML report. + Reports can only be generated after a test has been run. If no test records exist, + a ``LoadDensityHTMLException`` or ``LoadDensityGenerateJsonReportException`` will be raised. + +HTML Report +----------- + +Generates a styled HTML file with tables showing success and failure records. .. code-block:: python - from je_load_density import generate_html_report, start_test - start_test( - { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - } - ) - generate_html_report() + from je_load_density import generate_html_report + + # Generates "my_report.html" + generate_html_report("my_report") + +The HTML report includes: -Here's an example of generating an JSON report. +* **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_json_report, start_test - start_test( - { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - } - ) - generate_json_report() + 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 -Here's an example of generating an XML report. +JSON Report +----------- + +Generates structured JSON files for programmatic consumption. .. code-block:: python - from je_load_density import generate_xml_report, start_test - start_test( - { - "user": "fast_http_user", + 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:** + +.. code-block:: json + + { + "Success_Test1": { + "Method": "GET", + "test_url": "http://httpbin.org/get", + "name": "/get", + "status_code": "200", + "text": "...", + "content": "...", + "headers": "..." }, - 50, 10, 5, - **{ - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } + "Success_Test2": {} + } + +**Failure JSON format:** + +.. code-block:: json + + { + "Failure_Test1": { + "Method": "POST", + "test_url": "http://httpbin.org/status/500", + "name": "/status/500", + "status_code": "500", + "error": "..." } - ) - generate_xml_report() \ No newline at end of file + } + +To get raw JSON data structures without writing to a file: + +.. code-block:: python + + from je_load_density import generate_json + + success_dict, failure_dict = generate_json() + +XML Report +---------- + +Generates XML files for CI/CD integration. + +.. 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") + +The XML output is pretty-printed using ``xml.dom.minidom``. Each test record is wrapped +under an ```` root element. + +To get raw XML strings without writing to a file: + +.. code-block:: python + + from je_load_density import generate_xml + + success_xml_str, failure_xml_str = generate_xml() + +Using in JSON Scripts +--------------------- + +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 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 1862c1a..f30bcc8 100644 --- a/docs/source/En/doc/getting_started/getting_started_doc.rst +++ b/docs/source/En/doc/getting_started/getting_started_doc.rst @@ -1,51 +1,240 @@ -Getting started ----- +Getting Started +=============== -First, create project. +This guide walks you through the basics of using LoadDensity to run your first load test. -In LoadDensity, you can create a project which will automatically generate sample files once the project is created. -These sample files include a Python executor file and a keyword.json file. +User Types +---------- -To create a project, you can use the following method: +LoadDensity supports two types of Locust users: + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - 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()``: .. code-block:: python - from je_load_density import create_project_dir - # create on current workdir - create_project_dir() - # create project on project_path - create_project_dir("project_path") - # create project on project_path and dir name is My First Project - create_project_dir("project_path", "My First Project") + from je_load_density import start_test + + result = 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"}, + } + ) -Or using CLI, this will generate a project at the project_path location. +``start_test()`` Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: console +.. list-table:: + :header-rows: 1 + :widths: 20 15 10 55 - python -m je_load_density --create_project project_path + * - 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}`` -Then, you can enter the project folder, -navigate to the executor folder, -choose one of the executors to run and observe. -The keyword.json file in the keyword folder defines the actions to be executed. +Return Value +~~~~~~~~~~~~ -If you want to execute using pure Python, you can refer to the following example: -Attention! Only the following HTTP methods can be used: -['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] +``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: .. code-block:: python from je_load_density import start_test - start_test( - { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ + result = start_test( + user_detail_dict={"user": "http_user"}, + user_count=100, + spawn_rate=20, + test_time=30, + web_ui_dict={"host": "127.0.0.1", "port": 8089}, + tasks={ + "get": {"request_url": "http://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 +------------------------------------- + +You can define test scenarios as JSON files and execute them without writing Python code. + +Create a ``test_scenario.json`` file: + +.. 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"} } - } - ) \ No newline at end of file + }] + ] + +Execute from Python: + +.. code-block:: python + + from je_load_density import execute_action, read_action_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: + +.. code-block:: python + + from je_load_density import create_project_dir + + create_project_dir(project_path="./my_tests", parent_name="LoadDensity") + +Or via CLI: + +.. code-block:: bash + + python -m je_load_density -c ./my_tests + +This creates the following structure: + +.. code-block:: text + + 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/ + +* ``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/`` diff --git a/docs/source/En/doc/gui/gui_doc.rst b/docs/source/En/doc/gui/gui_doc.rst new file mode 100644 index 0000000..75deabe --- /dev/null +++ b/docs/source/En/doc/gui/gui_doc.rst @@ -0,0 +1,89 @@ +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. + +Requirements +------------ + +The GUI requires additional dependencies. Install with: + +.. code-block:: bash + + pip install je_load_density[gui] + +This installs: + +* **PySide6** (6.10.0) — Qt for Python bindings +* **qt-material** — Material design theme + +Launching the GUI +----------------- + +.. code-block:: python + + from je_load_density.gui.main_window import LoadDensityUI + from PySide6.QtWidgets import QApplication + import sys + + app = QApplication(sys.argv) + window = LoadDensityUI() + window.show() + sys.exit(app.exec()) + +GUI Features +------------ + +The GUI provides: + +* **Test Parameter Form** — Input fields for: + + * 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) + +* **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** (繁體中文) + +Language strings are managed via the ``language_wrapper`` module under +``je_load_density/gui/language_wrapper/``. + +Architecture +------------ + +The GUI consists of the following components: + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Component + - Description + * - ``LoadDensityUI`` + - Main window (``QMainWindow``). Applies theme and contains the widget. + * - ``LoadDensityWidget`` + - Central widget with form inputs, start button, and log panel. + * - ``LoadDensityGUIThread`` + - Background ``QThread`` that runs the load test without blocking the UI. + * - ``InterceptAllFilter`` + - Log filter that captures log messages into a queue for GUI display. + * - ``log_message_queue`` + - Thread-safe queue bridging the logger and the GUI log panel. + +.. note:: + + On Windows, the GUI sets ``AppUserModelID`` via ``ctypes`` so the taskbar correctly + identifies the application. diff --git a/docs/source/En/doc/installation/installation_doc.rst b/docs/source/En/doc/installation/installation_doc.rst index 8164256..9def6ca 100644 --- a/docs/source/En/doc/installation/installation_doc.rst +++ b/docs/source/En/doc/installation/installation_doc.rst @@ -1,15 +1,79 @@ Installation ----- +============ -.. code-block:: python +Requirements +------------ + +* Python **3.10** or later +* pip 19.3 or later + +Supported Platforms +~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Platform + - Version + * - Windows + - 10 / 11 + * - macOS + - 10.15 ~ 11 (Big Sur) + * - Linux + - Ubuntu 20.04 + * - Raspberry Pi + - 3B+ + +Basic Installation (CLI & Library) +---------------------------------- + +Install LoadDensity from PyPI: + +.. code-block:: bash pip install je_load_density -* Python & pip require version - * Python 3.7 & up - * pip 19.3 & up +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 + +Development Installation +------------------------- + +To install from source for development: + +.. 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 +------------------- + +After installation, verify that LoadDensity is correctly installed: + +.. code-block:: bash + + python -c "from je_load_density import start_test; print('LoadDensity installed successfully')" + +You can also check the installed version: + +.. code-block:: bash -* Dev env - * windows 11 - * osx 11 big sur - * ubuntu 20.0.4 + pip show je_load_density diff --git a/docs/source/En/doc/package_manager/package_manager_doc.rst b/docs/source/En/doc/package_manager/package_manager_doc.rst new file mode 100644 index 0000000..46e3ef5 --- /dev/null +++ b/docs/source/En/doc/package_manager/package_manager_doc.rst @@ -0,0 +1,76 @@ +Dynamic Package Loading +======================= + +The ``PackageManager`` allows you to dynamically import Python packages at runtime and +register all their public functions into the executor's event dictionary. + +Basic Usage +----------- + +.. code-block:: python + + from je_load_density import executor + + # Load a package and make all its functions available as executor actions + executor.execute_action([ + ["LD_add_package_to_executor", ["my_custom_package"]] + ]) + +After loading, all functions from the package can be called by name in JSON scripts +or via ``executor.execute_action()``. + +How It Works +------------ + +1. Uses ``importlib.util.find_spec()`` to locate the package +2. Imports the package with ``importlib.import_module()`` +3. Uses ``inspect.getmembers()`` with ``isfunction`` to find all functions in the package +4. Registers each function into the executor's ``event_dict`` + +.. note:: + + Only top-level functions in the package are registered. Classes, constants, and + submodules are not automatically added. + +PackageManager API +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Method + - Description + * - ``load_package_if_available(package)`` + - Try to import a package. Returns the module or ``None`` if not found. + * - ``add_package_to_executor(package)`` + - Import a package and register all its functions into the executor. + +Example: Using a Custom Package +-------------------------------- + +Suppose you have a custom package ``my_utils`` with a function ``compute()``: + +.. code-block:: python + + from je_load_density import executor + + # Register the package + executor.execute_action([ + ["LD_add_package_to_executor", ["my_utils"]] + ]) + + # Now you can call compute() by name + executor.execute_action([ + ["compute", [42]] + ]) + +Using in JSON Scripts +--------------------- + +.. code-block:: json + + [ + ["LD_add_package_to_executor", ["my_utils"]], + ["compute", [42]] + ] diff --git a/docs/source/En/doc/scheduler/scheduler_doc.rst b/docs/source/En/doc/scheduler/scheduler_doc.rst index aaf6bd3..97eff2a 100644 --- a/docs/source/En/doc/scheduler/scheduler_doc.rst +++ b/docs/source/En/doc/scheduler/scheduler_doc.rst @@ -1,19 +1,117 @@ Scheduler ----- +========= -You can use scheduling to perform repetitive tasks, either by using a simple wrapper for APScheduler or by consulting the API documentation to use it yourself. +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 import SchedulerManager + 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) +-------------------------------- - def test_scheduler(): - print("Test Scheduler") - scheduler.remove_blocking_job(id="test") - scheduler.shutdown_blocking_scheduler() +.. 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() - scheduler.add_interval_blocking_secondly(function=test_scheduler, id="test") + + 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 new file mode 100644 index 0000000..58f5a82 --- /dev/null +++ b/docs/source/En/doc/socket_server/socket_server_doc.rst @@ -0,0 +1,110 @@ +TCP Socket Server (Remote Execution) +===================================== + +LoadDensity includes a TCP server based on ``gevent`` that accepts JSON commands over the +network, enabling remote test execution. + +Starting the Server +------------------- + +.. code-block:: python + + from je_load_density import start_load_density_socket_server + + # Start server (blocking call) + start_load_density_socket_server(host="localhost", port=9940) + +.. 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. + +.. code-block:: 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() + +Server Protocol +--------------- + +* **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 + +Shutting Down the Server +------------------------ + +Send the string ``"quit_server"`` to gracefully shut down the server: + +.. 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" + sock.close() + +The server will close all connections and print ``Server shutdown complete``. + +Architecture +------------ + +The TCP server consists of two components: + +* **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. + +.. note:: + + ``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. diff --git a/docs/source/En/en_index.rst b/docs/source/En/en_index.rst index 86e6d25..9416629 100644 --- a/docs/source/En/en_index.rst +++ b/docs/source/En/en_index.rst @@ -1,12 +1,20 @@ -LoadDensity English Documentation +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. + .. toctree:: :maxdepth: 4 + :caption: User Guide - doc/installation/installation_doc.rst - doc/getting_started/getting_started_doc.rst - doc/cli/cli_doc.rst - doc/scheduler/scheduler_doc.rst - doc/generate_report/generate_report_doc.rst - + 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/callback/callback_doc + doc/package_manager/package_manager_doc + doc/gui/gui_doc diff --git a/docs/source/Zh/doc/callback/callback_doc.rst b/docs/source/Zh/doc/callback/callback_doc.rst new file mode 100644 index 0000000..aabb643 --- /dev/null +++ b/docs/source/Zh/doc/callback/callback_doc.rst @@ -0,0 +1,132 @@ +回呼執行器 +========== + +``CallbackFunctionExecutor`` 可將觸發函式與回呼函式串連。 +這對於測試後的工作流程非常有用 — 例如,執行測試後自動產生報告。 + +基本用法 +-------- + +.. code-block:: python + + from je_load_density import callback_executor + + 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"}}, + ) + +運作原理 +-------- + +1. 在執行器的 ``event_dict`` 中查找 ``trigger_function_name`` +2. 使用提供的 ``**kwargs`` 執行觸發函式 +3. 觸發函式完成後,呼叫 ``callback_function`` +4. 回傳觸發函式的回傳值 + +可用的觸發函式 +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 觸發名稱 + - 函式 + * - ``user_test`` + - ``start_test()`` — 執行負載測試 + * - ``LD_generate_html`` + - ``generate_html()`` — 產生 HTML 片段 + * - ``LD_generate_html_report`` + - ``generate_html_report()`` — 產生 HTML 報告檔案 + * - ``LD_generate_json`` + - ``generate_json()`` — 產生 JSON 資料 + * - ``LD_generate_json_report`` + - ``generate_json_report()`` — 產生 JSON 報告檔案 + * - ``LD_generate_xml`` + - ``generate_xml()`` — 產生 XML 字串 + * - ``LD_generate_xml_report`` + - ``generate_xml_report()`` — 產生 XML 報告檔案 + +傳遞參數給回呼函式 +-------------------- + +使用關鍵字參數(預設): + +.. code-block:: python + + def my_callback(report_name, format_type): + print(f"正在產生 {format_type} 報告:{report_name}") + + callback_executor.callback_function( + trigger_function_name="user_test", + callback_function=my_callback, + callback_function_param={"report_name": "final", "format_type": "html"}, + callback_param_method="kwargs", + user_detail_dict={"user": "fast_http_user"}, + user_count=10, + spawn_rate=5, + test_time=5, + tasks={"get": {"request_url": "http://httpbin.org/get"}}, + ) + +使用位置參數: + +.. code-block:: python + + def my_callback(arg1, arg2): + print(f"參數:{arg1}, {arg2}") + + callback_executor.callback_function( + trigger_function_name="user_test", + callback_function=my_callback, + callback_function_param=["value1", "value2"], + callback_param_method="args", + user_detail_dict={"user": "fast_http_user"}, + user_count=10, + spawn_rate=5, + test_time=5, + tasks={"get": {"request_url": "http://httpbin.org/get"}}, + ) + +參數說明 +-------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - 參數 + - 類型 + - 說明 + * - ``trigger_function_name`` + - ``str`` + - ``event_dict`` 中要觸發的函式名稱 + * - ``callback_function`` + - ``Callable`` + - 觸發後要執行的回呼函式 + * - ``callback_function_param`` + - ``dict``、``list`` 或 ``None`` + - 回呼函式的參數(dict 用於 kwargs,list 用於 args) + * - ``callback_param_method`` + - ``str`` + - ``"kwargs"``(預設)或 ``"args"`` + * - ``**kwargs`` + - — + - 傳遞給觸發函式的參數 + +錯誤處理 +-------- + +* 以下情況會拋出 ``CallbackExecutorException``: + + * ``trigger_function_name`` 不在 ``event_dict`` 中 + * ``callback_param_method`` 不是 ``"kwargs"`` 或 ``"args"`` diff --git a/docs/source/Zh/doc/cli/cli_doc.rst b/docs/source/Zh/doc/cli/cli_doc.rst index 6ef6878..e982be5 100644 --- a/docs/source/Zh/doc/cli/cli_doc.rst +++ b/docs/source/Zh/doc/cli/cli_doc.rst @@ -1,17 +1,106 @@ -命令列介面 ----- +命令列介面(CLI) +================== -我們可以使用 CLI 模式去執行 keyword.json 檔案或執行包含 Keyword.json files 的資料夾, -以下這個範例是去執行指定路徑的關鍵字 json 檔 +LoadDensity 提供完整的命令列介面,透過 ``python -m je_load_density`` 使用。 -.. code-block:: +CLI 參數 +-------- - python je_load_density --execute_file "your_file_path" +.. list-table:: + :header-rows: 1 + :widths: 25 10 65 + * - 參數 + - 簡寫 + - 說明 + * - ``--execute_file`` + - ``-e`` + - 執行單一 JSON 腳本檔案 + * - ``--execute_dir`` + - ``-d`` + - 執行目錄下所有 JSON 檔案 + * - ``--execute_str`` + - — + - 執行行內 JSON 字串 + * - ``--create_project`` + - ``-c`` + - 建置新專案(包含模板) +執行單一 JSON 檔案 +-------------------- -以下這個範例是去執行指定路徑資料夾下所有的 keyword json 檔 +執行定義在單一 JSON 關鍵字檔案中的測試: -.. code-block:: +.. code-block:: bash - python je_load_density --execute_dir "your_dir_path" \ No newline at end of file + python -m je_load_density -e test_scenario.json + +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"} + } + }] + ] + +執行目錄下所有 JSON 檔案 +-------------------------- + +遞迴執行指定目錄下所有 JSON 關鍵字檔案: + +.. code-block:: bash + + python -m je_load_density -d ./test_scripts/ + +此命令會掃描目錄中所有 ``.json`` 檔案,依序執行。 + +執行行內 JSON 字串 +-------------------- + +直接以字串形式執行 JSON 動作列表: + +.. code-block:: bash + + 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 會自動處理此差異。 + +建立專案 +-------- + +建置包含關鍵字模板與執行器腳本的新專案: + +.. code-block:: bash + + python -m je_load_density -c MyProject + +產生的專案目錄結構: + +.. code-block:: text + + MyProject/ + └── LoadDensity/ + ├── keyword/ + │ ├── keyword1.json + │ └── keyword2.json + └── executor/ + ├── executor_one_file.py + └── executor_folder.py + +錯誤處理 +-------- + +若未提供有效參數,CLI 會拋出 ``LoadDensityTestExecuteException`` 並以結束碼 1 退出。 +所有錯誤訊息會輸出至 stderr。 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 da984cc..d663dc4 100644 --- a/docs/source/Zh/doc/generate_report/generate_report_doc.rst +++ b/docs/source/Zh/doc/generate_report/generate_report_doc.rst @@ -1,68 +1,160 @@ 報告產生 ----- +======== -Generate Report 可以生成以下格式的報告 +LoadDensity 可以產生三種格式的測試報告:**HTML**、**JSON** 和 **XML**。 +報告是根據測試執行期間由 request hook 收集的測試紀錄產生的。 -* HTML -* JSON -* XML -* Generate Report 主要用來記錄與確認有哪些步驟執行,執行是否成功, -* 下面的範例有搭配 keyword and executor 如果看不懂可以先去看看 executor +.. note:: -以下是產生 HTML 的範例。 + 報告只能在測試執行後產生。若不存在測試紀錄,會拋出 + ``LoadDensityHTMLException`` 或 ``LoadDensityGenerateJsonReportException``。 + +HTML 報告 +--------- + +產生帶有樣式的 HTML 檔案,以表格顯示成功與失敗紀錄。 .. code-block:: python - from je_load_density import generate_html_report, start_test - start_test( - { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - } - ) - generate_html_report() + from je_load_density import generate_html_report + + # 產生 "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, start_test - start_test( + 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 格式:** + +.. 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 + { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} + "Failure_Test1": { + "Method": "POST", + "test_url": "http://httpbin.org/status/500", + "name": "/status/500", + "status_code": "500", + "error": "..." } } - ) - generate_json_report() -以下是產生 XML 的範例。 +若要取得原始 JSON 資料結構而不寫入檔案: .. code-block:: python - from je_load_density import generate_xml_report, start_test - start_test( - { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ - "tasks": { - "get": {"request_url": "http://httpbin.org/get"}, - "post": {"request_url": "http://httpbin.org/post"} - } - } - ) - generate_xml_report() \ No newline at end of file + from je_load_density import generate_json + + success_dict, failure_dict = generate_json() + +XML 報告 +-------- + +產生 XML 檔案,適用於 CI/CD 整合。 + +.. 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") + +XML 輸出使用 ``xml.dom.minidom`` 進行格式化。每筆測試紀錄包裝在 ```` 根節點下。 + +若要取得原始 XML 字串而不寫入檔案: + +.. code-block:: python + + from je_load_density import generate_xml + + success_xml_str, failure_xml_str = generate_xml() + +在 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 報告檔案,回傳路徑 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 158a214..c4066dd 100644 --- a/docs/source/Zh/doc/getting_started/getting_started_doc.rst +++ b/docs/source/Zh/doc/getting_started/getting_started_doc.rst @@ -1,49 +1,238 @@ 開始使用 ----- +======== -首先,創建專案。 +本指南將帶您了解如何使用 LoadDensity 執行第一個負載測試。 -在 LoadDensity 裡可以創建專案,創建專案後將會自動生成範例文件, -範例文件包含 python executor 檔案以及 keyword.json 檔案。 +使用者類型 +---------- -要創建專案可以用以下方式: +LoadDensity 支援兩種 Locust 使用者類型: + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - 使用者類型鍵值 + - 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()``: .. code-block:: python - from je_load_density import create_project_dir - # create on current workdir - create_project_dir() - # create project on project_path - create_project_dir("project_path") - # create project on project_path and dir name is My First Project - create_project_dir("project_path", "My First Project") + from je_load_density import start_test -或是這個方式將會在 project_path 路徑產生專案 + result = 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"}, + } + ) -.. code-block:: console +``start_test()`` 參數說明 +~~~~~~~~~~~~~~~~~~~~~~~~~~ - python -m je_load_density --create_project project_path +.. list-table:: + :header-rows: 1 + :widths: 20 15 10 55 -然後可以進入專案資料夾,executor 資料夾,選擇其中一個 executor 執行並觀察, -keyword 資料夾裡的 keyword json 檔案定義了要執行的動作。 + * - 參數 + - 類型 + - 預設值 + - 說明 + * - ``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}`` -如果想要透過純 python 來執行的話可以參考以下範例: +回傳值 +~~~~~~ -注意! 只能使用以下 HTTP Method ["get", "post", "put", "patch", "delete", "head", "options"] +``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 即時監控測試: .. code-block:: python from je_load_density import start_test - start_test( - { - "user": "fast_http_user", - }, - 50, 10, 5, - **{ + result = start_test( + user_detail_dict={"user": "http_user"}, + user_count=100, + spawn_rate=20, + test_time=30, + web_ui_dict={"host": "127.0.0.1", "port": 8089}, + tasks={ + "get": {"request_url": "http://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"} } - } - ) \ No newline at end of file + }] + ] + +從 Python 執行: + +.. code-block:: python + + from je_load_density import execute_action, read_action_json + + execute_action(read_action_json("test_scenario.json")) + +JSON 腳本格式 +~~~~~~~~~~~~~~ + +每個 JSON 腳本是一個動作陣列。每個動作是一個列表: + +* 使用關鍵字參數:``["action_name", {"param1": "value1"}]`` +* 使用位置參數:``["action_name", ["arg1", "arg2"]]`` +* 無參數:``["action_name"]`` + +串連多個動作 +~~~~~~~~~~~~ + +多個動作可以在單一 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"}] + ] + +字典格式 JSON +~~~~~~~~~~~~~~ + +JSON 腳本也可以用字典包裝,使用 ``"load_density"`` 鍵值: + +.. 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"}} + }] + ] + } + +專案建置 +-------- + +LoadDensity 可以自動產生專案目錄結構,包含關鍵字模板與執行器腳本: + +.. 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 + + 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 腳本 diff --git a/docs/source/Zh/doc/gui/gui_doc.rst b/docs/source/Zh/doc/gui/gui_doc.rst new file mode 100644 index 0000000..04e4c37 --- /dev/null +++ b/docs/source/Zh/doc/gui/gui_doc.rst @@ -0,0 +1,87 @@ +GUI(圖形化使用者介面) +====================== + +LoadDensity 包含一個可選的 PySide6 圖形化介面,可透過視覺化表單執行負載測試, +並即時顯示日誌。 + +安裝需求 +-------- + +GUI 需要額外的相依套件。請使用以下方式安裝: + +.. code-block:: bash + + pip install je_load_density[gui] + +這會安裝: + +* **PySide6** (6.10.0) — Qt for Python 綁定 +* **qt-material** — Material Design 主題 + +啟動 GUI +-------- + +.. code-block:: python + + from je_load_density.gui.main_window import LoadDensityUI + from PySide6.QtWidgets import QApplication + import sys + + 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`` 主題 + +語言支援 +-------- + +GUI 支援兩種語言: + +* **英文**(預設) +* **繁體中文** + +語言字串由 ``je_load_density/gui/language_wrapper/`` 下的 ``language_wrapper`` 模組管理。 + +架構 +---- + +GUI 由以下元件組成: + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - 元件 + - 說明 + * - ``LoadDensityUI`` + - 主視窗(``QMainWindow``)。套用主題並包含 widget。 + * - ``LoadDensityWidget`` + - 中央 widget,包含表單輸入、啟動按鈕和日誌面板。 + * - ``LoadDensityGUIThread`` + - 背景 ``QThread``,在不阻塞 UI 的情況下執行負載測試。 + * - ``InterceptAllFilter`` + - 日誌過濾器,將日誌訊息擷取到佇列中供 GUI 顯示。 + * - ``log_message_queue`` + - 執行緒安全的佇列,連接日誌系統與 GUI 日誌面板。 + +.. note:: + + 在 Windows 平台上,GUI 會透過 ``ctypes`` 設定 ``AppUserModelID``, + 讓工作列能正確識別應用程式。 diff --git a/docs/source/Zh/doc/installation/installation_doc.rst b/docs/source/Zh/doc/installation/installation_doc.rst index e637309..ebf5b6f 100644 --- a/docs/source/Zh/doc/installation/installation_doc.rst +++ b/docs/source/Zh/doc/installation/installation_doc.rst @@ -1,15 +1,78 @@ 安裝 ----- +==== -.. code-block:: python +系統需求 +-------- + +* Python **3.10** 或更新版本 +* pip 19.3 或更新版本 + +支援平台 +~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 平台 + - 版本 + * - Windows + - 10 / 11 + * - macOS + - 10.15 ~ 11 (Big Sur) + * - Linux + - Ubuntu 20.04 + * - Raspberry Pi + - 3B+ + +基本安裝(CLI 與函式庫) +-------------------------- + +從 PyPI 安裝 LoadDensity: + +.. code-block:: bash pip install je_load_density -* Python & pip require version - * Python 3.7 & up - * pip 19.3 & up +這會安裝核心函式庫與 CLI 工具。`Locust `_ 會作為相依套件自動安裝。 + +安裝 GUI 支援 +-------------- + +若要使用可選的 PySide6 圖形化介面: + +.. code-block:: bash + + pip install je_load_density[gui] + +這會額外安裝: + +* `PySide6 `_ — Qt for Python 綁定 +* `qt-material `_ — Material Design 主題 + +開發者安裝 +---------- + +從原始碼安裝進行開發: + +.. code-block:: bash + + git clone https://github.com/Intergration-Automation-Testing/LoadDensity.git + cd LoadDensity + pip install -e . + pip install -r dev_requirements.txt + +驗證安裝 +-------- + +安裝後,驗證 LoadDensity 是否正確安裝: + +.. code-block:: bash + + python -c "from je_load_density import start_test; print('LoadDensity 安裝成功')" + +也可以檢查已安裝的版本: + +.. code-block:: bash -* Dev env - * windows 11 - * osx 11 big sur - * ubuntu 20.0.4 + pip show je_load_density diff --git a/docs/source/Zh/doc/package_manager/package_manager_doc.rst b/docs/source/Zh/doc/package_manager/package_manager_doc.rst new file mode 100644 index 0000000..f69484f --- /dev/null +++ b/docs/source/Zh/doc/package_manager/package_manager_doc.rst @@ -0,0 +1,75 @@ +動態套件載入 +============ + +``PackageManager`` 可讓您在執行時動態匯入 Python 套件,並將其所有公開函式註冊到 +執行器的事件字典中。 + +基本用法 +-------- + +.. code-block:: python + + from je_load_density import executor + + # 載入套件並將其所有函式註冊為執行器動作 + executor.execute_action([ + ["LD_add_package_to_executor", ["my_custom_package"]] + ]) + +載入後,套件中的所有函式都可以透過名稱在 JSON 腳本或 +``executor.execute_action()`` 中呼叫。 + +運作原理 +-------- + +1. 使用 ``importlib.util.find_spec()`` 定位套件 +2. 使用 ``importlib.import_module()`` 匯入套件 +3. 使用 ``inspect.getmembers()`` 搭配 ``isfunction`` 找出套件中所有函式 +4. 將每個函式註冊到執行器的 ``event_dict`` + +.. note:: + + 僅會註冊套件中的頂層函式。類別、常數和子模組不會自動加入。 + +PackageManager API +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - 方法 + - 說明 + * - ``load_package_if_available(package)`` + - 嘗試匯入套件。回傳模組或 ``None``(若未找到)。 + * - ``add_package_to_executor(package)`` + - 匯入套件並將其所有函式註冊到執行器。 + +範例:使用自訂套件 +------------------- + +假設您有一個自訂套件 ``my_utils``,其中包含 ``compute()`` 函式: + +.. code-block:: python + + from je_load_density import executor + + # 註冊套件 + executor.execute_action([ + ["LD_add_package_to_executor", ["my_utils"]] + ]) + + # 現在可以透過名稱呼叫 compute() + executor.execute_action([ + ["compute", [42]] + ]) + +在 JSON 腳本中使用 +-------------------- + +.. code-block:: json + + [ + ["LD_add_package_to_executor", ["my_utils"]], + ["compute", [42]] + ] diff --git a/docs/source/Zh/doc/scheduler/scheduler_doc.rst b/docs/source/Zh/doc/scheduler/scheduler_doc.rst index bae98d2..ac6c0f9 100644 --- a/docs/source/Zh/doc/scheduler/scheduler_doc.rst +++ b/docs/source/Zh/doc/scheduler/scheduler_doc.rst @@ -1,19 +1,116 @@ -Scheduler ----- +排程器 +====== -可以使用排程來執行重複的任務,可以使用對 APScheduler 的簡易包裝或是觀看 API 文件自行使用 +LoadDensity 內建排程器,可讓您在指定的時間間隔排程重複執行測試。 +排程器支援阻塞(blocking)與非阻塞(non-blocking)兩種模式。 + +基本用法 +-------- .. code-block:: python - from je_load_density import SchedulerManager + 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 週執行一次 +間隔方法(非阻塞) +~~~~~~~~~~~~~~~~~~~~ - def test_scheduler(): - print("Test Scheduler") - scheduler.remove_blocking_job(id="test") - scheduler.shutdown_blocking_scheduler() +.. 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() - scheduler.add_interval_blocking_secondly(function=test_scheduler, id="test") + + 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 new file mode 100644 index 0000000..5cc1011 --- /dev/null +++ b/docs/source/Zh/doc/socket_server/socket_server_doc.rst @@ -0,0 +1,106 @@ +TCP Socket 伺服器(遠端執行) +============================== + +LoadDensity 內建基於 ``gevent`` 的 TCP 伺服器,可透過網路接收 JSON 指令,實現遠端測試執行。 + +啟動伺服器 +---------- + +.. code-block:: python + + from je_load_density import start_load_density_socket_server + + # 啟動伺服器(阻塞呼叫) + start_load_density_socket_server(host="localhost", port=9940) + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - 參數 + - 類型 + - 預設值 + - 說明 + * - ``host`` + - ``str`` + - ``"localhost"`` + - 伺服器綁定位址 + * - ``port`` + - ``int`` + - ``9940`` + - 伺服器綁定埠號 + +伺服器啟動後會輸出 ``Server started on {host}:{port}``。每個連入的連線會在獨立的 +``gevent`` greenlet 中處理,支援並行請求。 + +從客戶端發送指令 +----------------- + +指令以 JSON 編碼的動作列表發送 — 與 JSON 腳本檔案使用相同的格式。 + +.. code-block:: 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() + +伺服器協定 +---------- + +* **指令格式**:JSON 編碼的動作列表(與 JSON 腳本檔案格式相同) +* **回應**:每個動作的回傳值以一行傳回,最後以 ``Return_Data_Over_JE\n`` 結尾 +* **錯誤處理**:若執行過程中發生錯誤,錯誤訊息會傳回,後接 ``Return_Data_Over_JE\n`` +* **緩衝區大小**:每次接收 8192 bytes + +關閉伺服器 +---------- + +發送字串 ``"quit_server"`` 即可優雅地關閉伺服器: + +.. 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" + sock.close() + +伺服器會關閉所有連線並輸出 ``Server shutdown complete``。 + +架構 +---- + +TCP 伺服器由兩個元件組成: + +* **TCPServer** — 基於 ``gevent.socket`` 的主伺服器類別。監聽連線並為每個客戶端產生 greenlet。 +* **start_load_density_socket_server()** — 便利函式,呼叫 + ``gevent.monkey.patch_all()`` 並啟動伺服器。 + +.. note:: + + 啟動 socket 伺服器時會呼叫 ``gevent.monkey.patch_all()``。這會修補標準函式庫模組 + (socket、threading 等)以相容 gevent。若將 socket 伺服器整合到較大的應用程式中, + 請注意此行為。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index 7f73520..f15f817 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -1,11 +1,19 @@ -LoadDensity 繁體中文文件 +繁體中文文件 ============================================= +歡迎來到 LoadDensity 繁體中文文件。LoadDensity 是一個建構於 Locust 之上的負載與壓力測試自動化框架, +提供簡化的 API、JSON 驅動的測試腳本、多格式報告產生、可選的 GUI 介面,以及遠端執行功能。 + .. toctree:: :maxdepth: 4 + :caption: 使用指南 - doc/installation/installation_doc.rst - doc/getting_started/getting_started_doc.rst - doc/cli/cli_doc.rst - doc/scheduler/scheduler_doc.rst - doc/generate_report/generate_report_doc.rst + 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/callback/callback_doc + doc/package_manager/package_manager_doc + doc/gui/gui_doc diff --git a/docs/source/api/api_index.rst b/docs/source/api/api_index.rst index 55b9dc9..ba5bd7e 100644 --- a/docs/source/api/api_index.rst +++ b/docs/source/api/api_index.rst @@ -1,14 +1,19 @@ -LoadDensity API Documentation ----- +API Reference +============= + +This section provides the complete API reference for LoadDensity, covering the core +load testing functions, executor, report generation, callback executor, socket server, +scheduler, package manager, and file utilities. .. toctree:: :maxdepth: 4 + :caption: API Modules - utils/callback.rst - utils/executor.rst - utils/file.rst - utils/generate_report.rst - utils/package_manager.rst - utils/socket_server.rst - utils/scheduler.rst - loaddensity/loaddensity.rst \ No newline at end of file + loaddensity/loaddensity + utils/executor + utils/callback + utils/generate_report + utils/socket_server + utils/scheduler + utils/package_manager + utils/file diff --git a/docs/source/api/loaddensity/loaddensity.rst b/docs/source/api/loaddensity/loaddensity.rst index 835d977..dc02c23 100644 --- a/docs/source/api/loaddensity/loaddensity.rst +++ b/docs/source/api/loaddensity/loaddensity.rst @@ -1,35 +1,253 @@ -LoadDensity API ----- +LoadDensity Core API +==================== + +The core API provides the main entry points for starting load tests, creating Locust +environments, and accessing test records. + +start_test() +------------ + +The primary function for running a load test. .. code-block:: python def start_test( - user_detail_dict: dict, - user_count: int = 50, spawn_rate: int = 10, test_time: int = 60, - web_ui_dict: dict = None, - **kwargs - ): - """ - :param user_detail_dict: dict use to create user - :param user_count: how many user we want to spawn - :param spawn_rate: one time will spawn how many user - :param test_time: total test run time - :param web_ui_dict: web ui dict include host and port like {"host": "127.0.0.1", "port": 8089} - :param kwargs: to catch unknown param - :return: None - """ + user_detail_dict: Dict[str, Any], + user_count: int = 50, + spawn_rate: int = 10, + test_time: Optional[int] = 60, + web_ui_dict: Optional[Dict[str, Any]] = None, + **kwargs + ) -> Dict[str, Any] + +**Parameters:** + +.. list-table:: + :header-rows: 1 + :widths: 20 15 10 55 + + * - Parameter + - Type + - Default + - Description + * - ``user_detail_dict`` + - ``Dict[str, Any]`` + - (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`` + - ``Optional[int]`` + - ``60`` + - Test duration in seconds. Pass ``None`` for unlimited duration + * - ``web_ui_dict`` + - ``Optional[Dict]`` + - ``None`` + - Enable Locust Web UI. e.g. ``{"host": "127.0.0.1", "port": 8089}`` + * - ``**kwargs`` + - — + - — + - Additional parameters passed to user initialization + +**Returns:** ``Dict[str, Any]`` — Summary dictionary of the test configuration. + +**Raises:** ``ValueError`` — If an unsupported user type is specified. + +**Example:** + +.. code-block:: python + + from je_load_density import start_test + + result = 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"}, + } + ) + +prepare_env() +------------- + +Create a Locust environment, start the runner, and block until the test completes. + +.. code-block:: python + + def prepare_env( + user_class: List[User], + user_count: int = 50, + spawn_rate: int = 10, + test_time: int = 60, + web_ui_dict: dict = None, + **kwargs + ) -> None + +**Parameters:** + +.. list-table:: + :header-rows: 1 + :widths: 20 15 10 55 + + * - Parameter + - Type + - Default + - Description + * - ``user_class`` + - ``List[User]`` + - (required) + - Locust user class to run + * - ``user_count`` + - ``int`` + - ``50`` + - Number of users to spawn + * - ``spawn_rate`` + - ``int`` + - ``10`` + - Users spawned per second + * - ``test_time`` + - ``int`` + - ``60`` + - Test duration in seconds + * - ``web_ui_dict`` + - ``dict`` + - ``None`` + - Web UI configuration ``{"host": str, "port": int}`` + +create_env() +------------ + +Create a Locust ``Environment`` with a local runner and stats collection greenlets. + +.. code-block:: python + + def create_env( + user_class: List[User], + another_event: events = events + ) -> Environment + +**Parameters:** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Parameter + - Type + - Description + * - ``user_class`` + - ``List[User]`` + - Locust user class + * - ``another_event`` + - ``events`` + - Custom Locust event instance (default: ``locust.events``) + +**Returns:** ``locust.env.Environment`` — Configured Locust environment with local runner. + +TestRecord +---------- + +Stores success and failure test records collected by the request hook. .. code-block:: python - def prepare_env(user_class: [User], user_count: int = 50, spawn_rate: int = 10, test_time: int = 60, - web_ui_dict: dict = None, - **kwargs): - """ - :param user_class: locust user class - :param user_count: how many user we want to spawn - :param spawn_rate: one time will spawn how many user - :param test_time: total test run time - :param web_ui_dict: web ui dict include host and port like {"host": "127.0.0.1", "port": 8089} - :param kwargs: to catch unknown param - :return: None - """ + class TestRecord: + test_record_list: List[Dict] # Success records + error_record_list: List[Dict] # Failure records + + def clear_records(self) -> None: ... + +**Global instance:** ``test_record_instance`` + +**Success record fields:** + +.. list-table:: + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Type + - Description + * - ``Method`` + - ``str`` + - HTTP method (GET, POST, etc.) + * - ``test_url`` + - ``str`` + - Request URL + * - ``name`` + - ``str`` + - Request name (Locust grouping name) + * - ``status_code`` + - ``str`` + - HTTP status code + * - ``text`` + - ``str`` + - Response body text + * - ``content`` + - ``str`` + - Response body content (bytes as string) + * - ``headers`` + - ``str`` + - Response headers + * - ``error`` + - ``None`` + - Always ``None`` for success records + +**Failure record fields:** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Field + - Type + - Description + * - ``Method`` + - ``str`` + - HTTP method + * - ``test_url`` + - ``str`` + - Request URL + * - ``name`` + - ``str`` + - Request name + * - ``status_code`` + - ``str`` or ``None`` + - HTTP status code (if available) + * - ``text`` + - ``str`` or ``None`` + - Response body text (if available) + * - ``error`` + - ``str`` + - Exception message + +**Example:** + +.. code-block:: python + + from je_load_density import test_record_instance + + 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"]) + + test_record_instance.clear_records() + +request_hook +------------ + +A Locust event listener that automatically records all requests during test execution. +Registered via ``@events.request.add_listener``. + +This hook is loaded automatically when importing ``je_load_density`` and requires no +manual configuration. diff --git a/docs/source/api/utils/callback.rst b/docs/source/api/utils/callback.rst index 4b69dce..30ef8e0 100644 --- a/docs/source/api/utils/callback.rst +++ b/docs/source/api/utils/callback.rst @@ -1,21 +1,83 @@ Callback Function API ----- +===================== + +The ``CallbackFunctionExecutor`` provides a mechanism to trigger a function from its +event dictionary, then execute a callback function. + +CallbackFunctionExecutor Class +------------------------------- .. code-block:: python - def callback_function( + class CallbackFunctionExecutor: + event_dict: dict[str, Callable] + + def callback_function( self, trigger_function_name: str, - callback_function: typing.Callable, - callback_function_param: [dict, None] = None, + callback_function: Callable, + callback_function_param: Optional[Union[dict, list]] = None, callback_param_method: str = "kwargs", **kwargs - ): - """ - :param trigger_function_name: what function we want to trigger only accept function in event_dict - :param callback_function: what function we want to callback - :param callback_function_param: callback function's param only accept dict - :param callback_param_method: what type param will use on callback function only accept kwargs and args - :param kwargs: trigger_function's param - :return: trigger_function_name return value - """ \ No newline at end of file + ) -> Any: ... + +callback_function() +~~~~~~~~~~~~~~~~~~~ + +Execute a trigger function from ``event_dict``, then call the callback function. + +**Parameters:** + +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - Parameter + - Type + - Description + * - ``trigger_function_name`` + - ``str`` + - Name of function in ``event_dict`` to trigger + * - ``callback_function`` + - ``Callable`` + - Callback function to execute after the trigger + * - ``callback_function_param`` + - ``dict``, ``list``, or ``None`` + - Parameters for callback (dict for kwargs, list for args) + * - ``callback_param_method`` + - ``str`` + - ``"kwargs"`` (default) or ``"args"`` + * - ``**kwargs`` + - — + - Parameters passed to the trigger function + +**Returns:** Return value of the trigger function. + +**Raises:** ``CallbackExecutorException`` — If trigger function not found or invalid +param method. + +Available Trigger Functions +--------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Trigger Name + - Function + * - ``user_test`` + - ``start_test()`` + * - ``LD_generate_html`` + - ``generate_html()`` + * - ``LD_generate_html_report`` + - ``generate_html_report()`` + * - ``LD_generate_json`` + - ``generate_json()`` + * - ``LD_generate_json_report`` + - ``generate_json_report()`` + * - ``LD_generate_xml`` + - ``generate_xml()`` + * - ``LD_generate_xml_report`` + - ``generate_xml_report()`` + +**Global instance:** ``callback_executor`` diff --git a/docs/source/api/utils/executor.rst b/docs/source/api/utils/executor.rst index 48522f1..aec4980 100644 --- a/docs/source/api/utils/executor.rst +++ b/docs/source/api/utils/executor.rst @@ -1,26 +1,124 @@ Executor API ----- +============ + +The ``Executor`` class is the core event-driven system in LoadDensity. It maintains an +``event_dict`` that maps string action names to callable functions. + +Executor Class +-------------- + +.. code-block:: python + + class Executor: + event_dict: dict[str, Any] + + def execute_action(self, action_list: Union[list, dict]) -> dict[str, Any]: ... + def execute_files(self, execute_files_list: list[str]) -> list[dict[str, Any]]: ... + +execute_action() +~~~~~~~~~~~~~~~~ + +Execute a list of actions. + +.. code-block:: python + + def execute_action(self, action_list: Union[list, dict]) -> dict[str, Any] + +**Parameters:** + +* ``action_list`` — A list of actions, where each action is: + + * ``["action_name", {"kwarg1": value}]`` — Call with keyword arguments + * ``["action_name", [arg1, arg2]]`` — Call with positional arguments + * ``["action_name"]`` — Call with no arguments + + Can also be a dict with a ``"load_density"`` key containing the action list. + +**Returns:** ``dict[str, Any]`` — Execution record dictionary mapping action descriptions +to return values. + +execute_files() +~~~~~~~~~~~~~~~ + +Execute actions from multiple JSON files. + +.. code-block:: python + + def execute_files(self, execute_files_list: list[str]) -> list[dict[str, Any]] + +**Parameters:** + +* ``execute_files_list`` — List of JSON file paths to execute + +**Returns:** ``list[dict[str, Any]]`` — List of execution results per file. + +add_command_to_executor() +------------------------- + +Add custom commands to the global executor. .. code-block:: python - def execute_action(self, action_list: [list, dict]) -> dict: - """ - use to execute all action on action list(action file or program list) - :param action_list the list include action - for loop the list and execute action - """ + def add_command_to_executor(command_dict: dict[str, Any]) -> None + +**Parameters:** + +* ``command_dict`` — Dictionary mapping command names to functions. + Only ``types.MethodType`` and ``types.FunctionType`` are accepted. + +**Raises:** ``LoadDensityTestExecuteException`` — If a non-callable is provided. + +**Example:** .. code-block:: python - def execute_files(self, execute_files_list: list) -> list: - """ - :param execute_files_list: list include execute files path - :return: every execute detail as list - """ + from je_load_density import add_command_to_executor, executor + + def my_action(msg): + print(f"Custom: {msg}") + + add_command_to_executor({"my_action": my_action}) + executor.execute_action([["my_action", ["Hello"]]]) + +Built-in Actions +---------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Action Name + - Description + * - ``LD_start_test`` + - Start a load test + * - ``LD_generate_html`` + - Generate HTML fragments (returns data) + * - ``LD_generate_html_report`` + - Generate HTML report file + * - ``LD_generate_json`` + - Generate JSON data structures (returns data) + * - ``LD_generate_json_report`` + - Generate JSON report files + * - ``LD_generate_xml`` + - Generate XML strings (returns data) + * - ``LD_generate_xml_report`` + - Generate XML report files + * - ``LD_execute_action`` + - Execute a list of actions (recursive) + * - ``LD_execute_files`` + - Execute actions from multiple files + * - ``LD_add_package_to_executor`` + - Dynamically load a package into executor + +Additionally, all Python built-in functions (``print``, ``len``, ``type``, etc.) are +automatically registered. + +Global Convenience Functions +---------------------------- .. code-block:: python - def add_command_to_executor(command_dict: dict): - """ - :param command_dict: dict include command we want to add to event_dict - """ \ No newline at end of file + def execute_action(action_list: list) -> dict[str, Any] + def execute_files(execute_files_list: list[str]) -> list[dict[str, Any]] + +These call the corresponding methods on the global ``executor`` instance. diff --git a/docs/source/api/utils/file.rst b/docs/source/api/utils/file.rst index 1b1614a..adb42a5 100644 --- a/docs/source/api/utils/file.rst +++ b/docs/source/api/utils/file.rst @@ -1,14 +1,82 @@ -File process API ----- +File Processing API +=================== + +Utility functions for file and directory operations. + +get_dir_files_as_list() +----------------------- + +Get all files in a directory that match a given file extension. .. code-block:: python def get_dir_files_as_list( - dir_path: str = getcwd(), - default_search_file_extension: str = ".json") -> List[str]: - """ - get dir file when end with default_search_file_extension - :param dir_path: which dir we want to walk and get file list - :param default_search_file_extension: which extension we want to search - :return: [] if nothing searched or [file1, file2.... files] file was searched - """ \ No newline at end of file + dir_path: str = str(Path.cwd()), + default_search_file_extension: str = ".json" + ) -> List[str] + +**Parameters:** + +.. list-table:: + :header-rows: 1 + :widths: 30 15 55 + + * - Parameter + - Type + - Description + * - ``dir_path`` + - ``str`` + - Directory path to search (default: current working directory) + * - ``default_search_file_extension`` + - ``str`` + - File extension to filter (default: ``".json"``) + +**Returns:** ``List[str]`` — List of absolute file paths matching the extension. +Returns an empty list if the directory does not exist or an error occurs. + +The search is recursive (uses ``Path.rglob()``). + +**Example:** + +.. code-block:: python + + from je_load_density import get_dir_files_as_list + + # Get all JSON files in a directory + files = get_dir_files_as_list("./test_scripts/") + + # Get all Python files + files = get_dir_files_as_list("./src/", ".py") + +read_action_json() +------------------ + +Read a JSON action file and return its content. + +.. code-block:: python + + def read_action_json(json_file_path: str) -> Union[dict, list] + +**Parameters:** + +* ``json_file_path`` — Path to the JSON file + +**Returns:** ``dict`` or ``list`` — Parsed JSON content. + +**Raises:** ``LoadDensityTestJsonException`` — If the file cannot be found or read. + +write_action_json() +------------------- + +Write data to a JSON file. + +.. code-block:: python + + def write_action_json(json_save_path: str, action_json: Union[dict, list]) -> None + +**Parameters:** + +* ``json_save_path`` — Path to save the JSON file +* ``action_json`` — Data to write (dict or list) + +**Raises:** ``LoadDensityTestJsonException`` — If the file cannot be written. diff --git a/docs/source/api/utils/generate_report.rst b/docs/source/api/utils/generate_report.rst index b534d80..59edb25 100644 --- a/docs/source/api/utils/generate_report.rst +++ b/docs/source/api/utils/generate_report.rst @@ -1,47 +1,103 @@ -Generate Report API ----- +Report Generation API +===================== + +Functions for generating test reports in HTML, JSON, and XML formats. + +HTML Report +----------- + +generate_html() +~~~~~~~~~~~~~~~ + +Generate HTML fragments for success and failure records. .. code-block:: python - def generate_html() -> str: - """ - this function will create html string - :return: html_string - """ + def generate_html() -> Tuple[List[str], List[str]] + +**Returns:** ``(success_list, failure_list)`` — Lists of HTML table strings. + +**Raises:** ``LoadDensityHTMLException`` — If no test records exist. + +generate_html_report() +~~~~~~~~~~~~~~~~~~~~~~ + +Generate a complete HTML report file. .. code-block:: python - def generate_html_report(html_name: str = "default_name"): - """ - Output html report file - :param html_name: save html file name - """ + def generate_html_report(html_name: str = "default_name") -> str + +**Parameters:** + +* ``html_name`` — Output file name (without extension). Creates ``{html_name}.html``. + +**Returns:** File path of the generated HTML report. + +JSON Report +----------- + +generate_json() +~~~~~~~~~~~~~~~ + +Generate JSON data structures for success and failure records. .. code-block:: python - def generate_json(): - """ - :return: two dict {success_dict}, {failure_dict} - """ + def generate_json() -> Tuple[Dict[str, dict], Dict[str, dict]] + +**Returns:** ``(success_dict, failure_dict)`` + +* ``success_dict`` — Keys like ``"Success_Test1"``, values contain Method, test_url, name, + status_code, text, content, headers +* ``failure_dict`` — Keys like ``"Failure_Test1"``, values contain Method, test_url, name, + status_code, error + +**Raises:** ``LoadDensityGenerateJsonReportException`` — If no test records exist. + +generate_json_report() +~~~~~~~~~~~~~~~~~~~~~~ + +Generate JSON report files. .. code-block:: python - def generate_json_report(json_file_name: str = "default_name"): - """ - Output json report file - :param json_file_name: save json file's name - """ + def generate_json_report(json_file_name: str = "default_name") -> Tuple[str, str] + +**Parameters:** + +* ``json_file_name`` — Output file name prefix. Creates ``{name}_success.json`` and + ``{name}_failure.json``. + +**Returns:** ``(success_path, failure_path)`` + +XML Report +---------- + +generate_xml() +~~~~~~~~~~~~~~ + +Generate XML strings for success and failure records. .. code-block:: python - def generate_xml(): - """ - :return: two dict {success_dict}, {failure_dict} - """ + def generate_xml() -> Tuple[str, str] + +**Returns:** ``(success_xml_str, failure_xml_str)`` — XML strings wrapped under +```` root element. + +generate_xml_report() +~~~~~~~~~~~~~~~~~~~~~ + +Generate pretty-printed XML report files. .. code-block:: python - def generate_xml_report(xml_file_name: str = "default_name"): - """ - :param xml_file_name: save xml file name - """ \ No newline at end of file + def generate_xml_report(xml_file_name: str = "default_name") -> Tuple[str, str] + +**Parameters:** + +* ``xml_file_name`` — Output file name prefix. Creates ``{name}_success.xml`` and + ``{name}_failure.xml``. + +**Returns:** ``(success_path, failure_path)`` diff --git a/docs/source/api/utils/package_manager.rst b/docs/source/api/utils/package_manager.rst index d3276d1..817c241 100644 --- a/docs/source/api/utils/package_manager.rst +++ b/docs/source/api/utils/package_manager.rst @@ -1,101 +1,53 @@ Package Manager API ----- +=================== + +The ``PackageManager`` class provides dynamic package loading and registration into +the executor's event dictionary. + +PackageManager Class +-------------------- .. code-block:: python - from importlib import import_module - from importlib.util import find_spec - from inspect import getmembers, isfunction, isbuiltin, isclass - from sys import stderr - - - class PackageManager(object): - - def __init__(self): - self.installed_package_dict = { - } - self.executor = None - self.callback_executor = None - - def check_package(self, package: str): - """ - :param package: package to check exists or not - :return: package if find else None - """ - if self.installed_package_dict.get(package, None) is None: - found_spec = find_spec(package) - if found_spec is not None: - try: - installed_package = import_module(found_spec.name) - self.installed_package_dict.update( - {found_spec.name: installed_package}) - except ModuleNotFoundError as error: - print(repr(error), file=stderr) - return self.installed_package_dict.get(package, None) - - def add_package_to_executor(self, package): - """ - :param package: package's function will add to executor - """ - self.add_package_to_target( - package=package, - target=self.executor - ) - - def add_package_to_callback_executor(self, package): - """ - :param package: package's function will add to callback_executor - """ - self.add_package_to_target( - package=package, - target=self.callback_executor - ) - - def get_member(self, package, predicate, target): - """ - :param package: package we want to get member - :param predicate: predicate - :param target: which event_dict will be added - """ - installed_package = self.check_package(package) - if installed_package is not None and target is not None: - for member in getmembers(installed_package, predicate): - target.event_dict.update( - {str(package) + "_" + str(member[0]): member[1]}) - elif installed_package is None: - print(repr(ModuleNotFoundError(f"Can't find package {package}")), - file=stderr) - else: - print(f"Executor error {self.executor}", file=stderr) - - def add_package_to_target(self, package, target): - """ - :param package: package we want to get member - :param target: which event_dict will be added - """ - try: - self.get_member( - package=package, - predicate=isfunction, - target=target - ) - self.get_member( - package=package, - predicate=isbuiltin, - target=target - ) - self.get_member( - package=package, - predicate=isfunction, - target=target - ) - self.get_member( - package=package, - predicate=isclass, - target=target - ) - except Exception as error: - print(repr(error), file=stderr) - - - package_manager = PackageManager() \ No newline at end of file + class PackageManager: + installed_package_dict: dict[str, Any] + executor: Optional[Any] + + def load_package_if_available(self, package: str) -> Optional[Any]: ... + def add_package_to_executor(self, package: str) -> None: ... + +load_package_if_available() +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Try to import a package and cache it. + +.. code-block:: python + + def load_package_if_available(self, package: str) -> Optional[Any] + +**Parameters:** + +* ``package`` — Package name to import + +**Returns:** The imported module, or ``None`` if the package cannot be found. + +Uses ``importlib.util.find_spec()`` to locate the package and ``importlib.import_module()`` +to import it. Successfully imported packages are cached in ``installed_package_dict``. + +add_package_to_executor() +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Import a package and register all its functions into the executor's ``event_dict``. + +.. code-block:: python + + def add_package_to_executor(self, package: str) -> None + +**Parameters:** + +* ``package`` — Package name to load and register + +Uses ``inspect.getmembers()`` with ``isfunction`` predicate to find all functions +in the package. + +**Global instance:** ``package_manager`` diff --git a/docs/source/api/utils/scheduler.rst b/docs/source/api/utils/scheduler.rst index 7ee025f..00a39f2 100644 --- a/docs/source/api/utils/scheduler.rst +++ b/docs/source/api/utils/scheduler.rst @@ -1,192 +1,134 @@ Scheduler API ----- +============= -.. code-block:: python - - def add_blocking_job( - self, func: Callable, trigger: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, id: str = None, name: str = None, - misfire_grace_time: int = undefined, coalesce: bool = undefined, max_instances: int = undefined, - next_run_time: datetime = undefined, jobstore: str = 'default', executor: str = 'default', - replace_existing: bool = False, **trigger_args: Any) -> Job: - """ - Just an apscheduler add job wrapper. - :param func: callable (or a textual reference to one) to run at the given time - :param str|apscheduler.triggers.base.BaseTrigger trigger: trigger that determines when - ``func`` is called - :param list|tuple args: list of positional arguments to call func with - :param dict kwargs: dict of keyword arguments to call func with - :param str|unicode id: explicit identifier for the job (for modifying it later) - :param str|unicode name: textual description of the job - :param int misfire_grace_time: seconds after the designated runtime that the job is still - allowed to be run (or ``None`` to allow the job to run no matter how late it is) - :param bool coalesce: run once instead of many times if the scheduler determines that the - job should be run more than once in succession - :param int max_instances: maximum number of concurrently running instances allowed for this - job - :param datetime next_run_time: when to first run the job, regardless of the trigger (pass - ``None`` to add the job as paused) - :param str|unicode jobstore: alias of the job store to store the job in - :param str|unicode executor: alias of the executor to run the job with - :param bool replace_existing: ``True`` to replace an existing job with the same ``id`` - (but retain the number of runs from the existing one) - :return: Job - """ - -.. code-block:: python - - def add_nonblocking_job( - self, func: Callable, trigger: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, id: str = None, name: str = None, - misfire_grace_time: int = undefined, coalesce: bool = undefined, max_instances: int = undefined, - next_run_time: datetime = undefined, jobstore: str = 'default', executor: str = 'default', - replace_existing: bool = False, **trigger_args: Any) -> Job: - """ - Just an apscheduler add job wrapper. - :param func: callable (or a textual reference to one) to run at the given time - :param str|apscheduler.triggers.base.BaseTrigger trigger: trigger that determines when - ``func`` is called - :param list|tuple args: list of positional arguments to call func with - :param dict kwargs: dict of keyword arguments to call func with - :param str|unicode id: explicit identifier for the job (for modifying it later) - :param str|unicode name: textual description of the job - :param int misfire_grace_time: seconds after the designated runtime that the job is still - allowed to be run (or ``None`` to allow the job to run no matter how late it is) - :param bool coalesce: run once instead of many times if the scheduler determines that the - job should be run more than once in succession - :param int max_instances: maximum number of concurrently running instances allowed for this - job - :param datetime next_run_time: when to first run the job, regardless of the trigger (pass - ``None`` to add the job as paused) - :param str|unicode jobstore: alias of the job store to store the job in - :param str|unicode executor: alias of the executor to run the job with - :param bool replace_existing: ``True`` to replace an existing job with the same ``id`` - (but retain the number of runs from the existing one) - :return: Job - """ - -.. code-block:: python - - def get_blocking_scheduler(self) -> BlockingScheduler: - """ - Return self blocking scheduler - :return: BlockingScheduler - """ +The ``SchedulerManager`` class wraps APScheduler to provide both blocking and non-blocking +job scheduling. -.. code-block:: python - - def get_nonblocking_scheduler(self) -> BackgroundScheduler: - """ - Return self background scheduler - :return: BackgroundScheduler - """ - -.. code-block:: python - - def start_block_scheduler(self, *args: Any, **kwargs: Any) -> None: - """ - Start blocking scheduler - :return: None - """ +SchedulerManager Class +---------------------- -.. code-block:: python +Scheduler Control +~~~~~~~~~~~~~~~~~ - def start_nonblocking_scheduler(self, *args: Any, **kwargs: Any) -> None: - """ - Start background scheduler - :return: None - """ +.. list-table:: + :header-rows: 1 + :widths: 45 55 -.. code-block:: python + * - Method + - Description + * - ``start_block_scheduler()`` + - Start the blocking scheduler (blocks current thread) + * - ``start_nonblocking_scheduler()`` + - Start the background scheduler (non-blocking) + * - ``start_all_scheduler()`` + - Start both blocking and non-blocking schedulers + * - ``get_blocking_scheduler()`` + - Returns the ``BlockingScheduler`` instance + * - ``get_nonblocking_scheduler()`` + - Returns the ``BackgroundScheduler`` instance + * - ``shutdown_blocking_scheduler(wait=False)`` + - Shutdown the blocking scheduler + * - ``shutdown_nonblocking_scheduler(wait=False)`` + - Shutdown the non-blocking scheduler - def start_all_scheduler(self, *args: Any, **kwargs: Any) -> None: - """ - Start background and blocking scheduler - :return: None - """ +Interval Methods (Blocking) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python def add_interval_blocking_secondly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, seconds: int = 1, **trigger_args: Any) -> Job: + self, function: Callable, id: str = None, + args: Union[list, tuple] = None, kwargs: dict = None, + seconds: int = 1, **trigger_args + ) -> Job -.. code-block:: python + def add_interval_blocking_minutely(self, function, id=None, args=None, kwargs=None, minutes=1, **trigger_args) -> Job + def add_interval_blocking_hourly(self, function, id=None, args=None, kwargs=None, hours=1, **trigger_args) -> Job + def add_interval_blocking_daily(self, function, id=None, args=None, kwargs=None, days=1, **trigger_args) -> Job + def add_interval_blocking_weekly(self, function, id=None, args=None, kwargs=None, weeks=1, **trigger_args) -> Job - def add_interval_blocking_minutely( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, minutes: int = 1, **trigger_args: Any) -> Job: +**Common Parameters:** -.. code-block:: python +.. list-table:: + :header-rows: 1 + :widths: 20 15 65 - def add_interval_blocking_hourly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, hours: int = 1, **trigger_args: Any) -> Job: + * - Parameter + - Type + - Description + * - ``function`` + - ``Callable`` + - Function to execute on schedule + * - ``id`` + - ``str`` + - Unique job identifier (for later removal) + * - ``args`` + - ``list`` or ``tuple`` + - Positional arguments for the function + * - ``kwargs`` + - ``dict`` + - Keyword arguments for the function + * - ``seconds/minutes/hours/days/weeks`` + - ``int`` + - Interval duration -.. code-block:: python +**Returns:** ``Job`` — APScheduler Job instance. - def add_interval_blocking_daily( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, days: int = 1, **trigger_args: Any) -> Job: +Interval Methods (Non-blocking) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python - def add_interval_blocking_weekly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, weeks: int = 1, **trigger_args: Any) -> Job: + def add_interval_nonblocking_secondly(self, function, id=None, args=None, kwargs=None, seconds=1, **trigger_args) -> Job + def add_interval_nonblocking_minutely(self, function, id=None, args=None, kwargs=None, minutes=1, **trigger_args) -> Job + def add_interval_nonblocking_hourly(self, function, id=None, args=None, kwargs=None, hours=1, **trigger_args) -> Job + def add_interval_nonblocking_daily(self, function, id=None, args=None, kwargs=None, days=1, **trigger_args) -> Job + def add_interval_nonblocking_weekly(self, function, id=None, args=None, kwargs=None, weeks=1, **trigger_args) -> Job -.. code-block:: python - - def add_interval_nonblocking_secondly( - self, function: Callable, id: str = None, args: list = None, - kwargs: dict = None, seconds: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_nonblocking_minutely( - self, function: Callable, id: str = None, args: list = None, - kwargs: dict = None, minutes: int = 1, **trigger_args: Any) -> Job: +Same parameters as blocking variants, but jobs are scheduled on the background scheduler. -.. code-block:: python - - def add_interval_nonblocking_hourly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, hours: int = 1, **trigger_args: Any) -> Job: +Cron Methods +~~~~~~~~~~~~ .. code-block:: python - def add_interval_nonblocking_daily( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, days: int = 1, **trigger_args: Any) -> Job: + def add_cron_blocking(self, function: Callable, id: str = None, **trigger_args) -> Job + def add_cron_nonblocking(self, function: Callable, id: str = None, **trigger_args) -> Job -.. code-block:: python +Add cron-style scheduled jobs. Pass cron arguments via ``**trigger_args``. - def add_interval_nonblocking_weekly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, weeks: int = 1, **trigger_args: Any) -> Job: +Job Management +~~~~~~~~~~~~~~ .. code-block:: python - def add_cron_blocking( - self, function: Callable, id: str = None, **trigger_args: Any) -> Job: + def remove_blocking_job(self, id: str, jobstore: str = 'default') -> Any + def remove_nonblocking_job(self, id: str, jobstore: str = 'default') -> Any -.. code-block:: python +Remove a job by its ID from the specified jobstore. - def add_cron_nonblocking( - self, function: Callable, id: str = None, **trigger_args: Any) -> Job: +Low-level Job Methods +~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python - def remove_blocking_job(self, id: str, jobstore: str = 'default') -> Any: - -.. code-block:: python - - def remove_nonblocking_job(self, id: str, jobstore: str = 'default') -> Any: - -.. code-block:: python - - def shutdown_blocking_scheduler(self, wait: bool = False) -> None: - -.. code-block:: python + def add_blocking_job( + self, func: Callable, trigger=None, args=None, kwargs=None, + id=None, name=None, misfire_grace_time=undefined, + coalesce=undefined, max_instances=undefined, + next_run_time=undefined, jobstore='default', + executor='default', replace_existing=False, + **trigger_args + ) -> Job - def shutdown_nonblocking_scheduler(self, wait: bool = False) -> None: + def add_nonblocking_job( + self, func: Callable, trigger=None, args=None, kwargs=None, + id=None, name=None, misfire_grace_time=undefined, + coalesce=undefined, max_instances=undefined, + next_run_time=undefined, jobstore='default', + executor='default', replace_existing=False, + **trigger_args + ) -> Job + +Direct wrappers around APScheduler's ``add_job()`` method, providing full control over +job configuration. diff --git a/docs/source/api/utils/socket_server.rst b/docs/source/api/utils/socket_server.rst index 2bae132..66c3562 100644 --- a/docs/source/api/utils/socket_server.rst +++ b/docs/source/api/utils/socket_server.rst @@ -1,60 +1,77 @@ Socket Server API ----- +================= + +A TCP server based on ``gevent`` for remote test execution via JSON commands. + +TCPServer Class +--------------- + +.. code-block:: python + + class TCPServer: + close_flag: bool + server: socket.socket + + def socket_server(self, host: str, port: int) -> None: ... + def handle(self, connection: socket.socket) -> None: ... + +socket_server() +~~~~~~~~~~~~~~~ + +Start the TCP server. This is a blocking call. + +**Parameters:** + +* ``host`` — Server bind address +* ``port`` — Server bind port + +The server listens for connections and spawns a ``gevent`` greenlet for each client. + +handle() +~~~~~~~~ + +Handle a single client connection. + +* Receives up to 8192 bytes +* Parses the received data as JSON +* Executes the actions via ``execute_action()`` +* Sends results back line by line, terminated by ``Return_Data_Over_JE\n`` +* Special command ``"quit_server"`` shuts down the server + +start_load_density_socket_server() +---------------------------------- + +Convenience function to start the LoadDensity TCP server. .. code-block:: python - import json - import socketserver - import sys - import threading - - from je_auto_control.utils.executor.action_executor import execute_action - - - class TCPServerHandler(socketserver.BaseRequestHandler): - - def handle(self): - command_string = str(self.request.recv(8192).strip(), encoding="utf-8") - socket = self.request - print("command is: " + command_string, flush=True) - if command_string == "quit_server": - self.server.shutdown() - self.server.close_flag = True - print("Now quit server", flush=True) - else: - try: - execute_str = json.loads(command_string) - for execute_function, execute_return in execute_action(execute_str).items(): - socket.sendto(str(execute_return).encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - except Exception as error: - print(repr(error), file=sys.stderr) - try: - socket.sendto(str(error).encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - except Exception as error: - print(repr(error)) - - - class TCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): - - def __init__(self, server_address, RequestHandlerClass): - super().__init__(server_address, RequestHandlerClass) - self.close_flag: bool = False - - - def start_autocontrol_socket_server(host: str = "localhost", port: int = 9938): - if len(sys.argv) == 2: - host = sys.argv[1] - elif len(sys.argv) == 3: - host = sys.argv[1] - port = int(sys.argv[2]) - server = TCPServer((host, port), TCPServerHandler) - server_thread = threading.Thread(target=server.serve_forever) - server_thread.daemon = True - server_thread.start() - return server \ No newline at end of file + def start_load_density_socket_server( + host: str = "localhost", + port: int = 9940 + ) -> TCPServer + +**Parameters:** + +.. 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 + +**Returns:** ``TCPServer`` instance. + +.. note:: + + This function calls ``gevent.monkey.patch_all()`` before starting the server, + which patches standard library modules for gevent compatibility. diff --git a/docs/source/conf.py b/docs/source/conf.py index ec8b43c..9214756 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,53 +1,37 @@ # Configuration file for the Sphinx documentation builder. # -# This file only contains a selection of the most common options. For a full -# list see the documentation: +# For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +# -- Path setup -------------------------------------------------------------- +sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- - project = 'LoadDensity' copyright = '2022, JE-Chen' author = 'JE-Chen' # The full version, including alpha/beta/rc tags -release = '0.0.01' +release = '0.0.65' # -- General configuration --------------------------------------------------- +extensions = [] -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ -] - -# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'sphinx_rtd_theme' -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] + +# -- Options for HTML theme -------------------------------------------------- +html_theme_options = { + 'navigation_depth': 4, + 'collapse_navigation': False, + 'titles_only': False, +} diff --git a/docs/source/index.rst b/docs/source/index.rst index 9b55d69..5ffc108 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,22 @@ Welcome to LoadDensity's documentation! ============================================= +**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. + +.. note:: + + - **PyPI**: https://pypi.org/project/je_load_density/ + - **Source Code**: https://github.com/Intergration-Automation-Testing/LoadDensity + - **License**: MIT + .. toctree:: :maxdepth: 4 + :caption: Contents - En/en_index.rst - Zh/zh_index.rst - api/api_index.rst + En/en_index + Zh/zh_index + api/api_index