Skip to content

Commit

Permalink
Adds stress test
Browse files Browse the repository at this point in the history
  • Loading branch information
Cevin Neubauer committed Mar 25, 2025
1 parent a0b2073 commit 9d7e3ce
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 0 deletions.
1 change: 1 addition & 0 deletions stress_test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
75 changes: 75 additions & 0 deletions stress_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SecFit Application Stress Testing

This directory contains scripts for stress testing the SecFit application under load conditions. The tests are designed to simulate 10 users sending requests every 10 seconds for a duration of 2 minutes, adhering to the limit of not overloading the NTNU server.

## Prerequisites

- Python 3.x installed
- Locust installed (`pip install locust`)
- Running SecFit application instance (local or remote)

## Running the Tests

### Local Testing

1. Make sure your SecFit application is running (either locally or on a server).
2. Navigate to this directory (`stress_test/`).
3. Run the Locust server:

```bash
locust -f locustfile.py --host=http://localhost:8000
```

### Testing Against External Server

To test against an external server, provide the host URL as a command-line argument:

```bash
locust -f locustfile.py --host=https://your-external-server.example.com
```

Alternatively, you can set the host via an environment variable:

```bash
SECFIT_HOST=https://your-external-server.example.com locust -f locustfile.py
```

### Configuring the Test

1. Open a web browser and go to `http://localhost:8089/` to access the Locust web interface.
2. In the web interface, specify:
- Number of users: 10
- Spawn rate: 10 (users per second)
- Host: (should be pre-filled from the command line)
3. Click "Start swarming" to begin the test.

The test will automatically:
- Create test users if they don't already exist in the application
- Run the test for exactly 2 minutes
- Stop automatically after the specified duration

## Test Design Overview

The test script simulates realistic user behavior by:

1. Logging in to obtain authentication tokens
2. Browsing workouts and exercises (high-frequency tasks)
3. Viewing specific workout details
5. Adding comments to workouts

Tasks are weighted to simulate realistic user patterns, with browse operations occurring more frequently than write operations.

## Analyzing Results

Locust provides real-time statistics during the test run, including:
- Response times (min, max, average)
- Requests per second
- Failure rates
- Response time distribution

After the test completes, you can download a CSV report for further analysis.

## Notes

- Test users (user1, user2, etc.) will be created automatically if they don't exist in the application.
- Adjust the workout and user IDs in the script to match actual IDs in your database if needed.
116 changes: 116 additions & 0 deletions stress_test/locustfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import time
import random
import json
import logging
import os
import sys
from locust import HttpUser, task, between, tag, events
from user_setup import UserSetup
from user_setup import TEST_USERS
from workout_setup import WorkoutSetup

# Configure logging to be more verbose
# Set log level to DEBUG for detailed logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("secfit_stress_test")
logger.setLevel(logging.DEBUG)

# Enable detailed logging for requests and urllib3
logging.getLogger("requests").setLevel(logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.DEBUG)

# Enable detailed logging for Locust
logging.getLogger("locust").setLevel(logging.DEBUG)

# Quiet message for terminal
print("SecFit Stress Test: Starting...", file=sys.stderr)
print("Access the Locust web interface at http://localhost:8089", file=sys.stderr)
print("User setup in progress - please wait...", file=sys.stderr)

# Get host configuration from environment variable if available
DEFAULT_HOST = "http://localhost:8000"

class SecFitUser(HttpUser):
# Wait time between tasks - setting to 10 seconds per requirements
wait_time = between(9.5, 10.5) # Slight variation around 10s

# Store authorization token after login
token = None
username = None

def on_start(self):
"""User logs in when starting"""
# Randomly choose a username from our test users
user_data = random.choice(TEST_USERS)
self.username = user_data["username"]
self.password = user_data["password"]

# Login to get authentication token
response = self.client.post(
"/api/token/",
json={"username": self.username, "password": self.password},
name="/api/token/ (Login)"
)

if response.status_code == 200:
self.token = response.json()["access"]
logger.info(f"User {self.username} logged in successfully")
else:
logger.error(f"Login failed for {self.username}: {response.text}")

def _authenticated_request(self, method, endpoint, json_data=None, name=None):
"""Helper method to make authenticated requests"""
headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}

if method == "get":
return self.client.get(endpoint, headers=headers, name=name)
elif method == "post":
return self.client.post(endpoint, headers=headers, json=json_data, name=name)
elif method == "put":
return self.client.put(endpoint, headers=headers, json=json_data, name=name)
elif method == "delete":
return self.client.delete(endpoint, headers=headers, name=name)

@tag('browse')
@task(3)
def browse_workouts(self):
"""Browse workout listings - high frequency task"""
logger.info(f"{self.username} is browsing workouts")
self._authenticated_request("get", "/api/workouts/", name="/api/workouts/ (Browse)")

@tag('browse')
@task(2)
def view_workout_details(self):
"""View a specific workout's details"""
workout_id = random.randint(1, 10) # Assuming workout IDs 1-10 exist
logger.info(f"{self.username} is viewing workout details for workout {workout_id}")
self._authenticated_request("get", f"/api/workouts/{workout_id}/", name="/api/workouts/[id]/ (View Details)")

@tag('interaction')
@task(1)
def add_comment(self):
"""Add a comment to a workout"""
workout_id = random.randint(1, 10)
workout_url = f"/api/workouts/{workout_id}/"
comment_data = {
"content": f"Test comment from load testing {time.time()}",
"workout": workout_url
}
logger.info(f"{self.username} is adding a comment to workout {workout_id}")
self._authenticated_request("post", "/api/comments/", json_data=comment_data, name="/api/comments/ (Add Comment)")

