diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7a1431a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# https://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.{py,md}] +indent_size = 4 + +[Makefile] +indent_size = 4 +indent_style = tab diff --git a/.env b/.env index ef82abb..62f9d15 100644 --- a/.env +++ b/.env @@ -1,5 +1,2 @@ -HOST=currency-exchange.p.rapidapi.com +HOST=https://currency-exchange.p.rapidapi.com API_TOKEN=d26533cbe8msh020600a07386d87p18a610jsn31d39f87f802 - -# Locust -LOCUST_WEB_AUTH=ubuntu:debian diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e003c5c..ee82113 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,12 @@ on: - master schedule: - # Every Sunday at 0:37 UTC. - - cron: "37 0 * * 0" + # Run every 15 days at 12:37 AM UTC. + - cron: "37 0 */15 * *" concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: run-tests: @@ -26,7 +26,7 @@ jobs: # Use matrix strategy to run the tests on multiple Py versions on multiple OSs. matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -50,6 +50,3 @@ jobs: - name: Run the tests & Generate coverage report run: | make test - locust -f src/locustfile.py \ - --host https://currency-exchange.p.rapidapi.com/ \ - --headless -u 1 -r 5 --run-time=5 --exit-code-on-error 1 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..de2042c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,24 @@ +{ + "proseWrap": "preserve", + "semi": false, + "singleQuote": false, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 92, + "overrides": [ + { + "files": "**/*.md", + "options": { + "tabWidth": 4, + "useTabs": false, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "avoid", + "printWidth": 92, + "proseWrap": "always", + "embeddedLanguageFormatting": "off" + } + } + ] +} diff --git a/README.md b/README.md index 3fcd239..f3bf809 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,20 @@ - ## Description -[Locust][0] is a distributed and scalable open-source library that -helps you write effective load tests in pure Python. This repository demonstrates a -modular architecture to establish a template for quickly building a scalable stress -testing pipeline with Locust. +[Locust][0] is a distributed and scalable open-source library that helps you write effective +load tests in pure Python. This repository demonstrates a modular architecture that can work +as a template for quickly building a scalable stress testing pipeline with Locust. ## Locust terminology -If you're unfamiliar with the terminologies and the generic workflow of writing -stress-tests with Locust, then you should go through the official [documentation][1] -first. With that out of the way, let's go through a few terminologies that comes up in -this context quite often: +If you're unfamiliar with the terminologies and the generic workflow of writing stress-tests +with Locust, then I recommend going through the official [documentation][1] first. With that +out of the way, let's rehash a few terminologies that comes up in this context quite often: -**Task:** In Locust, a [Task][2] is the smallest unit of a test suite. Usually, it -means, any function or method that is decorated with the `@task` decorator. +**Task:** In Locust, a [Task][2] is the smallest unit of a test suite. Usually, it means, +any function or method that is decorated with the `@task` decorator. **TaskSet:** A [TaskSet][3] is a class that establishes a contextual boundary between different groups of Tasks. You can essentially group multiple similar Tasks inside a @@ -29,19 +26,19 @@ TaskSet. Then you use the TaskSets from your User class. **User:** In Locust, a [User][4] is a class that executes the tests either by directly calling the Task methods or via using TaskSets. -***In more complex cases, the tests can further be organized by arranging them in -multiple test modules. This template groups the Tasks using TaskSets and places multiple -TaskSets in separate test modules to ensure modularity and better scalability.*** +**_In more complex cases, the tests can further be organized by arranging them in multiple +test modules. This template groups the Tasks using TaskSets and places multiple TaskSets in +separate test modules to achieve better modularity._** ## Target API -This template uses [Rapid API's][5] currency-exchange [API][6] for showcasing the load -testing procedure. The API converts one currency to another using the current exchange -rate. +Here, we're using [Rapid API's][5] currency-exchange [API][6] to showcase the load testing +procedure. The API converts one currency to another using the current exchange rate. ### API anatomy It takes three parameters in its query string: + ``` 1. q : str - quantity 2. from : str - currency to convert from @@ -50,13 +47,12 @@ It takes three parameters in its query string: And returns the converted value. - ### Access the API -Sign up for a Rapid API [account][7] and get your token. You can access the API via -cURL like (You need to provide your API token in the header): +Sign up for a Rapid API [account][7] and get your token. You can access the API via cURL +like (You need to provide your API token in the header): -```bash +```sh curl --request GET \ --url 'https://currency-exchange.p.rapidapi.com/exchange?q=1.0&from=USD&to=BDT' \ --header 'x-rapidapi-host: currency-exchange.p.rapidapi.com' \ @@ -65,13 +61,12 @@ curl --request GET \ The response will look like this: -``` +```txt 84.91925⏎ ``` -Or, you might want to access it via Python. You can do so using the [HTTPx][8] library -like this: - +Or, you might want to access it via Python. You can do so using the [HTTPx][8] library like +this: ```python import httpx @@ -93,12 +88,10 @@ print(response.text) ## Stress testing pipeline -Below, you can see the core architecture of the load testing pipeline. For brevity's -sake—files regarding containerization, deployment, and dependency management have been -omitted. - +Below, you can see the core architecture of the load testing pipeline. For brevity's sake, +files regarding containerization, deployment, and dependency management have been omitted. -``` +```txt src/ ├── locustfiles # Directir where the load test modules live │ ├── __init__.py @@ -111,86 +104,82 @@ src/ └── settings.py # Read the environment variables here ``` -The test suite has three primary components— -`setup.py`, `locustfiles`, and the `locust.conf` file. +The test suite has three primary components: `setup.py`, `locustfiles`, and the +`locust.conf` file. ### [setup.py](src/setup.py) + The common elements required for testing, like `auth`, `login`, and `logout` functions reside in the `setup.py` file. ### [locustfiles](src/locustfiles/) -The load test modules reside in the **`locustfiles`** directory. Test modules import and -use the functions in the `setup.py` file before executing each test. + +The load test modules reside in the **`locustfiles`** directory. Test modules import and use +the functions in the `setup.py` file before executing each test. In the `locustfiles` directory, currently, there are only two load test modules— -`bdt_convert.py` and `rs_convert.py`. You can name your test modules whatever you want -and the load testing classes and functions should reside here. +`bdt_convert.py` and `rs_convert.py`. You can name your test modules whatever you want and +the load testing classes and functions should reside here. -* [bdt_convert.py](src/locustfiles/bdt_convert.py): This module houses a single TaskSet -named `BDTConvert` that has two Tasks—`usd_to_bdt` and `bdt_to_usd`. The first Task -tests the exchange API when the request query asks for USD to BDT currency conversion -and the second Task tests the API while requesting BDT to USD conversion. +- [bdt_convert.py](src/locustfiles/bdt_convert.py): This module houses a single TaskSet + named `BDTConvert` that has two Tasks—`usd_to_bdt` and `bdt_to_usd`. The first Task + tests the exchange API when the request query asks for USD to BDT currency conversion + and the second Task tests the API while requesting BDT to USD conversion. -* [rs_convert.py](src/locustfiles/rs_convert.py): The second test module does the same -things as the first one; only it tests the APIs while the request query asks for USD to -RS conversion and vice versa. +- [rs_convert.py](src/locustfiles/rs_convert.py): The second test module does the same + things as the first one; only it tests the APIs while the request query asks for USD to + RS conversion and vice versa. - The reason that there are two similar test modules is just to demonstrate how you - can organize your Tasks, TaskSets, and test modules. + The reason that there are two similar test modules is just to demonstrate how you + can organize your Tasks, TaskSets, and test modules. -* [locustfile.py](src/locustfile.py): This file works as the entrypoint of the workflow. -It imports the TaskSets from the `bdt_convert` and `usd_convert` modules and creates a -[HttpUser][9] that will execute the tasks. +- [locustfile.py](src/locustfile.py): This file works as the entrypoint of the workflow. + It imports the TaskSets from the `bdt_convert` and `usd_convert` modules and creates a + [HttpUser][9] that will execute the tasks. ### [locust.conf](src/locust.conf) -The **`locust.conf`** file defines the configurations like *hostname*, *number* of -*workers*, *number of simulated users*, *spawn rate*, etc. - +The **`locust.conf`** file defines the configurations like _hostname_, _number_ of +_workers_, _number of simulated users_, _spawn rate_, etc. ## Run the stress tests locally -* Make sure you have the latest version of [docker][10] and -docker-compose installed on your machine. +- Make sure you have the latest version of [docker][10] and docker-compose installed on + your machine. -* Clone this repository and go to the root directory. +- Clone this repository and go to the root directory. -* Place your rapidAPI token in the `.env` file. +- Place your rapidAPI token in the `.env` file. -* Run: +- Run: - ```bash + ```sh docker compose up -d ``` This will spin up a master container and a single worker container that will do the testing. If you want to deploy more workers to do the load testing then run: - ```bash + ```sh docker compose up -d --scale worker 2 ``` -* To access the Locust GUI, go to [http://localhost:8089/][11] on your browser. You'll -be prompted to provide a username and a password. Use `ubuntu` as the username and -`debian` as the password. You'll be greeted by a screen like below. You should see that -the fields of the form are already filled in since Locust pulls the values from the -`locust.conf` file: +- To access the Locust GUI, go to [http://localhost:8089/][11] on your browser. You'll be + greeted by a screen like below. You should see that the fields of the form are already + filled in since Locust pulls the values from the `locust.conf` file: - ![locust signin][12] + ![locust signin][12] -* Once you've pressed the *start swarming* button, you'll be taken to the following page: +- Once you've pressed the _start swarming_ button, you'll be taken to the following page: ![Screenshot from 2020-09-05 03-12-25][13] -* You can start, stop and control your tests from there. - +- You can start, stop and control your tests from there. ## Disclaimer This dockerized application is production-ready. However, you shouldn't expose your -environment file (.env) in production. Here, it was done only for demonstration -purposes. - +environment file (.env) in production. Here, it was done only for demonstration purposes. [0]: https://locust.io/ [1]: https://docs.locust.io/en/stable/ @@ -204,6 +193,7 @@ purposes. [9]: https://docs.locust.io/en/stable/writing-a-locustfile.html#making-http-requests [10]: https://www.docker.com/ [11]: http://localhost:8089/ - -[12]: https://user-images.githubusercontent.com/30027932/92285103-51988580-ef25-11ea-9155-c9d3f5dcaf42.png -[13]: https://user-images.githubusercontent.com/30027932/92285284-b94ed080-ef25-11ea-9f91-3f972fd844f1.png +[12]: + https://user-images.githubusercontent.com/30027932/92285103-51988580-ef25-11ea-9155-c9d3f5dcaf42.png +[13]: + https://user-images.githubusercontent.com/30027932/92285284-b94ed080-ef25-11ea-9f91-3f972fd844f1.png diff --git a/docker-compose.yml b/docker-compose.yml index 39fc347..00d19c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: container_name: locust-main build: context: ./ - dockerfile: ./dockerfiles/python311/Dockerfile + dockerfile: ./dockerfiles/python312/Dockerfile ports: - 8089:8089 volumes: @@ -21,7 +21,7 @@ services: - ./:/code/ build: context: ./ - dockerfile: ./dockerfiles/python311/Dockerfile + dockerfile: ./dockerfiles/python312/Dockerfile command: | bash -c 'locust -f /code/src/locustfile.py --worker \ --master-host main --config /code/src/locust.conf' diff --git a/dockerfiles/python310/Dockerfile b/dockerfiles/python310/Dockerfile index f580093..6096e51 100644 --- a/dockerfiles/python310/Dockerfile +++ b/dockerfiles/python310/Dockerfile @@ -1,5 +1,5 @@ # Use the latest locust image -FROM python:3.10-bullseye +FROM python:3.10-bookworm # Set working directory WORKDIR /code diff --git a/dockerfiles/python311/Dockerfile b/dockerfiles/python311/Dockerfile index 67f5488..b352096 100644 --- a/dockerfiles/python311/Dockerfile +++ b/dockerfiles/python311/Dockerfile @@ -1,5 +1,5 @@ # Use the latest locust image -FROM python:3.11-bullseye +FROM python:3.11-bookworm # Set working directory WORKDIR /code diff --git a/dockerfiles/python38/Dockerfile b/dockerfiles/python312/Dockerfile similarity index 94% rename from dockerfiles/python38/Dockerfile rename to dockerfiles/python312/Dockerfile index 6cd1b57..1e0cb4d 100644 --- a/dockerfiles/python38/Dockerfile +++ b/dockerfiles/python312/Dockerfile @@ -1,5 +1,5 @@ # Use the latest locust image -FROM python:3.8-bullseye +FROM python:3.12-bookworm # Set working directory WORKDIR /code diff --git a/dockerfiles/python39/Dockerfile b/dockerfiles/python39/Dockerfile index 9d93680..51a1dd6 100644 --- a/dockerfiles/python39/Dockerfile +++ b/dockerfiles/python39/Dockerfile @@ -1,5 +1,5 @@ # Use the latest locust image -FROM python:3.9-bullseye +FROM python:3.9-bookworm # Set working directory WORKDIR /code diff --git a/makefile b/makefile index f5c186a..5f8e462 100644 --- a/makefile +++ b/makefile @@ -10,7 +10,7 @@ endef .PHONY: lint -lint: black ruff mypy ## Apply all the linters. +lint: ruff-check ruff-format mypy ## Apply all the linters. .PHONY: lint-check @@ -19,27 +19,26 @@ lint-check: ## Check whether the codebase satisfies the linter rules. @echo "Checking linter rules..." @echo "========================" @echo - @black --check $(path) - @ruff $(path) + @ruff check $(path) @echo 'y' | mypy $(path) --install-types -.PHONY: black -black: ## Apply black. +.PHONY: ruff-check +ruff-check: ## Apply ruff-check. @echo - @echo "Applying black..." - @echo "=================" + @echo "Applying ruff-check..." + @echo "======================" @echo - @black --fast $(path) + @ruff check --fix $(path) @echo -.PHONY: ruff -ruff: ## Apply ruff. - @echo "Applying ruff..." - @echo "================" +.PHONY: ruff-format +ruff: ## Apply ruff-format. + @echo "Applying ruff-format..." + @echo "=======================" @echo - @ruff --fix $(path) + @ruff format $(path) .PHONY: mypy diff --git a/pyproject.toml b/pyproject.toml index 948317e..2ddf067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,8 +25,8 @@ per-file-ignores = {} # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -# Assume Python 3.11. -target-version = "py311" +# Assume Python 3.12. +target-version = "py312" [tool.ruff.mccabe] diff --git a/requirements-dev.in b/requirements-dev.in index 4d9c5cf..b206183 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,4 +1,3 @@ -black ruff mypy pip-tools diff --git a/requirements-dev.txt b/requirements-dev.txt index b14a401..4eccbd8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,46 +1,36 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --no-emit-options --output-file=requirements-dev.txt requirements-dev.in # -black==22.12.0 - # via -r requirements-dev.in -build==0.9.0 +build==1.0.3 + # via pip-tools +click==8.1.7 # via pip-tools -click==8.1.3 - # via - # black - # pip-tools iniconfig==2.0.0 # via pytest -mypy==1.7.1 +mypy==1.8.0 # via -r requirements-dev.in mypy-extensions==1.0.0 - # via - # black - # mypy -packaging==23.0 + # via mypy +packaging==23.2 # via # build # pytest -pathspec==0.10.3 - # via black -pep517==0.13.0 - # via build pip-tools==7.3.0 # via -r requirements-dev.in -platformdirs==2.6.2 - # via black -pluggy==1.0.0 +pluggy==1.3.0 # via pytest +pyproject-hooks==1.0.0 + # via build pytest==7.4.3 # via -r requirements-dev.in -ruff==0.1.6 +ruff==0.1.9 # via -r requirements-dev.in -typing-extensions==4.4.0 +typing-extensions==4.9.0 # via mypy -wheel==0.38.4 +wheel==0.42.0 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements.txt b/requirements.txt index 4a0ee92..57de863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,49 +1,51 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --no-emit-options --output-file=requirements.txt requirements.in # -anyio==3.6.2 +anyio==4.2.0 # via httpx -brotli==1.0.9 +blinker==1.7.0 + # via flask +brotli==1.1.0 # via geventhttpclient -certifi==2022.12.7 +certifi==2023.11.17 # via # geventhttpclient # httpcore # httpx # requests -charset-normalizer==2.1.1 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via flask configargparse==1.7 # via locust -flask==2.2.2 +flask==3.0.0 # via # flask-basicauth # flask-cors # locust flask-basicauth==0.2.0 # via locust -flask-cors==3.0.10 +flask-cors==4.0.0 # via locust -gevent==22.10.2 +gevent==23.9.1 # via # geventhttpclient # locust geventhttpclient==2.0.11 # via locust -greenlet==2.0.1 +greenlet==3.0.3 # via gevent h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx -httpx==0.25.2 +httpx==0.26.0 # via -r requirements.in -idna==3.4 +idna==3.6 # via # anyio # httpx @@ -52,41 +54,39 @@ itsdangerous==2.1.2 # via flask jinja2==3.1.2 # via flask -locust==2.19.1 +locust==2.20.0 # via -r requirements.in -markupsafe==2.1.1 +markupsafe==2.1.3 # via # jinja2 # werkzeug -msgpack==1.0.4 +msgpack==1.0.7 # via locust -psutil==5.9.4 +psutil==5.9.7 # via locust python-dotenv==1.0.0 # via -r requirements.in -pyzmq==25.1.1 +pyzmq==25.1.2 # via locust -requests==2.28.1 +requests==2.31.0 # via locust roundrobin==0.0.4 # via locust six==1.16.0 - # via - # flask-cors - # geventhttpclient + # via geventhttpclient sniffio==1.3.0 # via # anyio # httpx -urllib3==1.26.13 +urllib3==2.1.0 # via requests -werkzeug==2.2.2 +werkzeug==3.0.1 # via # flask # locust -zope-event==4.6 +zope-event==5.0 # via gevent -zope-interface==5.5.2 +zope-interface==6.1 # via gevent # The following packages are considered to be unsafe in a requirements file: diff --git a/src/locustfile.py b/src/locustfile.py index 4f181e8..4b43845 100644 --- a/src/locustfile.py +++ b/src/locustfile.py @@ -13,7 +13,7 @@ class PrimaryUser(HttpUser): @events.quitting.add_listener -def _(environment, **kwargs: Any) -> None: +def _(environment, **_: Any) -> None: print(type(environment)) if environment.stats.total.fail_ratio > 0: logging.error("Test failed due to failure ratio > 1%") diff --git a/tests/test_settings.py b/tests/test_settings.py index 6348934..8bb1fa4 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -9,6 +9,5 @@ ) @patch("src.settings.load_dotenv", autospec=True) def test_envvars(load_dotenv): - assert settings.os.environ["HOST"] == "dummy_host" assert settings.os.environ["API_TOKEN"] == "dummy_api_token"