Skip to content

Commit

Permalink
chore: add python linters and formatters to CI
Browse files Browse the repository at this point in the history
- add a job to execute python checks against load test solution in CI
- add make commands for load tests and lint
- apply linters/formatters to locustfile.py
- update README to include mypy and make commands
- (piggyback) optimize Dockerfile requirements installation and pin poetry

SYNC-3827
  • Loading branch information
Trinaa committed Jul 27, 2023
1 parent c82e104 commit 615b93a
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 67 deletions.
15 changes: 15 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ jobs:
- run:
command: cargo audit

load-test-checks:
docker:
- image: cimg/python:3.11
working_directory: "~/autopush-rs"
steps:
- checkout:
path: ~/autopush-rs/
- run:
name: isort, black, flake8 and mypy
command: make lint

test:
docker:
- image: circleci/python:3.10
Expand Down Expand Up @@ -212,6 +223,10 @@ workflows:
filters:
tags:
only: /.*/
- load-test-checks:
filters:
tags:
only: /.*/
- test:
filters:
tags:
Expand Down
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
SHELL := /bin/sh
CARGO = cargo
LOAD_TEST_DIR := tests/load
POETRY := poetry --directory $(LOAD_TEST_DIR)
PYPROJECT_TOML := $(LOAD_TEST_DIR)/pyproject.toml
FLAKE8_CONFIG := $(LOAD_TEST_DIR)/.flake8
STAGE_SERVER_URL := "wss://autopush.stage.mozaws.net"
STAGE_ENDPOINT_URL := "https://updates-autopush.stage.mozaws.net"

.PHONY: ddb

Expand All @@ -12,3 +18,25 @@ upgrade:
echo "\n$(CARGO) install cargo-edit failed, continuing.."
$(CARGO) upgrade
$(CARGO) update

lint:
$(POETRY) -V
$(POETRY) install
$(POETRY) run isort --sp $(PYPROJECT_TOML) -c $(LOAD_TEST_DIR)
$(POETRY) run black --quiet --diff --config $(PYPROJECT_TOML) --check $(LOAD_TEST_DIR)
$(POETRY) run flake8 --config $(FLAKE8_CONFIG) $(LOAD_TEST_DIR)
$(POETRY) run mypy $(LOAD_TEST_DIR) --config-file=$(PYPROJECT_TOML)

load:
SERVER_URL=$(STAGE_SERVER_URL) ENDPOINT_URL=$(STAGE_ENDPOINT_URL) \
docker-compose \
-f $(LOAD_TEST_DIR)/docker-compose.yml \
-p autopush-rs-load-tests \
up --scale locust_worker=1

load-clean:
docker-compose \
-f $(LOAD_TEST_DIR)/docker-compose.yml \
-p autopush-rs-load-tests \
down
docker rmi locust
12 changes: 5 additions & 7 deletions tests/load/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ ENV PATH="${PYTHON_VENV}/bin:${PATH}"

RUN python -m pip install --upgrade pip

# Setup poetry and extract requirements
# Setup poetry and install requirements
ENV POETRY_VIRTUALENVS_CREATE=false \
POETRY_VERSION=1.5.1
RUN python -m pip install --no-cache-dir --quiet poetry
WORKDIR /tmp
COPY pyproject.toml poetry.lock /tmp/
RUN poetry export --no-interaction --output requirements.txt --without-hashes

WORKDIR /
RUN python -m pip install -r /tmp/requirements.txt
COPY pyproject.toml poetry.lock ./
RUN poetry install --without dev --no-interaction --no-ansi

