devdox-ai-locust 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of devdox-ai-locust might be problematic. Click here for more details.
- devdox_ai_locust/__init__.py +9 -0
- devdox_ai_locust/cli.py +452 -0
- devdox_ai_locust/config.py +24 -0
- devdox_ai_locust/hybrid_loctus_generator.py +904 -0
- devdox_ai_locust/locust_generator.py +732 -0
- devdox_ai_locust/py.typed +0 -0
- devdox_ai_locust/schemas/__init__.py +0 -0
- devdox_ai_locust/schemas/processing_result.py +24 -0
- devdox_ai_locust/templates/base_workflow.py.j2 +180 -0
- devdox_ai_locust/templates/config.py.j2 +173 -0
- devdox_ai_locust/templates/custom_flows.py.j2 +95 -0
- devdox_ai_locust/templates/endpoint_template.py.j2 +34 -0
- devdox_ai_locust/templates/env.example.j2 +3 -0
- devdox_ai_locust/templates/fallback_locust.py.j2 +25 -0
- devdox_ai_locust/templates/locust.py.j2 +70 -0
- devdox_ai_locust/templates/readme.md.j2 +46 -0
- devdox_ai_locust/templates/requirement.txt.j2 +31 -0
- devdox_ai_locust/templates/test_data.py.j2 +276 -0
- devdox_ai_locust/templates/utils.py.j2 +335 -0
- devdox_ai_locust/utils/__init__.py +0 -0
- devdox_ai_locust/utils/file_creation.py +120 -0
- devdox_ai_locust/utils/open_ai_parser.py +431 -0
- devdox_ai_locust/utils/swagger_utils.py +94 -0
- devdox_ai_locust-0.1.1.dist-info/METADATA +424 -0
- devdox_ai_locust-0.1.1.dist-info/RECORD +29 -0
- devdox_ai_locust-0.1.1.dist-info/WHEEL +5 -0
- devdox_ai_locust-0.1.1.dist-info/entry_points.txt +3 -0
- devdox_ai_locust-0.1.1.dist-info/licenses/LICENSE +201 -0
- devdox_ai_locust-0.1.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pydantic import BaseModel, field_validator, ValidationInfo
|
|
2
|
+
from typing import Optional, Type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SwaggerProcessingRequest(BaseModel):
|
|
6
|
+
swagger_url: Optional[str] = None
|
|
7
|
+
|
|
8
|
+
@field_validator("swagger_url", mode="before")
|
|
9
|
+
@classmethod
|
|
10
|
+
def coerce_to_string(
|
|
11
|
+
cls: Type["SwaggerProcessingRequest"], v: Optional[str]
|
|
12
|
+
) -> Optional[str]:
|
|
13
|
+
if v is None:
|
|
14
|
+
return v
|
|
15
|
+
return str(v)
|
|
16
|
+
|
|
17
|
+
@field_validator("swagger_url")
|
|
18
|
+
@classmethod
|
|
19
|
+
def validate_url_when_source_is_url(
|
|
20
|
+
cls: Type["SwaggerProcessingRequest"], v: Optional[str], info: ValidationInfo
|
|
21
|
+
) -> Optional[str]:
|
|
22
|
+
if info.data.get("swagger_source") == "url" and not v:
|
|
23
|
+
raise ValueError("swagger_url is required when source is url")
|
|
24
|
+
return v
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Locust performance tests for {{api_info.title}}
|
|
3
|
+
|
|
4
|
+
API Information:
|
|
5
|
+
- Title: {{api_info.title}}
|
|
6
|
+
- Version: {{api_info.version}}
|
|
7
|
+
- Base URL: {{api_info.base_url}}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from locust import HttpUser, task, between, events, SequentialTaskSet
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
from urllib.parse import urljoin
|
|
15
|
+
|
|
16
|
+
from test_data import TestDataGenerator
|
|
17
|
+
from utils import ResponseValidator, RequestLogger, PerformanceMonitor
|
|
18
|
+
from config import LoadTestConfig
|
|
19
|
+
|
|
20
|
+
# Configure logging
|
|
21
|
+
logging.basicConfig(level=logging.INFO)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Initialize components
|
|
25
|
+
config = LoadTestConfig()
|
|
26
|
+
data_generator = TestDataGenerator()
|
|
27
|
+
response_validator = ResponseValidator()
|
|
28
|
+
performance_monitor = PerformanceMonitor()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseTaskMethods:
|
|
32
|
+
"""Mixin class with common task functionality - no inheritance conflicts"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *args, **kwargs):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
self._initialize_attributes()
|
|
37
|
+
|
|
38
|
+
def _initialize_attributes(self):
|
|
39
|
+
"""Initialize all required attributes"""
|
|
40
|
+
if not hasattr(self, 'auth_token'):
|
|
41
|
+
self.auth_token = None
|
|
42
|
+
if not hasattr(self, 'user_data'):
|
|
43
|
+
self.user_data = {}
|
|
44
|
+
if not hasattr(self, 'request_count'):
|
|
45
|
+
self.request_count = 0
|
|
46
|
+
if not hasattr(self, 'default_headers'):
|
|
47
|
+
self._setup_authentication()
|
|
48
|
+
self._setup_headers()
|
|
49
|
+
|
|
50
|
+
def on_start(self):
|
|
51
|
+
"""Initialize user session"""
|
|
52
|
+
self._initialize_attributes()
|
|
53
|
+
logger.info(f"TaskSet {self.__class__.__name__} started")
|
|
54
|
+
|
|
55
|
+
def on_stop(self):
|
|
56
|
+
"""Cleanup when user stops"""
|
|
57
|
+
if hasattr(self, 'request_count'):
|
|
58
|
+
logger.info(f"TaskSet {self.__class__.__name__} stopped after {self.request_count} requests")
|
|
59
|
+
else:
|
|
60
|
+
logger.info(f"TaskSet {self.__class__.__name__} stopped after {self.request_count} requests")
|
|
61
|
+
|
|
62
|
+
def _setup_authentication(self):
|
|
63
|
+
"""Setup authentication (override in subclasses if needed)"""
|
|
64
|
+
if not hasattr(self, 'auth_token'):
|
|
65
|
+
self.auth_token = None
|
|
66
|
+
if config.api_key:
|
|
67
|
+
self.auth_token = config.api_key
|
|
68
|
+
|
|
69
|
+
def _setup_headers(self):
|
|
70
|
+
"""Setup default headers for all requests"""
|
|
71
|
+
if not hasattr(self, 'auth_token'):
|
|
72
|
+
self._setup_authentication()
|
|
73
|
+
self.default_headers = {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"Accept": "application/json",
|
|
76
|
+
"User-Agent": "Locust-LoadTest/1.0"
|
|
77
|
+
}
|
|
78
|
+
if self.auth_token:
|
|
79
|
+
self.default_headers["Authorization"] = f"Bearer {self.auth_token}"
|
|
80
|
+
|
|
81
|
+
def make_request(self, method: str, path: str, **kwargs) -> Optional[Dict]:
|
|
82
|
+
"""Make HTTP request with logging, validation, and monitoring"""
|
|
83
|
+
self._initialize_attributes()
|
|
84
|
+
self.request_count += 1
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Merge headers
|
|
88
|
+
request_headers = {**self.default_headers, **kwargs.get("headers", {})}
|
|
89
|
+
|
|
90
|
+
# Set content-type for POST/PUT/PATCH
|
|
91
|
+
if method.upper() in ["POST", "PUT", "PATCH"]:
|
|
92
|
+
if "json" in kwargs:
|
|
93
|
+
request_headers["Content-Type"] = "application/json"
|
|
94
|
+
elif "data" in kwargs and isinstance(kwargs["data"], dict):
|
|
95
|
+
request_headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
96
|
+
|
|
97
|
+
kwargs["headers"] = request_headers
|
|
98
|
+
|
|
99
|
+
# Log request
|
|
100
|
+
RequestLogger.log_request(method, path, kwargs)
|
|
101
|
+
|
|
102
|
+
with self.client.request(
|
|
103
|
+
method=method,
|
|
104
|
+
url=urljoin(config.base_url, path),
|
|
105
|
+
catch_response=True,
|
|
106
|
+
**kwargs
|
|
107
|
+
) as response:
|
|
108
|
+
# Validate and monitor
|
|
109
|
+
is_valid = response_validator.validate_response(response, method, path)
|
|
110
|
+
performance_monitor.record_response(response, method, path)
|
|
111
|
+
|
|
112
|
+
if not is_valid:
|
|
113
|
+
response.failure(f"Response validation failed for {method} {path}")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
return response.json() if response.content else None
|
|
118
|
+
except json.JSONDecodeError:
|
|
119
|
+
if response.status_code < 400:
|
|
120
|
+
return {"raw_content": response.text}
|
|
121
|
+
response.failure(f"Invalid JSON response for {method} {path}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f"Request failed {method} {path}: {e}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def _store_response_data(self, method_name: str, data: Dict):
|
|
129
|
+
"""Store response data for future requests"""
|
|
130
|
+
if not hasattr(self, 'user_data'):
|
|
131
|
+
self.user_data = {}
|
|
132
|
+
if data:
|
|
133
|
+
self.user_data[method_name] = data
|
|
134
|
+
|
|
135
|
+
def _get_stored_data(self, method_name: str, key: str = None):
|
|
136
|
+
"""Retrieve stored response data from previous requests"""
|
|
137
|
+
stored_data = self.user_data.get(method_name)
|
|
138
|
+
if stored_data and key:
|
|
139
|
+
return stored_data.get(key)
|
|
140
|
+
return stored_data
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class BaseAPIUser(HttpUser):
|
|
144
|
+
"""Base class for API users with common functionality"""
|
|
145
|
+
|
|
146
|
+
abstract = True
|
|
147
|
+
wait_time = between(0.5, 1.5)
|
|
148
|
+
|
|
149
|
+
def on_start(self):
|
|
150
|
+
"""Initialize user session"""
|
|
151
|
+
self.auth_token = None
|
|
152
|
+
self.user_data = {}
|
|
153
|
+
self.request_count = 0
|
|
154
|
+
|
|
155
|
+
# Setup authentication if needed
|
|
156
|
+
self._setup_authentication()
|
|
157
|
+
|
|
158
|
+
# Initialize session headers
|
|
159
|
+
self._setup_headers()
|
|
160
|
+
|
|
161
|
+
logger.info(f"User {self.__class__.__name__} started")
|
|
162
|
+
|
|
163
|
+
def on_stop(self):
|
|
164
|
+
"""Cleanup when user stops"""
|
|
165
|
+
logger.info(f"User {self.__class__.__name__} stopped after {self.request_count} requests")
|
|
166
|
+
|
|
167
|
+
def _setup_authentication(self):
|
|
168
|
+
"""Setup authentication (override in subclasses if needed)"""
|
|
169
|
+
if config.api_key:
|
|
170
|
+
self.auth_token = config.api_key
|
|
171
|
+
|
|
172
|
+
def _setup_headers(self):
|
|
173
|
+
"""Setup default headers for all requests"""
|
|
174
|
+
self.default_headers = {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
"Accept": "application/json",
|
|
177
|
+
"User-Agent": "Locust-LoadTest/1.0"
|
|
178
|
+
}
|
|
179
|
+
if self.auth_token:
|
|
180
|
+
self.default_headers["Authorization"] = f"Bearer {self.auth_token}"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for Locust Load Tests
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Dict, List, Optional, Any
|
|
8
|
+
from urllib.parse import urljoin
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PerformanceThresholds:
|
|
16
|
+
"""Performance thresholds for test validation"""
|
|
17
|
+
max_response_time_ms: int = 2000 # Maximum acceptable response time
|
|
18
|
+
max_95th_percentile_ms: int = 5000 # 95th percentile response time
|
|
19
|
+
max_error_rate_percent: float = 1.0 # Maximum error rate percentage
|
|
20
|
+
min_requests_per_second: float = 10.0 # Minimum RPS threshold
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LoadTestScenario:
|
|
25
|
+
"""Load test scenario configuration"""
|
|
26
|
+
name: str
|
|
27
|
+
users: int
|
|
28
|
+
spawn_rate: int
|
|
29
|
+
run_time: str
|
|
30
|
+
description: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LoadTestConfig:
|
|
34
|
+
"""Main configuration class for load tests"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
# API Configuration
|
|
38
|
+
self.base_url = os.getenv('API_BASE_URL', '{{api_info.base_url}}')
|
|
39
|
+
self.api_version = os.getenv('API_VERSION', '{{api_info.version}}')
|
|
40
|
+
self.api_title = '{{api_info.title}}'
|
|
41
|
+
|
|
42
|
+
# Authentication
|
|
43
|
+
self.api_key = os.getenv('API_KEY')
|
|
44
|
+
self.auth_token = os.getenv('AUTH_TOKEN')
|
|
45
|
+
self.username = os.getenv('API_USERNAME')
|
|
46
|
+
self.password = os.getenv('API_PASSWORD')
|
|
47
|
+
|
|
48
|
+
# Load Test Parameters
|
|
49
|
+
self.users = int(os.getenv('LOCUST_USERS', '10'))
|
|
50
|
+
self.spawn_rate = int(os.getenv('LOCUST_SPAWN_RATE', '2'))
|
|
51
|
+
self.run_time = os.getenv('LOCUST_RUN_TIME', '5m')
|
|
52
|
+
self.host = os.getenv('LOCUST_HOST', self.base_url)
|
|
53
|
+
|
|
54
|
+
# Test Data Configuration
|
|
55
|
+
self.use_realistic_data = os.getenv('USE_REALISTIC_DATA', 'true').lower() == 'true'
|
|
56
|
+
self.data_seed = int(os.getenv('DATA_SEED', '42'))
|
|
57
|
+
|
|
58
|
+
# Monitoring and Reporting
|
|
59
|
+
self.enable_monitoring = os.getenv('ENABLE_MONITORING', 'true').lower() == 'true'
|
|
60
|
+
self.report_output_dir = os.getenv('REPORT_OUTPUT_DIR', './reports')
|
|
61
|
+
self.log_level = os.getenv('LOG_LEVEL', 'INFO')
|
|
62
|
+
|
|
63
|
+
# Performance Thresholds
|
|
64
|
+
self.thresholds = PerformanceThresholds(
|
|
65
|
+
max_response_time_ms=int(os.getenv('MAX_RESPONSE_TIME_MS', '2000')),
|
|
66
|
+
max_95th_percentile_ms=int(os.getenv('MAX_95TH_PERCENTILE_MS', '5000')),
|
|
67
|
+
max_error_rate_percent=float(os.getenv('MAX_ERROR_RATE_PERCENT', '1.0')),
|
|
68
|
+
min_requests_per_second=float(os.getenv('MIN_REQUESTS_PER_SECOND', '10.0'))
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Test Scenarios
|
|
72
|
+
self.scenarios = self._load_test_scenarios()
|
|
73
|
+
|
|
74
|
+
# Request Configuration
|
|
75
|
+
self.request_timeout = int(os.getenv('REQUEST_TIMEOUT', '30'))
|
|
76
|
+
self.max_retries = int(os.getenv('MAX_RETRIES', '3'))
|
|
77
|
+
|
|
78
|
+
# Load Balancing and Distribution
|
|
79
|
+
self.enable_distributed = os.getenv('ENABLE_DISTRIBUTED', 'false').lower() == 'true'
|
|
80
|
+
self.master_host = os.getenv('LOCUST_MASTER_HOST', 'localhost')
|
|
81
|
+
self.master_port = int(os.getenv('LOCUST_MASTER_PORT', '5557'))
|
|
82
|
+
|
|
83
|
+
# Feature Flags
|
|
84
|
+
self.enable_custom_flows = os.getenv('ENABLE_CUSTOM_FLOWS', 'true').lower() == 'true'
|
|
85
|
+
self.enable_response_validation = os.getenv('ENABLE_RESPONSE_VALIDATION', 'true').lower() == 'true'
|
|
86
|
+
self.enable_performance_monitoring = os.getenv('ENABLE_PERF_MONITORING', 'true').lower() == 'true'
|
|
87
|
+
|
|
88
|
+
def _load_test_scenarios(self) -> Dict[str, LoadTestScenario]:
|
|
89
|
+
"""Load predefined test scenarios"""
|
|
90
|
+
return {
|
|
91
|
+
'smoke': LoadTestScenario(
|
|
92
|
+
name='Smoke Test',
|
|
93
|
+
users=5,
|
|
94
|
+
spawn_rate=1,
|
|
95
|
+
run_time='2m',
|
|
96
|
+
description='Quick smoke test to verify basic functionality'
|
|
97
|
+
),
|
|
98
|
+
'load': LoadTestScenario(
|
|
99
|
+
name='Load Test',
|
|
100
|
+
users=50,
|
|
101
|
+
spawn_rate=5,
|
|
102
|
+
run_time='10m',
|
|
103
|
+
description='Standard load test with moderate user count'
|
|
104
|
+
),
|
|
105
|
+
'stress': LoadTestScenario(
|
|
106
|
+
name='Stress Test',
|
|
107
|
+
users=200,
|
|
108
|
+
spawn_rate=10,
|
|
109
|
+
run_time='15m',
|
|
110
|
+
description='Stress test to find breaking points'
|
|
111
|
+
),
|
|
112
|
+
'spike': LoadTestScenario(
|
|
113
|
+
name='Spike Test',
|
|
114
|
+
users=500,
|
|
115
|
+
spawn_rate=50,
|
|
116
|
+
run_time='5m',
|
|
117
|
+
description='Spike test with rapid user ramp-up'
|
|
118
|
+
),
|
|
119
|
+
'soak': LoadTestScenario(
|
|
120
|
+
name='Soak Test',
|
|
121
|
+
users=100,
|
|
122
|
+
spawn_rate=5,
|
|
123
|
+
run_time='60m',
|
|
124
|
+
description='Long-running test to identify memory leaks'
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def get_scenario(self, scenario_name: str) -> Optional[LoadTestScenario]:
|
|
129
|
+
"""Get specific test scenario"""
|
|
130
|
+
return self.scenarios.get(scenario_name)
|
|
131
|
+
|
|
132
|
+
def get_headers(self) -> Dict[str, str]:
|
|
133
|
+
"""Get default headers for requests"""
|
|
134
|
+
headers = {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'Accept': 'application/json',
|
|
137
|
+
'User-Agent': 'LoadTest/1.0'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if self.api_key:
|
|
141
|
+
headers['X-API-Key'] = self.api_key
|
|
142
|
+
|
|
143
|
+
if self.auth_token:
|
|
144
|
+
headers['Authorization'] = f'Bearer {self.auth_token}'
|
|
145
|
+
|
|
146
|
+
return headers
|
|
147
|
+
|
|
148
|
+
def get_full_url(self, path: str) -> str:
|
|
149
|
+
"""Construct full URL from path"""
|
|
150
|
+
return urljoin(self.base_url, path.lstrip('/'))
|
|
151
|
+
|
|
152
|
+
def validate_config(self) -> List[str]:
|
|
153
|
+
"""Validate configuration and return any errors"""
|
|
154
|
+
errors = []
|
|
155
|
+
|
|
156
|
+
if not self.base_url:
|
|
157
|
+
errors.append("Base URL is required")
|
|
158
|
+
|
|
159
|
+
if self.users <= 0:
|
|
160
|
+
errors.append("User count must be positive")
|
|
161
|
+
|
|
162
|
+
if self.spawn_rate <= 0:
|
|
163
|
+
errors.append("Spawn rate must be positive")
|
|
164
|
+
|
|
165
|
+
if self.spawn_rate > self.users:
|
|
166
|
+
errors.append("Spawn rate cannot exceed user count")
|
|
167
|
+
|
|
168
|
+
return errors
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Global configuration instance
|
|
172
|
+
config = LoadTestConfig()
|
|
173
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict, List, Any, Optional
|
|
4
|
+
from locust import HttpUser, task, between, SequentialTaskSet
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from test_data import TestDataGenerator
|
|
8
|
+
from utils import ResponseValidator, data_manager
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIWorkflowUser(HttpUser):
|
|
14
|
+
"""User that executes complex workflows"""
|
|
15
|
+
|
|
16
|
+
wait_time = between(2, 8)
|
|
17
|
+
weight = 2
|
|
18
|
+
|
|
19
|
+
tasks = []
|
|
20
|
+
|
|
21
|
+
def on_start(self):
|
|
22
|
+
self.workflow_data = {}
|
|
23
|
+
logger.info("Workflow user started")
|
|
24
|
+
|
|
25
|
+
def on_stop(self):
|
|
26
|
+
logger.info("Workflow user stopped")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DataDependentFlow(SequentialTaskSet):
|
|
30
|
+
"""Flow that demonstrates data dependencies between requests"""
|
|
31
|
+
|
|
32
|
+
@task
|
|
33
|
+
def create_resource(self):
|
|
34
|
+
resource_data = {
|
|
35
|
+
'name': f"resource_{random.randint(1000, 9999)}",
|
|
36
|
+
'type': random.choice(['document', 'image', 'video']),
|
|
37
|
+
'metadata': {
|
|
38
|
+
'created_by': 'load_test',
|
|
39
|
+
'test_run': True
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
response_data = self.user.make_request(
|
|
44
|
+
method="post",
|
|
45
|
+
url="/resources",
|
|
46
|
+
json=resource_data
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if response_data and 'id' in response_data:
|
|
50
|
+
data_manager.store_shared_data('last_resource_id', response_data['id'])
|
|
51
|
+
self.user.user_data['resource_id'] = response_data['id']
|
|
52
|
+
|
|
53
|
+
@task
|
|
54
|
+
def update_resource(self):
|
|
55
|
+
resource_id = self.user.user_data.get('resource_id') or data_manager.get_shared_data('last_resource_id')
|
|
56
|
+
if resource_id:
|
|
57
|
+
update_data = {
|
|
58
|
+
'name': f"updated_resource_{random.randint(1000, 9999)}",
|
|
59
|
+
'status': 'active'
|
|
60
|
+
}
|
|
61
|
+
self.user.make_request(
|
|
62
|
+
method="put",
|
|
63
|
+
url=f"/resources/{resource_id}",
|
|
64
|
+
json=update_data
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@task
|
|
68
|
+
def get_resource(self):
|
|
69
|
+
resource_id = self.user.user_data.get('resource_id')
|
|
70
|
+
if resource_id:
|
|
71
|
+
self.user.make_request(
|
|
72
|
+
method="get",
|
|
73
|
+
url=f"/resources/{resource_id}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@task
|
|
77
|
+
def delete_resource(self):
|
|
78
|
+
resource_id = self.user.user_data.get('resource_id')
|
|
79
|
+
if resource_id:
|
|
80
|
+
self.user.make_request(
|
|
81
|
+
method="delete",
|
|
82
|
+
url=f"/resources/{resource_id}"
|
|
83
|
+
)
|
|
84
|
+
self.user.user_data.pop('resource_id', None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ComplexFlowUser(HttpUser):
|
|
88
|
+
"""User that executes complex, realistic flows"""
|
|
89
|
+
|
|
90
|
+
wait_time = between(3, 10)
|
|
91
|
+
weight = 1
|
|
92
|
+
|
|
93
|
+
tasks = [
|
|
94
|
+
DataDependentFlow
|
|
95
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Locust performance tests for {{group}}
|
|
3
|
+
Generated on: {{ generated_at }}
|
|
4
|
+
|
|
5
|
+
API Information:
|
|
6
|
+
- Title: {{ api_info.title }}
|
|
7
|
+
- Version: {{ api_info.version }}
|
|
8
|
+
- Base URL: {{ api_info.base_url }}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from locust import HttpUser, task, between, events, SequentialTaskSet
|
|
12
|
+
from locust.runners import MasterRunner
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
from test_data import TestDataGenerator
|
|
16
|
+
from utils import ResponseValidator, RequestLogger, PerformanceMonitor
|
|
17
|
+
from workflows.base_workflow import BaseAPIUser, BaseTaskMethods
|
|
18
|
+
from config import LoadTestConfig
|
|
19
|
+
|
|
20
|
+
# Configure logging
|
|
21
|
+
logging.basicConfig(level=logging.INFO)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Initialize components
|
|
25
|
+
config = LoadTestConfig()
|
|
26
|
+
data_generator = TestDataGenerator()
|
|
27
|
+
response_validator = ResponseValidator()
|
|
28
|
+
performance_monitor = PerformanceMonitor()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class {{group}}TaskMethods(SequentialTaskSet, BaseTaskMethods):
|
|
32
|
+
"""Mixin class containing all API task methods"""
|
|
33
|
+
|
|
34
|
+
{{task_methods_content}}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fallback Locust test file for {api_info.get('title', 'API')}
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from locust import HttpUser, task, between
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BasicAPIUser(HttpUser):
|
|
12
|
+
wait_time = between(1, 5)
|
|
13
|
+
|
|
14
|
+
@task(1)
|
|
15
|
+
def health_check(self):
|
|
16
|
+
"""Basic health check"""
|
|
17
|
+
with self.client.get("/health", catch_response=True) as response:
|
|
18
|
+
if response.status_code == 200:
|
|
19
|
+
response.success()
|
|
20
|
+
else:
|
|
21
|
+
response.failure(f"Health check failed: {{response.status_code}}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
print("Running fallback Locust test for {{api_info.title}}")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Locust performance tests for {{ api_info.get('title', 'API') }}
|
|
3
|
+
Generated on: {{ generated_at }}
|
|
4
|
+
|
|
5
|
+
API Information:
|
|
6
|
+
- Title: {{ api_info.get('title', 'Unknown') }}
|
|
7
|
+
- Version: {{ api_info.get('version', 'Unknown') }}
|
|
8
|
+
- Base URL: {{ api_info.get('base_url', 'http://localhost') }}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from locust import HttpUser, task, between, events
|
|
12
|
+
from locust.runners import MasterRunner
|
|
13
|
+
import json
|
|
14
|
+
import random
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Dict, Any, Optional
|
|
17
|
+
from urllib.parse import urljoin
|
|
18
|
+
from workflows.base_workflow import BaseAPIUser
|
|
19
|
+
{{ import_group_tasks }}
|
|
20
|
+
|
|
21
|
+
from test_data import TestDataGenerator
|
|
22
|
+
from utils import ResponseValidator, RequestLogger, PerformanceMonitor
|
|
23
|
+
from config import LoadTestConfig
|
|
24
|
+
|
|
25
|
+
# Configure logging
|
|
26
|
+
logging.basicConfig(level=logging.INFO)
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Initialize components
|
|
30
|
+
config = LoadTestConfig()
|
|
31
|
+
data_generator = TestDataGenerator()
|
|
32
|
+
response_validator = ResponseValidator()
|
|
33
|
+
performance_monitor = PerformanceMonitor()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class APITaskMethods:
|
|
37
|
+
"""Mixin class containing all API task methods"""
|
|
38
|
+
|
|
39
|
+
{{ task_methods_content }}
|
|
40
|
+
|
|
41
|
+
tasks = {{ tasks_str }}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
{{ generated_task_classes }}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Event handlers
|
|
48
|
+
@events.request.add_listener
|
|
49
|
+
def on_request(request_type, name, response_time, response_length, exception, context, **kwargs):
|
|
50
|
+
performance_monitor.on_request_event(
|
|
51
|
+
request_type, name, response_time, response_length, exception, context
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@events.test_start.add_listener
|
|
56
|
+
def on_test_start(environment, **kwargs):
|
|
57
|
+
logger.info("Load test starting...")
|
|
58
|
+
performance_monitor.test_start()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@events.test_stop.add_listener
|
|
62
|
+
def on_test_stop(environment, **kwargs):
|
|
63
|
+
logger.info("Load test stopping...")
|
|
64
|
+
performance_monitor.test_stop()
|
|
65
|
+
performance_monitor.generate_report()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
from locust.main import main
|
|
70
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 🚀 Load Testing Suite for {{ api_info.title }}
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](https://locust.io/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Professional-grade performance testing suite for **{{ api_info.title }} {{ api_info.version }}** built with Locust.
|
|
8
|
+
|
|
9
|
+
{% if api_info.description -%}
|
|
10
|
+
{{ api_info.description }}
|
|
11
|
+
{% endif %}
|
|
12
|
+
|
|
13
|
+
## 📋 Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Quick Start](#-quick-start)
|
|
16
|
+
- [Test Scenarios](#-test-scenarios)
|
|
17
|
+
- [Configuration](#-configuration)
|
|
18
|
+
- [Running Tests](#-running-tests)
|
|
19
|
+
- [Monitoring & Reports](#-monitoring--reports)
|
|
20
|
+
- [Advanced Features](#-advanced-features)
|
|
21
|
+
- [Troubleshooting](#-troubleshooting)
|
|
22
|
+
- [Best Practices](#-best-practices)
|
|
23
|
+
|
|
24
|
+
## 🚀 Quick Start
|
|
25
|
+
|
|
26
|
+
### Prerequisites
|
|
27
|
+
|
|
28
|
+
- **Python 3.8+** with pip
|
|
29
|
+
- **Network access** to the target API
|
|
30
|
+
- **Minimum 2GB RAM** for load testing
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
```bash
|
|
34
|
+
# 1. Clone or download the test suite
|
|
35
|
+
git clone <repository-url>
|
|
36
|
+
cd load-testing-suite
|
|
37
|
+
|
|
38
|
+
# 2. Install dependencies
|
|
39
|
+
pip install -r requirements.txt
|
|
40
|
+
|
|
41
|
+
# 3. Configure environment
|
|
42
|
+
cp .env.example .env
|
|
43
|
+
# Edit .env with your API configuration
|
|
44
|
+
|
|
45
|
+
# 4. Run your first test
|
|
46
|
+
locust -f locust.py
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Core Locust dependencies
|
|
2
|
+
locust>=2.15.0,<3.0.0
|
|
3
|
+
|
|
4
|
+
# HTTP and API testing
|
|
5
|
+
requests>=2.31.0
|
|
6
|
+
urllib3>=1.26.0
|
|
7
|
+
|
|
8
|
+
# Data generation and manipulation
|
|
9
|
+
python-dateutil>=2.8.0
|
|
10
|
+
|
|
11
|
+
# Data handling
|
|
12
|
+
pandas>=2.0.0
|
|
13
|
+
numpy>=1.24.0
|
|
14
|
+
|
|
15
|
+
# Configuration and environment
|
|
16
|
+
python-dotenv>=1.0.0
|
|
17
|
+
pydantic>=2.0.0
|
|
18
|
+
|
|
19
|
+
# Monitoring and reporting
|
|
20
|
+
psutil>=5.9.0
|
|
21
|
+
matplotlib>=3.7.0
|
|
22
|
+
seaborn>=0.12.0
|
|
23
|
+
|
|
24
|
+
# Logging and debugging
|
|
25
|
+
structlog>=23.0.0
|
|
26
|
+
colorama>=0.4.6
|
|
27
|
+
Faker==37.6.0
|
|
28
|
+
|
|
29
|
+
# Optional: Database connectivity
|
|
30
|
+
# psycopg2-binary>=2.9.0 # PostgreSQL
|
|
31
|
+
# pymongo>=4.3.0 # MongoDB
|