Skip to content

Commit 5a0a74f

Browse files
🎨 Improve load test framework (yet again) (#6051)
1 parent ae25f91 commit 5a0a74f

File tree

18 files changed

+63
-140
lines changed

18 files changed

+63
-140
lines changed

tests/performance/Makefile

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,6 @@ test: ## runs osparc locust. Locust and test configuration are specified in ENV_
4949
fi
5050
docker compose --file docker-compose.yml up --scale worker=4 --exit-code-from=master
5151

52-
.PHONY: install-ci install-dev install
53-
54-
install-dev: install
55-
install-ci: install
56-
57-
install: _check_venv_active ## install python tools
58-
@uv pip install -r requirements.txt
59-
60-
6152
.PHONY: dashboards-up dashboards-down
6253

6354
dashboards-up: ## Create Grafana dashboard for inspecting locust results. See dashboard on localhost:3000
@@ -70,3 +61,17 @@ dashboards-up: ## Create Grafana dashboard for inspecting locust results. See da
7061

7162
dashboards-down:
7263
@locust-compose down
64+
65+
.PHONY: install-ci install-dev
66+
67+
install-dev:
68+
@uv pip install -r requirements/requirements-dev.txt
69+
70+
install-ci: install
71+
@uv pip install -r requirements/requirements-ci.txt
72+
73+
74+
.PHONY: config
75+
config:
76+
@$(call check_defined, input, please define inputs when calling $@ - e.g. ```make $@ input="--help"```)
77+
@python locust_settings.py $(input) | tee .env

tests/performance/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ In the [locust_files] folder are located the test files.
88

99
## Usage
1010

11-
1. Generate a `.env` file with setting for your test. After running `make install-dev` you can execute your test script in python and set settings as arguments. Once your settings are validated you pipe them to `.env`. E.g. if your testscript is `locust_files/platform_ping_test.py` you could run
11+
1. All settings are passed to the locust container as environment variables in `.env`. To generate locust env vars, run `make config` with appropriate `input`. To see what the possible settings are, run `make config input="--help"`. E.g. you could run
1212
```bash
13-
python locust_files/platform_ping_test.py --LOCUST_HOST=https://api.osparc-master.speag.com \
14-
--LOCUST_USERS=100 --LOCUST_RUN_TIME=0:10:00 --SC_USER_NAME=myname --SC_PASSWORD=mypassword > .env
13+
make config input="--LOCUST_HOST=https://api.osparc-master.speag.com
14+
--LOCUST_USERS=100 --LOCUST_RUN_TIME=0:10:00 --LOCUST_LOCUSTFILE=locust_files/platform_ping_test.py"
1515
```
16-
2. Run your test script using the Make `test` recipe, e.g.
16+
This will validate your settings and you should be good to go once you see a the settings printed in your terminal.
17+
2. Add settings related to your locust file. E.g. if your file expects to find an environment variable `MYENVVAR` you add it to `.env`:
18+
```bash
19+
echo "MYENVVAR=thisismyenvvar" >> .env
1720
```
21+
3. Once you have all settings setup you uun your test script using the Make `test` recipe:
22+
```bash
1823
make test
1924
```
2025

tests/performance/locust_files/catalog_services.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44

55
import logging
6-
from pathlib import Path
76
from time import time
87

98
import faker
@@ -56,13 +55,3 @@ def on_start(self):
5655

5756
def on_stop(self):
5857
print("Stopping", self.email)
59-
60-
61-
if __name__ == "__main__":
62-
from locust_settings import LocustSettings, dump_dotenv
63-
64-
dump_dotenv(
65-
LocustSettings(
66-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(Path(__file__).parent.parent)
67-
)
68-
)

tests/performance/locust_files/director_services.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44

55
import logging
6-
from pathlib import Path
76

87
from locust import task
98
from locust.contrib.fasthttp import FastHttpUser
@@ -30,13 +29,3 @@ def on_start(self): # pylint: disable=no-self-use
3029

3130
def on_stop(self): # pylint: disable=no-self-use
3231
print("Stopping")
33-
34-
35-
if __name__ == "__main__":
36-
from locust_settings import LocustSettings, dump_dotenv
37-
38-
dump_dotenv(
39-
LocustSettings(
40-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(Path(__file__).parent.parent)
41-
)
42-
)

tests/performance/locust_files/locustfile.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44

55
import logging
6-
from pathlib import Path
76
from uuid import UUID
87

98
import faker
@@ -75,16 +74,3 @@ def on_start(self):
7574
def on_stop(self):
7675
self.client.post("/v0/auth/logout")
7776
print("Stopping", self.email)
78-
79-
80-
if __name__ == "__main__":
81-
from locust_settings import LocustSettings, dump_dotenv
82-
83-
class LoadTestSettings(TemplateSettings, LocustSettings):
84-
pass
85-
86-
dump_dotenv(
87-
LoadTestSettings(
88-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(Path(__file__).parent.parent)
89-
)
90-
)

tests/performance/locust_files/metamodeling/workflow.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,3 @@ def upload_file(self, file: Path) -> UUID:
163163
file_uuid = response.json().get("id")
164164
assert file_uuid is not None
165165
return UUID(file_uuid)
166-
167-
168-
if __name__ == "__main__":
169-
from locust_settings import LocustSettings, dump_dotenv
170-
171-
class MetaModelingSettings(UserSettings, LocustSettings):
172-
pass
173-
174-
dump_dotenv(
175-
MetaModelingSettings(
176-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(
177-
Path(__file__).parent.parent.parent
178-
)
179-
)
180-
)

tests/performance/locust_files/platform_ping_test.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44

55
import logging
6-
from pathlib import Path
76

87
import locust_plugins
98
from locust import task
@@ -51,16 +50,3 @@ def on_start(self): # pylint: disable=no-self-use
5150

5251
def on_stop(self): # pylint: disable=no-self-use
5352
print("Stopping locust user")
54-
55-
56-
if __name__ == "__main__":
57-
from locust_settings import LocustSettings, dump_dotenv
58-
59-
class LoadTestSettings(LocustAuth, LocustSettings):
60-
pass
61-
62-
dump_dotenv(
63-
LoadTestSettings(
64-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(Path(__file__).parent.parent)
65-
)
66-
)

tests/performance/locust_files/user_basic_calls.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import logging
66
import time
7-
from pathlib import Path
87

98
import faker
109
from locust import task
@@ -92,13 +91,3 @@ def on_start(self):
9291

9392
def on_stop(self):
9493
self.logout(self.email)
95-
96-
97-
if __name__ == "__main__":
98-
from locust_settings import LocustSettings, dump_dotenv
99-
100-
dump_dotenv(
101-
LocustSettings(
102-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(Path(__file__).parent.parent)
103-
)
104-
)

tests/performance/locust_files/webserver_services.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44

55
import logging
6-
from pathlib import Path
76

87
import faker
98
from dotenv import load_dotenv
@@ -50,13 +49,3 @@ def on_start(self):
5049
def on_stop(self):
5150
self.client.post("/v0/auth/logout")
5251
print("Stopping", self.email)
53-
54-
55-
if __name__ == "__main__":
56-
from locust_settings import LocustSettings, dump_dotenv
57-
58-
dump_dotenv(
59-
LocustSettings(
60-
LOCUST_LOCUSTFILE=Path(__file__).relative_to(Path(__file__).parent.parent)
61-
)
62-
)

tests/performance/settings/locust_settings/_locust_settings.py renamed to tests/performance/locust_settings.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
# pylint: disable=no-self-use
33
# pylint: disable=no-name-in-module
44

5+
import json
56
from datetime import timedelta
67
from pathlib import Path
8+
from typing import Final
79

810
from parse import Result, parse
911
from pydantic import (
@@ -18,7 +20,10 @@
1820
)
1921
from pydantic_settings import BaseSettings, SettingsConfigDict
2022

21-
from ._dump_dotenv import dump_dotenv
23+
_TEST_DIR: Final[Path] = Path(__file__).parent.resolve()
24+
_LOCUST_FILES_DIR: Final[Path] = _TEST_DIR / "locust_files"
25+
assert _TEST_DIR.is_dir()
26+
assert _LOCUST_FILES_DIR.is_dir()
2227

2328

2429
class LocustSettings(BaseSettings):
@@ -30,12 +35,22 @@ class LocustSettings(BaseSettings):
3035
LOCUST_HOST: AnyHttpUrl = Field(
3136
default=..., examples=["https://api.osparc-master.speag.com"]
3237
)
33-
LOCUST_LOCUSTFILE: Path = Field(default=...)
38+
LOCUST_LOCUSTFILE: Path = Field(
39+
default=...,
40+
description="Test file. Path should be relative to `locust_files` dir",
41+
)
3442
LOCUST_PRINT_STATS: bool = Field(default=True)
3543
LOCUST_RUN_TIME: timedelta = Field(default=...)
3644
LOCUST_SPAWN_RATE: PositiveInt = Field(default=20)
37-
LOCUST_TIMESCALE: NonNegativeInt = Field(default=1, ge=0, le=1)
38-
LOCUST_USERS: PositiveInt = Field(default=...)
45+
LOCUST_TIMESCALE: NonNegativeInt = Field(
46+
default=1,
47+
ge=0,
48+
le=1,
49+
description="Send locust data to Timescale db for reading in Grafana dashboards",
50+
)
51+
LOCUST_USERS: PositiveInt = Field(
52+
default=..., description="Number of locust users you want to spawn"
53+
)
3954

4055
PGHOST: str = Field(default="postgres")
4156
PGPASSWORD: str = Field(default="password")
@@ -55,6 +70,16 @@ def validate_run_time(cls, v: str) -> str | timedelta:
5570
raise ValueError("Could not parse time")
5671
return timedelta(hours=hour, minutes=_min, seconds=sec)
5772

73+
@field_validator("LOCUST_LOCUSTFILE", mode="after")
74+
@classmethod
75+
def validate_locust_file(cls, v: Path) -> Path:
76+
v = v.resolve()
77+
if not v.is_file():
78+
raise ValueError(f"{v} must be an existing file")
79+
if not v.is_relative_to(_LOCUST_FILES_DIR):
80+
raise ValueError(f"{v} must be a test file relative to {_LOCUST_FILES_DIR}")
81+
return v.relative_to(_TEST_DIR)
82+
5883
@field_serializer("LOCUST_RUN_TIME")
5984
def serialize_run_time(self, td: timedelta, info: SerializationInfo) -> str:
6085
total_seconds = int(td.total_seconds())
@@ -70,4 +95,8 @@ def serialize_host(self, url: AnyHttpUrl, info: SerializationInfo) -> str:
7095

7196

7297
if __name__ == "__main__":
73-
dump_dotenv(LocustSettings())
98+
settings = LocustSettings()
99+
env_vars = [
100+
f"{key}={val}" for key, val in json.loads(settings.model_dump_json()).items()
101+
]
102+
print("\n".join(env_vars))

0 commit comments

Comments
 (0)