RUN useradd --create-home locust
WORKDIR /home/locust
Expand Down
57 changes: 28 additions & 29 deletions tests/load/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,39 @@ To install the dependencies execute:
poetry install
```

Contributors to this project are expected to execute the following tools for import
sorting, linting, style guide enforcement and static type checking.
Contributors to this project are expected to execute isort, black, flake8 and mypy for
import sorting, linting, style guide enforcement and static type checking respectively.
Configurations are set in the `pyproject.toml` and `.flake8` files.

**[isort][4]**
```shell
poetry run isort locustfile.py
```
The tools can be executed with the following command from the root directory:

**[black][5]**
```shell
poetry run black locustfile.py
```
```shell
make lint
```

**[flake8][6]**
```shell
poetry run flake8 locustfile.py
```
## Local Execution

Follow the steps bellow to execute the load tests locally:

## Local Execution
### Setup Environment

There are 3 docker-compose files pertaining to the different environments autopush can run on:
`docker-compose.prod.yml`
`docker-compose.stage.yml`
`docker-compose.dev.yml`
#### 1. Configure Environment Variables

You can select which environment you want to run against by choosing the appropriate file. The environment will be setup for you with the correct URLs.
Environment variables, listed bellow or specified by [Locust][8], can be set in
`tests\load\docker-compose.yml`.

Ex:
```shell
docker-compose -f docker-compose.stage.yml up --build --scale locust_worker=1
```
| Environment Variable | Node(s) | Description |
|----------------------|------------------|---------------------------------|
| SERVER_URL | master & worker | The autopush web socket address |
| ENDPOINT_URL | master & worker | The autopush HTTP address |

This will run build and start the locust session for the Stage environment.
#### 2. Host Locust via Docker

Execute the following from the root directory:

```shell
make load
```

### Run Test Session

Expand All @@ -72,15 +69,17 @@ the load test will stop automatically.

#### 1. Remove Load Test Docker Containers

Execute the following from the `load` directory:
Execute the following from the root directory:

```shell
docker-compose -f docker-compose.stage.yml down
docker rmi locust
make load-clean
```

[1]: https://python-poetry.org/docs/#installation
[2]: https://github.com/pyenv/pyenv#installation
[3]: https://github.com/pyenv/pyenv-virtualenv#installation
[4]: https://pycqa.github.io/isort/
[5]: https://black.readthedocs.io/en/stable/
[6]: https://flake8.pycqa.org/en/latest/
[6]: https://flake8.pycqa.org/en/latest/
[7]: https://mypy-lang.org/
[8]: https://docs.locust.io/en/stable/configuration.html#environment-variables
59 changes: 29 additions & 30 deletions tests/load/locustfile.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import base64
import json
import random
import time
import string
import time
import uuid
from contextlib import closing
from urllib.parse import urljoin, urlparse
Expand All @@ -23,6 +23,7 @@
"""


@events.init_command_line_parser.add_listener
def _(parser):
parser.add_argument(
Expand All @@ -42,7 +43,7 @@ def _(parser):


class ConnectionTaskSet(TaskSet):
""" Create a fake "encrypted" message.
"""Create a fake "encrypted" message.
The server doesn't care about encryption. It does, however, apply a
base64 encoding to the data (this is because it's possible to send pure
Expand All @@ -51,6 +52,7 @@ class ConnectionTaskSet(TaskSet):
padding. The max size we allow for a push message is 4K.
"""

encrypted_data = base64.urlsafe_b64decode(
"TestData"
+ "".join(
Expand All @@ -65,11 +67,12 @@ class ConnectionTaskSet(TaskSet):

@task
def test_basic(self):
""" Perform a "basic" transaction test.
"""Perform a "basic" transaction test.
Desktop Autopush clients use a websocket connection to exchange
JSON command and response messages. (See
[Autopush HTTP Endpoints for Notifications](https://mozilla-services.github.io/autopush-rs/http.html#push-service-http-api)
[Autopush HTTP Endpoints for Notifications]
(https://mozilla-services.github.io/autopush-rs/http.html#push-service-http-api)
for details).
This tests an "active" style connection
Expand All @@ -83,12 +86,11 @@ def test_basic(self):
# Create a connection to the Autoconnect server
with closing(
create_connection(
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
)
) as ws:

# Connections must say hello after connecting to the server, otherwise
# the connection is quickly dropped.
body = json.dumps(dict(messageType="hello", use_webpush=True))
Expand Down Expand Up @@ -138,7 +140,9 @@ def test_basic(self):

# Send an "ack" message to make the server delete the message
# Otherwise we would get the message re-sent to us on reconnect
ws.send(json.dumps(dict(messageType="ack", updates=dict(channelID=channel_id))))
ws.send(
json.dumps(dict(messageType="ack", updates=dict(channelID=channel_id)))
)

self.user.environment.events.request.fire(
request_type="WSS",
Expand All @@ -151,7 +155,7 @@ def test_basic(self):

@task
def test_basic_topic(self):
""" Test a basic message transaction using a "topic".
"""Test a basic message transaction using a "topic".
"Topic" messages will replace prior, queued instances. A topic can be
any UA defined, URL Safe base64 compliant string. Upon reconnection,
Expand All @@ -167,11 +171,11 @@ def test_basic_topic(self):
channel_id = str(uuid.uuid4())

# Create a connection to the Autoconnect server.
with closing (
with closing(
create_connection(
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
)
) as ws:
# Connections must say hello after connecting to the server, otherwise
Expand Down Expand Up @@ -241,7 +245,6 @@ def test_basic_topic(self):
timeout=60,
)
) as ws:

start_time = time.time()
# After we reconnect and say "Hello", we should start getting
# any pending messages.
Expand Down Expand Up @@ -351,12 +354,11 @@ def test_connect_stored(self):
# Connect and register to get a unique endpoint.
with closing(
create_connection(
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
)
) as ws:

body = json.dumps(dict(messageType="hello", use_webpush=True))
ws.send(body)
res = json.loads(ws.recv())
Expand Down Expand Up @@ -411,10 +413,10 @@ def test_connect_stored(self):
try:
with closing(
create_connection(
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
timeout=30,
self.user.environment.parsed_options.websocket_url,
header={"Origin": "http://localhost:1337"},
ssl=False,
timeout=30,
)
) as ws:
start_time = time.time()
Expand All @@ -439,7 +441,7 @@ def test_connect_stored(self):
finally:
self.user.environment.events.request.fire(
request_type="WSS",
name=f"WEBSOCKET test_connect_stored",
name="WEBSOCKET test_connect_stored",
response_time=int((end_time - start_time) * 1000),
response_length=len(res),
exception=exception,
Expand Down Expand Up @@ -504,7 +506,6 @@ def test_connect_forever(self):
# NOTE: Not sure why we're specifying a Topic here, but sure...?
self.headers.update({"Topic": "zyxw"})
while True:

# NOTE: This feels odd.
# We send a notification to the client, but then immediately
# drop the websocket connection. There's a small chance
Expand Down Expand Up @@ -544,12 +545,12 @@ def test_connect_forever(self):
ws.close()
break

## Hold a notification
# Hold a notification
@task
def test_notification_forever_unsubscribed(self):
"""
Create an "active" connection, that we immediately turn "passive", then hold open for a period of time.
Create an "active" connection, that we immediately turn "passive", then hold
open for a period of time.
"""

# A Channel ID is how the client User Agent differentiates between various
Expand All @@ -564,7 +565,6 @@ def test_notification_forever_unsubscribed(self):
ssl=False,
)
) as ws:

# Connections must say hello after connecting to the server, otherwise
# the connection is quickly dropped.
body = json.dumps(dict(messageType="hello", use_webpush=True))
Expand All @@ -582,7 +582,6 @@ def test_notification_forever_unsubscribed(self):
body = json.dumps(dict(messageType="unregister", channelID=channel_id))
ws.send(body)
while True:

# Send a Ping message with arbitrary text. This should result
# in a Broadcast message response.
ws.ping("hello")
Expand Down
2 changes: 1 addition & 1 deletion tests/load/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ flake8 = "^6.0.0"
mypy = "^1.2.0"

[build-system]
requires = ["poetry-core"]
requires = ["poetry-core>=1.5.1"]
build-backend = "poetry.core.masonry.api"

0 comments on commit 615b93a

Please sign in to comment.