├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .env.example ├── .env.external.samlpe ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── check_app_pr.yml │ ├── check_web_pr.yml │ ├── deploy_docker_compose.yml │ ├── django_unittest.yml │ ├── playwright_ui_test.yml │ ├── sonarqube.yml │ ├── upload_app_image.yml │ ├── upload_web_dist.yml │ ├── upload_web_image.yml │ └── web_deploy.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .vscode └── launch.json ├── Dockerfile ├── FasterRunner ├── __init__.py ├── auth.py ├── customer_swagger.py ├── database.py ├── log.py ├── mycelery.py ├── mycelery_dev.py ├── pagination.py ├── routers.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ ├── docker.py │ └── pro.py ├── swagger.py ├── urls.py ├── wsgi.py └── wsgi_docker.py ├── LICENSE ├── Makefile ├── README.md ├── curd ├── __init__.py ├── base_curd.py └── crud_helper.py ├── db ├── init.sql └── my.cnf ├── docker-compose-build-db-mq.yml ├── docker-compose-build-web.yml ├── docker-compose-for-fastup.yml ├── docker-compose-without-db-mq.yml ├── docker-compose.yml ├── fastrunner ├── __init__.py ├── admin.py ├── apps.py ├── dto │ ├── __init__.py │ └── tree_dto.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200509_1122.py │ ├── 0003_casestep_source_api_id.py │ ├── 0004_auto_20200814_1605.py │ ├── 0005_auto_20200814_1728.py │ ├── 0006_api_case_id.py │ ├── 0007_auto_20200822_1119.py │ ├── 0008_auto_20200822_1139.py │ ├── 0009_auto_20200822_1206.py │ ├── 0010_remove_case_apis.py │ ├── 0011_auto_20201012_2355.py │ ├── 0012_auto_20201013_1029.py │ ├── 0013_visit_ip.py │ ├── 0014_auto_20201013_1505.py │ ├── 0015_auto_20201017_1623.py │ ├── 0016_auto_20201017_1624.py │ ├── 0017_visit_project.py │ ├── 0018_auto_20210410_1950.py │ ├── 0019_auto_20210411_2040.py │ ├── 0020_auto_20210525_1844.py │ ├── 0021_auto_20210525_2113.py │ ├── 0022_auto_20210525_2145.py │ ├── 0023_auto_20210910_1155.py │ ├── 0024_project_is_deleted.py │ └── __init__.py ├── models.py ├── serializers.py ├── services │ ├── __init__.py │ └── tree_service_impl.py ├── tasks.py ├── templatetags │ ├── __init__.py │ └── custom_tags.py ├── test.py ├── tests │ ├── __init__.py │ └── test_tree_service_impl.py ├── urls.py ├── utils │ ├── __init__.py │ ├── convert2boomer.py │ ├── convert2hrp.py │ ├── day.py │ ├── decorator.py │ ├── ding_message.py │ ├── email_helper.py │ ├── host.py │ ├── lark_message.py │ ├── loader.py │ ├── middleware.py │ ├── parser.py │ ├── prepare.py │ ├── relation.py │ ├── response.py │ ├── runner.py │ ├── task.py │ ├── test_tree.py │ └── tree.py └── views │ ├── __init__.py │ ├── api.py │ ├── api_rig.py │ ├── ci.py │ ├── config.py │ ├── project.py │ ├── report.py │ ├── run.py │ ├── run_all_auto_case.py │ ├── schedule.py │ ├── suite.py │ ├── timer_task.py │ └── yapi.py ├── fastuser ├── __init__.py ├── admin.py ├── apps.py ├── common │ ├── __init__.py │ ├── response.py │ └── token.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200428_0124.py │ ├── 0002_auto_20200509_1122.py │ ├── 0003_merge_20200813_1655.py │ ├── 0004_auto_20200814_1605.py │ ├── 0005_auto_20200914_2222.py │ ├── 0006_myuser_show_hosts.py │ ├── 0007_alter_myuser_first_name.py │ └── __init__.py ├── models.py ├── serializers.py ├── urls.py └── views.py ├── httprunner ├── __about__.py ├── __init__.py ├── api.py ├── builtin │ ├── __init__.py │ ├── common_util.py │ ├── comparators.py │ ├── faker_helper.py │ ├── login_helper.py │ ├── rand_helper.py │ ├── request_helper.py │ └── time_helper.py ├── cli.py ├── client.py ├── compat.py ├── context.py ├── exceptions.py ├── loader.py ├── locustfile.py ├── locusts.py ├── logger.py ├── parser.py ├── report.py ├── response.py ├── runner.py ├── templates │ ├── locustfile.py │ ├── locustfile_template │ └── report_template.html ├── utils.py └── validator.py ├── manage.py ├── mock ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_mockapi_options_alter_mockapi_project.py │ ├── 0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more.py │ ├── 0004_remove_mockapi_followers_mockapi_enabled_and_more.py │ ├── 0005_mockapilog_request_id_alter_mockapi_api_id_and_more.py │ ├── 0006_mockapi_request_body_mockapi_version_and_more.py │ ├── 0007_mockapiresponsehandler.py │ ├── 0008_mockapiresponsehandler_description_and_more.py │ └── __init__.py ├── models.py ├── serializers.py ├── tasks.py ├── tests.py └── views.py ├── nginx-remote ├── Dockerfile └── nginx.conf ├── nginx.conf ├── nginx ├── Dockerfile └── nginx.conf ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── sql └── .gitkeep ├── start-dev.sh ├── start.bat ├── start.sh ├── static └── extent.js ├── static_root └── .gitkeep ├── system ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers │ ├── __init__.py │ └── log_record_serializer.py ├── tasks.py ├── tests.py └── views.py ├── tempWorkDir └── .gitkeep ├── templates └── report_template.html ├── tests ├── test_login_views.py └── test_mockapi_serializers.py ├── uwsgi.ini ├── uwsgi_docker.ini └── web ├── .babelrc ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── Dockerfile ├── Dockerfile-build ├── LICENSE ├── README.md ├── build ├── build.js ├── check-versions.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── index.html ├── nginx.conf ├── nginx_localhost.conf ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── images │ │ ├── bottom-left.webp │ │ └── bottom-right.webp │ └── styles │ │ ├── home.css │ │ ├── icon-font │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ └── iconfont.woff │ │ ├── iconfont.css │ │ ├── reports.css │ │ ├── swagger.css │ │ └── tree.css ├── main.js ├── mixins │ ├── apiListMixin.js │ ├── configMixin.js │ └── treeMixin.js ├── pages │ ├── auth │ │ ├── Login.vue │ │ └── Register.vue │ ├── common │ │ └── layout │ │ │ └── CommonLayout.vue │ ├── config │ │ ├── RecordConfig.vue │ │ └── components │ │ │ ├── ConfigBody.vue │ │ │ └── ConfigList.vue │ ├── fastrunner │ │ ├── api │ │ │ ├── RecordApi.vue │ │ │ └── components │ │ │ │ ├── ApiBody.vue │ │ │ │ └── ApiList.vue │ │ ├── case │ │ │ ├── AutoTest.vue │ │ │ └── components │ │ │ │ ├── EditTest.vue │ │ │ │ ├── TestBody.vue │ │ │ │ └── TestList.vue │ │ └── config │ │ │ ├── RecordConfig.vue │ │ │ └── components │ │ │ ├── ConfigBody.vue │ │ │ └── ConfigList.vue │ ├── home │ │ ├── Home.vue │ │ └── components │ │ │ ├── Header.vue │ │ │ ├── Side.vue │ │ │ └── SidebarItem.vue │ ├── httprunner │ │ ├── DebugTalk.vue │ │ └── components │ │ │ ├── CodeEditor.vue │ │ │ ├── Extract.vue │ │ │ ├── Headers.vue │ │ │ ├── Hooks.vue │ │ │ ├── Parameters.vue │ │ │ ├── Request.vue │ │ │ ├── RunCodeResult.vue │ │ │ ├── Validate.vue │ │ │ └── Variables.vue │ ├── mock_server │ │ ├── mock_api │ │ │ ├── CustomAceEditor.vue │ │ │ └── index.vue │ │ ├── mock_log │ │ │ └── index.vue │ │ └── mock_project │ │ │ └── index.vue │ ├── project │ │ ├── DataBase.vue │ │ ├── ProjectDashBoard.vue │ │ ├── ProjectDetail.vue │ │ └── ProjectList.vue │ ├── reports │ │ ├── DebugReport.vue │ │ └── ReportList.vue │ ├── task │ │ ├── AddTasks.vue │ │ └── Tasks.vue │ └── variables │ │ ├── GlobalEnv.vue │ │ └── HostAddress.vue ├── restful │ └── api.js ├── router │ └── index.js ├── store │ ├── index.js │ ├── mutations.js │ └── state.js ├── util │ ├── bus.js │ └── format.js └── validator.js ├── static ├── .gitkeep └── favicon.ico └── yarn.lock /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/python:3.9", 3 | "features": { 4 | "ghcr.io/devcontainers/features/node:1": { 5 | "version": "16" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/*.log 3 | **/*-log-* 4 | **/*.tar.gz 5 | Dockerfile 6 | .dockerignore 7 | *.md 8 | /web/node_modules/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # WEB 配置 2 | WEB_PORT=80 # Web 服务器端口号 3 | DJANGO_ADMIN_PORT=8000 # Django 管理界面端口号 4 | SERVER_IP=127.0.0.1 # 主机 IP 地址 5 | 6 | # MQ 配置 7 | RABBITMQ_DEFAULT_USER=rabbitMQ_username # RabbitMQ 默认用户名 8 | RABBITMQ_DEFAULT_PASS=rabbitMQ_password # RabbitMQ 默认密码 9 | MQ_PORT=5672 # MQ 端口号 10 | MQ_ADMIN_PORT=15672 # MQ 管理界面端口号 11 | MQ_HOST=mq # MQ 主机名 12 | MQ_VHOST=/ 13 | 14 | # MySQL 配置 15 | MYSQL_ROOT_PASSWORD=root_user_password # MySQL root 用户密码 16 | MYSQL_USER=db_username # MySQL 用户名 17 | MYSQL_PASSWORD=db_password # MySQL 用户密码 18 | MYSQL_DATABASE=db_name # 默认数据库名 19 | MYSQL_PORT_OUT=13306 # 映射端口号 20 | MYSQL_HOST=db # MySQL 主机名 21 | 22 | # 平台名称 23 | PLATFORM_NAME=FasterRunner 24 | 25 | # 邮件配置, 只跟异常告警相关,和定时任务无关 26 | EMAIL_HOST=smtp.qq.com # 邮箱 SMTP 服务器地址 27 | EMAIL_PORT=465 # 邮箱 SMTP 服务器端口号 28 | EMAIL_HOST_USER=your_email # 发件人邮箱 29 | EMAIL_HOST_PASSWORD=your_email_password # 发件人邮箱密码 30 | 31 | # 国外机器可以打开 32 | # DEBIAN_REPO=deb.debian.org # Debian 软件源地址 33 | # PIP_INDEX_URL=https://pypi.org/simple # PyPI 软件源地址 34 | 35 | 36 | # 模型是否为性能模式,默认是否,会输出日志,1表示是性能模式,不输出日志 37 | IS_PERF=0 -------------------------------------------------------------------------------- /.env.external.samlpe: -------------------------------------------------------------------------------- 1 | # 这份配置是给你已有数据库和独立消息队列使用,也就是你不需要通过本项目的docker-compose部署db和mq 2 | # 对应的docker-compose文件是docker-compose-without-db-mq.yml 3 | 4 | # 测试平台端口 5 | WEB_PORT=80 6 | 7 | # Django后台管理端口 8 | DJANGO_ADMIN_PORT=8000 9 | 10 | # Django API端口,发送测试报告到IM时,外链会使用这个端口 11 | DJANGO_API_PORT=18000 12 | 13 | # 服务器IP地址,用于发送测试报告到IM时,外链会使用这个IP,填写换成你的服务器IP,不能写localhost,127.0.0.1 14 | SERVER_IP=192.168.199.215 15 | 16 | 17 | # 消息队列 18 | 19 | # RabbitMQ默认用户名 20 | RABBITMQ_DEFAULT_USER=admin 21 | 22 | # RabbitMQ默认密码 23 | RABBITMQ_DEFAULT_PASS=111111 24 | 25 | # 消息队列端口 26 | MQ_PORT=5672 27 | 28 | # 消息队列主机, 不能写localhost,127.0.0.1 29 | MQ_HOST=192.168.199.215 30 | 31 | # 消息队列虚拟主机vhost 32 | MQ_VHOST=/ 33 | 34 | 35 | # MySQL数据库 36 | 37 | # MySQL主机, 不能写localhost,127.0.0.1 38 | MYSQL_HOST=192.168.199.215 39 | 40 | # MySQL root用户密码 41 | MYSQL_ROOT_PASSWORD=root 42 | 43 | # MySQL 用户名 44 | MYSQL_USER=faster 45 | 46 | # MySQL 用户密码 47 | MYSQL_PASSWORD=my_passwd 48 | 49 | # MySQL 数据库名 50 | MYSQL_DATABASE=fast_db 51 | 52 | # MySQL 端口 53 | MYSQL_PORT=13306 54 | 55 | # 平台名称 56 | PLATFORM_NAME=FasterRunner 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 提Bug模板 4 | title: '' 5 | labels: bug 6 | assignees: lihuacai168 7 | 8 | --- 9 | 10 | **描述问题** 11 | 清晰明确地描述这个问题是什么。 12 | 13 | **重现步骤** 14 | 重现行为的步骤: 15 | 1. 前往“...” 16 | 2. 点击“...” 17 | 3. 向下滚动至“...” 18 | 4. 查看错误 19 | 20 | **期望的行为** 21 | 清晰明确地描述你期望发生的事情。 22 | 23 | **截图** 24 | 如果适用的话,添加截图以帮助解释你的问题。 25 | 26 | **版本和代码(请填写以下信息):** 27 | - 操作系统:[例如MacOs 13, Win10] 28 | - 浏览器:[例如Chrome、Safari] 29 | - 代码版本:[例如V2.2.1,最新的代码哈希值] 30 | - Python版本: [Python3.9] 31 | 32 | 33 | 34 | **其他相关信息** 35 | 在此处添加任何与问题有关的其他上下文信息。 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: 新功能或者改进 4 | title: '' 5 | labels: enhancement 6 | assignees: lihuacai168 7 | 8 | --- 9 | 10 | **您的功能请求与问题有关吗?请描述。** 11 | 清晰明确地描述问题是什么。例如,每当[...]时,我总是感到很不好用。 12 | 13 | **描述您想要的解决方案** 14 | 清晰明确地描述您希望发生的事情。 15 | 16 | **描述您已考虑的替代方案** 17 | 清晰明确地描述您已经考虑过的任何替代解决方案或功能。 18 | 19 | **其他上下文** 20 | 在此处添加有关功能请求的任何其他上下文或截图。 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/check_app_pr.yml: -------------------------------------------------------------------------------- 1 | name: Check App PR 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | types: [opened, synchronize] 6 | paths: 7 | - '**.py' 8 | - 'requirements.txt' 9 | - '!./web/**' 10 | - 'Dockerfile' 11 | jobs: 12 | build_and_push: 13 | runs-on: ubuntu-latest 14 | 15 | env: 16 | DOCKERHUB_USERNAME: rikasai 17 | DOCKER_REPOSITORY_NAME: fast-runner-backend 18 | 19 | steps: 20 | - name: Check out the repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v1 25 | 26 | - name: Log in to Docker Hub 27 | uses: docker/login-action@v2 28 | with: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | 32 | - name: Get Git tag if exists 33 | id: get_tag 34 | run: | 35 | GIT_TAG=$(git describe --tags --exact-match ${{ github.sha }} 2> /dev/null || echo "") 36 | echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV 37 | 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@v2 40 | with: 41 | context: . 42 | file: ./Dockerfile 43 | platforms: linux/amd64 44 | push: false 45 | tags: | 46 | ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKER_REPOSITORY_NAME }}:latest 47 | ${{ env.GIT_TAG != '' && format('{0}/{1}:{2}', env.DOCKERHUB_USERNAME, env.DOCKER_REPOSITORY_NAME, env.GIT_TAG) || '' }} 48 | -------------------------------------------------------------------------------- /.github/workflows/check_web_pr.yml: -------------------------------------------------------------------------------- 1 | name: check web pr 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - master 8 | types: [opened, synchronize] 9 | paths-ignore: 10 | - '**/*.py' 11 | 12 | jobs: 13 | build_web_image: 14 | runs-on: ubuntu-latest 15 | 16 | env: 17 | DOCKERHUB_USERNAME: rikasai 18 | DOCKER_REPOSITORY_NAME: fast-runner-frontend 19 | 20 | steps: 21 | - name: Check out the repository 22 | uses: actions/checkout@v4 23 | 24 | 25 | - name: Log in to Docker Hub 26 | uses: docker/login-action@v2 27 | with: 28 | username: ${{ secrets.DOCKERHUB_USERNAME }} 29 | password: ${{ secrets.DOCKERHUB_TOKEN }} 30 | 31 | - name: Get Git tag if exists 32 | id: get_tag 33 | run: | 34 | GIT_TAG=$(git describe --tags --exact-match ${{ github.sha }} 2> /dev/null || echo "") 35 | echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV 36 | 37 | - name: Build Image 38 | uses: docker/build-push-action@v2 39 | with: 40 | context: ./web 41 | file: ./web/Dockerfile-build 42 | platforms: linux/amd64 43 | push: false 44 | tags: | 45 | ${{ env.DOCKERHUB_USERNAME }}/${{ env.DOCKER_REPOSITORY_NAME }}:latest 46 | ${{ env.GIT_TAG != '' && format('{0}/{1}:{2}', env.DOCKERHUB_USERNAME, env.DOCKER_REPOSITORY_NAME, env.GIT_TAG) || '' }} 47 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docker_compose.yml: -------------------------------------------------------------------------------- 1 | name: Execute remote shell script 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: 7 | - "Build Frontend and Push Docker Image" 8 | - "Build Backend and Push Docker Image" 9 | types: 10 | - completed 11 | 12 | jobs: 13 | execute_remote_script: 14 | runs-on: ubuntu-latest 15 | env: 16 | WORKFLOW_COUNT: 0 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: (nd) executing remote ssh commands using ssh key 22 | uses: appleboy/ssh-action@master 23 | with: 24 | host: ${{ secrets.REMOTE_SERVER_HOST }} 25 | username: ${{ secrets.REMOTE_SERVER_USER }} 26 | key: ${{ secrets.REMOTE_SERVER_PRIVATE_KEY }} 27 | port: ${{ secrets.REMOTE_SERVER_PORT }} 28 | script: sh ${{ secrets.REMOTE_SCRIPT_PATH }} 29 | -------------------------------------------------------------------------------- /.github/workflows/django_unittest.yml: -------------------------------------------------------------------------------- 1 | name: Python application test with pytest and coverage 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | types: [opened, synchronize] 6 | paths: 7 | - '**.py' 8 | - 'requirements.txt' 9 | - '!./web/**' 10 | - 'Dockerfile' 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Check out code 17 | uses: actions/checkout@v2 18 | - name: Set up Python 3.11 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.11 22 | 23 | - name: Install poetry 24 | run: | 25 | curl -sSL https://install.python-poetry.org | python3 - 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | poetry install 31 | 32 | - name: Test with pytest and coverage 33 | run: | 34 | pytest tests/ --cov=tests/ 35 | - name: Upload coverage to Codecove 36 | uses: codecov/codecov-action@v2 37 | with: 38 | fail_ci_if_error: true -------------------------------------------------------------------------------- /.github/workflows/playwright_ui_test.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | workflow_dispatch: 4 | workflow_run: 5 | workflows: 6 | - "Execute remote shell script" 7 | types: 8 | - completed 9 | schedule: 10 | - cron: '10 0,4,12 * * *' 11 | jobs: 12 | test: 13 | timeout-minutes: 60 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | base_url: ['https://fast.huacai.one'] 19 | 20 | steps: 21 | - name: Checkout repository and submodules 22 | uses: actions/checkout@v3 23 | with: 24 | submodules: true 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: '3.10' 29 | - name: Install dependencies 30 | run: | 31 | cd playwright_demo 32 | python -m pip install --upgrade pip 33 | pip install -r requirements.txt 34 | - name: Ensure browsers are installed 35 | run: python -m playwright install --with-deps 36 | - name: Run your tests 37 | run: | 38 | cd playwright_demo 39 | pytest --base-url=${{ matrix.base_url }} --reruns 3 --reruns-delay 2 40 | - uses: actions/upload-artifact@v3 41 | if: always() 42 | with: 43 | name: playwright-traces_and_results 44 | path: | 45 | playwright_demo/allure-results/ 46 | playwright_demo/*trace.zip 47 | -------------------------------------------------------------------------------- /.github/workflows/upload_web_dist.yml: -------------------------------------------------------------------------------- 1 | name: Build and Upload Web dist 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [ released ] 6 | pull_request: 7 | paths: 8 | - 'web/**' 9 | - '!web/dist/**' 10 | types: [closed] 11 | branches: 12 | - master 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Install Dependencies 21 | run: | 22 | cd web 23 | yarn install 24 | - name: Build 25 | run: | 26 | cd web 27 | npm run build 28 | - name: Upload Files 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: dist 32 | path: web/dist 33 | -------------------------------------------------------------------------------- /.github/workflows/web_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Web 2 | 3 | on: 4 | release: 5 | types: [released] 6 | paths: 7 | - '**.vue' 8 | - '**.js' 9 | workflow_dispatch: 10 | 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./web 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Install Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: '16' 24 | - name: Install npm dependencies 25 | run: yarn install 26 | - name: Run build task 27 | run: npm run build 28 | - name: Deploy to Server 29 | # https://github.com/easingthemes/ssh-deploy 30 | uses: easingthemes/ssh-deploy@main 31 | env: 32 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 33 | ARGS: "-rltgoDzvO --delete" 34 | SOURCE: "web/dist/" 35 | REMOTE_HOST: ${{ secrets.REMOTE_HOST }} 36 | REMOTE_USER: ${{ secrets.REMOTE_USER }} 37 | TARGET: ${{ secrets.REMOTE_TARGET }} 38 | EXCLUDE: "/dist/, /node_modules/" 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | *.iml 4 | .idea 5 | celerybeat.pid 6 | start.sh.bak 7 | tempWorkDir 8 | mysql 9 | .env 10 | /venv/ 11 | /CACHE/ 12 | 13 | 14 | # web 15 | .DS_Store 16 | web/node_modules/ 17 | web/dist/ 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Editor directories and files 23 | .idea 24 | .vscode 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "playwright_demo"] 2 | path = playwright_demo 3 | url = git@github.com:lihuacai168/playwright_demo.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.2.2 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [ --select, I, --fix ] 9 | # Run the formatter. 10 | - id: ruff-format -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | 5 | { 6 | "name": "Django debug", 7 | "type": "debugpy", 8 | "request": "launch", 9 | "program": "${workspaceFolder}/manage.py", 10 | "args": [ 11 | "runserver", 12 | "--settings=FasterRunner.settings.dev" 13 | ], 14 | "django": true, 15 | "justMyCode": true 16 | }, 17 | { 18 | "name": "Python: Current File", 19 | "type": "debugpy", 20 | "request": "launch", 21 | "program": "${file}", 22 | "console": "integratedTerminal", 23 | "cwd": "${fileDirname}", 24 | "env": { 25 | "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" 26 | } 27 | }, 28 | { 29 | "type": "node-terminal", 30 | "name": "npm dev", 31 | "request": "launch", 32 | "command": "npm run dev", 33 | "cwd": "${workspaceFolder}/web" 34 | }, 35 | { 36 | "type": "node-terminal", 37 | "name": "npm build", 38 | "request": "launch", 39 | "command": "npm run build", 40 | "cwd": "${workspaceFolder}/web" 41 | }, 42 | { 43 | "name": "Celery worker", 44 | "type": "debugpy", 45 | "module": "celery", 46 | "request": "launch", 47 | "args": [ 48 | "-A", 49 | "FasterRunner.mycelery", 50 | "worker", 51 | "-l", 52 | "info", 53 | "-P", 54 | "gevent" 55 | ], 56 | "env": { 57 | "DJANGO_SETTINGS_MODULE": "FasterRunner.settings.dev", 58 | "GEVENT_SUPPORT": "True" 59 | }, 60 | "console": "integratedTerminal", 61 | "justMyCode": true 62 | }, 63 | { 64 | "name": "Celery Beat", 65 | "type": "debugpy", 66 | "module": "celery", 67 | "request": "launch", 68 | "args": [ 69 | "-A", 70 | "FasterRunner.mycelery", 71 | "beat", 72 | "-l", 73 | "info", 74 | ], 75 | "env": { 76 | "DJANGO_SETTINGS_MODULE": "FasterRunner.settings.dev" 77 | }, 78 | "console": "integratedTerminal", 79 | "justMyCode": true 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 python:3.11-buster 作为基础镜像 2 | FROM python:3.11-buster as Base 3 | 4 | # 安装 Poetry 5 | RUN pip install poetry==1.8.5 6 | 7 | # 复制 pyproject.toml 和 poetry.lock 文件 8 | COPY pyproject.toml poetry.lock ./ 9 | 10 | # 使用 Poetry 生成 requirements.txt 文件 11 | RUN poetry export -f requirements.txt --output requirements.txt --without-hashes 12 | 13 | # 安装依赖 14 | ARG DEBIAN_REPO="deb.debian.org" 15 | ARG PIP_INDEX_URL="https://pypi.org/simple" 16 | 17 | RUN echo "deb http://$DEBIAN_REPO/debian/ buster main contrib non-free" > /etc/apt/sources.list && \ 18 | echo "deb-src http://$DEBIAN_REPO/debian/ buster main contrib non-free" >> /etc/apt/sources.list && \ 19 | echo "deb http://$DEBIAN_REPO/debian-security buster/updates main contrib non-free" >> /etc/apt/sources.list && \ 20 | echo "deb-src http://$DEBIAN_REPO/debian-security buster/updates main contrib non-free" >> /etc/apt/sources.list && \ 21 | echo "deb http://$DEBIAN_REPO/debian/ buster-updates main contrib non-free" >> /etc/apt/sources.list && \ 22 | echo "deb-src http://$DEBIAN_REPO/debian/ buster-updates main contrib non-free" >> /etc/apt/sources.list 23 | 24 | RUN apt-get update && \ 25 | apt-get install -y python3-dev build-essential netcat-openbsd libpcre3-dev libldap2-dev libsasl2-dev && \ 26 | pip install -r requirements.txt -i ${PIP_INDEX_URL} && \ 27 | apt-get remove -y python3-dev build-essential libpcre3-dev && \ 28 | apt-get autoremove -y && \ 29 | rm -rf /var/lib/apt/lists/* 30 | 31 | # 使用 python:3.11-buster 作为基础镜像 32 | FROM python:3.11-buster 33 | ENV TZ=Asia/Shanghai 34 | 35 | # 设置时区 36 | ARG DEBIAN_REPO="deb.debian.org" 37 | RUN echo "deb http://$DEBIAN_REPO/debian/ buster main contrib non-free" > /etc/apt/sources.list && \ 38 | echo "deb-src http://$DEBIAN_REPO/debian/ buster main contrib non-free" >> /etc/apt/sources.list && \ 39 | echo "deb http://$DEBIAN_REPO/debian-security buster/updates main contrib non-free" >> /etc/apt/sources.list && \ 40 | echo "deb-src http://$DEBIAN_REPO/debian-security buster/updates main contrib non-free" >> /etc/apt/sources.list && \ 41 | echo "deb http://$DEBIAN_REPO/debian/ buster-updates main contrib non-free" >> /etc/apt/sources.list && \ 42 | echo "deb-src http://$DEBIAN_REPO/debian/ buster-updates main contrib non-free" >> /etc/apt/sources.list 43 | 44 | RUN apt-get update && \ 45 | apt-get install -y tzdata && \ 46 | ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ 47 | echo $TZ > /etc/timezone && \ 48 | rm -rf /var/lib/apt/lists/* 49 | 50 | # 复制依赖 51 | COPY --from=Base /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages 52 | 53 | # 设置工作目录 54 | WORKDIR /app 55 | 56 | # 复制项目文件 57 | COPY . /app 58 | 59 | # 设置权限 60 | RUN chmod +x /app/start.sh 61 | 62 | # 收集静态文件 63 | RUN python manage.py collectstatic --settings=FasterRunner.settings.docker --no-input 64 | 65 | # 设置入口点 66 | ENTRYPOINT ["/app/start.sh"] 67 | -------------------------------------------------------------------------------- /FasterRunner/__init__.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | # 替换mysqlclient 4 | pymysql.version_info = (1, 4, 6, "final", 0) # 修改版本号为兼容版本 5 | pymysql.install_as_MySQLdb() 6 | -------------------------------------------------------------------------------- /FasterRunner/customer_swagger.py: -------------------------------------------------------------------------------- 1 | from drf_yasg.inspectors import SwaggerAutoSchema 2 | 3 | 4 | class CustomSwaggerAutoSchema(SwaggerAutoSchema): 5 | def get_tags(self, operation_keys=None): 6 | if hasattr(self.view, 'swagger_tag'): 7 | return [self.view.swagger_tag] 8 | return super().get_tags(operation_keys) 9 | -------------------------------------------------------------------------------- /FasterRunner/database.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author:梨花菜 5 | # @File: database.py 6 | # @Time : 2019/6/13 17:08 7 | # @Email: lihuacai168@gmail.com 8 | # @Software: PyCharm 9 | import platform 10 | 11 | 12 | class AutoChoiceDataBase: 13 | run_system = platform.system() 14 | is_windows = run_system == "Windows" 15 | 16 | def db_for_read(self, model, **hints): 17 | if self.is_windows: 18 | return "default" 19 | else: 20 | return "remote" 21 | 22 | def db_for_write(self, model, **hints): 23 | if self.is_windows: 24 | return "default" 25 | else: 26 | return "remote" 27 | -------------------------------------------------------------------------------- /FasterRunner/log.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: log.py 6 | # @Time : 2023/9/17 22:25 7 | # @Email: lihuacai168@gmail.com 8 | 9 | import logging 10 | from datetime import datetime 11 | 12 | from django.conf import settings 13 | 14 | from FasterRunner.mycelery import app 15 | 16 | 17 | class DatabaseLogHandler(logging.Handler): 18 | def emit(self, record: logging.LogRecord) -> None: 19 | from system.models import LogRecord # 引入上面定义的LogRecord模型 20 | 21 | LogRecord.objects.create( 22 | request_id=record.request_id, 23 | level=record.levelname, 24 | message=self.format(record), 25 | ) 26 | 27 | 28 | class LokiHandler(logging.Handler): 29 | def __init__(self): 30 | super().__init__() 31 | self.loki_url = settings.LOKI_URL or '' 32 | self.loki_username = settings.LOKI_USERNAME 33 | self.loki_password = settings.LOKI_PASSWORD 34 | 35 | def emit(self, record): 36 | log_entry = self.format(record) 37 | self.send_to_loki(log_entry, record.levelname.lower(), record.request_id) 38 | 39 | def send_to_loki(self, log_entry, level, trace_id: str): 40 | if not self.loki_url or not self.loki_username or not self.loki_username: 41 | return 42 | data = { 43 | "streams": [ 44 | { 45 | "stream": { 46 | "level": level, 47 | "job": "fastrunner", 48 | "traceID": trace_id, 49 | }, 50 | "values": [ 51 | [str(int(datetime.now().timestamp() * 1e9)), log_entry] 52 | ] 53 | } 54 | ] 55 | } 56 | try: 57 | kwargs = { 58 | 'loki_url': self.loki_url, 'data': data, 'loki_username': self.loki_username, 59 | 'loki_password': self.loki_password 60 | } 61 | app.send_task(name="system.tasks.send_log_to_loki", args=[], kwargs=kwargs) 62 | except Exception as e: 63 | logging.error(str(e)) 64 | -------------------------------------------------------------------------------- /FasterRunner/mycelery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from celery import Celery 5 | from celery.signals import after_setup_logger, setup_logging 6 | 7 | # set the default Django settings module for the 'celery' program. 8 | from django.conf import settings 9 | 10 | # from FasterRunner.settings import pro as settings 11 | 12 | # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings.SETTINGS_MODULE) 14 | 15 | app = Celery("FasterRunner") 16 | 17 | # Using a string here means the worker doesn't have to serialize 18 | # the configuration object to child processes. 19 | # - namespace='CELERY' means all celery-related configuration keys 20 | # should have a `CELERY_` prefix. 21 | # app.config_from_object('django.conf:settings') 22 | # app.config_from_object('FasterRunner.settings.pro') 23 | obj = os.getenv("DJANGO_SETTINGS_MODULE") 24 | app.config_from_object(obj, namespace="CELERY") 25 | 26 | # Load task modules from all registered Django app configs. 27 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 28 | 29 | 30 | app.conf.update( 31 | CELERY_BEAT_SCHEDULER="django_celery_beat.schedulers:DatabaseScheduler", 32 | task_reject_on_worker_lost=True, 33 | task_acks_late=True, 34 | # celery worker的并发数 根据并发量是适当配置,不易太大 35 | CELERYD_CONCURRENCY=20, 36 | # 每个worker执行了多少次任务后就会死掉,建议数量大一些 37 | CELERYD_MAX_TASKS_PER_CHILD=300, 38 | # 每个worker一次性拿的任务数 39 | CELERYD_PREFETCH_MULTIPLIER=1, 40 | # 完全禁用 Celery 的日志配置 41 | worker_hijack_root_logger=False, 42 | worker_redirect_stdouts=False, 43 | worker_redirect_stdouts_level='ERROR', # 只记录错误级别 44 | ) 45 | 46 | # 禁用 Celery 的日志设置 47 | @setup_logging.connect 48 | def setup_loggers_without_celery(*args, **kwargs): 49 | return True 50 | -------------------------------------------------------------------------------- /FasterRunner/mycelery_dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | # set the default Django settings module for the 'celery' program. 6 | # from django.conf import settings 7 | from FasterRunner.settings import dev as settings 8 | 9 | # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings.dev") 11 | 12 | app = Celery("FasterRunner") 13 | 14 | # Using a string here means the worker doesn't have to serialize 15 | # the configuration object to child processes. 16 | # - namespace='CELERY' means all celery-related configuration keys 17 | # should have a `CELERY_` prefix. 18 | # app.config_from_object('django.conf:settings') 19 | # app.config_from_object('FasterRunner.settings.pro') 20 | app.config_from_object(settings) 21 | 22 | # Load task modules from all registered Django app configs. 23 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 24 | -------------------------------------------------------------------------------- /FasterRunner/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework import pagination 2 | 3 | 4 | class MyCursorPagination(pagination.CursorPagination): 5 | """ 6 | Cursor 光标分页 性能高,安全 7 | """ 8 | 9 | page_size = 9 10 | ordering = "-update_time" 11 | page_size_query_param = "pages" 12 | max_page_size = 20 13 | 14 | 15 | class MyPageNumberPagination(pagination.PageNumberPagination): 16 | """ 17 | 普通分页,数据量越大性能越差 18 | """ 19 | 20 | page_size = 11 21 | page_size_query_param = "size" 22 | page_query_param = "page" 23 | max_page_size = 20 24 | -------------------------------------------------------------------------------- /FasterRunner/routers.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author:梨花菜 5 | # @File: database.py 6 | # @Time : 2019/6/13 17:08 7 | # @Email: lihuacai168@gmail.com 8 | # @Software: PyCharm 9 | import platform 10 | 11 | 12 | class AutoChoiceDataBase: 13 | run_system = platform.system() 14 | is_windows = run_system == "Windows" 15 | 16 | def db_for_read(self, model, **hints): 17 | if self.is_windows: 18 | return "default" 19 | else: 20 | return "remote" 21 | 22 | def db_for_write(self, model, **hints): 23 | if self.is_windows: 24 | return "default" 25 | else: 26 | return "remote" 27 | -------------------------------------------------------------------------------- /FasterRunner/settings/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author:梨花菜 5 | # @File: __init__.py.py 6 | # @Time : 2019/6/14 17:05 7 | # @Email: lihuacai168@gmail.com 8 | # @Software: PyCharm 9 | 10 | # 兼容已经移除的方法 11 | import django 12 | from django.utils.encoding import smart_str 13 | 14 | django.utils.encoding.smart_text = smart_str 15 | 16 | from django.utils.translation import gettext_lazy 17 | 18 | django.utils.translation.ugettext = gettext_lazy 19 | django.utils.translation.ugettext_lazy = gettext_lazy 20 | -------------------------------------------------------------------------------- /FasterRunner/settings/dev.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | 5 | from .base import * 6 | 7 | DEBUG = True 8 | 9 | # LOGGING["loggers"]["mock"]["level"] = "DEBUG" 10 | 11 | logger.remove() 12 | 13 | logger.add( 14 | sys.stdout, 15 | format="{time:YYYY-MM-DD HH:mm:ss.SSS}" 16 | " [pid:{process} -> thread:{thread.name}]" 17 | " {level}" 18 | " [{name}:{function}:{line}]" 19 | " {message}", 20 | level="DEBUG", 21 | ) 22 | 23 | 24 | DATABASES = { 25 | "default": { 26 | "ENGINE": "dj_db_conn_pool.backends.mysql", 27 | "NAME": "fast", # 新建数据库 28 | # 'NAME': 'fast_mb4', # 新建数据库名 29 | "HOST": "127.0.0.1", 30 | "USER": "root", # 数据库登录名 31 | "PASSWORD": "root", # 数据库登录密码 32 | 'OPTIONS': { 33 | 'charset': 'utf8mb4', 34 | }, 35 | 'POOL_OPTIONS': { 36 | 'POOL_SIZE': 20, 37 | 'MAX_OVERFLOW': 20, 38 | 'RECYCLE': 24 * 60 * 60, 39 | 'PRE_PING': True, 40 | 'ECHO': False, 41 | 'TIMEOUT': 30, 42 | }, 43 | "TEST": { 44 | "NAME": "test_fast_last", # 测试过程中会生成名字为test的数据库,测试结束后Django会自动删除该数据库 45 | }, 46 | } 47 | } 48 | 49 | 50 | 51 | # IM_REPORT_SETTING.update({'platform_name': '银河飞梭测试平台'}) 52 | 53 | BROKER_URL = "amqp://username:password@localhost:5672//" 54 | # 需要先在RabbitMQ上创建fast_dev这个vhost 55 | 56 | broker_url = "amqp://rabbitMQ_username:rabbitMQ_password@localhost:5672/fast_dev" 57 | 58 | 59 | BASE_REPORT_URL = "http://localhost:8000/api/fastrunner/reports" 60 | 61 | IM_REPORT_SETTING = { 62 | "base_url": "http://localhost", 63 | "port": 8000, 64 | "report_title": "自动化测试报告", 65 | } 66 | -------------------------------------------------------------------------------- /FasterRunner/settings/docker.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from os import environ 5 | 6 | from .base import * 7 | 8 | DEBUG = False 9 | 10 | # RabbitMQ和MySQL配置相关的设置 11 | 12 | # RabbitMQ 账号密码 13 | MQ_USER = environ.get("RABBITMQ_DEFAULT_USER") 14 | MQ_PASSWORD = environ.get("RABBITMQ_DEFAULT_PASS") 15 | MQ_PORT = environ.get("MQ_PORT") 16 | MQ_HOST = environ.get("MQ_HOST", "mq") 17 | MQ_VHOST = environ.get("MQ_VHOST", "/") 18 | 19 | 20 | # 数据库账号密码 21 | # FASTER_HOST = environ.get('FASTER_HOST') 22 | DB_NAME = environ.get("MYSQL_DATABASE") 23 | DB_PORT = environ.get("MYSQL_PORT", 3306) 24 | DB_HOST = environ.get("MYSQL_HOST", "db") 25 | DB_USER = environ.get("MYSQL_USER", "root") 26 | DB_PASSWORD = environ.get("MYSQL_PASSWORD", "root") 27 | PLATFORM_NAME = environ.get("PLATFORM_NAME") 28 | if PLATFORM_NAME: 29 | IM_REPORT_SETTING.update({"platform_name": PLATFORM_NAME}) 30 | 31 | SENTRY_DSN = environ.get("SENTRY_DSN") 32 | if SENTRY_DSN: 33 | import sentry_sdk 34 | from sentry_sdk.integrations.django import DjangoIntegration 35 | 36 | sentry_sdk.init( 37 | dsn=SENTRY_DSN, 38 | integrations=[DjangoIntegration()], 39 | # Set traces_sample_rate to 1.0 to capture 100% 40 | # of transactions for performance monitoring. 41 | # We recommend adjusting this value in production. 42 | traces_sample_rate=1.0, 43 | # If you wish to associate users to errors (assuming you are using 44 | # django.contrib.auth) you may enable sending PII data. 45 | send_default_pii=True, 46 | ) 47 | 48 | DATABASES = { 49 | "default": { 50 | "ENGINE": "dj_db_conn_pool.backends.mysql", 51 | "HOST": DB_HOST, 52 | "PORT": DB_PORT, 53 | "NAME": DB_NAME, # 新建数据库名 54 | "USER": DB_USER, # 数据库登录名 55 | "PASSWORD": DB_PASSWORD, # 数据库登录密码 56 | "OPTIONS": {"charset": "utf8mb4"}, 57 | 'POOL_OPTIONS': { 58 | 'POOL_SIZE': 20, 59 | 'MAX_OVERFLOW': 20, 60 | 'RECYCLE': 24 * 60 * 60, 61 | 'PRE_PING': True, 62 | 'ECHO': False, 63 | 'TIMEOUT': 30, 64 | }, 65 | } 66 | } 67 | 68 | # mq_user = environ.get('FASTER_MQ_USER') 69 | # mq_password = environ.get('FASTER_MQ_PASSWORD') 70 | # broker_url = f'amqp://{mq_user}:{mq_password}@mq:5672//' 71 | broker_url = f"amqp://{MQ_USER}:{MQ_PASSWORD}@{MQ_HOST}:{MQ_PORT}/{MQ_VHOST}" 72 | 73 | # STATIC_ROOT = os.path.join(BASE_DIR, 'static') 74 | 75 | SERVER_IP = environ.get("SERVER_IP", "") 76 | DJANGO_API_PORT = environ.get("DJANGO_API_PORT", "8000") 77 | BASE_REPORT_URL = f"http://{SERVER_IP}:{DJANGO_API_PORT}/api/fastrunner/reports" 78 | 79 | IM_REPORT_SETTING = { 80 | "base_url": f"http://{SERVER_IP}", 81 | "port": environ.get("DJANGO_API_PORT", "8000"), 82 | "report_title": "自动化测试报告", 83 | } 84 | -------------------------------------------------------------------------------- /FasterRunner/settings/pro.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from os import environ 5 | 6 | from dotenv import find_dotenv, load_dotenv 7 | 8 | from .base import * 9 | 10 | DEBUG = False 11 | 12 | # RabbitMQ和MySQL配置相关的设置 13 | if find_dotenv(): 14 | load_dotenv(find_dotenv()) 15 | # RabbitMQ 账号密码 16 | MQ_USER = environ.get("RABBITMQ_DEFAULT_USER") 17 | MQ_PASSWORD = environ.get("RABBITMQ_DEFAULT_PASS") 18 | MQ_HOST = environ.get("MQ_HOST") 19 | 20 | SERVER_IP = environ.get("SERVER_IP") 21 | # 数据库账号密码 22 | MYSQL_HOST = environ.get("MYSQL_HOST") 23 | DB_NAME = environ.get("MYSQL_DATABASE") 24 | DB_USER = environ.get("MYSQL_USER") 25 | DB_PASSWORD = environ.get("MYSQL_PASSWORD") 26 | PLATFORM_NAME = environ.get("PLATFORM_NAME") 27 | if PLATFORM_NAME: 28 | IM_REPORT_SETTING.update({"platform_name": PLATFORM_NAME}) 29 | 30 | SENTRY_DSN = environ.get("SENTRY_DSN") 31 | if SENTRY_DSN: 32 | import sentry_sdk 33 | from sentry_sdk.integrations.django import DjangoIntegration 34 | 35 | sentry_sdk.init( 36 | dsn=SENTRY_DSN, 37 | integrations=[DjangoIntegration()], 38 | # Set traces_sample_rate to 1.0 to capture 100% 39 | # of transactions for performance monitoring. 40 | # We recommend adjusting this value in production. 41 | traces_sample_rate=1.0, 42 | # If you wish to associate users to errors (assuming you are using 43 | # django.contrib.auth) you may enable sending PII data. 44 | send_default_pii=True, 45 | ) 46 | 47 | DATABASES = { 48 | "default": { 49 | "ENGINE": "dj_db_conn_pool.backends.mysql", 50 | "HOST": MYSQL_HOST, 51 | "NAME": DB_NAME, # 新建数据库名 52 | "USER": DB_USER, # 数据库登录名 53 | "PASSWORD": DB_PASSWORD, # 数据库登录密码 54 | "OPTIONS": {"charset": "utf8mb4"}, 55 | 'POOL_OPTIONS': { 56 | 'POOL_SIZE': 20, 57 | 'MAX_OVERFLOW': 20, 58 | 'RECYCLE': 24 * 60 * 60, 59 | 'PRE_PING': True, 60 | 'ECHO': False, 61 | 'TIMEOUT': 30, 62 | }, 63 | } 64 | } 65 | 66 | broker_url = f"amqp://{MQ_USER}:{MQ_PASSWORD}@{MQ_HOST}:5672//" 67 | 68 | BASE_REPORT_URL = f"http://{SERVER_IP}:8000/api/fastrunner/reports" 69 | 70 | # 用来直接url访问 71 | # STATIC_URL = '/static/' 72 | 73 | # 部署的时候执行python manage.py collectstatic,django会把所有App下的static文件都复制到STATIC_ROOT文件夹下 74 | # STATIC_ROOT = os.path.join(BASE_DIR, 'static') 75 | 76 | # 开发者模式中使用访问静态文 77 | # STATICFILES_DIRS = ( 78 | # os.path.join(BASE_DIR, "static"), 79 | # ) 80 | -------------------------------------------------------------------------------- /FasterRunner/swagger.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: swagger.py 6 | # @Time : 2020/12/28 22:04 7 | # @Email: lihuacai168@gmail.com 8 | 9 | from drf_yasg.inspectors import SwaggerAutoSchema 10 | 11 | 12 | class CustomSwaggerAutoSchema(SwaggerAutoSchema): 13 | def get_tags(self, operation_keys=None): 14 | tags = super().get_tags(operation_keys) 15 | 16 | if "api" in tags and len(operation_keys) >= 3: 17 | # `operation_keys` 内容像这样 ['v1', 'prize_join_log', 'create'] 18 | if operation_keys[2].startswith("run"): 19 | tags[0] = "run" 20 | else: 21 | tags[0] = operation_keys[2] 22 | 23 | return tags 24 | -------------------------------------------------------------------------------- /FasterRunner/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for FasterRunner project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings.pro") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /FasterRunner/wsgi_docker.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for FasterRunner project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings.docker") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 yinquanwang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #.PHONY: up logs up-tag down build build-tag ps config exec restart 2 | 3 | env := ${env} 4 | 5 | HOME_ENV := ${HOME}/.env 6 | CURRENT_ENV := .env.example 7 | env-file := $(CURRENT_ENV) 8 | 9 | ifndef ENV_FILE 10 | ifneq ($(wildcard $(HOME_ENV)),) 11 | env-file := $(HOME_ENV) 12 | else 13 | WARN_MSG := $(warning WARNING: ${HOME_ENV} not found and no ENV_FILE provided, using .env.example instead) 14 | endif 15 | else 16 | env-file := $(ENV_FILE) 17 | endif 18 | 19 | compose-file := docker-compose.yml 20 | 21 | ifdef COMPOSE_FILE 22 | compose-file := $(COMPOSE_FILE) 23 | endif 24 | 25 | cmd = docker-compose -f $(compose-file) --env-file $(env-file) 26 | 27 | 28 | tag := $(tag) 29 | 30 | service := $(word 1,$(MAKECMDGOALS)) 31 | 32 | .PHONY: fastup 33 | fastup: 34 | docker-compose -f docker-compose-for-fastup.yml --env-file .env.example up -d --build --remove-orphans 35 | docker-compose -f docker-compose-for-fastup.yml --env-file .env.example restart nginx web 36 | 37 | .PHONY: up 38 | up: 39 | $(cmd) up -d --build --remove-orphans 40 | $(cmd) restart nginx 41 | $(cmd) restart web 42 | 43 | .PHONY: up-no-build 44 | up-no-build: 45 | $(cmd) up -d --remove-orphans 46 | $(cmd) restart nginx 47 | $(cmd) restart web 48 | 49 | .PHONY: up-tag 50 | up-tag: 51 | export TAG=$(tag); $(cmd) up -d --build --remove-orphans 52 | $(cmd) restart nginx 53 | $(cmd) restart web 54 | 55 | 56 | .PHONY: down 57 | down: 58 | $(cmd) down 59 | 60 | .PHONY: build 61 | build: 62 | # build latest 63 | $(cmd) build 64 | 65 | git-tag: 66 | git checkout $(tag) 67 | 68 | .PHONY: build-tag 69 | build-tag:git-tag 70 | # Build the image with the tag 71 | export TAG=$(tag); $(cmd) build 72 | 73 | .PHONY: config 74 | config: 75 | $(cmd) config 76 | 77 | .PHONY: ps 78 | ps: 79 | $(cmd) ps 80 | 81 | .PHONY: logs 82 | logs: 83 | $(cmd) logs -f app celery-worker web nginx 84 | 85 | .PHONY: restart 86 | restart: 87 | $(cmd) restart $(service) 88 | 89 | .PHONY: exec 90 | exec: 91 | $(cmd) exec $(service) /bin/bash 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![LICENSE](https://img.shields.io/github/license/HttpRunner/FasterRunner.svg)](https://github.com/HttpRunner/FasterRunner/blob/master/LICENSE) 2 | # FasterRunner 3 | 4 | 🚀 让接口测试更简单,让自动化更快速! 5 | 6 | - [X] 🚀 **落地实战** - 已在 5+ 个公司中落地实战,效果显著 7 | - [X] 🔄 **无缝同步** - 支持一键同步 YAPI(Swagger,Postman)接口数据,告别手动录入的繁琐 8 | - [X] 💪 **强大引擎** - 基于Pythone3 + Requests 打造,轻松应对各类 HTTP(S) 测试场景,稳定可靠 9 | - [X] 🔐 **灵活扩展** - 通过 debugtalk.py 自定义函数,轻松实现接口签名、加解密等自定义功能 10 | - [X] 🎯 **完美联动** - 强大的 hook 机制,优雅处理接口间的token依赖和参数传递,打通测试全流程 11 | - [X] ⏰ **智能调度** - 内置 crontab 定时任务,无学习成本,帮你实现自动化监控 12 | - [X] 📊 **数据驱动** - 支持测试用例参数化,释放测试人员生产力 13 | - [X] 🔄 **持续集成** - 完美对接 Gitlab-CI、Jenkins 等CI工具,助力研发效能提升 14 | - [X] 📈 **清晰报告** - 简洁美观的测试报告,包含详尽的统计信息和日志记录,一目了然 15 | - [X] 📱 **即时通知** - 自动推送测试报告至飞书、钉钉、企业微信,随时掌握测试动态 16 | 17 | ![](https://cdn.jsdelivr.net/gh/lihuacai168/images/img/project_detail.png) 18 | 19 | # ⚠️ 注意 20 | > python版本需要>=3.9 21 | > 22 | > 3.9, 3.10和3.11都经过测试 23 | 24 | # 📚 文档 25 | - 使用文档 https://www.yuque.com/lihuacai/fasterunner 26 | 27 | # 🚀 Quick Start 28 | 29 | ## 拉取代码和启动服务 30 | ```shell 31 | # 拉取代码 32 | git clone git@github.com:lihuacai168/AnotherFasterRunner.git AnotherFasterRunner 33 | 34 | # 如果你的机器连接不上Github,可以用国内的Gitee 35 | # git clone git@gitee.com:lihuacai/AnotherFasterRunner.git AnotherFasterRunner 36 | 37 | # 使用makefile命令快速启动所有服务,没错,一个命令就搞定 38 | cd AnotherFasterRunner && make 39 | 40 | # 或者使用docker-compose原始的命令, 指定配置文件启动 41 | cd AnotherFasterRunner && docker-compose -f docker-compose-for-fastup.yml --env-file .env.example up -d 42 | ``` 43 | 44 | ## 访问服务 45 | ```shell 46 | # 默认是80端口,如果80端口被占用,修改env文件中的WEB_PORT即可 47 | 浏览器打开: 48 | http://你的ip/fastrunner/login 49 | 50 | 用户:test 51 | 密码:test2020 52 | ``` 53 | 54 | # 💻 Dev 55 | - [Django原生部署](https://www.jianshu.com/p/e26ccc21ddf2) 56 | 57 | # 🔧 uWSGI 58 | - [uWSGI+Nginx+Supervisor+Python虚拟环境部署](https://www.jianshu.com/p/577a966b0998) 59 | 60 | # ⭐ Star History 61 | 62 | ![Star History Chart](https://api.star-history.com/svg?repos=lihuacai168/AnotherFasterRunner&type=Date) 63 | 64 | # 👥 贡献者 65 | 66 | 67 | 68 | 69 | # 🙏 鸣谢 70 | 71 | 感谢 JetBrains 对开源项目的支持 72 | 73 | 74 | JetBrains 75 | 76 | -------------------------------------------------------------------------------- /curd/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: __init__.py.py 6 | # @Time : 2022/9/4 17:44 7 | # @Email: lihuacai168@gmail.com 8 | -------------------------------------------------------------------------------- /curd/base_curd.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: base_curd.py 6 | # @Time : 2022/9/4 17:45 7 | # @Email: lihuacai168@gmail.com 8 | 9 | 10 | from abc import ABC, abstractmethod 11 | from typing import Any 12 | 13 | from rest_framework.generics import get_object_or_404 14 | 15 | from curd import crud_helper 16 | 17 | 18 | class BaseCURD(ABC): 19 | def __init__(self, model): 20 | self.model = model 21 | 22 | @abstractmethod 23 | def create_obj(self, creator: str, payload: Any) -> int: 24 | ... 25 | 26 | @abstractmethod 27 | def get_obj_by_pk(self, pk: int) -> "obj": 28 | ... 29 | 30 | @abstractmethod 31 | def get_obj_by_unique_key(self, unique_key: dict) -> "obj": 32 | ... 33 | 34 | @abstractmethod 35 | def get_or_create(self, filter_kwargs: dict, defaults: dict) -> tuple["obj", bool]: 36 | ... 37 | 38 | @abstractmethod 39 | def list_obj(self, page_filter: dict) -> list[dict]: 40 | ... 41 | 42 | @abstractmethod 43 | def update_obj_by_pk(self, pk: int, updater: str, payload: dict) -> int: 44 | ... 45 | 46 | @abstractmethod 47 | def delete_obj_by_pk(self, pk: int) -> bool: 48 | ... 49 | 50 | 51 | class GenericCURD(BaseCURD): 52 | def create_obj(self, creator: str, payload: Any) -> int: 53 | return crud_helper.create(creator, self.model, payload) 54 | 55 | def get_obj_by_pk(self, pk: int) -> "obj": 56 | return get_object_or_404(self.model, id=pk) 57 | 58 | def get_obj_by_unique_key(self, unique_key: dict) -> "obj": 59 | return get_object_or_404(self.model, **unique_key) 60 | 61 | def get_or_create(self, filter_kwargs: dict, defaults: dict) -> tuple["obj", bool]: 62 | return crud_helper.get_or_create(self.model, filter_kwargs, defaults) 63 | 64 | def list_obj(self, page_filter: dict) -> list[dict]: 65 | return self.model.objects.filter(**page_filter) 66 | 67 | def update_obj_by_pk(self, pk: int, updater: str, payload: dict) -> int: 68 | return crud_helper.update(self.get_obj_by_pk(pk), updater, payload) 69 | 70 | def delete_obj_by_pk(self, pk: int) -> bool: 71 | self.get_obj_by_pk(pk).delete() 72 | return True 73 | -------------------------------------------------------------------------------- /curd/crud_helper.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author: 花菜 4 | # @File: crud_helper.py 5 | # @Time : 2022/9/4 17:59 6 | # @Email: lihuacai168@gmail.com 7 | 8 | import traceback 9 | from typing import TypeVar 10 | 11 | from loguru import logger 12 | 13 | from fastrunner.models import BaseTable 14 | 15 | BModel = TypeVar("BModel", bound=BaseTable) 16 | 17 | 18 | def create(creator: str, model: BModel, payload: dict) -> int: 19 | try: 20 | logger.info(f"input: create={model.__name__}, payload={payload}") 21 | obj = model.objects.create(creator=creator, **payload) 22 | except Exception as e: 23 | logger.warning(traceback.format_exc()) 24 | raise e 25 | logger.info(f"create {model.__name__} success, id: {obj.id}") 26 | 27 | 28 | def get_or_create(model: BModel, filter_kwargs: dict, defaults: dict) -> tuple["obj", bool]: 29 | """ 30 | :raises DoesNotExist 31 | """ 32 | logger.info(f"input: get_or_create={model.__name__}, filter_kwargs={filter_kwargs}, defaults={defaults}") 33 | obj, created = model.objects.get_or_create( 34 | defaults=defaults, 35 | **filter_kwargs, 36 | ) 37 | return obj, created 38 | 39 | 40 | def update( 41 | obj, 42 | updater: str, 43 | payload: dict, 44 | ) -> int: 45 | logger.info(f"input: update model={obj.__class__.__name__}, id={obj.id}, payload={payload}") 46 | if updater: 47 | obj.updater = updater 48 | for attr, value in payload.items(): 49 | if hasattr(obj, attr) is False: 50 | logger.warning(f"{attr=} not in obj fields, it will not update") 51 | setattr(obj, attr, value) 52 | try: 53 | obj.save() 54 | except Exception as e: 55 | logger.error(traceback.format_exc()) 56 | raise e 57 | logger.info(f"update {obj.__class__.__name__} success, id: {obj.id}") 58 | return obj.id 59 | -------------------------------------------------------------------------------- /db/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | pid-file=/var/run/mysqld/mysqld.pid 3 | socket=/var/run/mysqld/mysqld.sock 4 | datadir=/var/lib/mysql 5 | #log-error=/var/log/mysql/error.log 6 | # By default we only accept connections from localhost 7 | #bind-address = 127.0.0.1 8 | # Disabling symbolic-links is recommended to prevent assorted security risks 9 | symbolic-links=0 10 | # 1GB 11 | max_allowed_packet=1073741824 12 | # 大小写不敏感 13 | # lower_case_table_names=1 14 | # 慢查询 15 | slow_query_log = ON 16 | slow_query_log_file = /var/lib/mysql/slow.log 17 | long_query_time = 3 -------------------------------------------------------------------------------- /docker-compose-build-db-mq.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # 用来构建db和mq,因为这两个组件几乎不会有改动,所以单独抽离出来 4 | # db会默认初始化一些演示的数据, sql文件在db/init.sql 5 | 6 | x-env-db: &env_db 7 | MYSQL_DATABASE: ${MYSQL_DATABASE} 8 | MYSQL_USER: ${MYSQL_USER} 9 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 10 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 11 | MYSQL_HOST: ${MYSQL_HOST:-db} 12 | MYSQL_PORT: ${MYSQL_PORT:-3306} 13 | 14 | x-env-mq: &env_mq 15 | RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} 16 | RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} 17 | MQ_PORT: ${MQ_PORT} 18 | MQ_HOST: ${MQ_HOST:-mq} 19 | MQ_ADMIN_PORT: ${MQ_ADMIN_PORT} 20 | MQ_VHOST: ${MQ_VHOST:-/} 21 | 22 | services: 23 | db: 24 | #image: mysql:8.0.21 25 | image: mariadb:10.6.1 26 | # privileged: true 27 | environment: 28 | # 设置默认数据库和root默认密码,如果宿主机中/var/lib/mysql已经存在,这两个设置都不会生效 29 | <<: *env_db 30 | volumes: 31 | # - "/var/lib/mysql:/var/lib/mysql" # 挂载宿主机的mysql数据到docker中 32 | - ./db/my.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf 33 | - ./db:/docker-entrypoint-initdb.d/:ro 34 | # 端口映射,格式 宿主机端口:容器端口 35 | ports: 36 | - "${MYSQL_PORT_OUT}:3306" 37 | restart: always 38 | command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 39 | 40 | 41 | mq: 42 | image: rabbitmq:management-alpine 43 | environment: 44 | <<: *env_mq 45 | restart: always 46 | ports: 47 | - "${MQ_ADMIN_PORT}:15672" 48 | - "${MQ_PORT}:5672" 49 | 50 | -------------------------------------------------------------------------------- /docker-compose-for-fastup.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # 用来快速启动测试平台的前后端,mq和db 4 | # 默认会拉取dockerhub最新的镜像,不会用到本地的代码构建 5 | # 仅适用想快速体验测试平台和一直使用最新版的用户,不适合二开 6 | 7 | x-env-db: &env_db 8 | MYSQL_DATABASE: ${MYSQL_DATABASE} 9 | MYSQL_USER: ${MYSQL_USER} 10 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 11 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 12 | MYSQL_HOST: ${MYSQL_HOST:-db} 13 | MYSQL_PORT: ${MYSQL_PORT:-3306} 14 | 15 | x-env-mq: &env_mq 16 | RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} 17 | RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} 18 | MQ_PORT: ${MQ_PORT} 19 | MQ_HOST: ${MQ_HOST:-mq} 20 | MQ_ADMIN_PORT: ${MQ_ADMIN_PORT} 21 | MQ_VHOST: ${MQ_VHOST:-/} 22 | 23 | x-env-celery: &env_celery 24 | SERVER_IP: ${SERVER_IP} 25 | DJANGO_API_PORT: ${DJANGO_API_PORT} 26 | 27 | x-env-app: &env_app 28 | EMAIL_HOST: ${EMAIL_HOST} 29 | EMAIL_PORT: ${EMAIL_PORT} 30 | EMAIL_HOST_USER: ${EMAIL_HOST_USER} 31 | EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} 32 | LOKI_USERNAME: ${LOKI_USERNAME} 33 | LOKI_PASSWORD: ${LOKI_PASSWORD} 34 | LOKI_URL: ${LOKI_URL} 35 | 36 | services: 37 | app: 38 | image: rikasai/fast-runner-backend:latest 39 | environment: 40 | <<: [*env_db, *env_celery, *env_app, *env_mq] 41 | entrypoint: /app/start.sh 42 | command: "app" 43 | restart: always 44 | depends_on: 45 | - db 46 | - mq 47 | 48 | celery-worker: 49 | image: rikasai/fast-runner-backend:latest 50 | environment: 51 | <<: [*env_db, *env_celery, *env_app, *env_mq] 52 | entrypoint: /app/start.sh 53 | command: "celery-worker" 54 | restart: always 55 | depends_on: 56 | - db 57 | - mq 58 | 59 | celery-beat: 60 | image: rikasai/fast-runner-backend:latest 61 | environment: 62 | <<: [*env_db, *env_celery, *env_app, *env_mq] 63 | entrypoint: /app/start.sh 64 | command: "celery-beat" 65 | restart: always 66 | depends_on: 67 | - db 68 | - mq 69 | 70 | nginx: 71 | build: ./nginx-remote 72 | ports: 73 | - "${DJANGO_ADMIN_PORT}:8000" 74 | depends_on: 75 | - app 76 | restart: always 77 | environment: 78 | <<: *env_app 79 | 80 | web: 81 | image: rikasai/fast-runner-frontend:latest 82 | ports: 83 | - "${WEB_PORT}:80" 84 | depends_on: 85 | - app 86 | 87 | db: 88 | image: mariadb:10.6.1 89 | environment: 90 | <<: *env_db 91 | volumes: 92 | - ./db/my.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf 93 | - ./db:/docker-entrypoint-initdb.d/:ro 94 | ports: 95 | - "${MYSQL_PORT_OUT}:3306" 96 | restart: always 97 | command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 98 | 99 | mq: 100 | image: rabbitmq:management-alpine 101 | environment: 102 | <<: *env_mq 103 | ports: 104 | - "${MQ_ADMIN_PORT}:15672" 105 | - "${MQ_PORT}:5672" 106 | restart: always 107 | -------------------------------------------------------------------------------- /docker-compose-without-db-mq.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # 用来快速启动测试平台的前后端,不包含db和mq 4 | # 用到本地的代码构建 5 | # 可以搭配docker-compose-build-db—mq使用 6 | # 或者已有mq和db也是OK的 7 | 8 | 9 | 10 | x-env-mq: &env_mq 11 | RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} 12 | RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} 13 | MQ_PORT: ${MQ_PORT} 14 | MQ_HOST: ${MQ_HOST:-mq} 15 | MQ_ADMIN_PORT: ${MQ_ADMIN_PORT} 16 | MQ_VHOST: ${MQ_VHOST:-/} 17 | 18 | x-env-celery: &env_celery 19 | SERVER_IP: ${SERVER_IP} 20 | DJANGO_API_PORT: ${DJANGO_API_PORT} 21 | 22 | x-env-app: &env_app 23 | EMAIL_HOST: ${EMAIL_HOST} 24 | EMAIL_PORT: ${EMAIL_PORT} 25 | EMAIL_HOST_USER: ${EMAIL_HOST_USER} 26 | EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} 27 | LOKI_USERNAME: ${LOKI_USERNAME} 28 | LOKI_PASSWORD: ${LOKI_PASSWORD} 29 | LOKI_URL: ${LOKI_URL} 30 | 31 | services: 32 | app: 33 | image: fasterrunner_app:latest 34 | build: 35 | context: . 36 | dockerfile: Dockerfile 37 | args: 38 | PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple} 39 | DEBIAN_REPO: ${DEBIAN_REPO:-mirrors.aliyun.com} 40 | environment: 41 | <<: [*env_celery, *env_app, *env_mq] 42 | entrypoint: /app/start.sh 43 | command: "app" 44 | restart: always 45 | 46 | celery-worker: 47 | build: 48 | context: . 49 | dockerfile: Dockerfile 50 | args: 51 | PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple} 52 | DEBIAN_REPO: ${DEBIAN_REPO:-mirrors.aliyun.com} 53 | environment: 54 | <<: [*env_celery, *env_app, *env_mq] 55 | entrypoint: /app/start.sh 56 | command: "celery-worker" 57 | restart: always 58 | 59 | celery-beat: 60 | build: 61 | context: . 62 | dockerfile: Dockerfile 63 | args: 64 | PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple} 65 | DEBIAN_REPO: ${DEBIAN_REPO:-mirrors.aliyun.com} 66 | environment: 67 | <<: [*env_celery, *env_app, *env_mq] 68 | command: "celery-beat" 69 | restart: always 70 | 71 | # django-admin & app proxy 72 | nginx: 73 | build: ./nginx 74 | ports: 75 | - "${DJANGO_ADMIN_PORT}:8000" 76 | depends_on: 77 | - app 78 | restart: always 79 | environment: 80 | <<: [*env_celery, *env_app, *env_mq] 81 | web: 82 | build: 83 | context: ./web 84 | dockerfile: Dockerfile 85 | ports: 86 | - "${WEB_PORT}:80" 87 | volumes: 88 | - ./web/dist:/usr/share/nginx/html 89 | environment: 90 | - FasterRunner=${MYSQL_USER} 91 | depends_on: 92 | - app 93 | 94 | -------------------------------------------------------------------------------- /fastrunner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastrunner/__init__.py -------------------------------------------------------------------------------- /fastrunner/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /fastrunner/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FastrunnerConfig(AppConfig): 5 | name = "fastrunner" 6 | -------------------------------------------------------------------------------- /fastrunner/dto/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: __init__.py.py 6 | # @Time : 2022/9/4 19:05 7 | # @Email: lihuacai168@gmail.com 8 | -------------------------------------------------------------------------------- /fastrunner/dto/tree_dto.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: tree_dto.py 6 | # @Time : 2022/9/4 19:05 7 | # @Email: lihuacai168@gmail.com 8 | 9 | from pydantic import BaseModel, Field 10 | 11 | 12 | class TreeUniqueIn(BaseModel): 13 | project_id: int 14 | type: int 15 | 16 | 17 | class TreeUpdateIn(BaseModel): 18 | tree: list[dict] = Field(alias="body") 19 | 20 | 21 | class TreeOut(BaseModel): 22 | tree: list[dict] 23 | id: int 24 | max: int 25 | -------------------------------------------------------------------------------- /fastrunner/migrations/0002_auto_20200509_1122.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-05-09 11:22 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fastrunner", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="casestep", 15 | name="case", 16 | field=models.ForeignKey( 17 | db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to="fastrunner.Case" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /fastrunner/migrations/0003_casestep_source_api_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2020-08-13 16:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0002_auto_20200509_1122"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="casestep", 14 | name="source_api_id", 15 | field=models.IntegerField(default=0, verbose_name="api来源"), 16 | preserve_default=False, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /fastrunner/migrations/0005_auto_20200814_1728.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2020-08-14 17:28 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fastrunner", "0004_auto_20200814_1605"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="debugtalk", 15 | name="create_time", 16 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name="创建时间"), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name="debugtalk", 21 | name="creator", 22 | field=models.CharField(max_length=20, null=True, verbose_name="创建人"), 23 | ), 24 | migrations.AddField( 25 | model_name="debugtalk", 26 | name="update_time", 27 | field=models.DateTimeField(auto_now=True, verbose_name="更新时间"), 28 | ), 29 | migrations.AddField( 30 | model_name="debugtalk", 31 | name="updater", 32 | field=models.CharField(max_length=20, null=True, verbose_name="更新人"), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /fastrunner/migrations/0006_api_case_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-08-22 09:52 2 | 3 | import django_mysql.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fastrunner", "0005_auto_20200814_1728"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="api", 15 | name="case_id", 16 | field=django_mysql.models.ListCharField( 17 | models.IntegerField(default=0, verbose_name="case_id"), default=[], max_length=5, size=500 18 | ), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /fastrunner/migrations/0007_auto_20200822_1119.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-08-22 11:19 2 | 3 | import django_mysql.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fastrunner", "0006_api_case_id"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="api", 15 | name="case_id", 16 | field=django_mysql.models.ListCharField( 17 | models.IntegerField(default=0, verbose_name="case_id"), max_length=1000, size=500 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /fastrunner/migrations/0008_auto_20200822_1139.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-08-22 11:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0007_auto_20200822_1119"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="api", 14 | name="case_id", 15 | ), 16 | migrations.AddField( 17 | model_name="api", 18 | name="case_id", 19 | field=models.ManyToManyField(db_table="api_case", to="fastrunner.Case"), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /fastrunner/migrations/0009_auto_20200822_1206.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-08-22 12:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0008_auto_20200822_1139"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="api", 14 | name="case_id", 15 | ), 16 | migrations.AddField( 17 | model_name="case", 18 | name="apis", 19 | field=models.ManyToManyField(db_table="api_case", to="fastrunner.API"), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /fastrunner/migrations/0010_remove_case_apis.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-08-22 23:58 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0009_auto_20200822_1206"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="case", 14 | name="apis", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /fastrunner/migrations/0011_auto_20201012_2355.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-10-12 23:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0010_remove_case_apis"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Visit", 14 | fields=[ 15 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 16 | ("user", models.CharField(max_length=100, verbose_name="访问url的用户名")), 17 | ("url", models.CharField(max_length=100, verbose_name="被访问的url")), 18 | ( 19 | "request_method", 20 | models.CharField( 21 | choices=[ 22 | ("GET", "GET"), 23 | ("POST", "POST"), 24 | ("PUT", "PUT"), 25 | ("PATCH", "PATCH"), 26 | ("DELETE", "DELETE"), 27 | ("OPTION", "OPTION"), 28 | ], 29 | max_length=7, 30 | verbose_name="请求方法", 31 | ), 32 | ), 33 | ("times", models.IntegerField(verbose_name="访问次数")), 34 | ], 35 | options={ 36 | "db_table": "visit", 37 | }, 38 | ), 39 | migrations.AlterField( 40 | model_name="api", 41 | name="tag", 42 | field=models.IntegerField( 43 | choices=[(0, "未知"), (1, "成功"), (2, "失败"), (3, "自动成功")], default=0, verbose_name="API标签" 44 | ), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /fastrunner/migrations/0012_auto_20201013_1029.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-10-13 10:29 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fastrunner", "0011_auto_20201012_2355"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="visit", 15 | name="times", 16 | ), 17 | migrations.AddField( 18 | model_name="visit", 19 | name="create_time", 20 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name="创建时间"), 21 | preserve_default=False, 22 | ), 23 | migrations.AddField( 24 | model_name="visit", 25 | name="request_body", 26 | field=models.TextField(default=django.utils.timezone.now, verbose_name="请求体"), 27 | preserve_default=False, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /fastrunner/migrations/0013_visit_ip.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-10-13 11:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0012_auto_20201013_1029"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="visit", 14 | name="ip", 15 | field=models.CharField(default="127.0.0.1", max_length=20, verbose_name="用户的ip"), 16 | preserve_default=False, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /fastrunner/migrations/0014_auto_20201013_1505.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-10-13 15:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0013_visit_ip"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="api", 14 | name="url", 15 | field=models.CharField(db_index=True, max_length=255, verbose_name="请求地址"), 16 | ), 17 | migrations.AlterField( 18 | model_name="casestep", 19 | name="url", 20 | field=models.CharField(max_length=255, verbose_name="请求地址"), 21 | ), 22 | migrations.AlterField( 23 | model_name="visit", 24 | name="url", 25 | field=models.CharField(max_length=255, verbose_name="被访问的url"), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /fastrunner/migrations/0015_auto_20201017_1623.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-10-17 16:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0014_auto_20201013_1505"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="visit", 14 | name="path", 15 | field=models.CharField(default="", max_length=100, verbose_name="被访问的接口路径"), 16 | ), 17 | migrations.AddField( 18 | model_name="visit", 19 | name="request_params", 20 | field=models.CharField(default="", max_length=255, verbose_name="请求参数"), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /fastrunner/migrations/0016_auto_20201017_1624.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-10-17 16:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0015_auto_20201017_1623"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="visit", 14 | name="create_time", 15 | field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="创建时间"), 16 | ), 17 | migrations.AlterField( 18 | model_name="visit", 19 | name="ip", 20 | field=models.CharField(db_index=True, max_length=20, verbose_name="用户的ip"), 21 | ), 22 | migrations.AlterField( 23 | model_name="visit", 24 | name="path", 25 | field=models.CharField(db_index=True, default="", max_length=100, verbose_name="被访问的接口路径"), 26 | ), 27 | migrations.AlterField( 28 | model_name="visit", 29 | name="request_method", 30 | field=models.CharField( 31 | choices=[ 32 | ("GET", "GET"), 33 | ("POST", "POST"), 34 | ("PUT", "PUT"), 35 | ("PATCH", "PATCH"), 36 | ("DELETE", "DELETE"), 37 | ("OPTION", "OPTION"), 38 | ], 39 | db_index=True, 40 | max_length=7, 41 | verbose_name="请求方法", 42 | ), 43 | ), 44 | migrations.AlterField( 45 | model_name="visit", 46 | name="request_params", 47 | field=models.CharField(db_index=True, default="", max_length=255, verbose_name="请求参数"), 48 | ), 49 | migrations.AlterField( 50 | model_name="visit", 51 | name="url", 52 | field=models.CharField(db_index=True, max_length=255, verbose_name="被访问的url"), 53 | ), 54 | migrations.AlterField( 55 | model_name="visit", 56 | name="user", 57 | field=models.CharField(db_index=True, max_length=100, verbose_name="访问url的用户名"), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /fastrunner/migrations/0017_visit_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-11-05 14:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0016_auto_20201017_1624"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="visit", 14 | name="project", 15 | field=models.CharField(db_index=True, default=0, max_length=4, verbose_name="项目id"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /fastrunner/migrations/0018_auto_20210410_1950.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-04-10 19:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0017_visit_project"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="api", 14 | name="yapi_catid", 15 | field=models.IntegerField(default=0, null=True, verbose_name="yapi的分组id"), 16 | ), 17 | migrations.AddField( 18 | model_name="api", 19 | name="yapi_id", 20 | field=models.IntegerField(default=0, null=True, verbose_name="yapi的id"), 21 | ), 22 | migrations.AddField( 23 | model_name="api", 24 | name="ypai_add_time", 25 | field=models.CharField(default="", max_length=10, null=True, verbose_name="yapi创建时间"), 26 | ), 27 | migrations.AddField( 28 | model_name="api", 29 | name="ypai_up_time", 30 | field=models.CharField(default="", max_length=10, null=True, verbose_name="yapi更新时间"), 31 | ), 32 | migrations.AddField( 33 | model_name="api", 34 | name="ypai_username", 35 | field=models.CharField(default="", max_length=30, null=True, verbose_name="yapi的原作者"), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /fastrunner/migrations/0019_auto_20210411_2040.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-04-11 20:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0018_auto_20210410_1950"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="yapi_base_url", 15 | field=models.CharField(default="", max_length=100, verbose_name="yapi的openapi url"), 16 | ), 17 | migrations.AddField( 18 | model_name="project", 19 | name="yapi_openapi_token", 20 | field=models.CharField(default="", max_length=128, verbose_name="yapi openapi的token"), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /fastrunner/migrations/0020_auto_20210525_1844.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-05-25 18:44 2 | 3 | import jsonfield.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fastrunner", "0019_auto_20210411_2040"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="report", 15 | name="ci_metadata", 16 | field=jsonfield.fields.JSONField(default=dict), 17 | ), 18 | migrations.AlterField( 19 | model_name="project", 20 | name="yapi_base_url", 21 | field=models.CharField(blank=True, default="", max_length=100, verbose_name="yapi的openapi url"), 22 | ), 23 | migrations.AlterField( 24 | model_name="project", 25 | name="yapi_openapi_token", 26 | field=models.CharField(blank=True, default="", max_length=128, verbose_name="yapi openapi的token"), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /fastrunner/migrations/0021_auto_20210525_2113.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-05-25 21:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0020_auto_20210525_1844"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="report", 14 | name="ci_job_id", 15 | field=models.CharField( 16 | db_index=True, default=None, max_length=15, null=True, unique=True, verbose_name="gitlab的项目id" 17 | ), 18 | ), 19 | migrations.AddField( 20 | model_name="report", 21 | name="ci_project_id", 22 | field=models.IntegerField(db_index=True, default=0, verbose_name="gitlab的项目id"), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /fastrunner/migrations/0022_auto_20210525_2145.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-05-25 21:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0021_auto_20210525_2113"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="report", 14 | name="ci_project_id", 15 | field=models.IntegerField(db_index=True, default=0, null=True, verbose_name="gitlab的项目id"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /fastrunner/migrations/0023_auto_20210910_1155.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-09-10 11:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastrunner", "0022_auto_20210525_2145"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="jira_bearer_token", 15 | field=models.CharField(blank=True, default="", max_length=45, verbose_name="jira bearer_token"), 16 | ), 17 | migrations.AddField( 18 | model_name="project", 19 | name="jira_project_key", 20 | field=models.CharField(blank=True, default="", max_length=30, verbose_name="jira项目key"), 21 | ), 22 | migrations.AlterField( 23 | model_name="api", 24 | name="tag", 25 | field=models.IntegerField( 26 | choices=[(0, "未知"), (1, "成功"), (2, "失败"), (3, "自动成功"), (4, "废弃")], 27 | default=0, 28 | verbose_name="API标签", 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="case", 33 | name="tag", 34 | field=models.IntegerField( 35 | choices=[(1, "冒烟用例"), (2, "集成用例"), (3, "监控脚本"), (4, "核心用例")], 36 | default=2, 37 | verbose_name="用例标签", 38 | ), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /fastrunner/migrations/0024_project_is_deleted.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-15 01:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("fastrunner", "0023_auto_20210910_1155"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="project", 15 | name="is_deleted", 16 | field=models.IntegerField(default=0, null=True, verbose_name="是否删除"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /fastrunner/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastrunner/migrations/__init__.py -------------------------------------------------------------------------------- /fastrunner/services/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: __init__.py.py 6 | # @Time : 2022/9/4 17:42 7 | # @Email: lihuacai168@gmail.com 8 | -------------------------------------------------------------------------------- /fastrunner/services/tree_service_impl.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author: 花菜 4 | # @File: tree_service_impl.py 5 | # @Time : 2022/9/4 18:57 6 | # @Email: lihuacai168@gmail.com 7 | 8 | import traceback 9 | from typing import Union 10 | 11 | from loguru import logger 12 | 13 | from curd.base_curd import GenericCURD 14 | from fastrunner.dto.tree_dto import TreeOut, TreeUniqueIn, TreeUpdateIn 15 | from fastrunner.models import Relation 16 | from fastrunner.utils.response import TREE_ADD_SUCCESS, TREE_UPDATE_SUCCESS, StandResponse 17 | from fastrunner.utils.tree import get_tree_max_id 18 | 19 | 20 | class TreeService: 21 | def __init__(self): 22 | self.model = Relation 23 | self.curd = GenericCURD(self.model) 24 | 25 | def get_or_create(self, query: TreeUniqueIn) -> StandResponse[TreeOut]: 26 | default_tree: list = [{"id": 1, "label": "default node", "children": []}] 27 | tree_obj, created = self.curd.get_or_create( 28 | filter_kwargs=query.dict(), 29 | defaults={"tree": default_tree, "project_id": query.project_id}, 30 | ) 31 | if created: 32 | logger.info(f"tree created {query=}") 33 | body: list[dict] = tree_obj.tree 34 | else: 35 | logger.info(f"tree exist {query=}") 36 | body: list[dict] = eval(tree_obj.tree) 37 | tree = { 38 | "tree": body, 39 | "id": tree_obj.id, 40 | "success": True, 41 | "max": get_tree_max_id(body), 42 | } 43 | return StandResponse[TreeOut](**TREE_ADD_SUCCESS, data=TreeOut(**tree)) 44 | 45 | def patch(self, pk: int, payload: TreeUpdateIn) -> StandResponse[Union[TreeOut, None]]: 46 | try: 47 | pk: int = self.curd.update_obj_by_pk(pk, None, payload.dict()) 48 | except Exception: 49 | err: str = traceback.format_exc() 50 | logger.warning(f"update tree {err=}") 51 | return StandResponse[None](code="9999", success=False, msg=err, data=None) 52 | return StandResponse[TreeOut]( 53 | **TREE_UPDATE_SUCCESS, 54 | data=TreeOut(tree=payload.tree, id=pk, max=get_tree_max_id(payload.tree)), 55 | ) 56 | 57 | 58 | tree_service = TreeService() 59 | -------------------------------------------------------------------------------- /fastrunner/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastrunner/templatetags/__init__.py -------------------------------------------------------------------------------- /fastrunner/templatetags/custom_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from django import template 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.filter(name="json_dumps") 10 | def json_dumps(value): 11 | try: 12 | return json.dumps(json.loads(value), indent=4, separators=(",", ": "), ensure_ascii=False) 13 | except Exception: 14 | return value 15 | 16 | 17 | @register.filter(name="convert_timestamp") 18 | def convert_timestamp(value): 19 | try: 20 | return time.strftime("%Y--%m--%d %H:%M:%S", time.localtime(int(float(value)))) 21 | except: 22 | return value 23 | -------------------------------------------------------------------------------- /fastrunner/test.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author:梨花菜 5 | # @File: test.py 6 | # @Time : 2019/6/13 11:53 7 | # @Email: lihuacai168@gmail.com 8 | # @Software: PyCharm 9 | 10 | from django.test import TestCase 11 | 12 | from fastuser import models 13 | 14 | 15 | class ModelTest(TestCase): 16 | def setUp(self): 17 | """ 18 | 注册:{ 19 | "username": "demo" 20 | "password": "1321" 21 | "email": "1@1.com" 22 | } 23 | """ 24 | models.UserInfo.objects.create(username="rikasai", password="mypassword", email="lihuacai168@gmail.com") 25 | 26 | def test_user_register(self): 27 | res = models.UserInfo.objects.get(username="rikasai") 28 | self.assertEqual(res.email, "lihuacai168@gmail.com") 29 | -------------------------------------------------------------------------------- /fastrunner/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: __init__.py.py 6 | # @Time : 2022/9/5 00:22 7 | # @Email: lihuacai168@gmail.com 8 | -------------------------------------------------------------------------------- /fastrunner/tests/test_tree_service_impl.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author: 花菜 4 | # @File: test_tree_service_impl.py 5 | # @Time : 2022/9/5 00:22 6 | # @Email: lihuacai168@gmail.com 7 | 8 | 9 | from django.test import TestCase 10 | 11 | from fastrunner.dto.tree_dto import TreeUniqueIn, TreeUpdateIn 12 | from fastrunner.services.tree_service_impl import tree_service 13 | 14 | 15 | class TestTreeServiceImpl(TestCase): 16 | default_tree: list[dict] = [{"id": 1, "label": "default node", "children": []}] 17 | service = tree_service 18 | 19 | def test_get_or_create(self): 20 | assert self.service.get_or_create(TreeUniqueIn(project_id=100, type=1)).data.tree == self.default_tree 21 | 22 | assert ( 23 | # cover exist case 24 | self.service.get_or_create(TreeUniqueIn(project_id=100, type=1)).data.tree == self.default_tree 25 | ) 26 | 27 | def test_patch(self): 28 | input_body: list[dict] = [ 29 | { 30 | "id": 1, 31 | "label": "default node", 32 | "children": [{"id": 2, "label": "sub", "children": []}], 33 | } 34 | ] 35 | pk: int = self.service.get_or_create(TreeUniqueIn(project_id=101, type=1)).data.id 36 | 37 | self.service.patch(pk=pk, payload=TreeUpdateIn(body=input_body)) 38 | assert self.service.get_or_create(TreeUniqueIn(project_id=101, type=1)).data.tree == input_body 39 | 40 | assert ( 41 | self.service.patch( 42 | pk=999, 43 | payload=TreeUpdateIn(body=input_body), # pk not exist 44 | ).data 45 | is None 46 | ) 47 | -------------------------------------------------------------------------------- /fastrunner/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastrunner/utils/__init__.py -------------------------------------------------------------------------------- /fastrunner/utils/convert2boomer.py: -------------------------------------------------------------------------------- 1 | import json 2 | from enum import Enum 3 | from typing import Optional 4 | 5 | from pydantic import BaseModel, conint 6 | 7 | from fastrunner.utils.convert2hrp import Hrp 8 | 9 | 10 | class JsonValueType(str, Enum): 11 | int: str = "int" 12 | intArray: str = "intArray" 13 | string: str = "string" 14 | interface: str = "interface" 15 | 16 | 17 | class BoomerExtendCmd(BaseModel): 18 | max_rps: Optional[int] 19 | master_host: str = "10.129.144.24" 20 | master_port: int = 5557 21 | json_value_type: str = JsonValueType.interface.value 22 | replace_str_index: dict 23 | disable_keepalive: conint(gt=0, lt=1) = 0 24 | cpu_profile: Optional[str] 25 | cpu_profile_duration: Optional[int] 26 | mem_profile: Optional[str] 27 | mem_profile_duration: Optional[int] 28 | 29 | 30 | class BoomerIn(BaseModel): 31 | faster_request: dict 32 | extend_cmd: BoomerExtendCmd 33 | verbose: conint(gt=0, lt=1) = 0 34 | 35 | 36 | class Boomer(object): 37 | def __init__(self, hrp: Hrp, extend_cmd: BoomerExtendCmd): 38 | self.hrp = hrp 39 | self.extend_cmd = extend_cmd 40 | 41 | def to_boomer_cmd(self, verbose: bool = True) -> str: 42 | data_path: str = "/home/toc/SDE/code/locust-boomer/data.csv" 43 | image = "boomer:latest" 44 | end = " \\\n" 45 | base_cmd = f"docker run -v {data_path}:/app/data.csv {image}{end}" 46 | req_cmd = "" 47 | for k, v in self.hrp.get_request().dict().items(): 48 | if isinstance(v, (dict, list)): 49 | v = f"'{json.dumps(v, indent=4)}'" 50 | if isinstance(v, bool): 51 | v = 1 if v else 0 52 | 53 | if isinstance(v, (str, int)): 54 | v = f"{v}{end}" 55 | 56 | if k == "url": 57 | req_cmd += f"--{k}={v}" 58 | if k == "method": 59 | req_cmd += f"--{k}={v}" 60 | if k == "headers": 61 | req_cmd += f"--json-headers={v}" 62 | if k == "req_json": 63 | req_cmd += f"--raw-data={v}" 64 | cmd = f"{base_cmd}{req_cmd}" 65 | 66 | for k, v in self.extend_cmd.dict().items(): 67 | if isinstance(v, (str, int)): 68 | v = f"{v}{end}" 69 | if k == "master_host": 70 | cmd += f"--master-host={v}" 71 | if k == "master_port": 72 | cmd += f"--master-port={v}" 73 | if k == "disable-keepalive" and v: 74 | cmd += "--disable-keepalive 1" 75 | if k == "max_rps" and v is not None: 76 | cmd += f"--max-rps={v}" 77 | if k == "json_value_type": 78 | cmd += f"--json-value-type={v}" 79 | if k == "replace_str_index": 80 | cmd += f"--replace-str-index='{json.dumps(v, indent=4)}'{end}" 81 | 82 | if verbose: 83 | cmd += "--verbose 1" 84 | if cmd[-1] == "\\": 85 | cmd = cmd[:-1] 86 | return cmd 87 | -------------------------------------------------------------------------------- /fastrunner/utils/decorator.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | # from loguru import logger 5 | from fastrunner.utils import parser 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def request_log(level): 11 | def wrapper(func): 12 | @functools.wraps(func) 13 | def inner_wrapper(request, *args, **kwargs): 14 | msg_data = "before process request data:\n{data}".format(data=parser.format_json(request.data)) 15 | msg_params = "before process request params:\n{params}".format( 16 | params=parser.format_json(request.query_params) 17 | ) 18 | if level == "INFO": 19 | if request.data: 20 | logger.info(msg_data) 21 | if request.query_params: 22 | logger.info(msg_params) 23 | elif level == "DEBUG": 24 | if request.data: 25 | logger.debug(msg_data) 26 | if request.query_params: 27 | logger.debug(msg_params) 28 | return func(request, *args, **kwargs) 29 | 30 | return inner_wrapper 31 | 32 | return wrapper 33 | -------------------------------------------------------------------------------- /fastrunner/utils/email_helper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core import mail 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def send_mail(subject: str, html_message: str, from_email: str, recipient_list: list[str]) -> bool: 9 | return mail.send_mail( 10 | subject=subject, 11 | message=None, 12 | html_message=html_message, 13 | from_email=from_email, 14 | recipient_list=recipient_list, 15 | ) 16 | -------------------------------------------------------------------------------- /fastrunner/utils/host.py: -------------------------------------------------------------------------------- 1 | import re 2 | from urllib.parse import urlparse 3 | 4 | 5 | def parse_host(ip, api): 6 | if not isinstance(ip, list): 7 | return api 8 | if not api: 9 | return api 10 | try: 11 | parts = urlparse(api["request"]["url"]) 12 | except KeyError: 13 | parts = urlparse(api["request"]["base_url"]) 14 | # 返回值是Host:port 15 | host = parts.netloc 16 | host = host.split(":")[0] 17 | if host: 18 | for content in ip: 19 | content = content.strip() 20 | if host in content and not content.startswith("#"): 21 | ip = re.findall(r"\b(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b", content) 22 | # ip = re.findall(r'\b(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b:\d{0,5}', content) 23 | if ip: 24 | if "headers" in api["request"].keys(): 25 | api["request"]["headers"]["Host"] = host 26 | else: 27 | api["request"].setdefault("headers", {"Host": host}) 28 | try: 29 | api["request"]["url"] = api["request"]["url"].replace(host, ip[-1]) 30 | except KeyError: 31 | api["request"]["base_url"] = api["request"]["base_url"].replace(host, ip[-1]) 32 | return api 33 | -------------------------------------------------------------------------------- /fastrunner/utils/relation.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author:梨花菜 5 | # @File: relation.py 6 | # @Time : 2019/5/27 10:16 7 | # @Email: lihuacai168@gmail.com 8 | # @Software: PyCharm 9 | # api模块和数据库api表relation对应关系 10 | API_RELATION = { 11 | "default": 66, 12 | "energy.ball": 67, 13 | "manage": 68, 14 | "app_manage": 68, 15 | "artisan": 69, 16 | "goods": 70, 17 | "member": 71, 18 | "order": 72, 19 | "seller": 73, 20 | "payment": 74, 21 | "martketing": 75, 22 | "promotion": 76, 23 | "purchase": 77, 24 | "security": 78, 25 | "logistics": 79, 26 | "recycle": 80, 27 | "image-search": 81, 28 | "content": 82, 29 | "bmpm": 83, 30 | "bi": 84, 31 | } 32 | 33 | # Java同学项目分组 34 | API_AUTHOR = { 35 | "default": 1, 36 | "tangzhu": 85, 37 | "xuqirong": 86, 38 | "zhanghengjian": 87, 39 | "fengzhenwen": 88, 40 | "lingyunlong": 89, 41 | "chencanzhang": 90, 42 | } 43 | 44 | NIL = "无参数" 45 | SIGN = "time,rode,sign" 46 | SIGN_OR_TOKEN = SIGN + "(wb-token可选)" 47 | SIGN_AND_TOKEN = SIGN + ",wb-token" 48 | SESSION = "cookie: wb_sess:xxxxxx" 49 | COOKIE = "cookie: wbiao.securityservice.tokenid:xxxx" 50 | 51 | API_AUTH = { 52 | "0": ["NIL", NIL], 53 | "1": ["APP_GENERAL_AUTH", SIGN], 54 | "2": ["WXMP_GENERAL_AUTH", SIGN], 55 | "3": ["APP_MEMBER_AUTH", SIGN_AND_TOKEN], 56 | "4": ["APP_MEMBER_COMPATIBILITY_AUTH", SIGN_OR_TOKEN], 57 | "5": ["WXMP_MEMBER_AUTH", SIGN_AND_TOKEN], 58 | "6": ["WXMP_MEMBER_COMPATIBILITY_AUTH", SIGN_OR_TOKEN], 59 | "7": ["APP_USER_AUTH", SIGN_AND_TOKEN], 60 | "8": ["APP_USER_COMPATIBILITY_AUTH", SIGN_OR_TOKEN], 61 | "9": ["WXMP_USER_AUTH", SIGN_AND_TOKEN], 62 | "10": ["WXMP_USER_COMPATIBILITY_AUTH", SIGN_OR_TOKEN], 63 | "11": ["WXMP_MEMBER_COMPATIBILITY_AUTH", SESSION], 64 | "12": ["PM_USER_AUTH", COOKIE], 65 | "13": ["BACK_USER_AUTH", COOKIE], 66 | "14": ["APP_NIL", NIL], 67 | "15": ["WXMP_NIL", NIL], 68 | "16": ["PM_NIL", NIL], 69 | } 70 | -------------------------------------------------------------------------------- /fastrunner/utils/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import shutil 6 | import subprocess 7 | import sys 8 | import tempfile 9 | 10 | from FasterRunner.settings.base import BASE_DIR 11 | from fastrunner.utils import loader 12 | 13 | EXEC = sys.executable 14 | 15 | if "uwsgi" in EXEC: 16 | # 修复虚拟环境下,用uwsgi执行时,PYTHONPATH还是用了系统默认的 17 | EXEC = EXEC.replace("uwsgi", "python") 18 | 19 | 20 | class DebugCode(object): 21 | def __init__(self, code): 22 | self.__code = code 23 | self.resp = None 24 | self.temp = tempfile.mkdtemp(prefix="FasterRunner") 25 | 26 | def run(self): 27 | """dumps debugtalk.py and run""" 28 | try: 29 | os.chdir(self.temp) 30 | file_path = os.path.join(self.temp, "debugtalk.py") 31 | loader.FileLoader.dump_python_file(file_path, self.__code) 32 | # 修复自定义函数运行时,找不到内置httprunner包 33 | run_path = [BASE_DIR] 34 | run_path.extend(sys.path) 35 | env = {"PYTHONPATH": ":".join(run_path)} 36 | self.resp = decode( 37 | subprocess.check_output([EXEC, file_path], stderr=subprocess.STDOUT, timeout=60, env=env) 38 | ) 39 | 40 | except subprocess.CalledProcessError as e: 41 | self.resp = decode(e.output) 42 | 43 | except subprocess.TimeoutExpired: 44 | self.resp = "RunnerTimeOut" 45 | os.chdir(BASE_DIR) 46 | shutil.rmtree(self.temp) 47 | 48 | 49 | def decode(s): 50 | try: 51 | return s.decode("utf-8") 52 | 53 | except UnicodeDecodeError: 54 | return s.decode("gbk") 55 | -------------------------------------------------------------------------------- /fastrunner/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastrunner/views/__init__.py -------------------------------------------------------------------------------- /fastrunner/views/timer_task.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author:梨花菜 5 | # @File: timer_task.py 6 | # @Time : 2018/12/29 13:44 7 | # @Email: lihuacai168@gmail.com 8 | # @Software: PyCharm 9 | import datetime 10 | 11 | from fastrunner import models 12 | from fastrunner.utils.ding_message import DingMessage 13 | from fastrunner.utils.loader import debug_api, save_summary 14 | 15 | 16 | # 单个用例组 17 | def auto_run_testsuite_pk(**kwargs): 18 | """ 19 | :param pk: int 用例组主键 20 | :param config: int 运行环境 21 | :param project_id: int 项目id 22 | :return: 23 | """ 24 | 25 | pk = kwargs.get("pk") 26 | run_type = kwargs.get("run_type") 27 | project_id = kwargs.get("project_id") 28 | 29 | name = models.Case.objects.get(pk=pk).name 30 | 31 | # 通过主键获取单个用例 32 | test_list = models.CaseStep.objects.filter(case__id=pk).order_by("step").values("body") 33 | 34 | # 把用例加入列表 35 | testcase_list = [] 36 | for content in test_list: 37 | body = eval(content["body"]) 38 | 39 | if "base_url" in body["request"].keys(): 40 | config = eval(models.Config.objects.get(name=body["name"], project__id=project_id).body) 41 | continue 42 | testcase_list.append(body) 43 | 44 | summary = debug_api(testcase_list, project_id, name=name, config=config, save=False) 45 | 46 | save_summary(f"{name}_" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), summary, project_id, type=3) 47 | 48 | ding_message = DingMessage(run_type) 49 | ding_message.send_ding_msg(summary) 50 | -------------------------------------------------------------------------------- /fastrunner/views/yapi.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: yapi.py 6 | # @Time : 2021/3/31 22:56 7 | # @Email: lihuacai168@gmail.com 8 | # from loguru import logger 9 | import logging 10 | 11 | from django_bulk_update.helper import bulk_update 12 | from rest_framework.response import Response 13 | from rest_framework.views import APIView 14 | 15 | from fastrunner import models 16 | from fastrunner.utils import response 17 | from fastrunner.utils.parser import Yapi 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class YAPIView(APIView): 23 | def post(self, request, **kwargs): 24 | logger.info("开始批量导入yapi接口...") 25 | faster_project_id = kwargs["pk"] 26 | obj = models.Project.objects.get(pk=faster_project_id) 27 | token = obj.yapi_openapi_token 28 | yapi_base_url = obj.yapi_base_url 29 | yapi = Yapi(yapi_base_url=yapi_base_url, token=token, faster_project_id=faster_project_id) 30 | imported_apis = models.API.objects.filter(project_id=faster_project_id, creator="yapi", delete=0) 31 | imported_apis_mapping = {api.yapi_id: api.ypai_up_time for api in imported_apis} 32 | create_ids, update_ids = yapi.get_create_or_update_apis(imported_apis_mapping) 33 | try: 34 | # 获取yapi的分组,然后更新api tree 35 | yapi.create_relation_id(yapi.fast_project_id) 36 | 37 | # 通过id获取所有api的详情 38 | create_ids.extend(update_ids) 39 | if len(create_ids) == 0: 40 | logger.info("yapi没有需要导入到平台的接口...") 41 | return Response(response.YAPI_NOT_NEED_CREATE_OR_UPDATE) 42 | api_info = yapi.get_batch_api_detail(create_ids) 43 | except Exception as e: 44 | logger.error(f"导入yapi失败: {e}") 45 | return Response(response.YAPI_ADD_FAILED) 46 | 47 | # 把yapi解析成符合faster的api格式 48 | parsed_apis: list = yapi.get_parsed_apis(api_info) 49 | update_apis, new_apis = yapi.merge_api(parsed_apis, imported_apis) 50 | created_objs = models.API.objects.bulk_create(objs=new_apis) 51 | bulk_update(update_apis) 52 | 53 | created_apis_count = len(created_objs) 54 | updated_apis_count = len(update_apis) 55 | resp = { 56 | "createdCount": created_apis_count, 57 | "updatedCount": updated_apis_count, 58 | } 59 | logger.info(f"导入完成, {created_apis_count=}, {updated_apis_count=}") 60 | resp.update(response.YAPI_ADD_SUCCESS) 61 | return Response(resp) 62 | -------------------------------------------------------------------------------- /fastuser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastuser/__init__.py -------------------------------------------------------------------------------- /fastuser/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | # Register your models here. 7 | User = get_user_model() 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(BaseUserAdmin): 12 | list_display = ("username", "is_active", "belong_groups") 13 | 14 | # 编辑资料的时候显示的字段 15 | fieldsets = ( 16 | (None, {"fields": ("username", "password")}), 17 | # (_('Personal info'), {'fields': ('phone', 'first_name', 'last_name', 'email')}), 18 | # (_('Permissions'), { 19 | # 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), 20 | # }), 21 | ( 22 | _("Permissions"), 23 | { 24 | "fields": ( 25 | "is_active", 26 | "is_staff", 27 | "is_superuser", 28 | "groups", 29 | ), 30 | }, 31 | ), 32 | ) 33 | # 新增用户需要填写的字段 34 | add_fieldsets = ( 35 | ( 36 | None, 37 | { 38 | "classes": ("wide",), 39 | "fields": ("username", "password1", "password2", "is_active", "is_staff", "groups"), 40 | }, 41 | ), 42 | ) 43 | filter_horizontal = ("groups",) 44 | 45 | @admin.display(description="所属分组") 46 | def belong_groups(self, obj): 47 | return ", ".join([g.name for g in obj.groups.all()]) 48 | -------------------------------------------------------------------------------- /fastuser/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsermanagerConfig(AppConfig): 5 | name = "fastuser" 6 | -------------------------------------------------------------------------------- /fastuser/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastuser/common/__init__.py -------------------------------------------------------------------------------- /fastuser/common/response.py: -------------------------------------------------------------------------------- 1 | KEY_MISS = {"code": "0100", "success": False, "msg": "请求数据非法"} 2 | 3 | REGISTER_USERNAME_EXIST = {"code": "0101", "success": False, "msg": "用户名已被注册"} 4 | 5 | REGISTER_EMAIL_EXIST = {"code": "0101", "success": False, "msg": "邮箱已被注册"} 6 | 7 | SYSTEM_ERROR = {"code": "9999", "success": False, "msg": "System Error"} 8 | 9 | REGISTER_SUCCESS = {"code": "0001", "success": True, "msg": "register success"} 10 | 11 | LOGIN_FAILED = {"code": "0103", "success": False, "msg": "用户名或密码错误"} 12 | 13 | USER_NOT_EXISTS = {"code": "0104", "success": False, "msg": "该用户未注册"} 14 | 15 | USER_BLOCKED = {"code": "0105", "success": False, "msg": "用户被禁用"} 16 | 17 | LOGIN_SUCCESS = {"code": "0001", "success": True, "msg": "login success"} 18 | -------------------------------------------------------------------------------- /fastuser/common/token.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import time 3 | 4 | 5 | def generate_token(username): 6 | """ 7 | 生成token 8 | """ 9 | timestamp = str(time.time()) 10 | 11 | token = hashlib.md5(bytes(username, encoding="utf-8")) 12 | token.update(bytes(timestamp, encoding="utf-8")) 13 | 14 | return token.hexdigest() 15 | -------------------------------------------------------------------------------- /fastuser/migrations/0002_auto_20200428_0124.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-04-28 01:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastuser", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="myuser", 14 | name="phone", 15 | field=models.CharField(max_length=11, unique=True, verbose_name="手机号码"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /fastuser/migrations/0002_auto_20200509_1122.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-05-09 11:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastuser", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="myuser", 14 | name="phone", 15 | field=models.CharField(max_length=11, unique=True, verbose_name="手机号码"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /fastuser/migrations/0003_merge_20200813_1655.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2020-08-13 16:55 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastuser", "0002_auto_20200509_1122"), 9 | ("fastuser", "0002_auto_20200428_0124"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /fastuser/migrations/0004_auto_20200814_1605.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2020-08-14 16:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastuser", "0003_merge_20200813_1655"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="userinfo", 14 | name="creator", 15 | field=models.CharField(max_length=20, null=True, verbose_name="创建人"), 16 | ), 17 | migrations.AddField( 18 | model_name="userinfo", 19 | name="updater", 20 | field=models.CharField(max_length=20, null=True, verbose_name="更新人"), 21 | ), 22 | migrations.AddField( 23 | model_name="usertoken", 24 | name="creator", 25 | field=models.CharField(max_length=20, null=True, verbose_name="创建人"), 26 | ), 27 | migrations.AddField( 28 | model_name="usertoken", 29 | name="updater", 30 | field=models.CharField(max_length=20, null=True, verbose_name="更新人"), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /fastuser/migrations/0005_auto_20200914_2222.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-09-14 22:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastuser", "0004_auto_20200814_1605"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="myuser", 14 | name="phone", 15 | field=models.CharField(max_length=11, null=True, unique=True, verbose_name="手机号码"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /fastuser/migrations/0006_myuser_show_hosts.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2021-04-19 21:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fastuser", "0005_auto_20200914_2222"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="myuser", 14 | name="show_hosts", 15 | field=models.BooleanField( 16 | default=False, help_text="是否显示Hosts相关的信息", verbose_name="是否显示Hosts相关的信息" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /fastuser/migrations/0007_alter_myuser_first_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-15 01:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("fastuser", "0006_myuser_show_hosts"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="myuser", 15 | name="first_name", 16 | field=models.CharField( 17 | blank=True, max_length=150, verbose_name="first name" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /fastuser/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/fastuser/migrations/__init__.py -------------------------------------------------------------------------------- /fastuser/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | 4 | # Create your models here. 5 | 6 | 7 | class BaseTable(models.Model): 8 | """ 9 | 公共字段列 10 | """ 11 | 12 | class Meta: 13 | abstract = True 14 | verbose_name = "公共字段表" 15 | db_table = "base_table" 16 | 17 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 18 | update_time = models.DateTimeField("更新时间", auto_now=True) 19 | creator = models.CharField(verbose_name="创建人", max_length=20, null=True) 20 | updater = models.CharField(verbose_name="更新人", max_length=20, null=True) 21 | 22 | 23 | class UserInfo(BaseTable): 24 | """ 25 | 用户注册信息表 26 | """ 27 | 28 | class Meta: 29 | verbose_name = "用户信息" 30 | db_table = "user_info" 31 | 32 | level_type = ( 33 | (0, "普通用户"), 34 | (1, "管理员"), 35 | ) 36 | username = models.CharField("用户名", max_length=20, unique=True, null=False) 37 | password = models.CharField("登陆密码", max_length=100, null=False) 38 | email = models.EmailField("用户邮箱", unique=True, null=False) 39 | level = models.IntegerField("用户等级", choices=level_type, default=0) 40 | 41 | 42 | class UserToken(BaseTable): 43 | """ 44 | 用户登陆token 45 | """ 46 | 47 | class Meta: 48 | verbose_name = "用户登陆token" 49 | db_table = "user_token" 50 | 51 | user = models.OneToOneField(to=UserInfo, on_delete=models.CASCADE, db_constraint=False) 52 | token = models.CharField("token", max_length=50) 53 | 54 | 55 | class MyUser(AbstractUser): 56 | phone = models.CharField(verbose_name="手机号码", unique=True, null=True, max_length=11) 57 | show_hosts = models.BooleanField( 58 | verbose_name="是否显示Hosts相关的信息", default=False, help_text="是否显示Hosts相关的信息" 59 | ) 60 | 61 | class Meta(AbstractUser.Meta): 62 | pass 63 | -------------------------------------------------------------------------------- /fastuser/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | 4 | from fastuser import models 5 | 6 | User = get_user_model() 7 | 8 | 9 | class UserInfoSerializer(serializers.Serializer): 10 | """ 11 | 用户信息序列化 12 | 建议实现其他方法,否则会有警告 13 | """ 14 | 15 | username = serializers.CharField(required=True, error_messages={"code": "2001", "msg": "用户名校验失败"}) 16 | 17 | password = serializers.CharField(required=True, error_messages={"code": "2001", "msg": "密码校验失败"}) 18 | 19 | email = serializers.CharField(required=True, error_messages={"code": "2001", "msg": "邮箱校验失败"}) 20 | 21 | def create(self, validated_data): 22 | """ 23 | 实现create方法 24 | """ 25 | return models.UserInfo.objects.create(**validated_data) 26 | 27 | 28 | class UserLoginSerializer(serializers.Serializer): 29 | username = serializers.CharField(required=True) 30 | password = serializers.CharField(required=True, min_length=6) 31 | 32 | 33 | class UserModelSerializer(serializers.ModelSerializer): 34 | """ 35 | 访问统计序列化 36 | """ 37 | 38 | class Meta: 39 | model = User 40 | fields = ["id", "is_superuser", "username", "is_staff", "is_active", "groups"] 41 | depth = 1 42 | -------------------------------------------------------------------------------- /fastuser/urls.py: -------------------------------------------------------------------------------- 1 | """FasterRunner URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.urls import path 18 | 19 | from fastrunner.views import timer_task 20 | from fastuser import views 21 | 22 | urlpatterns = [ 23 | # 关闭注册入口,改为django admin创建用户 24 | # path('register/', views.RegisterView.as_view()), 25 | path("login/", views.LoginView.as_view(), name="login"), 26 | path("list/", views.UserView.as_view()), 27 | path("auto_run_testsuite_pk/", timer_task.auto_run_testsuite_pk, name="auto_run_testsuite_pk"), 28 | ] 29 | -------------------------------------------------------------------------------- /httprunner/__about__.py: -------------------------------------------------------------------------------- 1 | __title__ = "HttpRunner" 2 | __description__ = "One-stop solution for HTTP(S) testing." 3 | __url__ = "https://github.com/HttpRunner/HttpRunner" 4 | __version__ = "1.5.15" 5 | __author__ = "debugtalk" 6 | __author_email__ = "mail@debugtalk.com" 7 | __license__ = "MIT" 8 | __copyright__ = "Copyright 2017 debugtalk" 9 | __cake__ = "\u2728 \U0001f370 \u2728" 10 | -------------------------------------------------------------------------------- /httprunner/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | try: 4 | pass 5 | # monkey patch at beginning to avoid RecursionError when running locust. 6 | # from gevent import monkey; monkey.patch_all() 7 | except ImportError: 8 | pass 9 | -------------------------------------------------------------------------------- /httprunner/builtin/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: __init__.py.py 6 | # @Time : 2021/7/26 18:08 7 | # @Email: lihuacai168@gmail.com 8 | 9 | from httprunner.builtin.common_util import * 10 | from httprunner.builtin.comparators import * 11 | from httprunner.builtin.faker_helper import * 12 | from httprunner.builtin.login_helper import * 13 | from httprunner.builtin.rand_helper import * 14 | from httprunner.builtin.request_helper import * 15 | from httprunner.builtin.time_helper import * 16 | -------------------------------------------------------------------------------- /httprunner/builtin/common_util.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: common_util.py 6 | # @Time : 2021/7/26 18:10 7 | # @Email: lihuacai168@gmail.com 8 | import datetime 9 | import os 10 | import random 11 | import string 12 | import time 13 | 14 | from requests_toolbelt import MultipartEncoder 15 | 16 | from httprunner.compat import builtin_str, integer_types 17 | from httprunner.exceptions import ParamsError 18 | 19 | 20 | def gen_random_string(str_len): 21 | """generate random string with specified length""" 22 | return "".join(random.choice(string.ascii_letters + string.digits) for _ in range(str_len)) 23 | 24 | 25 | def get_timestamp(str_len=13): 26 | """get timestamp string, length can only between 0 and 16""" 27 | if isinstance(str_len, integer_types) and 0 < str_len < 17: 28 | return builtin_str(time.time()).replace(".", "")[:str_len] 29 | 30 | raise ParamsError("timestamp length can only between 0 and 16.") 31 | 32 | 33 | def get_current_date(fmt="%Y-%m-%d"): 34 | """get current date, default format is %Y-%m-%d""" 35 | return datetime.datetime.now().strftime(fmt) 36 | 37 | 38 | def multipart_encoder(field_name, file_path, file_type=None, file_headers=None): 39 | if not os.path.isabs(file_path): 40 | file_path = os.path.join(os.getcwd(), file_path) 41 | 42 | filename = os.path.basename(file_path) 43 | with open(file_path, "rb") as f: 44 | fields = {field_name: (filename, f.read(), file_type)} 45 | 46 | return MultipartEncoder(fields) 47 | 48 | 49 | def multipart_content_type(multipart_encoder): 50 | return multipart_encoder.content_type 51 | 52 | 53 | """ built-in hooks 54 | """ 55 | 56 | 57 | def setup_hook_prepare_kwargs(request): 58 | pass 59 | -------------------------------------------------------------------------------- /httprunner/builtin/faker_helper.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: faker_helper.py 6 | # @Time : 2021/8/6 16:13 7 | # @Email: lihuacai168@gmail.com 8 | from faker import Faker 9 | 10 | F = Faker(locale="zh_CN") 11 | 12 | # 假名f_name() 13 | # 假地址f_addr() 14 | # 假电话f_phone() 15 | 16 | f_name = lambda: F.name() 17 | f_addr = lambda: F.address() 18 | f_phone = lambda: F.phone_number() 19 | f_time = lambda: F.time() 20 | f_date = lambda: F.date() 21 | -------------------------------------------------------------------------------- /httprunner/builtin/login_helper.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: login_helper.py 6 | # @Time : 2021/8/9 17:30 7 | # @Email: lihuacai168@gmail.com 8 | import json 9 | import logging 10 | 11 | import pydash 12 | import requests 13 | 14 | # from loguru import logger 15 | 16 | uac_token_url = "http://192.168.22.19:8002/api/uac/token/" 17 | 18 | logger = logging.getLogger("httprunner") 19 | 20 | 21 | def _get_token(biz, account, password, env="qa"): 22 | data = {"biz": biz, "account": account, "password": password, "env": env} 23 | headers = {"Content-Type": "application/json; charset=utf-8"} 24 | res = requests.post(url=uac_token_url, headers=headers, data=json.dumps(data)).json() 25 | return res, data 26 | 27 | 28 | def get_userid(biz, account, password, env="qa"): 29 | res, data = _get_token(biz, account, password, env) 30 | user_id = pydash.get(res, "user_id") 31 | if user_id: 32 | logger.info(f"获取user_id成功: {user_id}") 33 | return user_id 34 | else: 35 | logger.warning(f"获取user_id失败,入参是: {data}, 响应是: {res}") 36 | raise Exception("获取user_id失败") 37 | 38 | 39 | def get_uac_token(biz, account, password, env="qa"): 40 | res, data = _get_token(biz, account, password, env) 41 | token = pydash.get(res, "token") 42 | if token: 43 | logger.info(f"获取token成功: {token}") 44 | return token 45 | else: 46 | logger.warning(f"获取token失败,入参是: {data}, 响应是: {res}") 47 | raise Exception("获取token失败") 48 | 49 | 50 | if __name__ == "__main__": 51 | get_userid("cm", "13533975028", "397726") 52 | get_uac_token("cm", "13533975028", "397726") 53 | -------------------------------------------------------------------------------- /httprunner/builtin/rand_helper.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: rand_helper.py 6 | # @Time : 2021/8/12 16:32 7 | # @Email: lihuacai168@gmail.com 8 | import random 9 | import string 10 | import uuid 11 | 12 | 13 | def rand_int(begin: int = 0, end: int = 10000): 14 | """生成0-10000的随机数""" 15 | return random.randint(begin, end) 16 | 17 | 18 | def rand_int4(): 19 | """4位随机数""" 20 | return rand_int(1000, 9999) 21 | 22 | 23 | def rand_int5(): 24 | """5位随机数""" 25 | return rand_int(10000, 99999) 26 | 27 | 28 | def rand_int6(): 29 | """6位随机数""" 30 | return rand_int(100000, 999999) 31 | 32 | 33 | def rand_str(n: int = 5): 34 | """获取大小写字母+数字的随机字符串,默认5位""" 35 | seq = string.ascii_letters + string.digits 36 | return "".join(random.choices(seq, k=n)) 37 | 38 | 39 | def uid(): 40 | return str(uuid.uuid1()) 41 | -------------------------------------------------------------------------------- /httprunner/builtin/request_helper.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: request_helper.py 6 | # @Time : 2021/7/26 18:33 7 | # @Email: lihuacai168@gmail.com 8 | 9 | import json 10 | import logging 11 | 12 | import pydash 13 | 14 | # from loguru import logger 15 | 16 | logger = logging.getLogger("httprunner") 17 | 18 | 19 | def _load_json(in_data): 20 | if isinstance(in_data, (dict, list)) is False: 21 | try: 22 | in_data = json.loads(in_data) 23 | except Exception as e: 24 | logger.error(str(e)) 25 | raise e 26 | return in_data 27 | 28 | 29 | def set_json(request_obj, in_data={}, include="", json_path="."): 30 | """ 31 | 修改请求体的json, 包含模式 32 | """ 33 | in_data = _load_json(in_data) 34 | request_data = pydash.get(request_obj["json"], json_path) 35 | include_keys = include.split("-") 36 | for k in include_keys: 37 | v = pydash.get(in_data, k) 38 | path = k.split(".")[-1] 39 | pydash.set_(request_data, path, v) 40 | 41 | 42 | def set_json_e(request_obj, in_data={}, exclude="", in_path="."): 43 | """ 44 | 修改请求体的json, 排除模式 45 | """ 46 | in_data = _load_json(in_data) 47 | request_data = pydash.get(request_obj["json"], in_path) 48 | exclude_keys = exclude.split("-") 49 | if isinstance(in_data, dict): 50 | for k, v in in_data.items(): 51 | if k in exclude_keys: 52 | continue 53 | pydash.set_(request_data, k, v) 54 | else: 55 | for index, value in enumerate(in_data): 56 | if index in exclude_keys: 57 | continue 58 | pydash.set_(request_data, index, value) 59 | -------------------------------------------------------------------------------- /httprunner/compat.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | httprunner.compat 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | This module handles import compatibility issues between Python 2 and 8 | Python 3. 9 | """ 10 | 11 | try: 12 | import simplejson as json 13 | except ImportError: 14 | import json 15 | 16 | import sys 17 | 18 | # ------- 19 | # Pythons 20 | # ------- 21 | 22 | # Syntax sugar. 23 | _ver = sys.version_info 24 | 25 | #: Python 2.x? 26 | is_py2 = _ver[0] == 2 27 | 28 | #: Python 3.x? 29 | is_py3 = _ver[0] == 3 30 | 31 | 32 | # --------- 33 | # Specifics 34 | # --------- 35 | 36 | try: 37 | JSONDecodeError = json.JSONDecodeError 38 | except AttributeError: 39 | JSONDecodeError = ValueError 40 | 41 | if is_py2: 42 | builtin_str = str 43 | bytes = str 44 | str = unicode 45 | basestring = basestring 46 | numeric_types = (int, long, float) 47 | integer_types = (int, long) 48 | 49 | FileNotFoundError = IOError 50 | 51 | elif is_py3: 52 | builtin_str = str 53 | str = str 54 | bytes = bytes 55 | basestring = (str, bytes) 56 | numeric_types = (int, float) 57 | integer_types = (int,) 58 | 59 | FileNotFoundError = FileNotFoundError 60 | -------------------------------------------------------------------------------- /httprunner/exceptions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from httprunner.compat import FileNotFoundError 4 | 5 | """ failure type exceptions 6 | these exceptions will mark test as failure 7 | """ 8 | 9 | 10 | class MyBaseFailure(Exception): 11 | pass 12 | 13 | 14 | class ValidationFailure(MyBaseFailure): 15 | pass 16 | 17 | 18 | class ExtractFailure(MyBaseFailure): 19 | pass 20 | 21 | 22 | class SetupHooksFailure(MyBaseFailure): 23 | pass 24 | 25 | 26 | class TeardownHooksFailure(MyBaseFailure): 27 | pass 28 | 29 | 30 | """ error type exceptions 31 | these exceptions will mark test as error 32 | """ 33 | 34 | 35 | class MyBaseError(Exception): 36 | pass 37 | 38 | 39 | class FileFormatError(MyBaseError): 40 | pass 41 | 42 | 43 | class ParamsError(MyBaseError): 44 | pass 45 | 46 | 47 | class NotFoundError(MyBaseError): 48 | pass 49 | 50 | 51 | class FileNotFound(FileNotFoundError, NotFoundError): 52 | pass 53 | 54 | 55 | class FunctionNotFound(NotFoundError): 56 | pass 57 | 58 | 59 | class VariableNotFound(NotFoundError): 60 | pass 61 | 62 | 63 | class ApiNotFound(NotFoundError): 64 | pass 65 | 66 | 67 | class TestcaseNotFound(NotFoundError): 68 | pass 69 | 70 | 71 | """Validate expression exception 72 | """ 73 | 74 | 75 | class ExpectValueParseFailure(MyBaseFailure): 76 | pass 77 | -------------------------------------------------------------------------------- /httprunner/locusts.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import io 4 | import logging 5 | import multiprocessing 6 | import os 7 | import sys 8 | 9 | # from httprunner.logger import color_print 10 | from locust.main import main 11 | 12 | logger = logging.getLogger("httprunner") 13 | 14 | 15 | def parse_locustfile(file_path): 16 | """parse testcase file and return locustfile path. 17 | if file_path is a Python file, assume it is a locustfile 18 | if file_path is a YAML/JSON file, convert it to locustfile 19 | """ 20 | if not os.path.isfile(file_path): 21 | # color_print("file path invalid, exit.", "RED") 22 | logger.error("file path invalid, exit.", "RED") 23 | sys.exit(1) 24 | 25 | file_suffix = os.path.splitext(file_path)[1] 26 | if file_suffix == ".py": 27 | locustfile_path = file_path 28 | elif file_suffix in [".yaml", ".yml", ".json"]: 29 | locustfile_path = gen_locustfile(file_path) 30 | else: 31 | # '' or other suffix 32 | color_print("file type should be YAML/JSON/Python, exit.", "RED") 33 | sys.exit(1) 34 | 35 | return locustfile_path 36 | 37 | 38 | def gen_locustfile(testcase_file_path): 39 | """generate locustfile from template.""" 40 | locustfile_path = "locustfile.py" 41 | template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "locustfile_template") 42 | 43 | with io.open(template_path, encoding="utf-8") as template: 44 | with io.open(locustfile_path, "w", encoding="utf-8") as locustfile: 45 | template_content = template.read() 46 | template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path) 47 | locustfile.write(template_content) 48 | 49 | return locustfile_path 50 | 51 | 52 | def start_master(sys_argv): 53 | sys_argv.append("--master") 54 | sys.argv = sys_argv 55 | main() 56 | 57 | 58 | def start_slave(sys_argv): 59 | if "--slave" not in sys_argv: 60 | sys_argv.extend(["--slave"]) 61 | 62 | sys.argv = sys_argv 63 | main() 64 | 65 | 66 | def run_locusts_with_processes(sys_argv, processes_count): 67 | processes = [] 68 | manager = multiprocessing.Manager() 69 | 70 | for _ in range(processes_count): 71 | p_slave = multiprocessing.Process(target=start_slave, args=(sys_argv,)) 72 | p_slave.daemon = True 73 | p_slave.start() 74 | processes.append(p_slave) 75 | 76 | try: 77 | if "--slave" in sys_argv: 78 | [process.join() for process in processes] 79 | else: 80 | start_master(sys_argv) 81 | except KeyboardInterrupt: 82 | manager.shutdown() 83 | -------------------------------------------------------------------------------- /httprunner/logger.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | 4 | from colorama import init 5 | 6 | init(autoreset=True) 7 | 8 | log_colors_config = { 9 | "DEBUG": "cyan", 10 | "INFO": "green", 11 | "WARNING": "yellow", 12 | "ERROR": "red", 13 | "CRITICAL": "red", 14 | } 15 | # logger = logging.getLogger("httprunner") 16 | 17 | # 18 | # def setup_logger(log_level, log_file=None): 19 | # """setup root logger with ColoredFormatter.""" 20 | # level = getattr(logging, log_level.upper(), None) 21 | # if not level: 22 | # color_print("Invalid log level: %s" % log_level, "RED") 23 | # sys.exit(1) 24 | # 25 | # # hide traceback when log level is INFO/WARNING/ERROR/CRITICAL 26 | # if level >= logging.INFO: 27 | # sys.tracebacklimit = 0 28 | # 29 | # formatter = ColoredFormatter( 30 | # u"%(log_color)s%(bg_white)s%(levelname)-8s%(reset)s %(message)s", 31 | # datefmt=None, 32 | # reset=True, 33 | # log_colors=log_colors_config 34 | # ) 35 | # 36 | # if log_file: 37 | # handler = logging.FileHandler(log_file) 38 | # else: 39 | # handler = logging.StreamHandler() 40 | # 41 | # handler.setFormatter(formatter) 42 | # logger.addHandler(handler) 43 | # logger.setLevel(level) 44 | # 45 | # 46 | # def coloring(text, color="WHITE"): 47 | # fore_color = getattr(Fore, color.upper()) 48 | # return fore_color + text 49 | # 50 | # 51 | # def color_print(msg, color="WHITE"): 52 | # fore_color = getattr(Fore, color.upper()) 53 | # print(fore_color + msg) 54 | # 55 | # 56 | # def log_with_color(level): 57 | # """ log with color by different level 58 | # """ 59 | # def wrapper(text): 60 | # color = log_colors_config[level.upper()] 61 | # getattr(logger, level.lower())(coloring(text, color)) 62 | # 63 | # return wrapper 64 | # 65 | # 66 | # log_debug = log_with_color("debug") 67 | # log_info = log_with_color("info") 68 | # log_warning = log_with_color("warning") 69 | # log_error = log_with_color("error") 70 | # log_critical = log_with_color("critical") 71 | -------------------------------------------------------------------------------- /httprunner/templates/locustfile.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from locust import HttpLocust, TaskSet, task 4 | from locust.events import request_failure 5 | 6 | from httprunner.exceptions import MyBaseError, MyBaseFailure 7 | from httprunner.loader import load_locust_tests 8 | from httprunner.runner import Runner 9 | 10 | 11 | class WebPageTasks(TaskSet): 12 | def on_start(self): 13 | self.test_runner = Runner(self.client) 14 | self.testcases = loader.load_locust_tests(self.locust.file_path) 15 | 16 | @task(weight=1) 17 | def test_any(self): 18 | teststeps = random.choice(self.locust.tests) 19 | for teststep in teststeps: 20 | try: 21 | test_runner.run_test(teststep) 22 | except (MyBaseError, MyBaseFailure) as ex: 23 | request_failure.fire( 24 | request_type=teststep.get("request", {}).get("method"), 25 | name=teststep.get("name"), 26 | response_time=0, 27 | exception=ex, 28 | ) 29 | break 30 | gevent.sleep(1) 31 | 32 | 33 | class WebPageUser(HttpLocust): 34 | host = "$HOST" 35 | task_set = WebPageTasks 36 | min_wait = 10 37 | max_wait = 30 38 | 39 | # file_path = "$TESTCASE_FILE" 40 | file_path = "tests/data/demo_locust.yml" 41 | config, tests = load_locust_tests(file_path) 42 | -------------------------------------------------------------------------------- /httprunner/templates/locustfile_template: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | import zmq 5 | from httprunner.exceptions import MyBaseError, MyBaseFailure 6 | from httprunner.loader import load_locust_tests 7 | from httprunner.runner import Runner 8 | from locust import HttpLocust, TaskSet, task 9 | from locust.events import request_failure 10 | 11 | logging.getLogger().setLevel(logging.CRITICAL) 12 | logging.getLogger('locust.main').setLevel(logging.INFO) 13 | logging.getLogger('locust.runners').setLevel(logging.INFO) 14 | 15 | 16 | class WebPageTasks(TaskSet): 17 | def on_start(self): 18 | self.test_runner = Runner(self.locust.config, self.client) 19 | self.testcases = load_locust_tests(self.locust.file_path) 20 | 21 | @task(weight=1) 22 | def test_any(self): 23 | teststeps = random.choice(self.locust.tests) 24 | for teststep in teststeps: 25 | try: 26 | self.test_runner.run_test(teststep) 27 | except (MyBaseError, MyBaseFailure) as ex: 28 | request_failure.fire( 29 | request_type=teststep.get("request", {}).get("method"), 30 | name=teststep.get("name"), 31 | response_time=0, 32 | exception=ex 33 | ) 34 | 35 | 36 | class WebPageUser(HttpLocust): 37 | task_set = WebPageTasks 38 | min_wait = 10 39 | max_wait = 30 40 | 41 | file_path = "$TESTCASE_FILE" 42 | locust_tests = load_locust_tests(file_path) 43 | config = locust_tests["config"] 44 | tests = locust_tests["tests"] 45 | 46 | host = config.get('request', {}).get('base_url', '') 47 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /mock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/mock/__init__.py -------------------------------------------------------------------------------- /mock/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /mock/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MockConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "mock" 7 | -------------------------------------------------------------------------------- /mock/migrations/0002_alter_mockapi_options_alter_mockapi_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-02-27 22:10 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("mock", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="mockapi", 15 | options={"ordering": ["-create_time"], "verbose_name": "mock接口表"}, 16 | ), 17 | migrations.AlterField( 18 | model_name="mockapi", 19 | name="project", 20 | field=models.ForeignKey( 21 | blank=True, 22 | db_constraint=False, 23 | null=True, 24 | on_delete=django.db.models.deletion.DO_NOTHING, 25 | to="mock.mockproject", 26 | to_field="project_id", 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /mock/migrations/0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-02-27 22:49 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("mock", "0002_alter_mockapi_options_alter_mockapi_project"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="mockapi", 15 | name="api_desc", 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="mockapi", 20 | name="api_id", 21 | field=models.CharField(default="4e9eb9a68bd8441d9c503f1347f156ff", max_length=32, unique=True), 22 | ), 23 | migrations.AddField( 24 | model_name="mockapi", 25 | name="api_name", 26 | field=models.CharField(blank=True, max_length=100, null=True), 27 | ), 28 | migrations.AddField( 29 | model_name="mockapi", 30 | name="followers", 31 | field=models.JSONField(blank=True, default=list, null=True, verbose_name="关注者"), 32 | ), 33 | migrations.AlterField( 34 | model_name="mockapi", 35 | name="project", 36 | field=models.ForeignKey( 37 | blank=True, 38 | db_constraint=False, 39 | null=True, 40 | on_delete=django.db.models.deletion.DO_NOTHING, 41 | related_name="mock_apis", 42 | to="mock.mockproject", 43 | to_field="project_id", 44 | ), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /mock/migrations/0004_remove_mockapi_followers_mockapi_enabled_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-02 11:52 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('mock', '0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='mockapi', 16 | name='followers', 17 | ), 18 | migrations.AddField( 19 | model_name='mockapi', 20 | name='enabled', 21 | field=models.BooleanField(default=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='mockapi', 25 | name='api_id', 26 | field=models.CharField(default='9b5855cb6dc945b1a7b8832d61a64069', max_length=32, unique=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='mockapi', 30 | name='request_method', 31 | field=models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PUT', 'PUT'), ('DELETE', 'DELETE'), ('PATCH', 'PATCH')], default='POST', max_length=10), 32 | ), 33 | migrations.CreateModel( 34 | name='MockAPILog', 35 | fields=[ 36 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), 38 | ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), 39 | ('creator', models.CharField(max_length=20, null=True, verbose_name='创建人')), 40 | ('updater', models.CharField(max_length=20, null=True, verbose_name='更新人')), 41 | ('request_obj', models.JSONField(default=dict)), 42 | ('response_obj', models.JSONField(default=dict)), 43 | ('api', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='logs', to='mock.mockapi', to_field='api_id')), 44 | ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='mock_logs', to='mock.mockproject', to_field='project_id')), 45 | ], 46 | options={ 47 | 'verbose_name': 'mock api log表', 48 | 'db_table': 'mock_api_log', 49 | 'ordering': ['-create_time'], 50 | }, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /mock/migrations/0005_mockapilog_request_id_alter_mockapi_api_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-02 12:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mock', '0004_remove_mockapi_followers_mockapi_enabled_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='mockapilog', 15 | name='request_id', 16 | field=models.CharField(blank=True, db_index=True, default='bc73e83696024977b002016c6ebd9967', max_length=100, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='mockapi', 20 | name='api_id', 21 | field=models.CharField(default='28ee07527bf8431c955fdeaeb6f1626e', max_length=32, unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='mockapilog', 25 | name='request_obj', 26 | field=models.JSONField(blank=True, default=dict), 27 | ), 28 | migrations.AlterField( 29 | model_name='mockapilog', 30 | name='response_obj', 31 | field=models.JSONField(blank=True, default=dict, null=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /mock/migrations/0006_mockapi_request_body_mockapi_version_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-15 01:18 2 | 3 | from django.db import migrations, models 4 | import mock.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("mock", "0005_mockapilog_request_id_alter_mockapi_api_id_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="mockapi", 16 | name="request_body", 17 | field=models.JSONField(blank=True, default=dict, null=True), 18 | ), 19 | migrations.AddField( 20 | model_name="mockapi", 21 | name="version", 22 | field=models.IntegerField(default=1), 23 | ), 24 | migrations.AlterField( 25 | model_name="mockapi", 26 | name="api_id", 27 | field=models.CharField( 28 | default=mock.models.generate_uuid, max_length=32, unique=True 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="mockapi", 33 | name="api_name", 34 | field=models.CharField(max_length=100), 35 | ), 36 | migrations.AlterField( 37 | model_name="mockapi", 38 | name="response_text", 39 | field=models.TextField( 40 | default='\ndef execute(req, resp):\n import requests\n\n url = "http://localhost:8000/api/mock/mock_api/"\n\n payload = {}\n headers = {\n "accept": "application/json",\n "X-CSRFToken": "fk5wQDlKC6ufRjk7r38pfbqyq7mTtyc5NUUqkFN5lbZf6nyHVSbAUVoqbwaGcQHT",\n }\n\n response = requests.request("GET", url, headers=headers, data=payload)\n resp.data = response.json()\n' 41 | ), 42 | ), 43 | migrations.AlterField( 44 | model_name="mockapilog", 45 | name="request_id", 46 | field=models.CharField( 47 | blank=True, 48 | db_index=True, 49 | default=mock.models.generate_uuid, 50 | max_length=100, 51 | null=True, 52 | ), 53 | ), 54 | migrations.AlterField( 55 | model_name="mockproject", 56 | name="project_id", 57 | field=models.CharField( 58 | default=mock.models.generate_uuid, max_length=100, unique=True 59 | ), 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /mock/migrations/0007_mockapiresponsehandler.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-21 23:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("mock", "0006_mockapi_request_body_mockapi_version_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="MockAPIResponseHandler", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ( 27 | "create_time", 28 | models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), 29 | ), 30 | ( 31 | "update_time", 32 | models.DateTimeField(auto_now=True, verbose_name="更新时间"), 33 | ), 34 | ( 35 | "creator", 36 | models.CharField(max_length=20, null=True, verbose_name="创建人"), 37 | ), 38 | ( 39 | "updater", 40 | models.CharField(max_length=20, null=True, verbose_name="更新人"), 41 | ), 42 | ( 43 | "condition_expression", 44 | models.CharField( 45 | max_length=255, unique=True, verbose_name="条件判断表达式" 46 | ), 47 | ), 48 | ( 49 | "response_set_value_expression", 50 | models.CharField(max_length=255, verbose_name="响应设置值表达式"), 51 | ), 52 | ( 53 | "response_set_value", 54 | models.CharField(max_length=255, verbose_name="响应设置的值"), 55 | ), 56 | ("enabled", models.BooleanField(default=True, verbose_name="是否启用")), 57 | ( 58 | "api", 59 | models.ForeignKey( 60 | blank=True, 61 | db_constraint=False, 62 | null=True, 63 | on_delete=django.db.models.deletion.DO_NOTHING, 64 | related_name="response_handlers", 65 | to="mock.mockapi", 66 | to_field="api_id", 67 | ), 68 | ), 69 | ], 70 | options={ 71 | "verbose_name": "mock api response handler", 72 | "db_table": "mock_api_response_handler", 73 | "ordering": ["-create_time"], 74 | }, 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /mock/migrations/0008_mockapiresponsehandler_description_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.13 on 2024-03-22 00:15 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("mock", "0007_mockapiresponsehandler"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="mockapiresponsehandler", 16 | name="description", 17 | field=models.TextField(blank=True, null=True, verbose_name="描述"), 18 | ), 19 | migrations.AddField( 20 | model_name="mockapiresponsehandler", 21 | name="name", 22 | field=models.CharField( 23 | db_index=True, default=django.utils.timezone.now, max_length=255 24 | ), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /mock/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/mock/migrations/__init__.py -------------------------------------------------------------------------------- /mock/tasks.py: -------------------------------------------------------------------------------- 1 | # tasks.py 2 | from celery import shared_task 3 | from mock.models import MockAPILog 4 | 5 | 6 | @shared_task 7 | def log_mock_api(request_obj, request_id, api_id, project_id, req_time, response_obj): 8 | log_obj = MockAPILog.objects.create( 9 | request_obj=request_obj, 10 | request_id=request_id, 11 | api_id=api_id, 12 | project_id=project_id, 13 | create_time=req_time 14 | ) 15 | log_obj.response_obj = response_obj 16 | log_obj.save() 17 | return log_obj.id 18 | -------------------------------------------------------------------------------- /mock/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /nginx-remote/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rikasai/fast-runner-backend:latest as Base 2 | 3 | FROM nginx:1.21-alpine 4 | 5 | RUN rm /etc/nginx/conf.d/default.conf 6 | COPY nginx.conf /etc/nginx/conf.d 7 | COPY --from=Base /app/static_root /www/FasterRunner/static -------------------------------------------------------------------------------- /nginx-remote/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server app:8000; 3 | } 4 | server { 5 | listen 8000; 6 | # server_name 127.0.0.1; 7 | charset utf-8; 8 | 9 | client_max_body_size 75M; # adjust to taste 10 | 11 | location /static { 12 | alias /www/FasterRunner/static; # your Django project's static files - amend as required 13 | gzip on; 14 | gzip_comp_level 6; 15 | gzip_min_length 1k; 16 | gzip_buffers 4 16k; 17 | gzip_proxied any; 18 | gzip_vary on; 19 | gzip_types 20 | application/javascript 21 | application/x-javascript 22 | text/javascript 23 | text/css 24 | text/xml 25 | application/xhtml+xml 26 | application/xml 27 | application/atom+xml 28 | application/rdf+xml 29 | application/rss+xml 30 | application/geo+json 31 | application/json 32 | application/ld+json 33 | application/manifest+json 34 | application/x-web-app-manifest+json 35 | image/svg+xml 36 | text/x-cross-domain-policy; 37 | gzip_static on; 38 | } 39 | 40 | location / { 41 | proxy_pass http://django; 42 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 43 | proxy_set_header Host $host; 44 | proxy_redirect off; 45 | } 46 | } -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8000; 3 | server_name 127.0.0.1; 4 | charset utf-8; 5 | 6 | client_max_body_size 75M; # adjust to taste 7 | location /media { 8 | alias /opt/workspace/FasterRunner/templates; # your Django project's media files - amend as required 9 | } 10 | 11 | location /static { 12 | alias /opt/workspace/FasterRunner/static; # your Django project's static files - amend as required 13 | } 14 | 15 | location / { 16 | include uwsgi_params; 17 | uwsgi_pass unix:/opt/workspace/FasterRunner/FasterRunner.sock; 18 | } 19 | } -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fasterrunner_app:latest as Base 2 | 3 | FROM nginx:1.21-alpine 4 | 5 | RUN rm /etc/nginx/conf.d/default.conf 6 | COPY nginx.conf /etc/nginx/conf.d 7 | COPY --from=Base /app/static_root /www/FasterRunner/static -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server app:8000; 3 | } 4 | server { 5 | listen 8000; 6 | # server_name 127.0.0.1; 7 | charset utf-8; 8 | 9 | client_max_body_size 75M; # adjust to taste 10 | 11 | location /static { 12 | alias /www/FasterRunner/static; # your Django project's static files - amend as required 13 | gzip on; 14 | gzip_comp_level 6; 15 | gzip_min_length 1k; 16 | gzip_buffers 4 16k; 17 | gzip_proxied any; 18 | gzip_vary on; 19 | gzip_types 20 | application/javascript 21 | application/x-javascript 22 | text/javascript 23 | text/css 24 | text/xml 25 | application/xhtml+xml 26 | application/xml 27 | application/atom+xml 28 | application/rdf+xml 29 | application/rss+xml 30 | application/geo+json 31 | application/json 32 | application/ld+json 33 | application/manifest+json 34 | application/x-web-app-manifest+json 35 | image/svg+xml 36 | text/x-cross-domain-policy; 37 | gzip_static on; 38 | } 39 | 40 | location / { 41 | proxy_pass http://django; 42 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 43 | proxy_set_header Host $host; 44 | proxy_redirect off; 45 | } 46 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fasterrunner" 3 | version = "3.0.0" 4 | description = "" 5 | authors = ["lihuacai "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | aiocontextvars = "0.2.2" 11 | amqp = "^5.2.0" 12 | beautifulsoup4 = "4.6.3" 13 | celery = ">=5.2.7,<5.3.0" 14 | certifi = "2019.9.11" 15 | chardet = "3.0.4" 16 | colorama = "0.4.1" 17 | colorlog = "4.0.2" 18 | contextvars = "2.4" 19 | coreapi = "2.3.3" 20 | coreschema = "0.0.4" 21 | dingtalkchatbot = "1.3.0" 22 | django = "4.1.13" 23 | django-celery-beat = "^2.5.0" 24 | django-cors-headers = "4.3.1" 25 | django-jsonfield = "1.4.0" 26 | django-model-utils = "4.0.0" 27 | django-rest-swagger = "2.2.0" 28 | django-simpleui = "2023.12.12" 29 | django-utils-six = "2.0" 30 | djangorestframework = "3.14.0" 31 | djangorestframework-jwt = "1.11.0" 32 | drf-yasg = "1.21.7" 33 | packaging = "23.2" 34 | har2case = "0.3.1" 35 | idna = "2.8" 36 | inflection = "0.5.0" 37 | jinja2 = "2.10.3" 38 | loguru = "0.5.1" 39 | markupsafe = "1.1.1" 40 | openapi-codec = "1.3.2" 41 | pyjwt = "1.7.1" 42 | pyparsing = "2.4.7" 43 | python-dotenv = "0.10.3" 44 | pytz = "2022.2.1" 45 | pyyaml = ">=5.3.1,<5.4.0" 46 | requests = "2.22.0" 47 | requests-toolbelt = "0.9.1" 48 | simplejson = "3.17.0" 49 | six = "1.15.0" 50 | sqlparse = "0.3.1" 51 | tornado = "6.4" 52 | uritemplate = "3.0.1" 53 | urllib3 = "1.25.7" 54 | django-mysql = "^4.12.0" 55 | json5 = "0.9.5" 56 | django-bulk-update = "2.2.0" 57 | xmltodict = ">=0.12.0,<0.13.0" 58 | genson = ">=1.2.2,<1.3.0" 59 | faker = ">=7.0.1,<7.1.0" 60 | curlify = ">=2.2.1,<2.3.0" 61 | pydash = ">=5.0.1,<5.1.0" 62 | jsonpath = ">=0.82,<1.0" 63 | pydantic = "1.9.0" 64 | gunicorn = "^21.2.0" 65 | sentry-sdk = ">=1.5.8,<1.6.0" 66 | croniter = ">=1.3.5,<1.4.0" 67 | django-log-request-id = ">=2.0.0,<2.1.0" 68 | gevent = "22.10.2" 69 | django-filter = ">=2.4.0,<2.5.0" 70 | django-auth-ldap = "2.3.0" 71 | pymysql = "1.1.0" 72 | pytest = "^8.1.1" 73 | pytest-django = "^4.8.0" 74 | django-db-connection-pool = {extras = ["mysql"], version = "^1.2.5"} 75 | 76 | 77 | [tool.poetry.group.dev.dependencies] 78 | ruff = "^0.2.2" 79 | 80 | [build-system] 81 | requires = ["poetry-core"] 82 | build-backend = "poetry.core.masonry.api" 83 | 84 | 85 | [project] 86 | # Support Python 3.11+. 87 | requires-python = ">=3.11" 88 | 89 | [tool.ruff] 90 | # Set the maximum line length to 120. 91 | line-length = 120 92 | exclude = ["web/*", "migrations/*", "static/*", "static_root/*"] 93 | 94 | [tool.ruff.lint] 95 | # Add the `line-too-long` rule to the enforced rule set. By default, Ruff omits rules that 96 | # overlap with the use of a formatter, like Black, but we can override this behavior by 97 | # explicitly adding the rule. 98 | extend-select = ["E501"] 99 | select = [ 100 | # isort 101 | "I" 102 | ] 103 | 104 | [tool.ruff.lint.pydocstyle] 105 | convention = "google" -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = FasterRunner.settings.dev 3 | # -- recommended but optional: 4 | python_files = tests.py test_*.py *_tests.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiocontextvars==0.2.2 2 | amqp==5.2.0 3 | beautifulsoup4==4.6.3 4 | celery==5.2.7,<5.3.0 5 | certifi==2019.9.11 6 | chardet==3.0.4 7 | colorama==0.4.1 8 | colorlog==4.0.2 9 | contextvars==2.4 10 | coreapi==2.3.3 11 | coreschema==0.0.4 12 | dingtalkchatbot==1.3.0 13 | django==4.1.13 14 | django-celery-beat==2.5.0 15 | django-cors-headers==4.3.1 16 | django-jsonfield==1.4.0 17 | django-model-utils==4.0.0 18 | django-rest-swagger==2.2.0 19 | django-simpleui==2023.12.12 20 | django-utils-six==2.0 21 | djangorestframework==3.14.0 22 | djangorestframework-jwt==1.11.0 23 | drf-yasg==1.21.7 24 | packaging==23.2 25 | har2case==0.3.1 26 | idna==2.8 27 | inflection==0.5.0 28 | jinja2==2.10.3 29 | loguru==0.5.1 30 | markupsafe==1.1.1 31 | openapi-codec==1.3.2 32 | pyjwt==1.7.1 33 | pyparsing==2.4.7 34 | python-dotenv==0.10.3 35 | pytz==2022.2.1 36 | pyyaml==5.3.1,<5.4.0 37 | requests==2.22.0 38 | requests-toolbelt==0.9.1 39 | simplejson==3.17.0 40 | six==1.15.0 41 | sqlparse==0.3.1 42 | tornado==6.4 43 | uritemplate==3.0.1 44 | urllib3==1.25.7 45 | django-mysql==4.12.0 46 | json5==0.9.5 47 | django-bulk-update==2.2.0 48 | xmltodict==0.12.0,<0.13.0 49 | genson==1.2.2,<1.3.0 50 | faker==7.0.1,<7.1.0 51 | curlify==2.2.1,<2.3.0 52 | pydash==5.0.1,<5.1.0 53 | jsonpath==0.82,<1.0 54 | pydantic==1.9.0 55 | gunicorn==21.2.0 56 | sentry-sdk==1.5.8,<1.6.0 57 | croniter==1.3.5,<1.4.0 58 | django-log-request-id==2.0.0,<2.1.0 59 | gevent==22.10.2 60 | django-filter==2.4.0,<2.5.0 61 | #django-auth-ldap==2.3.0 62 | pymysql==1.1.0 63 | pytest==8.1.1 64 | pytest-django==4.8.0 65 | django-db-connection-pool[mysql]==1.2.5 66 | -------------------------------------------------------------------------------- /sql/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/sql/.gitkeep -------------------------------------------------------------------------------- /start-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $1 = "app" ]; then 4 | echo "start app" 5 | # /usr/local/bin/python -m gunicorn FasterRunner.wsgi_docker -b 0.0.0.0 -w4 6 | # export IS_PERF=1; python -m gunicorn FasterRunner.wsgi_dev -b 0.0.0.0 -w16 -k gevent 7 | export IS_PERF=1; gunicorn FasterRunner.wsgi_dev -b 0.0.0.0:8000 -w 2 -k gevent 8 | fi 9 | 10 | if [ $1 = "worker" ]; then 11 | echo "start celery" 12 | export DJANGO_SETTINGS_MODULE=FasterRunner.settings.dev; python -m celery -A FasterRunner.mycelery worker -l error --concurrency=2 13 | fi 14 | 15 | 16 | if [ $1 = "beat" ]; then 17 | echo "start celery beat" 18 | export DJANGO_SETTINGS_MODULE=FasterRunner.settings.dev; python -m celery -A FasterRunner.mycelery beat -l info 19 | fi -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set input=%1 3 | 4 | if "%input%"=="app" ( 5 | echo start app 6 | set DJANGO_SETTINGS_MODULE=FasterRunner.settings.dev 7 | python manage.py runserver localhost:8000 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # start nginx service 4 | #service nginx start 5 | 6 | # start celery worker 7 | #celery multi start w1 -A FasterRunner -l info --logfile=./logs/worker.log 8 | #python3 manage.py celery -A FasterRunner.mycelery worker -l info -P solo --settings=FasterRunner.settings.pro --logfile=./logs/worker.log 2>&1 & 9 | 10 | 11 | # start celery beat 12 | #nohup python3 manage.py celery beat -l info > ./logs/beat.log 2>&1 & 13 | #nohup python3 manage.py FasterRunner.mycelery beat -l info > ./logs/beat.log 2>&1 & 14 | #nohup celery -A FasterRunner.mycelery beat -l info > ./logs/beat.log 2>&1 & 15 | #if [ -f celerybeat.pid ]; then rm celerybeat.pid;fi 16 | #python3 manage.py celery -A FasterRunner.mycelery beat -l info --settings=FasterRunner.settings.pro --logfile=./logs/beat.log 2>&1 & 17 | 18 | # start fastrunner 19 | #uwsgi --ini ./uwsgi_docker.ini 20 | #uwsgi --ini ./uwsgi_docker.ini --logto ./logs/uwsgi.log 21 | #gunicorn FasterRunner.wsgi_docker -b 0.0.0.0 -w 4 22 | #python3 manage.py runserver --settings=FasterRunner.settings.docker 23 | 24 | 25 | if [ $1 = "app" ]; then 26 | echo "start app" 27 | # /usr/local/bin/python -m gunicorn FasterRunner.wsgi_docker -b 0.0.0.0 -w4 28 | /usr/local/bin/python -m gunicorn FasterRunner.wsgi_docker -b 0.0.0.0 -w4 -k gevent 29 | fi 30 | 31 | if [ $1 = "celery-worker" ]; then 32 | echo "start celery" 33 | export DJANGO_SETTINGS_MODULE=FasterRunner.settings.docker; /usr/local/bin/python -m celery -A FasterRunner.mycelery worker -l info --concurrency=4 34 | fi 35 | 36 | 37 | if [ $1 = "celery-beat" ]; then 38 | echo "start celery beat" 39 | export DJANGO_SETTINGS_MODULE=FasterRunner.settings.docker; /usr/local/bin/python -m celery -A FasterRunner.mycelery beat -l info 40 | fi -------------------------------------------------------------------------------- /static_root/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/static_root/.gitkeep -------------------------------------------------------------------------------- /system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/system/__init__.py -------------------------------------------------------------------------------- /system/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /system/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SystemConfig(AppConfig): 5 | name = "system" 6 | -------------------------------------------------------------------------------- /system/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2023-09-17 22:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="LogRecord", 14 | fields=[ 15 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 16 | ("create_time", models.DateTimeField(auto_now_add=True, verbose_name="创建时间")), 17 | ("update_time", models.DateTimeField(auto_now=True, verbose_name="更新时间")), 18 | ("creator", models.CharField(max_length=20, null=True, verbose_name="创建人")), 19 | ("updater", models.CharField(max_length=20, null=True, verbose_name="更新人")), 20 | ("request_id", models.CharField(db_index=True, max_length=100, null=True)), 21 | ("level", models.CharField(max_length=20)), 22 | ("message", models.TextField(db_index=True)), 23 | ], 24 | options={ 25 | "db_table": "log_record", 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /system/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/system/migrations/__init__.py -------------------------------------------------------------------------------- /system/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from fastuser.models import BaseTable 4 | 5 | # Create your models here. 6 | 7 | 8 | class LogRecord(BaseTable): 9 | class Meta: 10 | db_table = "log_record" 11 | 12 | request_id = models.CharField(max_length=100, null=True, db_index=True) 13 | level = models.CharField(max_length=20) 14 | message = models.TextField(db_index=True) 15 | -------------------------------------------------------------------------------- /system/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: __init__.py.py 6 | # @Time : 2023/9/17 22:55 7 | # @Email: lihuacai168@gmail.com 8 | -------------------------------------------------------------------------------- /system/serializers/log_record_serializer.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # @Author: 花菜 5 | # @File: log_record_serializer.py 6 | # @Time : 2023/9/17 22:55 7 | # @Email: lihuacai168@gmail.com 8 | 9 | from rest_framework import serializers 10 | 11 | from system.models import LogRecord 12 | 13 | 14 | class LogRecordSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = LogRecord 17 | fields = "__all__" 18 | -------------------------------------------------------------------------------- /system/tasks.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from celery import shared_task 4 | from requests.auth import HTTPBasicAuth 5 | 6 | import requests 7 | 8 | 9 | @shared_task 10 | def send_log_to_loki(*args, **kwargs): 11 | url = kwargs['loki_url'] 12 | loki_username = kwargs['loki_username'] 13 | loki_password = kwargs['loki_password'] 14 | auth = HTTPBasicAuth(username=loki_username, password=loki_password) 15 | data = kwargs['data'] 16 | headers = { 17 | 'Content-Type': 'application/json' 18 | } 19 | requests.post(url, headers=headers, data=json.dumps(data), auth=auth) 20 | -------------------------------------------------------------------------------- /system/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /system/views.py: -------------------------------------------------------------------------------- 1 | from django_filters import rest_framework as django_filters 2 | from rest_framework import filters, status, viewsets 3 | from rest_framework.response import Response 4 | 5 | from system.serializers.log_record_serializer import LogRecordSerializer 6 | 7 | from .models import LogRecord 8 | 9 | 10 | class LogRecordFilter(django_filters.FilterSet): 11 | request_id = django_filters.CharFilter(field_name="request_id", lookup_expr="exact") 12 | message = django_filters.CharFilter(field_name="message", lookup_expr="icontains") 13 | 14 | class Meta: 15 | model = LogRecord 16 | fields = ["request_id", "message"] 17 | 18 | 19 | class LogRecordViewSet(viewsets.ModelViewSet): 20 | queryset = LogRecord.objects.all() 21 | serializer_class = LogRecordSerializer 22 | filter_backends = (django_filters.DjangoFilterBackend, filters.OrderingFilter) 23 | filterset_class = LogRecordFilter 24 | ordering_fields = ["create_time"] 25 | ordering = ["create_time"] 26 | 27 | def destroy(self, request, *args, **kwargs): 28 | return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) 29 | 30 | def update(self, request, *args, **kwargs): 31 | return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) 32 | 33 | def partial_update(self, request, *args, **kwargs): 34 | return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) 35 | -------------------------------------------------------------------------------- /tempWorkDir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/tempWorkDir/.gitkeep -------------------------------------------------------------------------------- /tests/test_login_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | from rest_framework import status 4 | from rest_framework.test import APIClient 5 | from django.contrib.auth import get_user_model 6 | 7 | # 假设LoginView用的Url名字是'login' 8 | login_url = reverse("login") 9 | 10 | 11 | @pytest.mark.django_db # 如果你的测试需要数据库操作 12 | class TestLoginView: 13 | def setup_method(self): 14 | User = get_user_model() 15 | User.objects.create_user('validUser', 'email@example.com', 'validPassword') 16 | 17 | def test_login_with_correct_credentials(self): 18 | client = APIClient() 19 | user_data = {"username": "validUser", "password": "validPassword"} 20 | response = client.post(login_url, user_data, format="json") 21 | assert response.status_code == status.HTTP_200_OK 22 | assert ( 23 | "token" in response.data 24 | ) # assuming that generate_token_and_respond returns a token in response 25 | 26 | def test_login_with_incorrect_credentials(self): 27 | client = APIClient() 28 | user_data = {"username": "invalidUser", "password": "invalidPassword"} 29 | response = client.post(login_url, user_data, format="json") 30 | # body 31 | assert response.json() == {'code': "0103", 'success': False, 'msg': "用户名或密码错误"} 32 | assert ( 33 | response.status_code == status.HTTP_200_OK 34 | ) # assuming LOGIN_FAILED responds with status 401 35 | 36 | def test_login_with_no_credentials(self): 37 | client = APIClient() 38 | user_data = {} 39 | response = client.post(login_url, user_data, format="json") 40 | assert response.status_code == status.HTTP_400_BAD_REQUEST 41 | 42 | def test_login_with_partial_credentials(self): 43 | client = APIClient() 44 | 45 | # Missing username 46 | user_data = {"password": "validPassword"} 47 | response = client.post(login_url, user_data, format="json") 48 | assert response.status_code == status.HTTP_400_BAD_REQUEST 49 | 50 | # Missing password 51 | user_data = {"username": "validUser"} 52 | response = client.post(login_url, user_data, format="json") 53 | assert response.status_code == status.HTTP_400_BAD_REQUEST 54 | -------------------------------------------------------------------------------- /tests/test_mockapi_serializers.py: -------------------------------------------------------------------------------- 1 | from mock.serializers import MockAPISerializer 2 | from django.test import TestCase 3 | from rest_framework import serializers 4 | import ast 5 | import textwrap 6 | 7 | 8 | class TestMockAPISerializer(TestCase): 9 | def setUp(self): 10 | """ 11 | This will run before every test method. 12 | """ 13 | self.serializer = MockAPISerializer() 14 | 15 | def test_invalid_response_text(self): 16 | # Checking invalid response text 17 | invalid_response_text = "invalid code" 18 | with self.assertRaises(serializers.ValidationError): 19 | self.serializer.validate_response_text(invalid_response_text) 20 | 21 | def test_response_text_no_execute(self): 22 | # Checking a response text with no 'execute' function 23 | response_text_no_execute = """ 24 | def other_function(): 25 | pass 26 | """ 27 | with self.assertRaises(serializers.ValidationError): 28 | self.serializer.validate_response_text(response_text_no_execute) 29 | 30 | def test_response_text_bad_execute(self): 31 | # Checking response text with incorrect 'execute' function arguments 32 | response_text_bad_execute = """ 33 | def execute(a, b, c): 34 | pass 35 | """ 36 | with self.assertRaises(serializers.ValidationError): 37 | self.serializer.validate_response_text(response_text_bad_execute) 38 | 39 | def test_valid_response_text(self): 40 | # Checking valid response text 41 | valid_response_text = """ 42 | def execute(req, resp): 43 | pass 44 | """ 45 | self.assertEqual( 46 | valid_response_text, 47 | self.serializer.validate_response_text(valid_response_text), 48 | ) 49 | 50 | def test_indent_error_text(self): 51 | # Checking response text with incorrect indentation 52 | indent_error_text = """ 53 | def execute(req, resp): 54 | a = '1' 55 | """ 56 | with self.assertRaises(serializers.ValidationError): 57 | self.serializer.validate_response_text(indent_error_text) -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | # myweb_uwsgi.ini file 2 | [uwsgi] 3 | 4 | # Django-related settings 5 | project = FasterRunner 6 | base = /home/toc/SDE/code 7 | 8 | py-autoreload = 1 9 | 10 | chdir = %(base)/%(project) 11 | module = %(project).wsgi:application 12 | 13 | master = true 14 | processes = 4 15 | 16 | 17 | socket = %(base)/%(project)/%(project).sock 18 | chmod-socket = 666 19 | vacuum = true 20 | 21 | # 请求超时300秒 22 | # 还需要在nginx中配置 23 | # uwsgi_read_timeout 600; 24 | socket-timeout = 300 25 | http-timeout = 300 -------------------------------------------------------------------------------- /uwsgi_docker.ini: -------------------------------------------------------------------------------- 1 | # myweb_uwsgi.ini file 2 | [uwsgi] 3 | 4 | # Django-related settings 5 | project = FasterRunner 6 | base = /app 7 | 8 | py-autoreload = 1 9 | 10 | chdir = %(base)/%(project) 11 | module = %(project).wsgi:application 12 | 13 | 14 | master = true 15 | processes = 4 16 | 17 | 18 | socket = %(base)/%(project)/%(project).sock 19 | chmod-socket = 666 20 | vacuum = true -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /web/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/*.log 3 | **/*-log-* 4 | **/*.tar.gz 5 | Dockerfile 6 | .dockerignore 7 | *.md 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /web/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21-alpine 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | 5 | RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone 6 | 7 | COPY nginx.conf /etc/nginx/conf.d/default.conf 8 | #COPY dist/ /usr/share/nginx/html/ 9 | -------------------------------------------------------------------------------- /web/Dockerfile-build: -------------------------------------------------------------------------------- 1 | FROM node:16 as build-stage 2 | 3 | # make the 'app' folder the current working directory 4 | WORKDIR /app 5 | 6 | # copy both 'package.json' and 'package-lock.json' (if available) 7 | COPY package*.json ./ 8 | 9 | # install project dependencies 10 | # RUN yarn install --registry https://registry.npm.taobao.org/ 11 | RUN yarn install 12 | 13 | # copy project files and folders to the current working directory (i.e. 'app' folder) 14 | COPY . . 15 | 16 | #ENV NODE_OPTIONS="--max_old_space_size=2048" 17 | #ARG API_URL="http://119.91.147.215:8000" 18 | #ENV API_URL=$API_URL 19 | #ENV FasterRunner="myFasterRunner" 20 | # build app for production with minification 21 | 22 | RUN npm run build 23 | 24 | FROM nginx:1.21-alpine 25 | 26 | RUN rm /etc/nginx/conf.d/default.conf 27 | 28 | RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone 29 | 30 | COPY --from=build-stage /app/dist /usr/share/nginx/html 31 | 32 | COPY nginx.conf /etc/nginx/conf.d/default.conf 33 | 34 | EXPOSE 80 35 | 36 | CMD ["nginx", "-g", "daemon off;"] 37 | -------------------------------------------------------------------------------- /web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 yinquanwang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # FasterWeb 2 | 3 | ![LICENSE](https://img.shields.io/github/license/yinquanwang/FasterRunner.svg) 4 | 5 | ## 本地开发环境部署 6 | 7 | ``` bash 8 | # install dependencies 9 | yarn install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /web/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /web/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /web/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | const FasterRunner = process.env.FasterRunner ? process.env.FasterRunner : "Another FasterRunner" 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | FasterRunner: "'" + FasterRunner + "'" 8 | }) 9 | -------------------------------------------------------------------------------- /web/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: { 14 | '/api': { 15 | target: 'http://localhost:8000', // 这是本地用node写的一个服务,用webpack-dev-server起的服务默认端口是8080 16 | // pathRewrite: {"": ""}, // 后台在转接的时候url中是没有 /api 的 17 | changeOrigin: true, // 加了这个属性,那后端收到的请求头中的host是目标地址 target 18 | } , 19 | '/static/extent.js': { 20 | target: 'http://localhost:8000', // 这是本地用node写的一个服务,用webpack-dev-server起的服务默认端口是8080 21 | // pathRewrite: {"": ""}, // 后台在转接的时候url中是没有 /api 的 22 | changeOrigin: true, // 加了这个属性,那后端收到的请求头中的host是目标地址 target 23 | } 24 | }, 25 | // Various Dev Server settings 26 | host: 'localhost', // can be overwritten by process.variables.HOST 27 | port: 8080, // can be overwritten by process.variables.PORT, if port is in use, a free one will be determined 28 | autoOpenBrowser: false, 29 | errorOverlay: true, 30 | notifyOnErrors: true, 31 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 32 | 33 | 34 | /** 35 | * Source Maps 36 | */ 37 | 38 | // https://webpack.js.org/configuration/devtool/#development 39 | devtool: 'cheap-module-eval-source-map', 40 | 41 | // If you have problems debugging vue-files in devtools, 42 | // set this to false - it *may* help 43 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 44 | cacheBusting: true, 45 | 46 | cssSourceMap: true 47 | }, 48 | 49 | build: { 50 | // Template for index.html 51 | index: path.resolve(__dirname, '../dist/index.html'), 52 | 53 | // Paths 54 | assetsRoot: path.resolve(__dirname, '../dist'), 55 | assetsSubDirectory: 'static', 56 | assetsPublicPath: '/', 57 | 58 | /** 59 | * Source Maps 60 | */ 61 | 62 | productionSourceMap: false, 63 | // https://webpack.js.org/configuration/devtool/#production 64 | devtool: '#source-map', 65 | 66 | // Gzip off by default as many popular static hosts such as 67 | // Surge or Netlify already gzip all static assets for you. 68 | // Before setting to `true`, make sure to: 69 | // npm install --save-dev compression-webpack-plugin 70 | productionGzip: false, 71 | productionGzipExtensions: ['js', 'css'], 72 | 73 | // Run the build command with an extra argument to 74 | // View the bundle analyzer report after build finishes: 75 | // `npm run build --report` 76 | // Set to `true` or `false` to always turn it on or off 77 | bundleAnalyzerReport: process.env.npm_config_report 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /web/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const FasterRunner = process.env.FasterRunner ? process.env.FasterRunner : "Another FasterRunner" 3 | const API_URL = process.env.API_URL ? process.env.API_URL : "" 4 | console.log('process args: ' + process.argv) 5 | console.log('get env from env API_URL: ' + API_URL) 6 | module.exports = { 7 | NODE_ENV: '"production"', 8 | FasterRunner: "'" + FasterRunner + "'", 9 | API_URL: "'" + API_URL + "'", 10 | } 11 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FastRunner 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /web/nginx.conf: -------------------------------------------------------------------------------- 1 | # upstream django { 2 | # server app:8000; 3 | # } 4 | server { 5 | listen 80; 6 | #server_name 8.129.237.137; # 修改为docker服务宿主机的ip 7 | 8 | proxy_set_header Host $host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | proxy_set_header X-Forwarded-Proto $scheme; 12 | 13 | location / { 14 | root /usr/share/nginx/html; 15 | index index.html index.htm; 16 | try_files $uri $uri/ /index.html =404; 17 | gzip on; 18 | gzip_comp_level 6; 19 | gzip_min_length 1k; 20 | gzip_buffers 4 16k; 21 | gzip_proxied any; 22 | gzip_vary on; 23 | gzip_types 24 | application/javascript 25 | application/x-javascript 26 | text/javascript 27 | text/css 28 | text/xml 29 | application/xhtml+xml 30 | application/xml 31 | application/atom+xml 32 | application/rdf+xml 33 | application/rss+xml 34 | application/geo+json 35 | application/json 36 | application/ld+json 37 | application/manifest+json 38 | application/x-web-app-manifest+json 39 | image/svg+xml 40 | image/jpeg 41 | image/gif 42 | image/png 43 | text/x-cross-domain-policy; 44 | gzip_static on; 45 | } 46 | 47 | location /api { 48 | proxy_pass http://app:8000; 49 | } 50 | 51 | location /mock { 52 | proxy_pass http://app:8000; 53 | } 54 | 55 | # 测试报告接口 56 | location /api/fastrunner/reports { 57 | proxy_pass http://nginx:8000; 58 | } 59 | # 测试报告静态文件 60 | location /static/extent.js { 61 | proxy_pass http://nginx:8000; 62 | } 63 | 64 | error_page 500 502 503 504 /50x.html; 65 | location = /50x.html { 66 | root html; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /web/nginx_localhost.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | #server_name 8.129.237.137; # 修改为docker服务宿主机的ip 5 | 6 | proxy_set_header Host $host; 7 | proxy_set_header X-Real-IP $remote_addr; 8 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 9 | proxy_set_header X-Forwarded-Proto $scheme; 10 | 11 | location / { 12 | root ./dist; 13 | index index.html index.htm; 14 | try_files $uri $uri/ /index.html =404; 15 | gzip on; 16 | gzip_comp_level 6; 17 | gzip_min_length 1k; 18 | gzip_buffers 4 16k; 19 | gzip_proxied any; 20 | gzip_vary on; 21 | gzip_types 22 | application/javascript 23 | application/x-javascript 24 | text/javascript 25 | text/css 26 | text/xml 27 | application/xhtml+xml 28 | application/xml 29 | application/atom+xml 30 | application/rdf+xml 31 | application/rss+xml 32 | application/geo+json 33 | application/json 34 | application/ld+json 35 | application/manifest+json 36 | application/x-web-app-manifest+json 37 | image/svg+xml 38 | image/jpeg 39 | image/gif 40 | image/png 41 | text/x-cross-domain-policy; 42 | gzip_static on; 43 | } 44 | 45 | location /api { 46 | proxy_pass http://localhost:8000; 47 | } 48 | 49 | location /mock { 50 | proxy_pass http://localhost:8000; 51 | } 52 | 53 | # 测试报告接口 54 | location /api/fastrunner/reports { 55 | proxy_pass http://localhost:8000; 56 | } 57 | # 测试报告静态文件 58 | location /static/extent.js { 59 | proxy_pass http://localhost:8000; 60 | } 61 | 62 | error_page 500 502 503 504 /50x.html; 63 | location = /50x.html { 64 | root html; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-web", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "尹全旺 <1263374981@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "@babel/runtime": "^7.24.0", 14 | "@femessage/el-data-table": "^1.23.2", 15 | "@femessage/el-form-renderer": "^1.24.0", 16 | "ajv": "^6.12.6", 17 | "apexcharts": "^3.27.3", 18 | "axios": "^0.18.0", 19 | "babel-preset-es2015": "^6.24.1", 20 | "brace": "^0.11.1", 21 | "element-ui": "2.15.13", 22 | "sass-loader": "^11.0.1", 23 | "v-jsoneditor": "^1.4.4", 24 | "vue": "^2.5.2", 25 | "vue-apexcharts": "^1.6.2", 26 | "vue-clipboard2": "^0.3.1", 27 | "vue-codemirror": "^4.0.6", 28 | "vue-loader": "^13.7.3", 29 | "vue-router": "^3.0.1", 30 | "vuedraggable": "^2.16.0", 31 | "vuex": "^3.0.1" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^7.1.2", 35 | "babel-core": "^6.22.1", 36 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 37 | "babel-loader": "^7.1.1", 38 | "babel-plugin-syntax-jsx": "^6.18.0", 39 | "babel-plugin-transform-runtime": "^6.22.0", 40 | "babel-plugin-transform-vue-jsx": "^3.5.0", 41 | "babel-preset-env": "^1.3.2", 42 | "babel-preset-stage-2": "^6.22.0", 43 | "chalk": "^2.0.1", 44 | "copy-webpack-plugin": "^4.0.1", 45 | "css-loader": "^0.28.0", 46 | "extract-text-webpack-plugin": "^3.0.0", 47 | "file-loader": "^1.1.4", 48 | "friendly-errors-webpack-plugin": "^1.6.1", 49 | "html-webpack-plugin": "^2.30.1", 50 | "node-notifier": "^5.1.2", 51 | "optimize-css-assets-webpack-plugin": "^3.2.0", 52 | "ora": "^1.2.0", 53 | "portfinder": "^1.0.13", 54 | "postcss-import": "^11.0.0", 55 | "postcss-loader": "^2.0.8", 56 | "postcss-url": "^7.2.1", 57 | "rimraf": "^2.6.0", 58 | "semver": "^5.3.0", 59 | "shelljs": "^0.7.6", 60 | "uglifyjs-webpack-plugin": "^1.1.1", 61 | "url-loader": "^0.5.8", 62 | "vue-easytable": "^1.7.1", 63 | "vue-loader": "^13.7.3", 64 | "vue-style-loader": "^3.0.1", 65 | "vue-template-compiler": "^2.5.2", 66 | "vue2-ace-editor": "^0.0.15", 67 | "webpack": "^3.6.0", 68 | "webpack-bundle-analyzer": "^4.8.0", 69 | "webpack-dev-server": "^2.9.1", 70 | "webpack-merge": "^4.1.0", 71 | "zip-webpack-plugin": "^2.0.0" 72 | }, 73 | "engines": { 74 | "node": ">= 6.0.0", 75 | "npm": ">= 3.0.0" 76 | }, 77 | "browserslist": [ 78 | "> 1%", 79 | "last 2 versions", 80 | "not ie <= 8" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /web/src/assets/images/bottom-left.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/src/assets/images/bottom-left.webp -------------------------------------------------------------------------------- /web/src/assets/images/bottom-right.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/src/assets/images/bottom-right.webp -------------------------------------------------------------------------------- /web/src/assets/styles/icon-font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/src/assets/styles/icon-font/iconfont.eot -------------------------------------------------------------------------------- /web/src/assets/styles/icon-font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/src/assets/styles/icon-font/iconfont.ttf -------------------------------------------------------------------------------- /web/src/assets/styles/icon-font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/src/assets/styles/icon-font/iconfont.woff -------------------------------------------------------------------------------- /web/src/assets/styles/reports.css: -------------------------------------------------------------------------------- 1 | .success { 2 | font-weight: bold; 3 | font-size: medium; 4 | color: #67c23a; 5 | } 6 | 7 | .error { 8 | font-weight: bold; 9 | font-size: medium; 10 | color: red; 11 | } 12 | 13 | .failure { 14 | font-weight: bold; 15 | font-size: medium; 16 | color: salmon; 17 | } 18 | 19 | .skipped { 20 | font-weight: bold; 21 | font-size: medium; 22 | color: #909399; 23 | } 24 | 25 | .POST { 26 | color: #49cc90; 27 | } 28 | 29 | 30 | .PUT { 31 | color: #fca130; 32 | } 33 | 34 | .GET { 35 | color: #61affe; 36 | } 37 | 38 | 39 | .DELETE { 40 | color: #f93e3e; 41 | } 42 | 43 | 44 | .PATCH { 45 | color: #50e3c2; 46 | } 47 | 48 | .HEAD { 49 | color: #e6a23c; 50 | } 51 | 52 | .OPTIONS { 53 | color: #409eff; 54 | } 55 | 56 | 57 | .code-block { 58 | 59 | border: 1px solid #ebedef; 60 | border-radius: 4px; 61 | color: #222 !important; 62 | font-family: Consolas, monospace; 63 | font-size: 13px; 64 | margin: 0; 65 | padding: 7px 10px; 66 | overflow: auto; 67 | } 68 | -------------------------------------------------------------------------------- /web/src/assets/styles/tree.css: -------------------------------------------------------------------------------- 1 | ul li { 2 | list-style: none; 3 | } 4 | 5 | .operation-li { 6 | line-height: 60px; 7 | color: #555; 8 | font-size: 12px; 9 | width: 240px; 10 | margin-top: 1px; 11 | } 12 | 13 | .api-tree { 14 | padding: 0; 15 | margin: 0; 16 | } 17 | 18 | .nav-api-header { 19 | height: 48px; 20 | border-bottom: 1px solid #ddd; 21 | background-color: #F7F7F7; 22 | 23 | } 24 | 25 | .nav-api-side { 26 | overflow: auto; 27 | border: 1px solid #ddd; 28 | width: 240px; 29 | margin-left: 10px; 30 | border-radius: 4px; 31 | position: fixed; 32 | top: 110px; 33 | bottom: 0; 34 | 35 | } 36 | 37 | .custom-tree-node { 38 | flex: 1; 39 | display: flex; 40 | align-items: center; 41 | justify-content: space-between; 42 | font-size: 14px; 43 | padding-right: 8px; 44 | } 45 | 46 | 47 | .tree { 48 | overflow-y: auto; 49 | overflow-x: auto; 50 | width: 280px; 51 | height: 500px; 52 | } 53 | 54 | .el-tree { 55 | min-width: 100%; 56 | display: inline-block !important; 57 | } 58 | 59 | .el-tree-node__expand-icon { 60 | padding: 4px !important; 61 | } 62 | 63 | .el-tree-node__content { 64 | height: 50px; 65 | } 66 | 67 | .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content { 68 | background-color: rgba(64, 158, 255, .1); 69 | color: #409EFF; 70 | } 71 | -------------------------------------------------------------------------------- /web/src/mixins/apiListMixin.js: -------------------------------------------------------------------------------- 1 | // apiListMixin.js 2 | export default { 3 | methods: { 4 | getAPIList(params) { 5 | this.$nextTick(() => { 6 | const defaultParams = { 7 | page: this.listCurrentPage || 1, // 提供默认值 8 | node: this.currentNode || this.node, // 支持不同的命名 9 | project: this.project, 10 | search: this.search, 11 | tag: this.visibleTag || this.tag, 12 | rigEnv: this.rigEnv, 13 | onlyMe: this.onlyMe, 14 | showYAPI: this.showYAPI, 15 | creator: this.selectUser 16 | }; 17 | 18 | // 使用传入的参数覆盖默认参数 19 | const apiParams = { ...defaultParams, ...params }; 20 | 21 | this.$api.apiList({ params: apiParams }).then(res => { 22 | this.apiData = res; 23 | }); 24 | }); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /web/src/mixins/configMixin.js: -------------------------------------------------------------------------------- 1 | // configMixin.js 2 | export default { 3 | data() { 4 | return { 5 | configOptions: [], 6 | currentConfig: null 7 | }; 8 | }, 9 | methods: { 10 | getConfig({ addPlaceholder = false, setDefaultConfig = false } = {}) { 11 | this.$api.getAllConfig(this.$route.params.id).then(resp => { 12 | this.configOptions = resp; 13 | 14 | // 根据配置决定是否添加占位符,并设置默认选项 15 | if (addPlaceholder) { 16 | const placeHolderOption = { name: '请选择' }; 17 | if (setDefaultConfig) { 18 | this.configOptions.unshift(placeHolderOption); 19 | const _config = this.configOptions.find(item => item.is_default === true); 20 | this.currentConfig = _config || placeHolderOption; 21 | } else { 22 | this.configOptions.push(placeHolderOption); 23 | } 24 | } 25 | }); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /web/src/mixins/treeMixin.js: -------------------------------------------------------------------------------- 1 | // treeMixin.js 2 | export default { 3 | methods: { 4 | getTree(callback) { 5 | this.$api.getTree(this.$route.params.id, { params: { type: 1 } }).then(resp => { 6 | this.dataTree = resp['tree']; 7 | 8 | // 如果有提供回调函数,则调用它 9 | if (callback && typeof callback === 'function') { 10 | callback(resp); 11 | } 12 | }); 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /web/src/pages/common/layout/CommonLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /web/src/pages/home/Home.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 41 | -------------------------------------------------------------------------------- /web/src/pages/home/components/Header.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 51 | 52 | 88 | -------------------------------------------------------------------------------- /web/src/pages/home/components/Side.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 68 | 69 | 81 | -------------------------------------------------------------------------------- /web/src/pages/home/components/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 45 | -------------------------------------------------------------------------------- /web/src/pages/httprunner/components/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 67 | -------------------------------------------------------------------------------- /web/src/pages/httprunner/components/RunCodeResult.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /web/src/pages/mock_server/mock_api/CustomAceEditor.vue: -------------------------------------------------------------------------------- 1 | // CustomAceEditor.vue 2 | 3 | 15 | 16 | 48 | -------------------------------------------------------------------------------- /web/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import state from './state' 4 | import mutations from './mutations' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | state, 10 | mutations 11 | }) 12 | -------------------------------------------------------------------------------- /web/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | isLogin(state, value) { 4 | state.token = value 5 | }, 6 | 7 | setUser(state, value) { 8 | state.user = value 9 | }, 10 | setRouterName(state, value) { 11 | state.routerName = value 12 | }, 13 | setProjectName(state, value) { 14 | if (value !== '' ){ 15 | value = ' / ' + value.replaceAll('/', '').replaceAll(' ', '') 16 | } 17 | state.projectName = value 18 | }, 19 | 20 | setIsSuperuser(state, value) { 21 | state.is_superuser = value 22 | }, 23 | setShowHots(state, value) { 24 | state.show_hosts = value 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/src/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | routerName: null, 3 | projectName: '', 4 | token: null, 5 | user: null, 6 | is_superuser: false, 7 | show_hosts: false, 8 | duration: 2000, 9 | FasterRunner: process.env.FasterRunner 10 | } 11 | -------------------------------------------------------------------------------- /web/src/util/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default new Vue() 3 | -------------------------------------------------------------------------------- /web/src/util/format.js: -------------------------------------------------------------------------------- 1 | export const datetimeObj2str = function (time, format = 'YYYY-MM-DD hh:mm:ss') { 2 | let date = new Date(time); 3 | let year = date.getFullYear(), 4 | month = (date.getMonth() + 1).toString().padStart(2, '0'), 5 | day = date.getDate().toString().padStart(2, '0'), 6 | hour = date.getHours().toString().padStart(2, '0'), 7 | min = date.getMinutes().toString().padStart(2, '0'), 8 | sec = date.getSeconds().toString().padStart(2, '0'); 9 | 10 | let newTime = format.replace(/YYYY/g, year) 11 | .replace(/MM/g, month) 12 | .replace(/DD/g, day) 13 | .replace(/hh/g, hour) 14 | .replace(/mm/g, min) 15 | .replace(/ss/g, sec); 16 | 17 | return newTime; 18 | } 19 | 20 | export const timestamp2time = function (timestamp) { 21 | if (!timestamp) { 22 | return '' 23 | } 24 | let date = new Date(timestamp * 1000); 25 | const Y = date.getFullYear() + '-'; 26 | 27 | // js的月份从0开始 28 | const month = date.getMonth() + 1; 29 | const M = (month < 10 ? '0' + month : month) + '-'; 30 | 31 | const days = date.getDate(); 32 | const D = (days + 1 < 10 ? '0' + days : days) + ' '; 33 | 34 | const hours = date.getHours(); 35 | const h = (hours + 1 < 10 ? '0' + hours : hours) + ':'; 36 | 37 | const minutes = date.getMinutes(); 38 | const m = (minutes + 1 < 10 ? '0' + minutes : minutes) + ':'; 39 | 40 | const seconds = date.getSeconds(); 41 | const s = seconds + 1 < 10 ? '0' + seconds : seconds; 42 | 43 | return Y + M + D + h + m + s; 44 | } 45 | -------------------------------------------------------------------------------- /web/src/validator.js: -------------------------------------------------------------------------------- 1 | export const isNumArray = (rule, value, callback) => { 2 | if (value === ""){ 3 | callback() 4 | } 5 | const numStr = /^[0-9,]*$/ 6 | if (!numStr.test(value)) { 7 | callback(new Error('只能为整数和英文逗号, 且不能包含空格')) 8 | try { 9 | eval(value.split(",")) 10 | } catch (err) { 11 | callback(new Error('字符串转换为整数列表错误: ' + err)) 12 | } 13 | } else { 14 | callback() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/static/.gitkeep -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihuacai168/AnotherFasterRunner/ac517bfa1852291df7fba846ca545a9cc269558d/web/static/favicon.ico --------------------------------------------------------------------------------