├── .circleci └── config.yml ├── .gitea └── workflows │ ├── build-python-wheels_on_linux.yml │ └── build-python-wheels_on_win.yml ├── .github └── workflows │ ├── build_wheels_centos8.yml │ ├── build_wheels_linux(musl).yml │ ├── build_wheels_linux.yml │ ├── build_wheels_macos.yml │ └── build_wheels_windows.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── build.sh ├── img ├── architecture.png ├── atomic-bomb-engine-logo.png └── img.png ├── pyproject.toml ├── python └── atomic_bomb_engine │ ├── __init__.py │ ├── __init__.pyi │ ├── dist │ └── .gitkeep │ ├── middleware.py │ └── server.py └── src ├── lib.rs ├── py_lib ├── assert_option_func.rs ├── batch_runner.rs ├── endpoint_func.rs ├── jsonpath_extract_func.rs ├── mod.rs ├── multipart_option_func.rs ├── setup_option_func.rs ├── step_option_func.rs └── think_time_option_func.rs └── utils ├── create_api_results_dict.rs ├── create_assert_err_dict.rs ├── create_http_err_dict.rs ├── mod.rs ├── parse_api_endpoints.rs ├── parse_assert_options.rs ├── parse_multipart_options.rs ├── parse_setup_options.rs └── parse_step_options.rs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | version: 2 5 | build_and_deploy: 6 | jobs: 7 | - build-wheels: 8 | filters: 9 | branches: 10 | only: 11 | - release 12 | 13 | jobs: 14 | build-wheels: 15 | macos: 16 | xcode: "11.3.0" 17 | steps: 18 | - checkout 19 | 20 | - run: 21 | name: Checkout frontend 22 | command: | 23 | git clone https://github.com/GiantAxeWhy/atomic-bomb-engine-front vue-project 24 | 25 | - run: 26 | name: Start Alpine container 27 | command: | 28 | docker run --name python-alpine-container -d -v "${PWD}:/__w" -w "/__w" alpine:latest sleep 3600 29 | 30 | - run: 31 | name: Install Node.js and build tools 32 | command: | 33 | docker exec python-alpine-container apk add --no-cache nodejs npm build-base 34 | 35 | - run: 36 | name: Build frontend 37 | command: | 38 | docker exec python-alpine-container sh -c "cd vue-project && npm install && npm run build" 39 | 40 | - run: 41 | name: Install pyenv and specific Python versions 42 | command: | 43 | docker exec python-alpine-container apk add --no-cache bash git curl openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev 44 | docker exec python-alpine-container sh -c "curl https://pyenv.run | bash" 45 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" && for version in $(echo 3.8 3.9 3.10 3.11 3.12); do pyenv install $version; pyenv global $version; curl https://bootstrap.pypa.io/get-pip.py | python; done' 46 | 47 | - run: 48 | name: Install Rust, maturin, and patchelf 49 | command: | 50 | docker exec python-alpine-container apk add --no-cache rust cargo patchelf 51 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/shims:$PATH" && pip install maturin' 52 | 53 | - run: 54 | name: Build wheels using maturin 55 | command: | 56 | docker exec python-alpine-container sh -c ' 57 | export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" 58 | for version in $(echo 3.8 3.9 3.10 3.11 3.12); do 59 | pyenv global $version 60 | maturin build --release 61 | done' 62 | 63 | - run: 64 | name: Copy wheels to Runner 65 | command: docker cp python-alpine-container:/__w/target/wheels ./ 66 | 67 | - run: 68 | name: Install Twine 69 | command: pip install twine 70 | 71 | - run: 72 | name: Upload wheels to PyPI 73 | command: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ wheels/*.whl 74 | environment: 75 | TWINE_USERNAME: __token__ 76 | TWINE_PASSWORD: $PYPI_PASSWORD 77 | -------------------------------------------------------------------------------- /.gitea/workflows/build-python-wheels_on_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build Python Wheels on Linux 2 | on: 3 | push: 4 | branches: 5 | - release 6 | jobs: 7 | build: 8 | runs-on: linux_amd64 9 | 10 | env: 11 | http_proxy: http://10.0.0.54:1080 12 | https_proxy: http://10.0.0.54:1080 13 | all_proxy: socks5://10.0.0.54:1080 14 | 15 | steps: 16 | - name: 签出代码 17 | uses: actions/checkout@v4 18 | 19 | - name: 设置cargo环境变量 20 | run: echo "PATH=$PATH:/home/qyzhg/.cargo/bin" >> $GITHUB_ENV 21 | 22 | - name: 构建python包 23 | run: /home/qyzhg/maturin-builder/bin/maturin build --release -i python3.8 -i python3.9 -i python3.10 -i python3.11 -i python3.12 24 | 25 | - name: 上传构建的whl包到PyPI 26 | run: | 27 | /home/qyzhg/maturin-builder/bin/twine upload --repository pypi --config-file /home/qyzhg/.pypirc ./target/wheels/* 28 | -------------------------------------------------------------------------------- /.gitea/workflows/build-python-wheels_on_win.yml: -------------------------------------------------------------------------------- 1 | name: Build Python Wheels on Windows 2 | on: 3 | push: 4 | branches: 5 | - release 6 | jobs: 7 | build: 8 | runs-on: win_amd64 9 | # Start-Process -FilePath "C:\Users\Administrator\act_runner.exe" -ArgumentList "daemon -c C:\Users\Administrator\.runner" 10 | 11 | env: 12 | http_proxy: http://10.0.0.54:1080 13 | https_proxy: http://10.0.0.54:1080 14 | all_proxy: socks5://10.0.0.54:1080 15 | 16 | steps: 17 | - name: 签出代码 18 | uses: actions/checkout@v3 19 | - name: 构建python包 20 | run: | 21 | cargo --version 22 | C:\Users\Administrator\rust-build\Scripts\maturin.exe build --release --interpreter python3.8 --interpreter python3.9 --interpreter python3.10 --interpreter python3.11 --interpreter python3.12 23 | env: 24 | PYTHONIOENCODING: utf-8 25 | - name: 上传构建的whl包到PyPI 26 | run: | 27 | C:\Users\Administrator\rust-build\Scripts\twine.exe upload --repository pypi --config-file C:\Users\Administrator\.pypirc .\target\wheels\* 28 | env: 29 | PYTHONIOENCODING: utf-8 30 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_centos8.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for CentOS 8 2 | 3 | on: 4 | push: 5 | branches: 6 | - build-centos8 7 | 8 | jobs: 9 | build-wheels: 10 | name: Build wheels on CentOS 8 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 15 | 16 | steps: 17 | - name: 签出代码 18 | uses: actions/checkout@v2 19 | 20 | - name: 签出前端 21 | uses: actions/checkout@v2 22 | with: 23 | repository: GiantAxeWhy/atomic-bomb-engine-front 24 | path: vue-project 25 | 26 | - name: 启动 Node.js 容器 27 | run: | 28 | docker run --name node-container -d -v "${{ github.workspace }}:/workspace" -w "/workspace/vue-project" node:18 sleep 3600 29 | 30 | - name: 构建前端 31 | run: | 32 | docker exec node-container npm install 33 | docker exec node-container npm run build 34 | 35 | - name: 停止并删除 Node.js 容器 36 | run: | 37 | docker stop node-container 38 | docker rm node-container 39 | 40 | - name: 启动 AlmaLinux 容器 41 | run: | 42 | docker run --name python-almalinux-container -d -v "${{ github.workspace }}:/workspace" -w "/workspace" almalinux:8 sleep 3600 43 | 44 | - name: 安装构建工具 45 | run: | 46 | docker exec python-almalinux-container yum install -y epel-release 47 | docker exec python-almalinux-container yum install -y gcc gcc-c++ make 48 | 49 | - name: 安装 pyenv 和指定版本的 Python 50 | run: | 51 | docker exec python-almalinux-container yum install -y git curl openssl-devel bzip2-devel zlib-devel readline-devel sqlite-devel 52 | docker exec python-almalinux-container sh -c "curl https://pyenv.run | bash" 53 | docker exec python-almalinux-container sh -c 'export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" && for version in ${{ join(matrix.python-version, ' ') }}; do pyenv install $version; pyenv global $version; curl https://bootstrap.pypa.io/get-pip.py | python; done' 54 | 55 | - name: 安装 Rust、maturin 和 patchelf 56 | run: | 57 | docker exec python-almalinux-container yum remove -y patchelf 58 | docker exec python-almalinux-container yum install -y epel-release 59 | docker exec python-almalinux-container yum install -y https://download-ib01.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/p/patchelf-0.12-1.el8.x86_64.rpm 60 | docker exec python-almalinux-container patchelf --version 61 | docker exec python-almalinux-container yum install -y rust cargo 62 | docker exec python-almalinux-container sh -c 'export PATH="/root/.pyenv/shims:$PATH" && pip install maturin' 63 | 64 | - name: 使用 maturin 构建 wheels 65 | run: | 66 | docker exec python-almalinux-container sh -c ' 67 | export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" 68 | for version in ${{ join(matrix.python-version, ' ') }}; do 69 | pyenv global $version 70 | maturin build --release --skip-auditwheel 71 | done' 72 | 73 | - name: 复制 wheels 到 Runner 74 | run: docker cp python-almalinux-container:/workspace/target/wheels ./ 75 | 76 | - name: 安装 Twine 77 | run: pip install twine 78 | 79 | - name: 上传 wheels 到 PyPI 80 | env: 81 | TWINE_USERNAME: __token__ 82 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 83 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ wheels/*.whl 84 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_linux(musl).yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for musl/Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels: 11 | name: Build wheels on Alpine 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 16 | 17 | steps: 18 | - name: 签出代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 启动 Alpine 容器 28 | run: | 29 | docker run --name python-alpine-container -d -v "${{ github.workspace }}:/__w" -w "/__w" alpine:latest sleep 3600 30 | 31 | - name: 安装 Node.js 和构建工具 32 | run: | 33 | docker exec python-alpine-container apk add --no-cache nodejs npm build-base 34 | 35 | - name: 构建前端 36 | run: | 37 | docker exec python-alpine-container sh -c "cd vue-project && npm install && npm run build" 38 | 39 | - name: 安装 pyenv 和指定版本的 Python 40 | run: | 41 | docker exec python-alpine-container apk add --no-cache bash git curl openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev 42 | docker exec python-alpine-container sh -c "curl https://pyenv.run | bash" 43 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" && for version in ${{ join(matrix.python-version, ' ') }}; do pyenv install $version; pyenv global $version; curl https://bootstrap.pypa.io/get-pip.py | python; done' 44 | 45 | - name: 安装 Rust、maturin 和 patchelf 46 | run: | 47 | docker exec python-alpine-container apk add --no-cache rust cargo patchelf 48 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/shims:$PATH" && pip install maturin' 49 | 50 | - name: 使用 maturin 构建 wheels 51 | run: | 52 | docker exec python-alpine-container sh -c ' 53 | export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" 54 | for version in ${{ join(matrix.python-version, ' ') }}; do 55 | pyenv global $version 56 | maturin build --release 57 | done' 58 | 59 | - name: 复制 wheels 到 Runner 60 | run: docker cp python-alpine-container:/__w/target/wheels ./ 61 | 62 | - name: 安装 Twine 63 | run: pip install twine 64 | 65 | - name: 上传 wheels 到 PyPI 66 | env: 67 | TWINE_USERNAME: __token__ 68 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 69 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ wheels/*.whl 70 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels: 11 | name: Build wheels on Ubuntu 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 16 | 17 | steps: 18 | - name: 签出rust代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 安装Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '18' 31 | 32 | - name: 构建前端 33 | run: | 34 | cd vue-project 35 | npm install 36 | npm run build 37 | 38 | - name: 删除原有dist 39 | run: rm -rf python/atomic_bomb_engine/dist/* 40 | 41 | - name: 复制dist到Python项目 42 | run: cp -r vue-project/dist/* python/atomic_bomb_engine/dist/ 43 | 44 | - name: 设置python环境 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | 49 | - name: 安装Rust 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | override: true 55 | 56 | - name: 安装maturin 57 | run: pip install maturin 58 | 59 | - name: 构建wheels 60 | run: maturin build --release --interpreter python${{ matrix.python-version }} 61 | 62 | - name: 安装Twine 63 | run: pip install twine 64 | 65 | - name: 上传wheels到PyPI 66 | env: 67 | TWINE_USERNAME: __token__ 68 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 69 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ target/wheels/*.whl 70 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_macos.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for macOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels-macos: 11 | # runs-on: macos-latest 12 | runs-on: macos-12 13 | strategy: 14 | matrix: 15 | arch: ['x86_64-apple-darwin', 'aarch64-apple-darwin'] 16 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 17 | 18 | steps: 19 | - name: 签出rust代码 20 | uses: actions/checkout@v2 21 | 22 | - name: 签出前端 23 | uses: actions/checkout@v2 24 | with: 25 | repository: GiantAxeWhy/atomic-bomb-engine-front 26 | path: vue-project 27 | 28 | - name: 安装Node.js 29 | uses: actions/setup-node@v2 30 | with: 31 | node-version: '18' 32 | 33 | - name: 构建前端 34 | run: | 35 | cd vue-project 36 | npm install 37 | npm run build 38 | 39 | - name: 删除原有dist 40 | run: rm -rf python/atomic_bomb_engine/dist/* 41 | 42 | - name: 复制dist到Python项目 43 | run: cp -r vue-project/dist/* python/atomic_bomb_engine/dist/ 44 | 45 | - name: 设置python环境 46 | uses: actions/setup-python@v5 47 | with: 48 | python-version: ${{ matrix.python-version }} 49 | 50 | - name: 安装Rust 51 | uses: actions-rs/toolchain@v1 52 | with: 53 | profile: minimal 54 | toolchain: stable 55 | override: true 56 | 57 | - name: 为目标架构添加Rust目标 58 | run: | 59 | rustup target add x86_64-apple-darwin 60 | rustup target add aarch64-apple-darwin 61 | 62 | - name: 安装maturin 63 | run: pip install maturin 64 | 65 | - name: 构建wheels 66 | run: maturin build --release --target ${{ matrix.arch }} -i python${{ matrix.python-version }} 67 | 68 | - name: 安装Twine 69 | run: pip install twine 70 | 71 | - name: 上传wheels到PyPI 72 | env: 73 | TWINE_USERNAME: __token__ 74 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 75 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ target/wheels/*.whl -------------------------------------------------------------------------------- /.github/workflows/build_wheels_windows.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels: 11 | name: Build wheels on Windows 12 | runs-on: windows-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 16 | 17 | steps: 18 | - name: 签出rust代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 安装Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '18' 31 | 32 | - name: 构建前端 33 | run: | 34 | cd vue-project 35 | npm install 36 | npm run build 37 | 38 | - name: 清空目标路径 39 | run: Remove-Item -Path python\atomic_bomb_engine\dist\* -Recurse -Force 40 | 41 | - name: 复制dist到Python项目 42 | run: Copy-Item vue-project/dist/* python\atomic_bomb_engine\dist\ -Recurse 43 | 44 | - name: 设置python环境 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | 49 | - name: 安装Rust 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | override: true 55 | 56 | - name: 安装maturin 57 | run: pip install maturin 58 | 59 | - name: 构建wheels 60 | run: maturin build --release --interpreter python${{ matrix.python-version }} 61 | 62 | - name: 安装Twine 63 | run: pip install twine 64 | 65 | - name: 上传wheels到PyPI 66 | env: 67 | TWINE_USERNAME: __token__ 68 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 69 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ target/wheels/*.whl 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.venv 3 | node_modules 4 | .DS_Store 5 | 6 | .idea/ 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atomic-bomb-engine-py" 3 | version = "0.41.3" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "atomic_bomb_engine" 8 | crate-type = ["cdylib"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | atomic-bomb-engine = "0.41.1" 14 | #atomic-bomb-engine = { git = "https://github.com/we-lsp/atomic-bomb-engine.git", branch = "ExponentialMovingAverage"} 15 | tokio = "1.36.0" 16 | pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "async-std"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = { version = "1.0", features = [] } 19 | serde-pyobject = "0.2.1" 20 | async-std = "1.12.0" 21 | async-stream = "0.3.5" 22 | futures = "0.3.30" 23 | tokio-stream = "0.1.15" 24 | anyhow = "1.0.83" 25 | 26 | [build] 27 | rustflags = ["-C", "target-feature=+crt-static"] 28 | 29 | [dependencies.pyo3] 30 | version = "0.20.3" 31 | features = ["extension-module", "auto-initialize"] 32 | 33 | [tool.maturin] 34 | name = "atomic_bomb_engine" 35 | scripts = "python/atomic_bomb_engine" 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atomic-bomb-engine-py 2 | #### [atomic-bomb-engine](https://github.com/we-lsp/atomic-bomb-engine)的python包装实现 3 | 4 | logo 5 | 6 | 7 | ## 前端仓库 8 | #### [atomic-bomb-engine-front](https://github.com/GiantAxeWhy/atomic-bomb-engine-front) 9 | 10 | ## 使用条件: 11 | - python版本 >= 3.8 12 | - windows(x86), linux(x86), mac 13 | 14 | ## 使用方法: 15 | - ### 准备开始 16 | 通过pip安装 (0.5.0版本之前) 17 | ```shell 18 | pip install atomic-bomb-engine-py 19 | ``` 20 | 在python中引用时注意,需要引用atomic_bomb_engine, 而不是atomic_bomb_engine_py 21 |
为了避免混淆,0.5.0版本之后,pip更换了包名,更改为atomic-bomb-engine, 22 | ```shell 23 | pip install atomic-bomb-engine 24 | ``` 25 | 在python中导入 26 | ```python 27 | import atomic_bomb_engine 28 | ``` 29 | 异步使用的时候,还需要引用asyncio 30 | ```python 31 | import asyncio 32 | ``` 33 | - ### 开始压测 34 | - ~~单接口压测~~ (功能与多接口压测重叠,已废除) 35 | 36 | - 多接口压测 37 | 38 | 多接口压测可以先使用 39 | ```python 40 | runner = atomic_bomb_engine.BatchRunner() 41 | ``` 42 | 实例化一个runner类 43 | 通过runner类中的run方法开启压测 44 | run方法函数签名如下 45 | ```python 46 | def run( 47 | self, 48 | test_duration_secs: int, 49 | concurrent_requests: int, 50 | api_endpoints:List[Dict], 51 | step_option:Dict[str, int]|None=None, 52 | setup_options:List[Dict[str, Any]]|None=None, 53 | verbose:bool=False, 54 | should_prevent:bool=False, 55 | assert_channel_buffer_size:int=1024, 56 | timeout_secs=0, 57 | cookie_store_enable=True 58 | ) -> None: 59 | """ 60 | 批量压测 61 | :param test_duration_secs: 测试持续时间 62 | :param concurrent_requests: 并发数 63 | :param api_endpoints: 接口信息 64 | :param step_option: 阶梯加压选项 65 | :param setup_options: 初始化选项 66 | :param verbose: 打印详细信息 67 | :param should_prevent: 是否禁用睡眠 68 | :param assert_channel_buffer_size: 断言队列buffer大小 69 | :param timeout_secs: http超时时间 70 | :param cookie_store_enable: 是否为客户端启用持久性cookie存储。 71 | """ 72 | ``` 73 | 74 | 使用assert_option方法可以返回断言选项字典 75 | ```python 76 | assert_options=[ 77 | atomic_bomb_engine.assert_option("$.code", 429), 78 | atomic_bomb_engine.assert_option("$.code", 200) 79 | ]) 80 | ``` 81 | jsonpath如果不会用的话,建议去[jsonpath](https://jsonpath.com/)学习 82 | 83 | 使用step_option方法可以返回阶梯加压选项字典 84 | ```python 85 | def step_option(increase_step: int, increase_interval: int) -> Dict[str, int]: 86 | """ 87 | 生成step option 88 | :param increase_step: 阶梯步长 89 | :param increase_interval: 阶梯间隔 90 | """ 91 | ``` 92 | 93 | 同样的本包中也包含了一个对api_endpoint的包装:endpoint方法,方便调用,endpoint中的assert_options中也可以套用assert_option方法 94 | ```python 95 | async def run_batch(): 96 | result = await atomic_bomb_engine.batch_async( 97 | # 测试持续时间 98 | test_duration_secs=60, 99 | # 并发量 100 | concurrent_requests=200, 101 | # 阶梯设置(每5秒增加30个并发) 102 | step_option=atomic_bomb_engine.step_option(increase_step=30, increase_interval=5), 103 | # 接口超时时间 104 | timeout_secs=10, 105 | # 是否开启客户端启用持久性cookie存储 106 | cookie_store_enable=True, 107 | # 全局初始化 108 | setup_options=[ 109 | atomic_bomb_engine.setup_option( 110 | name="初始化-1", 111 | url="http://localhost:8080/setup", 112 | method="get", 113 | jsonpath_extract=[ 114 | atomic_bomb_engine.jsonpath_extract_option(key="test-msg", jsonpath="$.msg"), 115 | atomic_bomb_engine.jsonpath_extract_option(key="test-code", jsonpath="$.code"), 116 | ] 117 | )], 118 | # 是否开启详细日志 119 | verbose=False, 120 | # 被压接口设置 121 | api_endpoints=[ 122 | atomic_bomb_engine.endpoint( 123 | # 接口任务命名 124 | name="test-1", 125 | # 针对每个接口初始化 126 | setup_options=[ 127 | atomic_bomb_engine.setup_option( 128 | name="api-初始化-1", 129 | url="http://localhost:8080/api_setup", 130 | method="get", 131 | jsonpath_extract=[ 132 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 133 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 134 | ] 135 | ) 136 | ], 137 | # 被压接口url 138 | url="http://localhost:8080/direct", 139 | # 请求方式 140 | method="POST", 141 | # 权重 142 | weight=1, 143 | # 发送json请求 144 | json={"name": "{{api-test-msg-1}}", "number": 1}, 145 | # 断言选项 146 | assert_options=[ 147 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 148 | ], 149 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 150 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 151 | ), 152 | ]) 153 | print(result) 154 | return result 155 | ``` 156 | 157 | 监听时可以在使用完run方法后,继续迭代runner即可 158 | 159 | 压测+同时监听 160 | 161 | ```python 162 | import asyncio 163 | 164 | 165 | async def batch_async(): 166 | runner = atomic_bomb_engine.BatchRunner() 167 | runner.run( 168 | test_duration_secs=30, 169 | concurrent_requests=30, 170 | step_option=atomic_bomb_engine.step_option(increase_step=3, increase_interval=3), 171 | timeout_secs=15, 172 | cookie_store_enable=True, 173 | verbose=False, 174 | api_endpoints=[ 175 | atomic_bomb_engine.endpoint( 176 | name="test-1", 177 | url="http://127.0.0.1:8080/direct", 178 | method="POST", 179 | json={"name": "test-1", "number": 1}, 180 | weight=100, 181 | assert_options=[ 182 | atomic_bomb_engine.assert_option(jsonpath="$.msg", reference_object="操作成功"), 183 | ], 184 | ), 185 | ]) 186 | return runner 187 | 188 | 189 | async def main(): 190 | results = await batch_async() 191 | for res in results: 192 | if res.get("should_wait"): 193 | await asyncio.sleep(0.1) 194 | print(res) 195 | 196 | 197 | if __name__ == '__main__': 198 | asyncio.run(main()) 199 | ``` 200 | 201 | # 压测时使用ui界面监控 202 | 203 | 0.5.0版本后,添加了ui页面,支持批量压测方法 204 |
导入 205 | ```python 206 | from atomic_bomb_engine import server 207 | ``` 208 | 使用 209 | ```python 210 | import asyncio 211 | 212 | import atomic_bomb_engine 213 | from atomic_bomb_engine import server 214 | 215 | 216 | @server.ui(port=8000) 217 | async def batch_async(): 218 | runner = atomic_bomb_engine.BatchRunner() 219 | runner.run( 220 | # 测试持续时间 221 | test_duration_secs=60, 222 | # 并发量 223 | concurrent_requests=200, 224 | # 阶梯设置(每5秒增加30个并发) 225 | step_option=atomic_bomb_engine.step_option(increase_step=30, increase_interval=5), 226 | # 接口超时时间 227 | timeout_secs=10, 228 | # 是否开启客户端启用持久性cookie存储 229 | cookie_store_enable=True, 230 | # 全局初始化 231 | setup_options=[ 232 | atomic_bomb_engine.setup_option( 233 | name="初始化-1", 234 | url="http://localhost:8080/setup", 235 | method="get", 236 | jsonpath_extract=[ 237 | atomic_bomb_engine.jsonpath_extract_option(key="test-msg", jsonpath="$.msg"), 238 | atomic_bomb_engine.jsonpath_extract_option(key="test-code", jsonpath="$.code"), 239 | ] 240 | )], 241 | # 是否开启详细日志 242 | verbose=False, 243 | # 被压接口设置 244 | api_endpoints=[ 245 | atomic_bomb_engine.endpoint( 246 | # 接口任务命名 247 | name="test-1", 248 | # 针对每个接口初始化 249 | setup_options=[ 250 | atomic_bomb_engine.setup_option( 251 | name="api-初始化-1", 252 | url="http://localhost:8080/api_setup", 253 | method="get", 254 | jsonpath_extract=[ 255 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 256 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 257 | ] 258 | ) 259 | ], 260 | # 被压接口url 261 | url="http://localhost:8080/direct", 262 | # 请求方式 263 | method="POST", 264 | # 权重 265 | weight=1, 266 | # 发送json请求 267 | json={"name": "{{api-test-msg-1}}", "number": 1}, 268 | # 断言选项 269 | assert_options=[ 270 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 271 | ], 272 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 273 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 274 | ), 275 | ]) 276 | return runner 277 | 278 | 279 | if __name__ == '__main__': 280 | asyncio.run(batch_async()) 281 | ``` 282 | 283 | 使用server.ui装饰器,可以给批量压测方法启动一个简单的web服务器,不需要再手动监听BatchListenIter生成器 284 | 285 | ## 内部架构图 286 | ![architecture.png](img/architecture.png) 287 | 288 | ## [0.19.0] - 2024-04-16 289 | ### Added 290 | - 增加了初始化和参数模版功能 291 | ```python 292 | setup_options=[ 293 | atomic_bomb_engine.setup_option( 294 | name="初始化-1", 295 | url="http://localhost:8080/setup", 296 | method="get", 297 | jsonpath_extract=[ 298 | atomic_bomb_engine.jsonpath_extract_option(key="test-msg", jsonpath="$.msg"), 299 | atomic_bomb_engine.jsonpath_extract_option(key="test-code", jsonpath="$.code"), 300 | ] 301 | )] 302 | ``` 303 | 上述实例展示了如何在初始化的时候调用某个接口,并且通过jsonpath将数据提取出来,保存在全局变量test-msg和test-code中 304 | 提取完全局变量后,就可以在后续的api_endpoints中使用 305 | ```python 306 | api_endpoints=[ 307 | atomic_bomb_engine.endpoint( 308 | # 接口任务命名 309 | name="test-1", 310 | # 针对每个接口初始化 311 | setup_options=[ 312 | atomic_bomb_engine.setup_option( 313 | name="api-初始化-1", 314 | url="http://localhost:8080/api_setup", 315 | method="get", 316 | jsonpath_extract=[ 317 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 318 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 319 | ] 320 | ) 321 | ], 322 | # 被压接口url 323 | url="http://localhost:8080/direct", 324 | # 请求方式 325 | method="POST", 326 | # 权重 327 | weight=1, 328 | # 发送json请求 329 | json={"name": "{{api-test-msg-1}}", "number": 1}, 330 | # 断言选项 331 | assert_options=[ 332 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 333 | ], 334 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 335 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 336 | ), 337 | ] 338 | ``` 339 | 上述实例展示了如何在请求中使用全局变量,使用双大括号即可使用 340 | 341 | ### Fixed 342 | - 修复了如果http状态码错误时,不会记录 343 | - 修复了json反序列化的问题 344 | 345 | ## [0.20.0] - 2024-04-17 346 | ### Added 347 | 断言更改为异步生产消费,提升性能 348 | 349 | ## bug和需求 350 | - 如果发现了bug,把复现步骤一起写到Issus中哈 351 | - 如果有需求也可以在Issues中讨论 352 | - 本程序是本人业余时间开发,不太准备保证时效性,但是如果有时间,一定第一时间回复和修改bug 353 | 354 | ## [0.22.0] - 2024-04-18 355 | ### Added 356 | 前端进行了性能优化 357 | 358 | ## [0.24.0] - 2024-04-22 359 | ### Added 360 | 异步断言使用了补偿消息,保证消息的一致性 361 | 362 | ## [0.25.0] - 2024-04-23 363 | ### Added 364 | 在endpoints中增加思考时间,模拟用户行为 365 | ```python 366 | think_time_option(min_millis=200, max_millis=300) 367 | ``` 368 | - min_millis:最小思考时间(毫秒) 369 | - max_millis:最大思考时间(毫秒) 370 | 371 | 使用时在endpoint中增加think_time_option参数 372 | 373 | ```python 374 | api_endpoints=[ 375 | atomic_bomb_engine.endpoint( 376 | name="test-1", 377 | url="http://localhost:8080/a", 378 | method="POST", 379 | weight=1, 380 | timeout_secs=10, 381 | json={"name": "{{test-msg}}", "number": "{{test-code}}"}, 382 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=200, max_millis=300), 383 | ), 384 | ] 385 | ``` 386 | 387 | ## [0.26.0] - 2024-04-24 388 | ### Added 389 | - 增加endpoint中的setup,在并发中可以做接口断言 390 | - 增加有关联条件下的cookie自动管理功能 391 | ```python 392 | atomic_bomb_engine.endpoint( 393 | # 接口任务命名 394 | name="test-1", 395 | # 针对每个接口初始化 396 | setup_options=[ 397 | atomic_bomb_engine.setup_option( 398 | name="api-初始化-1", 399 | url="http://localhost:8080/api_setup", 400 | method="get", 401 | jsonpath_extract=[ 402 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 403 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 404 | ] 405 | ) 406 | ], 407 | # 被压接口url 408 | url="http://localhost:8080/direct", 409 | # 请求方式 410 | method="POST", 411 | # 权重 412 | weight=1, 413 | # 发送json请求 414 | json={"name": "{{api-test-msg-1}}", "number": 1}, 415 | # 断言选项 416 | assert_options=[ 417 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 418 | ], 419 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 420 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 421 | ) 422 | ``` 423 | - 参数cookie_store_enable控制是否自动管理cookie,前置条件的cookie会带入到最终的压测接口中 424 | - 在endpoint中使用setup_options可以传入多个接口,并且提取参数 425 | - 提取到的参数如果和全局的setup的key冲突,会覆盖全局提取到的参数 426 | - 接口中提取的参数只能在本线程(v-user)中使用 427 | - ⚠️ 使用时注意:setup_options是顺序执行的,没有并发,但是相当于添加了think time 428 | 429 | ## [0.28.0] - 2024-04-25 430 | ### Added 431 | - 将持久化cookie添加到全局选项中 432 | - 复用http client 433 | - 选择性开启断言任务 434 | - 接口初始化时出现错误等待后重试## 435 | 436 | ## [0.29.0] - 2024-04-25 437 | ### Added 438 | - 优化并发逻辑 439 | - 前端更改为web worker发送心跳 440 | 441 | ## [0.38.0] - 2024-05-7 442 | ### Added 443 | - 增加附件上传功能 444 | - 在初始化和每个接口中增加了multipart_options参数用于附件上传 445 | - 增加multipart_option方法封装附件参数 446 | - form_key: form表单的key 447 | - path: 附件路径 448 | - file_name: 附件名 449 | - mime: 附件类型 (类型可以参考[这里](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)) 450 | ```python 451 | api_endpoints=[ 452 | atomic_bomb_engine.endpoint( 453 | name="test-file", 454 | url="http://127.0.0.1:8888/upload", 455 | method="post", 456 | weight=100, 457 | multipart_options=[atomic_bomb_engine.multipart_option(form_key="file", path="./ui.py", file_name="ui.py", mime="text/plain")], 458 | assert_options=[ 459 | atomic_bomb_engine.assert_option(jsonpath="$.message", reference_object="File uploaded successfully!"), 460 | ], 461 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 462 | ),] 463 | ``` 464 | 465 | ## [0.39.0] - 2024-05-15 466 | ### Added 467 | - 启用BatchRunner类,每次执行可以返回一个迭代器 468 | - 废除run_batch方法 469 | - 废除ResultsIter迭代器 470 | 471 | ## [0.40.0] - 2024-05-16 472 | ### Added 473 | - 将rps统计改为滑动窗口的形式 474 | 475 | ## [0.41.0] - 2024-05-20 476 | ### Added 477 | - run方法增加指数滑动平均参数: ema_alpha 478 | - 参数为0-1之间的一个浮点数 479 | - 参数为0时不启用 480 | - 数值越大越平滑,但是失真越多 481 | - 建议使用0.1以下 482 | 483 | ## bug和需求 484 | - 如果发现了bug,把复现步骤一起写到Issus中哈 485 | - 如果有需求也可以在Issues中讨论 486 | - 本程序是本人业余时间开发,不太准备保证时效性,但是如果有时间,一定第一时间回复和修改bug 487 | 488 | ## TODO 489 | - [x] 前端展示页面 ✅ 490 | - [x] 接口关联 ✅ 491 | - [x] 每个接口可以配置思考时间 ✅ 492 | - [x] 增加form支持 ✅ 493 | - [ ] 增加代理支持 494 | - [x] 增加附件支持 ✅ 495 | - [ ] 断言支持不等于等更多表达方式 496 | 497 | ## 联系方式 498 | - 邮箱:[qyzhg@qyzhg.com](mailto:qyzhg@qyzhg.com) 499 | - 微信:qy-zhg 500 | 501 | ## 👏🏻👏🏻👏🏻欢迎加群交流 502 | ![img.png](img/img.png) 503 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | maturin build --release --target x86_64-apple-darwin -i python3.8 -i python3.9 -i python3.10 -i python3.11 -i python3.12 && 2 | maturin build --release -i python3.8 -i python3.9 -i python3.10 -i python3.11 -i python3.12 && 3 | twine upload --repository pypi --config-file ~/.pypirc ./target/wheels/* && 4 | rm ./target/wheels/* 5 | -------------------------------------------------------------------------------- /img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/817ccbc9c8fa20c972e2108c097a34712a697764/img/architecture.png -------------------------------------------------------------------------------- /img/atomic-bomb-engine-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/817ccbc9c8fa20c972e2108c097a34712a697764/img/atomic-bomb-engine-logo.png -------------------------------------------------------------------------------- /img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/817ccbc9c8fa20c972e2108c097a34712a697764/img/img.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.4"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "atomic_bomb_engine" 7 | version = "0.41.3" 8 | description = "使用rust开发的高性能python压测工具" 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/qyzhg/atomic-bomb-engine-py" 12 | keywords = ["rust", "python", "binding"] 13 | classifiers = [ 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Rust", 16 | "Operating System :: OS Independent", 17 | ] 18 | authors = [{name = "qyzhg", email = "qyzhg@qyzhg.com"}] 19 | dependencies = ["aiohttp", "aiosqlite"] 20 | 21 | 22 | [tool.maturin] 23 | name = "atomic_bomb_engine" 24 | python-source = "python" 25 | 26 | [tool.poetry.include] 27 | include = ["python/atomic_bomb_engine/dist/**/*"] 28 | -------------------------------------------------------------------------------- /python/atomic_bomb_engine/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: (Apache-2.0 OR MIT) 2 | 3 | from .atomic_bomb_engine import * 4 | 5 | __doc__ = atomic_bomb_engine.__doc__ 6 | if hasattr(atomic_bomb_engine, "__all__"): 7 | __all__ = atomic_bomb_engine.__all__ 8 | 9 | -------------------------------------------------------------------------------- /python/atomic_bomb_engine/__init__.pyi: -------------------------------------------------------------------------------- 1 | from typing import Iterator, Optional, List, Dict, Any 2 | 3 | def assert_option(jsonpath: str, reference_object: any) -> Dict[str, Any]: 4 | """ 5 | 生成assert option 6 | :param jsonpath: jsonpath取值地址 7 | :param reference_object: 断言的值 8 | """ 9 | 10 | 11 | def step_option(increase_step: int, increase_interval: int) -> Dict[str, int]: 12 | """ 13 | 生成step option 14 | :param increase_step: 阶梯步长 15 | :param increase_interval: 阶梯间隔 16 | """ 17 | 18 | def think_time_option(min_millis: int, max_millis: int) -> Dict[str, int]: 19 | """ 20 | 思考时间选项 21 | :param min_millis: 22 | :param max_millis: 23 | :return: 24 | """ 25 | 26 | def endpoint( 27 | name: str, 28 | url: str, 29 | method: str, 30 | weight: int, 31 | json: Dict | None = None, 32 | form_data: Dict | None = None, 33 | multipart_options: List[Dict]| None = None, 34 | headers: Dict | None = None, 35 | cookies: str | None = None, 36 | assert_options: List | None = None, 37 | think_time_option: Dict[str, int] | None = None, 38 | setup_options: List| None = None, 39 | ) -> Dict[str, Any]: 40 | """ 41 | 生成endpoint 42 | :param assert_options: 43 | :param form_data: 44 | :param name: 接口名称 45 | :param url: 接口地址 46 | :param method: 请求方法 47 | :param weight 权重 48 | :param json: 请求json 49 | :param form_data: 请求form表单 50 | :multipart_options: 附件 51 | :param headers: 请求头 52 | :param cookies: cookie 53 | :param assert_options: 断言参数 54 | :param think_time_option: 思考时间 55 | :param setup_options: 接口初始化选项 56 | """ 57 | 58 | 59 | def setup_option( 60 | name: str, 61 | url: str, 62 | method: str, 63 | json: Dict| None = None, 64 | form_data: Dict| None = None, 65 | multipart_options: List[Dict]| None = None, 66 | headers: Dict| None = None, 67 | cookies: str | None = None, 68 | jsonpath_extract: List| None = None) ->Dict[str, Any]: 69 | """ 70 | 初始化选项 71 | :param name: 接口名称 72 | :param url: 接口地址 73 | :param method: 请求方法 74 | :param json: 请求json 75 | :param form_data: 请求form表单 76 | :multipart_options: 附件 77 | :param headers: 请求头 78 | :param cookies: cookie 79 | :param jsonpath_extract: 通过jsonpath提取参数 80 | :return: 81 | """ 82 | 83 | def jsonpath_extract_option(key: str, jsonpath: str) -> Dict[str, str]: 84 | """ 85 | jsonpath提取参数设置 86 | :param key: 全局key 87 | :param jsonpath: 提取jsonpath路径 88 | :return: 89 | """ 90 | 91 | def multipart_option( 92 | form_key: str, 93 | path: str, 94 | file_name: str, 95 | mime: str) -> Dict: 96 | """ 97 | 上传附件选项 98 | :param form_key: form表单的key,根据服务端选择,e.g: file, file1 99 | :param path: 文件路径 100 | :param file_name: 文件名 101 | :param mime: 文件类型,e.g: application/octet-stream,可以参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 102 | """ 103 | 104 | class BatchRunner: 105 | def __init__(self) -> None: 106 | ... 107 | 108 | def run( 109 | self, 110 | test_duration_secs: int, 111 | concurrent_requests: int, 112 | api_endpoints:List[Dict], 113 | step_option:Dict[str, int]|None=None, 114 | setup_options:List[Dict[str, Any]]|None=None, 115 | verbose:bool=False, 116 | should_prevent:bool=False, 117 | assert_channel_buffer_size:int=1024, 118 | timeout_secs=0, 119 | cookie_store_enable=True, 120 | ema_alpha: float=0, 121 | ) -> None: 122 | """ 123 | 批量压测 124 | :param test_duration_secs: 测试持续时间 125 | :param concurrent_requests: 并发数 126 | :param api_endpoints: 接口信息 127 | :param step_option: 阶梯加压选项 128 | :param setup_options: 初始化选项 129 | :param verbose: 打印详细信息 130 | :param should_prevent: 是否禁用睡眠 131 | :param assert_channel_buffer_size: 断言队列buffer大小 132 | :param timeout_secs: http超时时间 133 | :param cookie_store_enable: 是否为客户端启用持久性cookie存储。 134 | :param ema_alpha: 指数滑动平均参数,0-1之间,0为不使用,值越大曲线越平滑,但是越失真,建议使用0.1以下 135 | """ 136 | ... 137 | 138 | def __iter__(self) -> 'BatchRunner': 139 | ... 140 | 141 | def __next__(self) -> Optional[Any]: 142 | ... -------------------------------------------------------------------------------- /python/atomic_bomb_engine/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/817ccbc9c8fa20c972e2108c097a34712a697764/python/atomic_bomb_engine/dist/.gitkeep -------------------------------------------------------------------------------- /python/atomic_bomb_engine/middleware.py: -------------------------------------------------------------------------------- 1 | async def cors_middleware(app, handler): 2 | async def cors_handler(request): 3 | response = await handler(request) 4 | response.headers['Access-Control-Allow-Origin'] = '*' 5 | response.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' 6 | response.headers['Access-Control-Allow-Headers'] = 'X-Requested-With, Content-Type' 7 | return response 8 | return cors_handler -------------------------------------------------------------------------------- /python/atomic_bomb_engine/server.py: -------------------------------------------------------------------------------- 1 | import atomic_bomb_engine 2 | import os 3 | import sys 4 | import asyncio 5 | import webbrowser 6 | import time 7 | import aiohttp 8 | import aiosqlite 9 | import json 10 | from typing import Dict 11 | from aiohttp import web 12 | from atomic_bomb_engine import middleware 13 | 14 | 15 | def ui(port: int = 8000, auto_open: bool = True): 16 | if port > 65535 or port < 0: 17 | raise ValueError(f"端口必须为0-65535") 18 | # 数据库连接 19 | db_connection = None 20 | 21 | # 运行锁 22 | class RunningState: 23 | def __init__(self): 24 | self._running = False 25 | self._lock = asyncio.Lock() 26 | 27 | async def set_running(self): 28 | async with self._lock: 29 | self._running = True 30 | 31 | async def is_running(self) -> bool: 32 | async with self._lock: 33 | return self._running 34 | 35 | running = RunningState() 36 | 37 | async def get_db_connection(): 38 | nonlocal db_connection 39 | if db_connection is None: 40 | db_connection = await aiosqlite.connect(":memory:") 41 | await db_connection.execute('CREATE TABLE results (id INTEGER PRIMARY KEY, data JSON)') 42 | return db_connection 43 | 44 | async def create_table(): 45 | db = await get_db_connection() 46 | await db.commit() 47 | 48 | async def insert_result_data(data): 49 | db = await get_db_connection() 50 | json_data = json.dumps(data) 51 | await db.execute('INSERT INTO results (data) VALUES (?)', (json_data,)) 52 | await db.commit() 53 | 54 | async def fetch_all_result_data(): 55 | db = await get_db_connection() 56 | cursor = await db.execute('SELECT data FROM results ORDER BY id ASC') 57 | rows = await cursor.fetchall() 58 | results = [json.loads(row[0]) for row in rows] 59 | return results 60 | 61 | class WsConn: 62 | """连接池对象""" 63 | 64 | def __init__(self, ws: aiohttp.web_ws.WebSocketResponse, heartbeat_time: float): 65 | self.ws = ws 66 | self.heartbeat_time = heartbeat_time 67 | 68 | # ws连接池 69 | connections: Dict[str, WsConn] = dict() 70 | 71 | # 结果迭代器 72 | res_iter = None 73 | 74 | def decorator(func): 75 | async def start_service(*args, **kwargs): 76 | # 建表 77 | await create_table() 78 | 79 | # 定义ws接口 80 | async def websocket_handler(request): 81 | # 获取id 82 | if (client_id := request.match_info.get("id", None)) is None: 83 | return web.Response(status=400, text="缺少id参数") 84 | 85 | ws = web.WebSocketResponse() 86 | await ws.prepare(request) 87 | # 将id加入连接池 88 | connections[client_id] = WsConn(ws, time.time()) 89 | 90 | # 心跳检测 91 | async def check_heartbeat(): 92 | while True: 93 | await asyncio.sleep(0.5) 94 | if (ws_conn := connections.get(client_id)) is None: 95 | break 96 | if time.time() - ws_conn.heartbeat_time > 5: 97 | sys.stderr.write(f"{time.ctime()}客户端{client_id} 未发送心跳,断开连接\n") 98 | sys.stderr.flush() 99 | connections.pop(client_id, None) 100 | await ws_conn.ws.close() 101 | break 102 | 103 | async def push_result(): 104 | nonlocal res_iter 105 | 106 | while res_iter is None: 107 | await asyncio.sleep(0.1) 108 | 109 | for item in res_iter: 110 | if item: 111 | # 插入数据 112 | await insert_result_data(item) 113 | # 推送result 114 | for cid, conn in list(connections.items()): 115 | try: 116 | await conn.ws.send_json(item) 117 | except ConnectionResetError: 118 | sys.stderr.write(f'{time.ctime()}-WebSocket ID {cid} 断开, 无法推送\n') 119 | sys.stderr.flush() 120 | # 从连接池中移除断开的连接 121 | connections.pop(cid, None) 122 | return 123 | 124 | for cid, conn in list(connections.items()): 125 | try: 126 | await conn.ws.send_str("DONE") 127 | except ConnectionResetError: 128 | sys.stderr.write(f'{time.ctime()}-WebSocket ID {cid} 断开, 无法推送\n') 129 | sys.stderr.flush() 130 | # 从连接池中移除断开的连接 131 | connections.pop(cid, None) 132 | return 133 | 134 | # 推送任务 135 | push_task = asyncio.create_task(push_result()) 136 | # 心跳任务 137 | check_heartbeat_task = asyncio.create_task(check_heartbeat()) 138 | 139 | async for msg in ws: 140 | if msg.type is web.WSMsgType.TEXT: 141 | if msg.data.upper() == "PING": 142 | # 更新心跳时间 143 | if (ws_conn := connections.get(client_id, None)) is not None: 144 | ws_conn.heartbeat_time = time.time() 145 | await ws.send_str("PONG") 146 | elif msg.type is web.WSMsgType.ERROR: 147 | sys.stderr.write(f'WebSocket连接错误{ws.exception()}\n') 148 | sys.stderr.flush() 149 | 150 | await push_task 151 | sys.stderr.write('WebSocket连接关闭\n') 152 | sys.stderr.flush() 153 | 154 | await check_heartbeat_task 155 | connections.pop(client_id, None) 156 | return ws 157 | 158 | # 定义run接口 159 | async def run_decorated_function(request): 160 | if await running.is_running(): 161 | return web.json_response({"message": "任务正在运行中", "success": False}) 162 | await running.set_running() 163 | nonlocal res_iter 164 | res_iter = await func(*args, **kwargs) 165 | return web.json_response({"message": "压测任务已启动", "success": True}) 166 | 167 | # 定义history接口 168 | async def history(request): 169 | results = await fetch_all_result_data() 170 | return web.json_response(results) 171 | 172 | # 重定向到首页 173 | async def redirect_to_index(request): 174 | response = web.HTTPFound('/static/index.html') 175 | # 禁用缓存 176 | response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' 177 | response.headers['Pragma'] = 'no-cache' 178 | response.headers['Expires'] = '0' 179 | return response 180 | 181 | app = web.Application(middlewares=[middleware.cors_middleware]) 182 | # 静态页面 183 | app.router.add_static('/static', path=os.path.join(os.path.dirname(__file__), 'dist'), name='dist') 184 | # 路由 185 | app.add_routes([web.get('/', redirect_to_index), 186 | web.get('/ws/{id}', websocket_handler), 187 | web.get('/run', run_decorated_function), 188 | web.get('/history', history), 189 | ]) 190 | runner = web.AppRunner(app) 191 | await runner.setup() 192 | site = web.TCPSite(runner, '0.0.0.0', port) 193 | await site.start() 194 | # 等待协程运行完成 195 | await asyncio.Event().wait() 196 | 197 | sys.stderr.write(f"服务启动成功: http://localhost:{port}\n") 198 | sys.stderr.flush() 199 | if auto_open: 200 | webbrowser.open(f"http://localhost:{port}") 201 | return start_service 202 | 203 | return decorator 204 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | mod py_lib; 3 | mod utils; 4 | 5 | #[pymodule] 6 | #[pyo3(name = "atomic_bomb_engine")] 7 | fn atomic_bomb_engine(_py: Python, m: &PyModule) -> PyResult<()> { 8 | m.add_function(wrap_pyfunction!( 9 | py_lib::assert_option_func::assert_option, 10 | m 11 | )?)?; 12 | m.add_function(wrap_pyfunction!(py_lib::endpoint_func::endpoint, m)?)?; 13 | m.add_function(wrap_pyfunction!(py_lib::step_option_func::step_option, m)?)?; 14 | m.add_function(wrap_pyfunction!( 15 | py_lib::setup_option_func::setup_option, 16 | m 17 | )?)?; 18 | m.add_function(wrap_pyfunction!( 19 | py_lib::jsonpath_extract_func::jsonpath_extract_option, 20 | m 21 | )?)?; 22 | m.add_function(wrap_pyfunction!( 23 | py_lib::think_time_option_func::think_time_option, 24 | m 25 | )?)?; 26 | m.add_function(wrap_pyfunction!( 27 | py_lib::multipart_option_func::multipart_option, 28 | m 29 | )?)?; 30 | m.add_class::()?; 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /src/py_lib/assert_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::PyDict; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python, ToPyObject}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | jsonpath, 7 | reference_object, 8 | ))] 9 | pub(crate) fn assert_option( 10 | py: Python, 11 | jsonpath: String, 12 | reference_object: PyObject, 13 | ) -> PyResult { 14 | let dict = PyDict::new(py); 15 | dict.set_item("jsonpath", jsonpath)?; 16 | dict.set_item("reference_object", reference_object)?; 17 | Ok(dict.to_object(py)) 18 | } 19 | -------------------------------------------------------------------------------- /src/py_lib/batch_runner.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use atomic_bomb_engine::models::result::BatchResult; 3 | use futures::stream::BoxStream; 4 | use futures::StreamExt; 5 | use pyo3::types::{PyDict, PyList}; 6 | use pyo3::{pyclass, pymethods, PyObject, PyRefMut, PyResult, Python, ToPyObject}; 7 | use std::sync::Arc; 8 | use tokio::sync::Mutex; 9 | 10 | #[pyclass] 11 | pub(crate) struct BatchRunner { 12 | runtime: tokio::runtime::Runtime, 13 | stream: Arc, anyhow::Error>>>>>, 14 | is_done: Arc>, 15 | } 16 | 17 | #[pymethods] 18 | impl BatchRunner { 19 | #[new] 20 | fn new() -> Self { 21 | BatchRunner { 22 | runtime: tokio::runtime::Runtime::new().unwrap(), 23 | stream: Arc::new(Mutex::new(None)), 24 | is_done: Arc::new(Mutex::new(false)), 25 | } 26 | } 27 | 28 | #[pyo3(signature = ( 29 | test_duration_secs, 30 | concurrent_requests, 31 | api_endpoints, 32 | step_option=None, 33 | setup_options=None, 34 | verbose=false, 35 | should_prevent=false, 36 | assert_channel_buffer_size=1024, 37 | timeout_secs=0, 38 | cookie_store_enable=true, 39 | ema_alpha=0f64, 40 | ))] 41 | fn run( 42 | &self, 43 | py: Python, 44 | test_duration_secs: u64, 45 | concurrent_requests: usize, 46 | api_endpoints: &PyList, 47 | step_option: Option<&PyDict>, 48 | setup_options: Option<&PyList>, 49 | verbose: bool, 50 | should_prevent: bool, 51 | assert_channel_buffer_size: usize, 52 | timeout_secs: u64, 53 | cookie_store_enable: bool, 54 | ema_alpha: f64, 55 | ) -> PyResult { 56 | let stream_clone = self.stream.clone(); 57 | let endpoints = utils::parse_api_endpoints::new(py, api_endpoints) 58 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; 59 | let step_opt = utils::parse_step_options::new(step_option) 60 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; 61 | let setup_opts = utils::parse_setup_options::new(py, setup_options) 62 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; 63 | 64 | let fut = async move { 65 | let stream = atomic_bomb_engine::core::run_batch::run_batch( 66 | test_duration_secs, 67 | concurrent_requests, 68 | timeout_secs, 69 | cookie_store_enable, 70 | verbose, 71 | should_prevent, 72 | endpoints, 73 | step_opt, 74 | setup_opts, 75 | assert_channel_buffer_size, 76 | ema_alpha, 77 | ) 78 | .await; 79 | *stream_clone.lock().await = Some(stream); 80 | Ok::<(), pyo3::PyErr>(()) 81 | }; 82 | 83 | Python::with_gil(|py| { 84 | pyo3_asyncio::tokio::future_into_py(py, fut).map(|py_any| py_any.to_object(py)) 85 | }) 86 | } 87 | 88 | fn __iter__(slf: PyRefMut) -> PyResult> { 89 | Ok(slf) 90 | } 91 | 92 | fn __next__(slf: PyRefMut<'_, Self>, py: Python) -> PyResult> { 93 | let is_done_clone = slf.is_done.clone(); 94 | 95 | let mut stream_guard = slf.runtime.block_on(async { 96 | let stream = slf.stream.lock().await; 97 | stream 98 | }); 99 | 100 | let is_done = slf.runtime.block_on(async { 101 | let done = is_done_clone.lock().await; 102 | *done 103 | }); 104 | 105 | if is_done { 106 | return Ok(None); 107 | } 108 | 109 | match stream_guard.as_mut() { 110 | Some(stream) => { 111 | let next_stream = slf.runtime.block_on(async { 112 | let batch_result = stream.next().await; 113 | batch_result 114 | }); 115 | 116 | match next_stream { 117 | Some(Ok(result)) => { 118 | if result.is_none() { 119 | let done = slf.is_done.clone(); 120 | slf.runtime.block_on(async { 121 | let mut done_lock = done.lock().await; 122 | *done_lock = true; 123 | }); 124 | } 125 | 126 | let dict = PyDict::new(py); 127 | if let Some(test_result) = result { 128 | dict.set_item("total_duration", test_result.total_duration)?; 129 | dict.set_item("success_rate", test_result.success_rate)?; 130 | dict.set_item("error_rate", test_result.error_rate)?; 131 | dict.set_item( 132 | "median_response_time", 133 | test_result.median_response_time, 134 | )?; 135 | dict.set_item("response_time_95", test_result.response_time_95)?; 136 | dict.set_item("response_time_99", test_result.response_time_99)?; 137 | dict.set_item("total_requests", test_result.total_requests)?; 138 | dict.set_item("rps", test_result.rps)?; 139 | dict.set_item("max_response_time", test_result.max_response_time)?; 140 | dict.set_item("min_response_time", test_result.min_response_time)?; 141 | dict.set_item("err_count", test_result.err_count)?; 142 | dict.set_item("total_data_kb", test_result.total_data_kb)?; 143 | dict.set_item( 144 | "throughput_per_second_kb", 145 | test_result.throughput_per_second_kb, 146 | )?; 147 | let http_error_list = 148 | utils::create_http_err_dict::create_http_error_dict( 149 | py, 150 | &test_result.http_errors, 151 | )?; 152 | dict.set_item("http_errors", http_error_list)?; 153 | let assert_error_list = 154 | utils::create_assert_err_dict::create_assert_error_dict( 155 | py, 156 | &test_result.assert_errors, 157 | )?; 158 | dict.set_item("assert_errors", assert_error_list)?; 159 | dict.set_item("timestamp", test_result.timestamp)?; 160 | let api_results = 161 | utils::create_api_results_dict::create_api_results_dict( 162 | py, 163 | test_result.api_results, 164 | )?; 165 | dict.set_item("api_results", api_results)?; 166 | dict.set_item( 167 | "total_concurrent_number", 168 | test_result.total_concurrent_number, 169 | )?; 170 | dict.set_item("errors_per_second", test_result.errors_per_second)?; 171 | }; 172 | Ok(Some(dict.to_object(py))) 173 | } 174 | Some(Err(e)) => Err(pyo3::exceptions::PyRuntimeError::new_err(e.to_string())), 175 | None => { 176 | let done = slf.is_done.clone(); 177 | slf.runtime.block_on(async { 178 | let mut done_lock = done.lock().await; 179 | *done_lock = true; 180 | }); 181 | Ok(None) 182 | } 183 | } 184 | } 185 | None => { 186 | eprintln!("stream未初始化,请等待"); 187 | let dict = PyDict::new(py); 188 | dict.set_item("should_wait", true)?; 189 | Ok(Some(dict.to_object(py))) 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/py_lib/endpoint_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyDict, PyList}; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | name, 7 | url, 8 | method, 9 | weight, 10 | json=None, 11 | form_data=None, 12 | multipart_options=None, 13 | headers=None, 14 | cookies=None, 15 | assert_options=None, 16 | think_time_option=None, 17 | setup_options=None, 18 | ))] 19 | pub(crate) fn endpoint( 20 | py: Python, 21 | name: String, 22 | url: String, 23 | method: String, 24 | weight: u32, 25 | json: Option, 26 | form_data: Option, 27 | multipart_options: Option<&PyList>, 28 | headers: Option, 29 | cookies: Option, 30 | assert_options: Option<&PyList>, 31 | think_time_option: Option, 32 | setup_options: Option<&PyList>, 33 | ) -> PyResult { 34 | let dict = PyDict::new(py); 35 | dict.set_item("name", name)?; 36 | dict.set_item("url", url)?; 37 | dict.set_item("method", method)?; 38 | dict.set_item("weight", weight)?; 39 | if let Some(json) = json { 40 | dict.set_item("json", json)?; 41 | }; 42 | if let Some(form_data) = form_data { 43 | dict.set_item("form_data", form_data)?; 44 | }; 45 | if let Some(multipart_options) = multipart_options { 46 | dict.set_item("multipart_options", multipart_options)?; 47 | }; 48 | if let Some(headers) = headers { 49 | dict.set_item("headers", headers)?; 50 | }; 51 | if let Some(cookies) = cookies { 52 | dict.set_item("cookies", cookies)?; 53 | }; 54 | if let Some(assert_options) = assert_options { 55 | dict.set_item("assert_options", assert_options)?; 56 | }; 57 | if let Some(think_time_option) = think_time_option { 58 | dict.set_item("think_time_option", think_time_option)?; 59 | }; 60 | if let Some(setup_options) = setup_options { 61 | dict.set_item("setup_options", setup_options)?; 62 | } 63 | Ok(dict.into()) 64 | } 65 | -------------------------------------------------------------------------------- /src/py_lib/jsonpath_extract_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::PyDict; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python, ToPyObject}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | key, 7 | jsonpath, 8 | ))] 9 | pub(crate) fn jsonpath_extract_option( 10 | py: Python, 11 | key: String, 12 | jsonpath: String, 13 | ) -> PyResult { 14 | let dict = PyDict::new(py); 15 | dict.set_item("key", key)?; 16 | dict.set_item("jsonpath", jsonpath)?; 17 | Ok(dict.to_object(py)) 18 | } 19 | -------------------------------------------------------------------------------- /src/py_lib/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod assert_option_func; 2 | pub(crate) mod batch_runner; 3 | pub(crate) mod endpoint_func; 4 | pub(crate) mod jsonpath_extract_func; 5 | pub(crate) mod multipart_option_func; 6 | pub(crate) mod setup_option_func; 7 | pub(crate) mod step_option_func; 8 | pub(crate) mod think_time_option_func; 9 | -------------------------------------------------------------------------------- /src/py_lib/multipart_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::PyDict; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | form_key, 7 | path, 8 | file_name, 9 | mime, 10 | ))] 11 | pub(crate) fn multipart_option( 12 | py: Python, 13 | form_key: String, 14 | path: String, 15 | file_name: String, 16 | mime: String, 17 | ) -> PyResult { 18 | let dict = PyDict::new(py); 19 | dict.set_item("form_key", form_key)?; 20 | dict.set_item("path", path)?; 21 | dict.set_item("file_name", file_name)?; 22 | dict.set_item("mime", mime)?; 23 | Ok(dict.into()) 24 | } 25 | -------------------------------------------------------------------------------- /src/py_lib/setup_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyDict, PyList}; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | name, 7 | url, 8 | method, 9 | json=None, 10 | form_data=None, 11 | multipart_options=None, 12 | headers=None, 13 | cookies=None, 14 | jsonpath_extract=None, 15 | ))] 16 | pub(crate) fn setup_option( 17 | py: Python, 18 | name: String, 19 | url: String, 20 | method: String, 21 | json: Option, 22 | form_data: Option, 23 | multipart_options: Option<&PyList>, 24 | headers: Option, 25 | cookies: Option, 26 | jsonpath_extract: Option<&PyList>, 27 | ) -> PyResult { 28 | let dict = PyDict::new(py); 29 | dict.set_item("name", name)?; 30 | dict.set_item("url", url)?; 31 | dict.set_item("method", method)?; 32 | if let Some(json) = json { 33 | dict.set_item("json", json)?; 34 | }; 35 | if let Some(form_data) = form_data { 36 | dict.set_item("form_data", form_data)?; 37 | }; 38 | if let Some(multipart_options) = multipart_options { 39 | dict.set_item("multipart_options", multipart_options)?; 40 | }; 41 | if let Some(headers) = headers { 42 | dict.set_item("headers", headers)?; 43 | }; 44 | if let Some(cookies) = cookies { 45 | dict.set_item("cookies", cookies)?; 46 | }; 47 | if let Some(jsonpath_extract) = jsonpath_extract { 48 | dict.set_item("jsonpath_extract", jsonpath_extract)?; 49 | }; 50 | Ok(dict.into()) 51 | } 52 | -------------------------------------------------------------------------------- /src/py_lib/step_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::PyDict; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python, ToPyObject}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | increase_step, 7 | increase_interval, 8 | ))] 9 | pub(crate) fn step_option( 10 | py: Python, 11 | increase_step: usize, 12 | increase_interval: usize, 13 | ) -> PyResult { 14 | let dict = PyDict::new(py); 15 | dict.set_item("increase_step", increase_step)?; 16 | dict.set_item("increase_interval", increase_interval)?; 17 | Ok(dict.to_object(py)) 18 | } 19 | -------------------------------------------------------------------------------- /src/py_lib/think_time_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::PyDict; 2 | use pyo3::{pyfunction, PyObject, PyResult, Python, ToPyObject}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | min_millis, 7 | max_millis, 8 | ))] 9 | pub(crate) fn think_time_option( 10 | py: Python, 11 | min_millis: u64, 12 | max_millis: u64, 13 | ) -> PyResult { 14 | let dict = PyDict::new(py); 15 | dict.set_item("min_millis", min_millis)?; 16 | dict.set_item("max_millis", max_millis)?; 17 | Ok(dict.to_object(py)) 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/create_api_results_dict.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models::result::ApiResult; 2 | use pyo3::types::{PyDict, PyList}; 3 | use pyo3::{PyResult, Python}; 4 | pub fn create_api_results_dict(py: Python, api_results: Vec) -> PyResult<&PyList> { 5 | if api_results.is_empty() { 6 | return Ok(PyList::empty(py)); 7 | } 8 | 9 | let mut results = Vec::new(); 10 | 11 | for result in api_results { 12 | let res_dict = PyDict::new(py); 13 | 14 | res_dict.set_item("name", result.name)?; 15 | res_dict.set_item("url", result.url)?; 16 | res_dict.set_item("host", result.host)?; 17 | res_dict.set_item("path", result.path)?; 18 | res_dict.set_item("method", result.method)?; 19 | res_dict.set_item("success_rate", result.success_rate)?; 20 | res_dict.set_item("error_rate", result.error_rate)?; 21 | res_dict.set_item("median_response_time", result.median_response_time)?; 22 | res_dict.set_item("response_time_95", result.response_time_95)?; 23 | res_dict.set_item("response_time_99", result.response_time_99)?; 24 | res_dict.set_item("total_requests", result.total_requests)?; 25 | res_dict.set_item("rps", result.rps)?; 26 | res_dict.set_item("max_response_time", result.max_response_time)?; 27 | res_dict.set_item("min_response_time", result.min_response_time)?; 28 | res_dict.set_item("err_count", result.err_count)?; 29 | res_dict.set_item("total_data_kb", result.total_data_kb)?; 30 | res_dict.set_item("throughput_per_second_kb", result.throughput_per_second_kb)?; 31 | res_dict.set_item("concurrent_number", result.concurrent_number)?; 32 | results.push(res_dict) 33 | } 34 | Ok(PyList::new(py, results)) 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/create_assert_err_dict.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models::assert_error_stats::AssertErrKey; 2 | use pyo3::types::{PyDict, PyList}; 3 | use pyo3::{PyResult, Python}; 4 | use std::collections::HashMap; 5 | 6 | pub fn create_assert_error_dict<'py>( 7 | py: Python<'py>, 8 | assert_errors: &HashMap, 9 | ) -> PyResult<&'py PyList> { 10 | if assert_errors.is_empty() { 11 | return Ok(PyList::empty(py)); 12 | } 13 | 14 | let mut result_errors = Vec::new(); 15 | for (assert_error_key, count) in assert_errors { 16 | let assert_error_dict = PyDict::new(py); 17 | let assert_err_key_clone = assert_error_key.clone(); 18 | 19 | assert_error_dict.set_item("name", assert_err_key_clone.name)?; 20 | assert_error_dict.set_item("message", assert_err_key_clone.msg)?; 21 | assert_error_dict.set_item("url", assert_err_key_clone.url)?; 22 | assert_error_dict.set_item("host", assert_err_key_clone.host)?; 23 | assert_error_dict.set_item("path", assert_err_key_clone.path)?; 24 | assert_error_dict.set_item("count", count)?; 25 | 26 | result_errors.push(assert_error_dict) 27 | } 28 | Ok(PyList::new(py, result_errors)) 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/create_http_err_dict.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models::http_error_stats::HttpErrKey; 2 | use pyo3::types::{PyDict, PyList}; 3 | use pyo3::{PyResult, Python}; 4 | use std::collections::HashMap; 5 | 6 | pub fn create_http_error_dict<'py>( 7 | py: Python<'py>, 8 | http_errors: &HashMap, 9 | ) -> PyResult<&'py PyList> { 10 | if http_errors.is_empty() { 11 | return Ok(PyList::empty(py)); 12 | } 13 | let mut http_errors_list = Vec::new(); 14 | for (http_error_key, count) in http_errors { 15 | let http_error_key_clone = http_error_key.clone(); 16 | let http_error_dict = PyDict::new(py); 17 | http_error_dict.set_item("name", http_error_key_clone.name)?; 18 | http_error_dict.set_item("code", http_error_key_clone.code)?; 19 | http_error_dict.set_item("message", http_error_key_clone.msg)?; 20 | http_error_dict.set_item("url", http_error_key_clone.url)?; 21 | http_error_dict.set_item("host", http_error_key_clone.host)?; 22 | http_error_dict.set_item("path", http_error_key_clone.path)?; 23 | http_error_dict.set_item("source", http_error_key_clone.source)?; 24 | http_error_dict.set_item("count", count)?; 25 | http_errors_list.push(http_error_dict) 26 | } 27 | Ok(PyList::new(py, http_errors_list)) 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_api_results_dict; 2 | pub mod create_assert_err_dict; 3 | pub mod create_http_err_dict; 4 | pub mod parse_api_endpoints; 5 | pub mod parse_assert_options; 6 | pub mod parse_multipart_options; 7 | pub mod parse_setup_options; 8 | pub mod parse_step_options; 9 | -------------------------------------------------------------------------------- /src/utils/parse_api_endpoints.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use atomic_bomb_engine::models; 3 | use atomic_bomb_engine::models::api_endpoint::ThinkTime; 4 | use atomic_bomb_engine::models::assert_option::AssertOption; 5 | use pyo3::prelude::*; 6 | use pyo3::types::{PyDict, PyList}; 7 | use serde_json::Value; 8 | use serde_pyobject::from_pyobject; 9 | use std::collections::HashMap; 10 | 11 | pub fn new(py: Python, api_endpoints: &PyList) -> PyResult> { 12 | let mut endpoints: Vec = Vec::new(); 13 | for item in api_endpoints.iter() { 14 | if let Ok(dict) = item.downcast::() { 15 | let name: String = match dict.get_item("name") { 16 | Ok(name) => match name { 17 | None => { 18 | return Err(PyErr::new::( 19 | "name不能为空".to_string(), 20 | )) 21 | } 22 | Some(name) => name.to_string(), 23 | }, 24 | Err(e) => { 25 | return Err(PyErr::new::(format!( 26 | "Error: {:?}", 27 | e 28 | ))) 29 | } 30 | }; 31 | 32 | let url: String = match dict.get_item("url") { 33 | Ok(url) => match url { 34 | None => { 35 | return Err(PyErr::new::( 36 | "url不能为空".to_string(), 37 | )) 38 | } 39 | Some(url) => url.to_string(), 40 | }, 41 | Err(e) => { 42 | return Err(PyErr::new::(format!( 43 | "Error: {:?}", 44 | e 45 | ))) 46 | } 47 | }; 48 | 49 | let method: String = match dict.get_item("method") { 50 | Ok(method) => match method { 51 | None => { 52 | return Err(PyErr::new::( 53 | "method不能为空".to_string(), 54 | )) 55 | } 56 | Some(method) => method.to_string(), 57 | }, 58 | Err(e) => { 59 | return Err(PyErr::new::(format!( 60 | "Error: {:?}", 61 | e 62 | ))) 63 | } 64 | }; 65 | 66 | let weight: u32 = match dict.get_item("weight") { 67 | Ok(weight) => match weight { 68 | None => { 69 | return Err(PyErr::new::( 70 | "weight不能为空".to_string(), 71 | )) 72 | } 73 | Some(weight) => weight.to_string().parse()?, 74 | }, 75 | Err(e) => { 76 | return Err(PyErr::new::(format!( 77 | "Error: {:?}", 78 | e 79 | ))) 80 | } 81 | }; 82 | 83 | let json_obj: PyObject = dict.get_item("json").unwrap().to_object(py); 84 | let json: Option = match from_pyobject(json_obj.as_ref(py)) { 85 | Ok(val) => val, 86 | Err(e) => { 87 | return Err(PyErr::new::(format!( 88 | "Error: {:?}", 89 | e 90 | ))) 91 | } 92 | }; 93 | 94 | let form_data_obj: PyObject = dict.get_item("form_data").unwrap().to_object(py); 95 | let form_data: Option> = 96 | match from_pyobject(form_data_obj.as_ref(py)) { 97 | Ok(val) => val, 98 | Err(e) => { 99 | return Err(PyErr::new::(format!( 100 | "Error: {:?}", 101 | e 102 | ))) 103 | } 104 | }; 105 | 106 | let headers_obj: PyObject = dict.get_item("headers").unwrap().to_object(py); 107 | let headers: Option> = 108 | match from_pyobject(headers_obj.as_ref(py)) { 109 | Ok(val) => val, 110 | Err(e) => { 111 | return Err(PyErr::new::(format!( 112 | "Error: {:?}", 113 | e 114 | ))) 115 | } 116 | }; 117 | 118 | let cookies_obj: PyObject = dict.get_item("cookies").unwrap().to_object(py); 119 | let cookies: Option = match from_pyobject(cookies_obj.as_ref(py)) { 120 | Ok(val) => val, 121 | Err(e) => { 122 | return Err(PyErr::new::(format!( 123 | "Error: {:?}", 124 | e 125 | ))) 126 | } 127 | }; 128 | 129 | let assert_options: Option> = match dict.get_item("assert_options") { 130 | Ok(op_py_any) => match op_py_any { 131 | None => None, 132 | Some(py_any) => { 133 | let pyobj = py_any.to_object(py); 134 | match from_pyobject(pyobj.as_ref(py)) { 135 | Ok(val) => val, 136 | Err(e) => { 137 | return Err(PyErr::new::( 138 | format!("Error: {:?}", e), 139 | )) 140 | } 141 | } 142 | } 143 | }, 144 | Err(e) => { 145 | return Err(PyErr::new::(format!( 146 | "Error: {:?}", 147 | e 148 | ))) 149 | } 150 | }; 151 | 152 | let think_time_option: Option = match dict.get_item("think_time_option") { 153 | Ok(op_py_any) => match op_py_any { 154 | None => None, 155 | Some(py_any) => { 156 | let pyobj = py_any.to_object(py); 157 | match from_pyobject(pyobj.as_ref(py)) { 158 | Ok(val) => val, 159 | Err(e) => { 160 | return Err(PyErr::new::( 161 | format!("Error: {:?}", e), 162 | )) 163 | } 164 | } 165 | } 166 | }, 167 | Err(e) => { 168 | return Err(PyErr::new::(format!( 169 | "Error: {:?}", 170 | e 171 | ))) 172 | } 173 | }; 174 | let setup_options_pylist: Option<&PyList> = match dict.get_item("setup_options") { 175 | Ok(opts) => match opts { 176 | None => None, 177 | Some(py_any) => match py_any.extract::<&PyList>() { 178 | Ok(py_list) => Some(py_list), 179 | Err(e) => { 180 | return Err(PyErr::new::(format!( 181 | "Error: {:?}", 182 | e 183 | ))) 184 | } 185 | }, 186 | }, 187 | Err(e) => { 188 | return Err(PyErr::new::(format!( 189 | "Error: {:?}", 190 | e 191 | ))) 192 | } 193 | }; 194 | let setup_options = utils::parse_setup_options::new(py, setup_options_pylist)?; 195 | 196 | let multipart_options_pylist: Option<&PyList> = 197 | match dict.get_item("multipart_options") { 198 | Ok(opts) => match opts { 199 | None => None, 200 | Some(py_any) => match py_any.extract::<&PyList>() { 201 | Ok(py_list) => Some(py_list), 202 | Err(e) => { 203 | return Err(PyErr::new::( 204 | format!("Error: {:?}", e), 205 | )) 206 | } 207 | }, 208 | }, 209 | Err(e) => { 210 | return Err(PyErr::new::(format!( 211 | "Error: {:?}", 212 | e 213 | ))) 214 | } 215 | }; 216 | let multipart_options = 217 | utils::parse_multipart_options::new(py, multipart_options_pylist)?; 218 | 219 | endpoints.push(models::api_endpoint::ApiEndpoint { 220 | name, 221 | url, 222 | method, 223 | weight, 224 | json, 225 | form_data, 226 | multipart_options, 227 | headers, 228 | cookies, 229 | assert_options, 230 | think_time_option, 231 | setup_options, 232 | }); 233 | } 234 | } 235 | Ok(endpoints) 236 | } 237 | -------------------------------------------------------------------------------- /src/utils/parse_assert_options.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyList}; 4 | use serde_json::Value; 5 | use serde_pyobject::from_pyobject; 6 | 7 | pub fn _new( 8 | py: Python, 9 | assert_options: Option<&PyList>, 10 | ) -> PyResult>> { 11 | match assert_options { 12 | None => Ok(None), 13 | Some(ops_list) => { 14 | let mut ops: Vec = Vec::new(); 15 | for item in ops_list.iter() { 16 | if let Ok(dict) = item.downcast::() { 17 | let jsonpath: String = match dict.get_item("jsonpath") { 18 | Ok(json_path_option) => match json_path_option { 19 | None => { 20 | return Err(PyErr::new::( 21 | "必须输入一个jsonpath".to_string(), 22 | )) 23 | } 24 | Some(jsonpath) => jsonpath.to_string(), 25 | }, 26 | Err(e) => { 27 | return Err(PyErr::new::(format!( 28 | "Error: {:?}", 29 | e 30 | ))) 31 | } 32 | }; 33 | 34 | let reference_object: PyObject = 35 | dict.get_item("reference_object").unwrap().to_object(py); 36 | let reference_value: Value = match from_pyobject(reference_object.as_ref(py)) { 37 | Ok(val) => val, 38 | Err(e) => { 39 | return Err(PyErr::new::(format!( 40 | "Error: {:?}", 41 | e 42 | ))) 43 | } 44 | }; 45 | ops.push(models::assert_option::AssertOption { 46 | jsonpath, 47 | reference_object: reference_value, 48 | }); 49 | } 50 | } 51 | Ok(Some(ops)) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/parse_multipart_options.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyList}; 4 | 5 | pub fn new( 6 | _py: Python, 7 | py_multipart_options: Option<&PyList>, 8 | ) -> PyResult>> { 9 | return match py_multipart_options { 10 | None => Ok(None), 11 | Some(ops) => { 12 | let mut multipart_options: Vec = Vec::new(); 13 | for item in ops.iter() { 14 | if let Ok(dict) = item.downcast::() { 15 | let form_key: String = match dict.get_item("form_key") { 16 | Ok(form_key) => match form_key { 17 | None => { 18 | return Err(PyErr::new::( 19 | "form_key".to_string(), 20 | )) 21 | } 22 | Some(form_key) => form_key.to_string(), 23 | }, 24 | Err(e) => { 25 | return Err(PyErr::new::(format!( 26 | "Error: {:?}", 27 | e 28 | ))) 29 | } 30 | }; 31 | 32 | let path: String = match dict.get_item("path") { 33 | Ok(path) => match path { 34 | None => { 35 | return Err(PyErr::new::( 36 | "path".to_string(), 37 | )) 38 | } 39 | Some(path) => path.to_string(), 40 | }, 41 | Err(e) => { 42 | return Err(PyErr::new::(format!( 43 | "Error: {:?}", 44 | e 45 | ))) 46 | } 47 | }; 48 | 49 | let file_name: String = match dict.get_item("file_name") { 50 | Ok(file_name) => match file_name { 51 | None => { 52 | return Err(PyErr::new::( 53 | "file_name".to_string(), 54 | )) 55 | } 56 | Some(file_name) => file_name.to_string(), 57 | }, 58 | Err(e) => { 59 | return Err(PyErr::new::(format!( 60 | "Error: {:?}", 61 | e 62 | ))) 63 | } 64 | }; 65 | 66 | let mime: String = match dict.get_item("mime") { 67 | Ok(mime) => match mime { 68 | None => { 69 | return Err(PyErr::new::( 70 | "mime".to_string(), 71 | )) 72 | } 73 | Some(mime) => mime.to_string(), 74 | }, 75 | Err(e) => { 76 | return Err(PyErr::new::(format!( 77 | "Error: {:?}", 78 | e 79 | ))) 80 | } 81 | }; 82 | 83 | multipart_options.push(models::multipart_option::MultipartOption { 84 | form_key, 85 | path, 86 | file_name, 87 | mime, 88 | }) 89 | } 90 | } 91 | Ok(Option::from(multipart_options)) 92 | } 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/parse_setup_options.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use atomic_bomb_engine::models; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyDict, PyList}; 6 | use serde_json::Value; 7 | use serde_pyobject::from_pyobject; 8 | 9 | pub fn new( 10 | py: Python, 11 | py_setup_options: Option<&PyList>, 12 | ) -> PyResult>> { 13 | return match py_setup_options { 14 | None => Ok(None), 15 | Some(ops) => { 16 | let mut setup_options: Vec = Vec::new(); 17 | for item in ops.iter() { 18 | if let Ok(dict) = item.downcast::() { 19 | let name: String = match dict.get_item("name") { 20 | Ok(name) => match name { 21 | None => { 22 | return Err(PyErr::new::( 23 | "name不能为空".to_string(), 24 | )) 25 | } 26 | Some(name) => name.to_string(), 27 | }, 28 | Err(e) => { 29 | return Err(PyErr::new::(format!( 30 | "Error: {:?}", 31 | e 32 | ))) 33 | } 34 | }; 35 | 36 | let url: String = match dict.get_item("url") { 37 | Ok(url) => match url { 38 | None => { 39 | return Err(PyErr::new::( 40 | "url不能为空".to_string(), 41 | )) 42 | } 43 | Some(url) => url.to_string(), 44 | }, 45 | Err(e) => { 46 | return Err(PyErr::new::(format!( 47 | "Error: {:?}", 48 | e 49 | ))) 50 | } 51 | }; 52 | 53 | let method: String = match dict.get_item("method") { 54 | Ok(method) => match method { 55 | None => { 56 | return Err(PyErr::new::( 57 | "method不能为空".to_string(), 58 | )) 59 | } 60 | Some(method) => method.to_string(), 61 | }, 62 | Err(e) => { 63 | return Err(PyErr::new::(format!( 64 | "Error: {:?}", 65 | e 66 | ))) 67 | } 68 | }; 69 | 70 | let json_obj: PyObject = dict.get_item("json").unwrap().to_object(py); 71 | let json: Option = match from_pyobject(json_obj.as_ref(py)) { 72 | Ok(val) => val, 73 | Err(e) => { 74 | return Err(PyErr::new::(format!( 75 | "Error: {:?}", 76 | e 77 | ))) 78 | } 79 | }; 80 | 81 | let form_data_obj: PyObject = dict.get_item("form_data").unwrap().to_object(py); 82 | let form_data: Option> = 83 | match from_pyobject(form_data_obj.as_ref(py)) { 84 | Ok(val) => val, 85 | Err(e) => { 86 | return Err(PyErr::new::( 87 | format!("Error: {:?}", e), 88 | )) 89 | } 90 | }; 91 | 92 | let headers_obj: PyObject = dict.get_item("headers").unwrap().to_object(py); 93 | let headers: Option> = 94 | match from_pyobject(headers_obj.as_ref(py)) { 95 | Ok(val) => val, 96 | Err(e) => { 97 | return Err(PyErr::new::( 98 | format!("Error: {:?}", e), 99 | )) 100 | } 101 | }; 102 | 103 | let cookies_obj: PyObject = dict.get_item("cookies").unwrap().to_object(py); 104 | let cookies: Option = match from_pyobject(cookies_obj.as_ref(py)) { 105 | Ok(val) => val, 106 | Err(e) => { 107 | return Err(PyErr::new::(format!( 108 | "Error: {:?}", 109 | e 110 | ))) 111 | } 112 | }; 113 | 114 | let jsonpath_extract: Option> = match dict 115 | .get_item("jsonpath_extract") 116 | { 117 | Ok(op_py_any) => match op_py_any { 118 | None => None, 119 | Some(py_any) => { 120 | let pyobj = py_any.to_object(py); 121 | match from_pyobject(pyobj.as_ref(py)) { 122 | Ok(val) => val, 123 | Err(e) => { 124 | return Err( 125 | PyErr::new::( 126 | format!("Error: {:?}", e), 127 | ), 128 | ) 129 | } 130 | } 131 | } 132 | }, 133 | Err(e) => { 134 | return Err(PyErr::new::(format!( 135 | "Error: {:?}", 136 | e 137 | ))) 138 | } 139 | }; 140 | 141 | let multipart_options: Option> = 142 | match dict.get_item("multipart_options") { 143 | Ok(op_py_any) => match op_py_any { 144 | None => None, 145 | Some(py_any) => { 146 | let pyobj = py_any.to_object(py); 147 | match from_pyobject(pyobj.as_ref(py)) { 148 | Ok(val) => val, 149 | Err(e) => { 150 | return Err(PyErr::new::< 151 | pyo3::exceptions::PyRuntimeError, 152 | _, 153 | >( 154 | format!( 155 | "Error: {:?}", 156 | e 157 | ) 158 | )) 159 | } 160 | } 161 | } 162 | }, 163 | Err(e) => { 164 | return Err(PyErr::new::( 165 | format!("Error: {:?}", e), 166 | )) 167 | } 168 | }; 169 | 170 | setup_options.push(models::setup::SetupApiEndpoint { 171 | name, 172 | url, 173 | method, 174 | json, 175 | form_data, 176 | multipart_options, 177 | headers, 178 | cookies, 179 | jsonpath_extract, 180 | }) 181 | } 182 | } 183 | Ok(Option::from(setup_options)) 184 | } 185 | }; 186 | } 187 | -------------------------------------------------------------------------------- /src/utils/parse_step_options.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | 5 | pub fn new(step_option: Option<&PyDict>) -> PyResult> { 6 | match step_option { 7 | None => Ok(None), 8 | Some(ops_dict) => { 9 | let increase_step: usize = match ops_dict.get_item("increase_step") { 10 | Ok(increase_step_py_any) => match increase_step_py_any { 11 | None => { 12 | return Err(PyErr::new::( 13 | "必须输入一个increase_step".to_string(), 14 | )) 15 | } 16 | Some(increase_step) => { 17 | let result: PyResult = increase_step.extract::(); 18 | match result { 19 | Ok(res) => res, 20 | Err(e) => { 21 | return Err(PyErr::new::( 22 | format!("increase_step必须是一个整数::{:?}", e), 23 | )) 24 | } 25 | } 26 | } 27 | }, 28 | Err(e) => { 29 | return Err(PyErr::new::(format!( 30 | "Error: {:?}", 31 | e 32 | ))) 33 | } 34 | }; 35 | 36 | let increase_interval: u64 = match ops_dict.get_item("increase_interval") { 37 | Ok(increase_interval_py_any) => match increase_interval_py_any { 38 | None => { 39 | return Err(PyErr::new::( 40 | "必须输入一个increase_interval".to_string(), 41 | )) 42 | } 43 | Some(increase_interval) => { 44 | let result: PyResult = increase_interval.extract::(); 45 | match result { 46 | Ok(res) => res, 47 | Err(e) => { 48 | return Err(PyErr::new::( 49 | format!("increase_interval必须是一个整数::{:?}", e), 50 | )) 51 | } 52 | } 53 | } 54 | }, 55 | Err(e) => { 56 | return Err(PyErr::new::(format!( 57 | "Error: {:?}", 58 | e 59 | ))) 60 | } 61 | }; 62 | 63 | let step_opt = models::step_option::StepOption { 64 | increase_step, 65 | increase_interval, 66 | }; 67 | Ok(Some(step_opt)) 68 | } 69 | } 70 | } 71 | --------------------------------------------------------------------------------