# Register event handlers to control test duration and user setup
@events.init.add_listener
def on_locust_init(environment, **kwargs):
"""Initialize the test with a time limit and create test users"""
# Setup test users before starting the test
if environment.host:
host = environment.host
else:
host = os.environ.get("SECFIT_HOST", DEFAULT_HOST)
environment.host = host

# Create the test users
UserSetup.ensure_users_exist(host)
# Create the test workouts
WorkoutSetup.ensure_workouts_exist(host)
2 changes: 2 additions & 0 deletions stress_test/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
locust
83 changes: 83 additions & 0 deletions stress_test/user_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import requests
from requests.exceptions import RequestException
import random
import json
import logging
import sys

# Configure logging
logger = logging.getLogger("secfit_stress_test")

# Test user configuration with stronger passwords and additional required fields
TEST_USERS = [
{"username": "locust_user1", "password": "StrongP@ssw0rd123!A", "email": "locust_user1@test.com"},
{"username": "locust_user2", "password": "StrongP@ssw0rd123!B", "email": "locust_user2@test.com"},
{"username": "locust_user3", "password": "StrongP@ssw0rd123!C", "email": "locust_user3@test.com"},
{"username": "locust_user4", "password": "StrongP@ssw0rd123!D", "email": "locust_user4@test.com"},
{"username": "locust_user5", "password": "StrongP@ssw0rd123!E", "email": "locust_user5@test.com"}
]

class UserSetup:
"""Helper class to set up test users"""
@staticmethod
def ensure_users_exist(host):
"""Ensure all test users exist in the application"""

# Suppress excessive logging during setup
user_created_count = 0
user_existed_count = 0

for user_data in TEST_USERS:
username = user_data["username"]
password = user_data["password"]

# First try to authenticate to check if user exists
try:
login_response = requests.post(
f"{host}/api/token/",
json={"username": username, "password": password}
)

# Log the full request and response for debugging
logger.info(f"Login request for {username}: {json.dumps({'username': username, 'password': password})}")
logger.info(f"Login response for {username}: {login_response.status_code} - {login_response.text}")

# If login successful, user exists
if login_response.status_code == 200:
user_existed_count += 1
continue

# User doesn't exist, register them
# Add all required fields for registration based on the error messages
registration_data = {
"username": username,
"password": password,
"password1": password, # Additional password confirmation field
"email": user_data["email"],
"phone_number": f"+471234{random.randint(1000, 9999)}",
"isCoach": False, # Assuming all test users are athletes
"athletes": [], # Empty list as required by the API
"workouts": [], # Empty list as required by the API
"coach_files": [], # Empty list as required by the API
"athlete_files": [] # Empty list as required by the API
}

# Register the user
register_response = requests.post(
f"{host}/api/users/",
json=registration_data
)

# Log the full request and response for debugging
logger.info(f"Registration request for {username}: {json.dumps(registration_data)}")
logger.info(f"Registration response for {username}: {register_response.status_code} - {register_response.text}")

if register_response.status_code == 201:
user_created_count += 1

except RequestException as e:
# Log the exception for debugging
logger.error(f"Error during user setup for {username}: {str(e)}")
pass

print(f"User setup complete: {user_created_count} users created, {user_existed_count} users already exist", file=sys.stderr)
74 changes: 74 additions & 0 deletions stress_test/workout_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import requests
from requests.exceptions import RequestException
import time
import json
import logging
import sys
from user_setup import TEST_USERS

# Configure logging
logger = logging.getLogger("secfit_stress_test")

class WorkoutSetup:
"""Helper class to set up workouts"""
@staticmethod
def ensure_workouts_exist(host):
"""Ensure all test workouts exist in the application"""

# Suppress excessive logging during setup
workout_created_count = 0
workout_existed_count = 0

# Use a separate user for creating workouts
workout_creator = TEST_USERS[0]
username = workout_creator["username"]
password = workout_creator["password"]

try:
login_response = requests.post(
f"{host}/api/token/",
json={"username": username, "password": password}
)

# Log the full request and response for debugging
logger.info(f"Login request for {username}: {json.dumps({'username': username, 'password': password})}")
logger.info(f"Login response for {username}: {login_response.status_code} - {login_response.text}")

# If login successful, user exists
if login_response.status_code == 200:
token = login_response.json()["access"]

for workout_id in range(1, 11): # Assuming workout IDs 1-10
# Create a workout for the user
workout_data = {
"name": f"Test Workout {workout_id}",
"description": "This is a test workout.",
"visibility": "PU", # Public visibility
"owner": username,
"exercise_instances": [], # Empty list as required by the API
"date" : time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()), # Current date and time,
"notes": "Test notes for the workout.",
}

# Register the workout
create_workout_response = requests.post(
f"{host}/api/workouts/",
headers={"Authorization": f"Bearer {token}"},
json=workout_data
)

# Log the full request and response for debugging
logger.info(f"Create workout request for {username}: {json.dumps(workout_data)}")
logger.info(f"Create workout response for {username}: {create_workout_response.status_code} - {create_workout_response.text}")

if create_workout_response.status_code == 201:
workout_created_count += 1
else:
workout_existed_count += 1

except RequestException as e:
# Log the exception for debugging
logger.error(f"Error during workout setup for {username}: {str(e)}")
pass

print(f"Workout setup complete: {workout_created_count} workouts created, {workout_existed_count} workouts already exist", file=sys.stderr)

0 comments on commit 9d7e3ce

Please sign in to comment.