Skip to content

Commit

Permalink
add locust for testing (#251)
Browse files Browse the repository at this point in the history
* add locust for testing

* add tahmo scenario test

* update the user scenario tests

* fix lint

* remove unused docker-compose
  • Loading branch information
danangmassandy authored Nov 8, 2024
1 parent 8c88362 commit 23c946e
Show file tree
Hide file tree
Showing 14 changed files with 719 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ docs/mkdocs.yml

# ignore .env in root project for vscode
.env

# locust auth files
locust_auth.json
86 changes: 86 additions & 0 deletions locust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Tomorrow Now GAP Load Testing Using Locust

## Description

Load test using Locust.

- Python based class
- Easy to generate scenario test using python
- Nice UI and charts that updates in real time


## Authentication Config

Create a json file under locust directory called `locust_auth.json`.
Below is the sample:

```
[
{
"username": "YOUR_USERNAME",
"password": "YOUR_PASSWORD",
"wait_time_start": null,
"wait_time_end": null
}
]
```

We can configure `wait_time_start` and `wait_time_end` for each user. If it is null, then the wait_time by default is a constant 1 second.


## Usage: Virtual env

1. Create virtual environment
```
mkvirtualenv tn_locust
```

Or activate existing virtual environment
```
workon tn_locust
```

2. Install locust
```
pip3 install locust
```

3. Run locust master
```
locust -f weather --class-picker
```

There are currently 4 task types:
- `rand_var`: Random attributes length
- `rand_out`: Random output_type
- `rand_date`: Random date range
- `rand_all`: Random all

These types are represented as task tag, so we can filter out the task that we only want to run by using parameter in the command line.

For example, we want to run task with random attributes length:
```
locust -f weather --class-picker --tags rand_var
```

The tags can also be configured in web ui for each UserClass.
Web UI is available on http://localhost:8089/


## Usage: Docker Compose

TODO: docker compose for running locust


## Using Locust Web UI

TODO: add screenshots.

To start a new test:
1. Pick one or more the User class
2. (Optional) Configure tags in User class
3. Set number of users
4. Set ramp up
5. Set the host
6. (Advanced Options) Set maximum run time
7. Click Start
Empty file added locust/common/__init__.py
Empty file.
109 changes: 109 additions & 0 deletions locust/common/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: API Class for Locust Load Testing
"""


class ApiTaskTag:
"""Represent the tag for a task."""

RANDOM_VAR = 'rand_var'
RANDOM_OUTPUT = 'rand_out'
RANDOM_DATE = 'rand_date'
RANDOM_ALL = 'rand_all'


class ApiWeatherGroupMode:
"""Represents how to group the API requests."""

BY_PRODUCT_TYPE = 1
BY_OUTPUT_TYPE = 2
BY_ATTRIBUTE_LENGTH = 3
BY_DATE_COUNT = 4
BY_QUERY_TYPE = 5

@staticmethod
def as_list():
"""Return the enum as list."""
return [
ApiWeatherGroupMode.BY_PRODUCT_TYPE,
ApiWeatherGroupMode.BY_OUTPUT_TYPE,
ApiWeatherGroupMode.BY_ATTRIBUTE_LENGTH,
ApiWeatherGroupMode.BY_DATE_COUNT,
ApiWeatherGroupMode.BY_QUERY_TYPE,
]


class Api:
"""Provides api call to TNGAP."""

def __init__(self, client, user):
"""Initialize the class."""
self.client = client
self.user = user

def get_weather_request_name(
self, group_modes, product_type, output_type, attributes,
start_date, end_date, lat=None, lon=None, bbox=None,
location_name=None, default_name=None):
"""Return request name."""
names = []
for mode in ApiWeatherGroupMode.as_list():
if mode not in group_modes:
continue

name = ''
if mode == ApiWeatherGroupMode.BY_PRODUCT_TYPE:
name = product_type
elif mode == ApiWeatherGroupMode.BY_OUTPUT_TYPE:
name = output_type
elif mode == ApiWeatherGroupMode.BY_ATTRIBUTE_LENGTH:
name = f'ATTR{len(attributes)}'
elif mode == ApiWeatherGroupMode.BY_DATE_COUNT:
name = f'DT{(end_date - start_date).days}'
elif mode == ApiWeatherGroupMode.BY_QUERY_TYPE:
name = 'point'
if bbox is not None:
name = 'bbox'
elif location_name is not None:
name = 'loc'

if name:
names.append(name)

return default_name if len(names) == 0 else '_'.join(names)

def weather(
self, product_type, output_type, attributes, start_date, end_date,
lat=None, lon=None, bbox=None, location_name=None, group_modes=None
):
"""Call weather API."""
if group_modes is None:
group_modes = [
ApiWeatherGroupMode.BY_PRODUCT_TYPE,
ApiWeatherGroupMode.BY_OUTPUT_TYPE
]
request_name = self.get_weather_request_name(
group_modes, product_type, output_type, attributes,
start_date, end_date, lat=lat, lon=lon, bbox=bbox,
location_name=location_name, default_name='weather'
)
attributes_str = ','.join(attributes)
url = (
f'/api/v1/measurement/?lat={lat}&lon={lon}&bbox={bbox}&' +
f'location_name={location_name}&attributes={attributes_str}&' +
f'start_date={start_date}&end_date={end_date}&' +
f'product={product_type}&output_type={output_type}'
)

headers = {
'Authorization': self.user['auth'],
'user-agent': (
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'
)
}

self.client.get(url, headers=headers, name=request_name)
44 changes: 44 additions & 0 deletions locust/common/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# coding=utf-8
"""
Tomorrow Now GAP.
.. note:: Auth for Locust Load Testing
"""

import json
import random
from base64 import b64encode
from locust import between, constant


def basic_auth(username, password):
"""Encode username and password as basic auth."""
token = b64encode(
f"{username}:{password}".encode('utf-8')).decode("ascii")
return f'Basic {token}'


class AuthConfig:
"""Auth users from config json file."""

DEFAULT_WAIT_TIME = 1 # 1 second

def __init__(self, file_path='/mnt/locust/locust_auth.json'):
"""Initialize the class."""
with open(file_path, 'r') as json_file:
self.users = json.load(json_file)

def get_user(self):
"""Get random user."""
user = random.choice(self.users)
wait_time = constant(self.DEFAULT_WAIT_TIME)
if user['wait_time_start'] and user['wait_time_end']:
wait_time = between(
user['wait_time_start'], user['wait_time_end'])
return {
'auth': basic_auth(user['username'], user['password']),
'wait_time': wait_time
}


auth_config = AuthConfig('locust_auth.json')
Loading

0 comments on commit 23c946e

Please sign in to comment.