devdox-ai-locust 0.1.2__tar.gz → 0.1.7__tar.gz
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.
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/PKG-INFO +32 -5
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/README.md +31 -4
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/pyproject.toml +2 -2
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/cli.py +21 -5
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/config.py +1 -1
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/hybrid_loctus_generator.py +152 -43
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/locust_generator.py +85 -10
- devdox_ai_locust-0.1.7/src/devdox_ai_locust/prompt/test_data.j2 +186 -0
- devdox_ai_locust-0.1.7/src/devdox_ai_locust/prompt/workflow.j2 +483 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/endpoint_template.py.j2 +1 -1
- devdox_ai_locust-0.1.7/src/devdox_ai_locust/templates/mongo/data_provider.py.j2 +303 -0
- devdox_ai_locust-0.1.7/src/devdox_ai_locust/templates/mongo/db_config.py.j2 +271 -0
- devdox_ai_locust-0.1.7/src/devdox_ai_locust/templates/mongo/db_integration.j2 +352 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/readme.md.j2 +3 -1
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/requirement.txt.j2 +5 -2
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/test_data.py.j2 +5 -1
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/PKG-INFO +32 -5
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/SOURCES.txt +3 -1
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_cli.py +5 -2
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_config.py +4 -4
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_hybrid_loctus_generator.py +206 -12
- devdox_ai_locust-0.1.2/src/devdox_ai_locust/prompt/test_data.j2 +0 -62
- devdox_ai_locust-0.1.2/src/devdox_ai_locust/prompt/workflow.j2 +0 -145
- devdox_ai_locust-0.1.2/tests/test_data.py +0 -505
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/LICENSE +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/setup.cfg +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/__init__.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/prompt/domain.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/prompt/locust.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/prompt/validation.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/py.typed +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/schemas/__init__.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/schemas/processing_result.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/base_workflow.py.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/config.py.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/custom_flows.py.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/env.example.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/fallback_locust.py.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/locust.py.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/templates/utils.py.j2 +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/__init__.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/file_creation.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/open_ai_parser.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/utils/swagger_utils.py +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/dependency_links.txt +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/entry_points.txt +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/requires.txt +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust.egg-info/top_level.txt +0 -0
- {devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/tests/test_locust_generator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devdox_ai_locust
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: AI-powered Locust load test generator from API documentation
|
|
5
5
|
Author-email: Hayat Bourji <hayat.bourgi@montyholding.com>
|
|
6
6
|
Maintainer-email: Hayat Bourji <hayat.bourgi@montyholding.com>
|
|
@@ -67,11 +67,20 @@ Dynamic: license-file
|
|
|
67
67
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
68
68
|
[](https://www.python.org/downloads/)
|
|
69
69
|
[](https://github.com/psf/black)
|
|
70
|
+
[](https://sonarcloud.io/dashboard?id=montymobile1_devdox-ai-locust)
|
|
70
71
|
|
|
71
72
|
> **AI-powered Locust load test generator from API documentation**
|
|
72
73
|
|
|
73
74
|
DevDox AI Locust automatically generates comprehensive Locust load testing scripts from your API documentation (OpenAPI/Swagger specs). Using advanced AI capabilities, it creates realistic test scenarios, handles complex authentication flows, and generates production-ready performance tests.
|
|
74
75
|
|
|
76
|
+
## [0.1.7] - 2025-11-11
|
|
77
|
+
## 🆕 What's New in 0.1.7
|
|
78
|
+
|
|
79
|
+
**Improved Naming Convention:** Generated class names now automatically capitalize group names to ensure consistent and professional naming in generated Python code.
|
|
80
|
+
|
|
81
|
+
**MongoDB Environment Setup:** Added example MongoDB variables (MONGODB_URI, MONGODB_DB_NAME, etc.) in the .env.example file to make it easier to configure MongoDB connections.
|
|
82
|
+
|
|
83
|
+
|
|
75
84
|
## ✨ Features
|
|
76
85
|
|
|
77
86
|
- 🤖 **AI-Enhanced Generation**: Uses Together AI to create intelligent, realistic load test scenarios
|
|
@@ -82,6 +91,7 @@ DevDox AI Locust automatically generates comprehensive Locust load testing scrip
|
|
|
82
91
|
- 🛠️ **Template-Based**: Highly customizable Jinja2 templates for different testing needs
|
|
83
92
|
- 🔄 **Hybrid Approach**: Combines rule-based generation with AI enhancement
|
|
84
93
|
- 📈 **Comprehensive Coverage**: Handles various HTTP methods, content types, and response scenarios
|
|
94
|
+
- ⚡ **Asynchronous Processing**: Fast, non-blocking test generation with async/await
|
|
85
95
|
|
|
86
96
|
## 🚀 Quick Start
|
|
87
97
|
|
|
@@ -121,18 +131,35 @@ echo "API_KEY=your_together_ai_api_key_here" > .env
|
|
|
121
131
|
# Generate from OpenAPI URL
|
|
122
132
|
devdox_ai_locust generate --openapi-url https://api.example.com/openapi.json --output ./tests
|
|
123
133
|
|
|
124
|
-
# Generate from local file
|
|
125
|
-
dal generate --openapi-file ./api-spec.yaml --output ./load-tests
|
|
126
|
-
|
|
127
134
|
# Generate with custom configuration
|
|
128
135
|
devdox_ai_locust generate \
|
|
129
|
-
https://
|
|
136
|
+
https://petstore3.swagger.io/api/v3/openapi.json \
|
|
130
137
|
--output ./petstore-tests \
|
|
131
138
|
--together-api-key your_api_key \
|
|
132
139
|
|
|
140
|
+
# Generate with db integration
|
|
141
|
+
devdox_ai_locust generate \
|
|
142
|
+
https://petstore3.swagger.io/api/v3/openapi.json \
|
|
143
|
+
--output ./petstore-tests \
|
|
144
|
+
--db-type mongo \
|
|
133
145
|
```
|
|
134
146
|
|
|
135
147
|
|
|
148
|
+
## 🚀 Installation with Inputs
|
|
149
|
+
|
|
150
|
+
Add this step to your GitHub Actions workflow:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
- name: DevDox Locust Test Generator
|
|
154
|
+
uses: montymobile1/devdox-ai-locust@v0.1.6
|
|
155
|
+
with:
|
|
156
|
+
swagger_url: "https://portal-api.devdox.ai/openapi.json"
|
|
157
|
+
output: "generated_tests"
|
|
158
|
+
users: "15"
|
|
159
|
+
spawn_rate: "3"
|
|
160
|
+
run_time: "10m"
|
|
161
|
+
together_api_key: ${{ secrets.TOGETHER_API_KEY }}
|
|
162
|
+
|
|
136
163
|
|
|
137
164
|
## 📖 Documentation
|
|
138
165
|
|
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
5
|
[](https://github.com/psf/black)
|
|
6
|
+
[](https://sonarcloud.io/dashboard?id=montymobile1_devdox-ai-locust)
|
|
6
7
|
|
|
7
8
|
> **AI-powered Locust load test generator from API documentation**
|
|
8
9
|
|
|
9
10
|
DevDox AI Locust automatically generates comprehensive Locust load testing scripts from your API documentation (OpenAPI/Swagger specs). Using advanced AI capabilities, it creates realistic test scenarios, handles complex authentication flows, and generates production-ready performance tests.
|
|
10
11
|
|
|
12
|
+
## [0.1.7] - 2025-11-11
|
|
13
|
+
## 🆕 What's New in 0.1.7
|
|
14
|
+
|
|
15
|
+
**Improved Naming Convention:** Generated class names now automatically capitalize group names to ensure consistent and professional naming in generated Python code.
|
|
16
|
+
|
|
17
|
+
**MongoDB Environment Setup:** Added example MongoDB variables (MONGODB_URI, MONGODB_DB_NAME, etc.) in the .env.example file to make it easier to configure MongoDB connections.
|
|
18
|
+
|
|
19
|
+
|
|
11
20
|
## ✨ Features
|
|
12
21
|
|
|
13
22
|
- 🤖 **AI-Enhanced Generation**: Uses Together AI to create intelligent, realistic load test scenarios
|
|
@@ -18,6 +27,7 @@ DevDox AI Locust automatically generates comprehensive Locust load testing scrip
|
|
|
18
27
|
- 🛠️ **Template-Based**: Highly customizable Jinja2 templates for different testing needs
|
|
19
28
|
- 🔄 **Hybrid Approach**: Combines rule-based generation with AI enhancement
|
|
20
29
|
- 📈 **Comprehensive Coverage**: Handles various HTTP methods, content types, and response scenarios
|
|
30
|
+
- ⚡ **Asynchronous Processing**: Fast, non-blocking test generation with async/await
|
|
21
31
|
|
|
22
32
|
## 🚀 Quick Start
|
|
23
33
|
|
|
@@ -57,18 +67,35 @@ echo "API_KEY=your_together_ai_api_key_here" > .env
|
|
|
57
67
|
# Generate from OpenAPI URL
|
|
58
68
|
devdox_ai_locust generate --openapi-url https://api.example.com/openapi.json --output ./tests
|
|
59
69
|
|
|
60
|
-
# Generate from local file
|
|
61
|
-
dal generate --openapi-file ./api-spec.yaml --output ./load-tests
|
|
62
|
-
|
|
63
70
|
# Generate with custom configuration
|
|
64
71
|
devdox_ai_locust generate \
|
|
65
|
-
https://
|
|
72
|
+
https://petstore3.swagger.io/api/v3/openapi.json \
|
|
66
73
|
--output ./petstore-tests \
|
|
67
74
|
--together-api-key your_api_key \
|
|
68
75
|
|
|
76
|
+
# Generate with db integration
|
|
77
|
+
devdox_ai_locust generate \
|
|
78
|
+
https://petstore3.swagger.io/api/v3/openapi.json \
|
|
79
|
+
--output ./petstore-tests \
|
|
80
|
+
--db-type mongo \
|
|
69
81
|
```
|
|
70
82
|
|
|
71
83
|
|
|
84
|
+
## 🚀 Installation with Inputs
|
|
85
|
+
|
|
86
|
+
Add this step to your GitHub Actions workflow:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
- name: DevDox Locust Test Generator
|
|
90
|
+
uses: montymobile1/devdox-ai-locust@v0.1.6
|
|
91
|
+
with:
|
|
92
|
+
swagger_url: "https://portal-api.devdox.ai/openapi.json"
|
|
93
|
+
output: "generated_tests"
|
|
94
|
+
users: "15"
|
|
95
|
+
spawn_rate: "3"
|
|
96
|
+
run_time: "10m"
|
|
97
|
+
together_api_key: ${{ secrets.TOGETHER_API_KEY }}
|
|
98
|
+
|
|
72
99
|
|
|
73
100
|
## 📖 Documentation
|
|
74
101
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "devdox_ai_locust"
|
|
8
|
-
version = "0.1.
|
|
8
|
+
version = "0.1.7"
|
|
9
9
|
description = "AI-powered Locust load test generator from API documentation"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = {text = "Apache-2.0" }
|
|
@@ -101,7 +101,7 @@ exclude = ["tests*"]
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
[tool.setuptools.package-data]
|
|
104
|
-
devdox_ai_locust = ["schemas/*.json", "templates/*.j2","prompt/*.j2", "py.typed","*.j2"]
|
|
104
|
+
devdox_ai_locust = ["schemas/*.json", "templates/*.j2","templates/mongo/*","prompt/*.j2", "py.typed","*.j2"]
|
|
105
105
|
|
|
106
106
|
# flake8 configuration (legacy - remove if using ruff)
|
|
107
107
|
[tool.flake8]
|
|
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
|
|
|
6
6
|
from typing import Optional, Tuple, Union, List, Dict, Any
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
from rich.table import Table
|
|
9
|
-
from together import
|
|
9
|
+
from together import AsyncTogether
|
|
10
10
|
|
|
11
11
|
from .hybrid_loctus_generator import HybridLocustGenerator
|
|
12
12
|
from .config import Settings
|
|
@@ -197,19 +197,20 @@ async def _generate_and_create_tests(
|
|
|
197
197
|
custom_requirement: Optional[str] = "",
|
|
198
198
|
host: Optional[str] = "0.0.0.0",
|
|
199
199
|
auth: bool = False,
|
|
200
|
+
db_type: str = "",
|
|
200
201
|
) -> List[Dict[Any, Any]]:
|
|
201
202
|
"""Generate tests using AI and create test files"""
|
|
202
|
-
together_client =
|
|
203
|
+
together_client = AsyncTogether(api_key=api_key)
|
|
203
204
|
|
|
204
205
|
with console.status("[bold green]Generating Locust tests with AI..."):
|
|
205
206
|
generator = HybridLocustGenerator(ai_client=together_client)
|
|
206
|
-
|
|
207
207
|
test_files, test_directories = await generator.generate_from_endpoints(
|
|
208
208
|
endpoints=endpoints,
|
|
209
209
|
api_info=api_info,
|
|
210
210
|
custom_requirement=custom_requirement,
|
|
211
211
|
target_host=host,
|
|
212
212
|
include_auth=auth,
|
|
213
|
+
db_type=db_type,
|
|
213
214
|
)
|
|
214
215
|
|
|
215
216
|
# Create test files
|
|
@@ -271,6 +272,12 @@ def cli(ctx: click.Context, verbose: bool) -> None:
|
|
|
271
272
|
)
|
|
272
273
|
@click.option("--host", "-H", type=str, help="Target host URL")
|
|
273
274
|
@click.option("--auth/--no-auth", default=True, help="Include authentication in tests")
|
|
275
|
+
@click.option(
|
|
276
|
+
"--db-type",
|
|
277
|
+
type=click.Choice(["", "mongo", "postgresql"], case_sensitive=False),
|
|
278
|
+
default="",
|
|
279
|
+
help="Database type for testing (empty for no database, mongo, or postgresql)",
|
|
280
|
+
)
|
|
274
281
|
@click.option("--dry-run", is_flag=True, help="Generate tests without running them")
|
|
275
282
|
@click.option(
|
|
276
283
|
"--custom-requirement", type=str, help="Custom requirements for test generation"
|
|
@@ -291,6 +298,7 @@ def generate(
|
|
|
291
298
|
run_time: str,
|
|
292
299
|
host: Optional[str],
|
|
293
300
|
auth: bool,
|
|
301
|
+
db_type: str,
|
|
294
302
|
dry_run: bool,
|
|
295
303
|
custom_requirement: Optional[str],
|
|
296
304
|
together_api_key: Optional[str],
|
|
@@ -309,6 +317,7 @@ def generate(
|
|
|
309
317
|
run_time,
|
|
310
318
|
host,
|
|
311
319
|
auth,
|
|
320
|
+
db_type,
|
|
312
321
|
dry_run,
|
|
313
322
|
custom_requirement,
|
|
314
323
|
together_api_key,
|
|
@@ -332,6 +341,7 @@ async def _async_generate(
|
|
|
332
341
|
run_time: str,
|
|
333
342
|
host: Optional[str],
|
|
334
343
|
auth: bool,
|
|
344
|
+
db_type: str,
|
|
335
345
|
dry_run: bool,
|
|
336
346
|
custom_requirement: Optional[str],
|
|
337
347
|
together_api_key: Optional[str],
|
|
@@ -343,7 +353,6 @@ async def _async_generate(
|
|
|
343
353
|
try:
|
|
344
354
|
_, api_key = _initialize_config(together_api_key)
|
|
345
355
|
output_dir = _setup_output_directory(output)
|
|
346
|
-
|
|
347
356
|
# Display configuration
|
|
348
357
|
if ctx.obj["verbose"]:
|
|
349
358
|
_display_configuration(
|
|
@@ -363,7 +372,14 @@ async def _async_generate(
|
|
|
363
372
|
)
|
|
364
373
|
|
|
365
374
|
created_files = await _generate_and_create_tests(
|
|
366
|
-
api_key,
|
|
375
|
+
api_key,
|
|
376
|
+
endpoints,
|
|
377
|
+
api_info,
|
|
378
|
+
output_dir,
|
|
379
|
+
custom_requirement,
|
|
380
|
+
host,
|
|
381
|
+
auth,
|
|
382
|
+
db_type,
|
|
367
383
|
)
|
|
368
384
|
|
|
369
385
|
# Show results
|
{devdox_ai_locust-0.1.2 → devdox_ai_locust-0.1.7}/src/devdox_ai_locust/hybrid_loctus_generator.py
RENAMED
|
@@ -19,12 +19,22 @@ import shutil
|
|
|
19
19
|
from devdox_ai_locust.utils.open_ai_parser import Endpoint
|
|
20
20
|
from devdox_ai_locust.utils.file_creation import FileCreationConfig, SafeFileCreator
|
|
21
21
|
from devdox_ai_locust.locust_generator import LocustTestGenerator, TestDataConfig
|
|
22
|
-
from together import
|
|
22
|
+
from together import AsyncTogether
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
test_data_file_path = "test_data.py"
|
|
28
|
+
data_provider_path = "data_provider.py"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ErrorClassification:
|
|
33
|
+
"""Classification of an error for retry logic"""
|
|
34
|
+
|
|
35
|
+
is_retryable: bool
|
|
36
|
+
backoff_seconds: float
|
|
37
|
+
error_type: str
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
@dataclass
|
|
@@ -109,6 +119,7 @@ class EnhancementProcessor:
|
|
|
109
119
|
base_files: Dict[str, str],
|
|
110
120
|
directory_files: List[Dict[str, Any]],
|
|
111
121
|
grouped_endpoints: Dict[str, List[Endpoint]],
|
|
122
|
+
db_type: str = "",
|
|
112
123
|
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
|
113
124
|
"""Process workflow enhancements"""
|
|
114
125
|
enhanced_directory_files: List[Dict[str, Any]] = []
|
|
@@ -125,10 +136,13 @@ class EnhancementProcessor:
|
|
|
125
136
|
first_workflow = base_workflow_files[0]
|
|
126
137
|
# Get the content from the dictionary - adjust key name as needed
|
|
127
138
|
base_workflow_content = first_workflow.get("base_workflow.py", "")
|
|
128
|
-
|
|
129
139
|
for workflow_item in directory_files:
|
|
130
140
|
enhanced_workflow_item = await self._enhance_single_workflow(
|
|
131
|
-
workflow_item,
|
|
141
|
+
workflow_item,
|
|
142
|
+
base_files,
|
|
143
|
+
base_workflow_content,
|
|
144
|
+
grouped_endpoints,
|
|
145
|
+
db_type,
|
|
132
146
|
)
|
|
133
147
|
if enhanced_workflow_item:
|
|
134
148
|
enhanced_directory_files.append(enhanced_workflow_item["files"])
|
|
@@ -142,6 +156,7 @@ class EnhancementProcessor:
|
|
|
142
156
|
base_files: Dict[str, str],
|
|
143
157
|
base_workflow_files: str,
|
|
144
158
|
grouped_endpoints: Dict[str, List[Endpoint]],
|
|
159
|
+
db_type: str = "",
|
|
145
160
|
) -> Dict[str, Any] | None:
|
|
146
161
|
"""Enhance a single workflow file"""
|
|
147
162
|
for key, value in workflow_item.items():
|
|
@@ -155,6 +170,7 @@ class EnhancementProcessor:
|
|
|
155
170
|
base_workflow=base_workflow_files,
|
|
156
171
|
grouped_enpoints=workflow_endpoints_dict,
|
|
157
172
|
auth_endpoints=auth_endpoints,
|
|
173
|
+
db_type=db_type,
|
|
158
174
|
)
|
|
159
175
|
if enhanced_workflow:
|
|
160
176
|
return {
|
|
@@ -165,14 +181,19 @@ class EnhancementProcessor:
|
|
|
165
181
|
return None
|
|
166
182
|
|
|
167
183
|
async def process_test_data_enhancement(
|
|
168
|
-
self, base_files: Dict[str, str], endpoints: List[Endpoint]
|
|
184
|
+
self, base_files: Dict[str, str], endpoints: List[Endpoint], db_type: str = ""
|
|
169
185
|
) -> Tuple[Dict[str, str], List[str]]:
|
|
170
186
|
"""Process test data enhancement"""
|
|
171
187
|
enhanced_files = {}
|
|
172
188
|
enhancements = []
|
|
173
189
|
if self.ai_config and self.ai_config.enhance_test_data:
|
|
174
190
|
enhanced_test_data = await self.locust_generator.enhance_test_data_file(
|
|
175
|
-
base_files.get(test_data_file_path, ""),
|
|
191
|
+
base_files.get(test_data_file_path, ""),
|
|
192
|
+
endpoints,
|
|
193
|
+
db_type,
|
|
194
|
+
base_files.get(data_provider_path, ""),
|
|
195
|
+
base_files.get("db_config.py", ""),
|
|
196
|
+
data_provider_path,
|
|
176
197
|
)
|
|
177
198
|
if enhanced_test_data:
|
|
178
199
|
enhanced_files[test_data_file_path] = enhanced_test_data
|
|
@@ -202,7 +223,7 @@ class HybridLocustGenerator:
|
|
|
202
223
|
|
|
203
224
|
def __init__(
|
|
204
225
|
self,
|
|
205
|
-
ai_client:
|
|
226
|
+
ai_client: AsyncTogether,
|
|
206
227
|
ai_config: Optional[AIEnhancementConfig] = None,
|
|
207
228
|
test_config: Optional[TestDataConfig] = None,
|
|
208
229
|
prompt_dir: str = "prompt",
|
|
@@ -211,7 +232,12 @@ class HybridLocustGenerator:
|
|
|
211
232
|
self.ai_config = ai_config or AIEnhancementConfig()
|
|
212
233
|
self.template_generator = LocustTestGenerator(test_config)
|
|
213
234
|
self.prompt_dir = self._find_project_root() / prompt_dir
|
|
235
|
+
self._api_semaphore = asyncio.Semaphore(5)
|
|
214
236
|
self._setup_jinja_env()
|
|
237
|
+
self.MAX_RETRIES = 3
|
|
238
|
+
self.RATE_LIMIT_BACKOFF = 10
|
|
239
|
+
self.NON_RETRYABLE_CODES = ["401", "403", "unauthorized", "forbidden"]
|
|
240
|
+
self.RATE_LIMIT_INDICATORS = ["429", "rate limit"]
|
|
215
241
|
|
|
216
242
|
def _find_project_root(self) -> Path:
|
|
217
243
|
"""Find the project root by looking for setup.py, pyproject.toml, or .git"""
|
|
@@ -229,6 +255,45 @@ class HybridLocustGenerator:
|
|
|
229
255
|
autoescape=False,
|
|
230
256
|
)
|
|
231
257
|
|
|
258
|
+
def _classify_error(self, error: Exception, attempt: int) -> ErrorClassification:
|
|
259
|
+
"""
|
|
260
|
+
Classify an error to determine retry behavior.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
error: The exception that occurred
|
|
264
|
+
attempt: Current attempt number (0-indexed)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
ErrorClassification with retry decision and backoff time
|
|
268
|
+
"""
|
|
269
|
+
error_str = str(error).lower()
|
|
270
|
+
|
|
271
|
+
# Non-retryable errors (auth/permission)
|
|
272
|
+
if any(code in error_str for code in self.NON_RETRYABLE_CODES):
|
|
273
|
+
logger.error(f"Authentication error, not retrying: {error}")
|
|
274
|
+
return ErrorClassification(
|
|
275
|
+
is_retryable=False, backoff_seconds=0, error_type="auth"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Rate limit errors (retryable with longer backoff)
|
|
279
|
+
if any(indicator in error_str for indicator in self.RATE_LIMIT_INDICATORS):
|
|
280
|
+
logger.warning(f"Rate limit hit on attempt {attempt + 1}")
|
|
281
|
+
return ErrorClassification(
|
|
282
|
+
is_retryable=True,
|
|
283
|
+
backoff_seconds=self.RATE_LIMIT_BACKOFF,
|
|
284
|
+
error_type="rate_limit",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Other retryable errors (exponential backoff)
|
|
288
|
+
logger.warning(
|
|
289
|
+
f"Retryable error on attempt {attempt + 1}: {type(error).__name__}"
|
|
290
|
+
)
|
|
291
|
+
return ErrorClassification(
|
|
292
|
+
is_retryable=True,
|
|
293
|
+
backoff_seconds=2**attempt, # Exponential: 1s, 2s, 4s
|
|
294
|
+
error_type="retryable",
|
|
295
|
+
)
|
|
296
|
+
|
|
232
297
|
async def generate_from_endpoints(
|
|
233
298
|
self,
|
|
234
299
|
endpoints: List[Endpoint],
|
|
@@ -236,6 +301,7 @@ class HybridLocustGenerator:
|
|
|
236
301
|
custom_requirement: Optional[str] = None,
|
|
237
302
|
target_host: Optional[str] = None,
|
|
238
303
|
include_auth: bool = True,
|
|
304
|
+
db_type: str = "",
|
|
239
305
|
) -> Tuple[Dict[str, str], List[Dict[str, Any]]]:
|
|
240
306
|
"""
|
|
241
307
|
Generate Locust tests using hybrid approach
|
|
@@ -255,6 +321,7 @@ class HybridLocustGenerator:
|
|
|
255
321
|
api_info,
|
|
256
322
|
include_auth=include_auth,
|
|
257
323
|
target_host=target_host,
|
|
324
|
+
db_type=db_type,
|
|
258
325
|
)
|
|
259
326
|
)
|
|
260
327
|
|
|
@@ -269,6 +336,7 @@ class HybridLocustGenerator:
|
|
|
269
336
|
directory_files,
|
|
270
337
|
grouped_enpoints,
|
|
271
338
|
custom_requirement,
|
|
339
|
+
db_type,
|
|
272
340
|
)
|
|
273
341
|
if enhancement_result.success:
|
|
274
342
|
logger.info(
|
|
@@ -369,6 +437,7 @@ class HybridLocustGenerator:
|
|
|
369
437
|
directory_files: List[Dict[str, Any]],
|
|
370
438
|
grouped_endpoints: Dict[str, List[Endpoint]],
|
|
371
439
|
custom_requirement: Optional[str] = None,
|
|
440
|
+
db_type: str = "",
|
|
372
441
|
) -> EnhancementResult:
|
|
373
442
|
"""Enhance base files with AI - Refactored for reduced cognitive complexity"""
|
|
374
443
|
start_time = asyncio.get_event_loop().time()
|
|
@@ -381,6 +450,7 @@ class HybridLocustGenerator:
|
|
|
381
450
|
directory_files,
|
|
382
451
|
grouped_endpoints,
|
|
383
452
|
custom_requirement,
|
|
453
|
+
db_type,
|
|
384
454
|
)
|
|
385
455
|
|
|
386
456
|
processing_time = asyncio.get_event_loop().time() - start_time
|
|
@@ -409,6 +479,7 @@ class HybridLocustGenerator:
|
|
|
409
479
|
directory_files: List[Dict[str, Any]],
|
|
410
480
|
grouped_endpoints: Dict[str, List[Endpoint]],
|
|
411
481
|
custom_requirement: Optional[str] = None,
|
|
482
|
+
db_type: str = "",
|
|
412
483
|
) -> EnhancementResult:
|
|
413
484
|
"""Process all enhancements using the enhancement processor"""
|
|
414
485
|
processor = EnhancementProcessor(self.ai_config, self)
|
|
@@ -423,7 +494,7 @@ class HybridLocustGenerator:
|
|
|
423
494
|
processor.process_domain_flows_enhancement(
|
|
424
495
|
endpoints, api_info, custom_requirement
|
|
425
496
|
),
|
|
426
|
-
processor.process_test_data_enhancement(base_files, endpoints),
|
|
497
|
+
processor.process_test_data_enhancement(base_files, endpoints, db_type),
|
|
427
498
|
processor.process_validation_enhancement(base_files, endpoints),
|
|
428
499
|
]
|
|
429
500
|
|
|
@@ -448,7 +519,7 @@ class HybridLocustGenerator:
|
|
|
448
519
|
workflow_files,
|
|
449
520
|
workflow_enhancements,
|
|
450
521
|
) = await processor.process_workflow_enhancements(
|
|
451
|
-
base_files, directory_files, grouped_endpoints
|
|
522
|
+
base_files, directory_files, grouped_endpoints, db_type
|
|
452
523
|
)
|
|
453
524
|
enhanced_directory_files.extend(workflow_files)
|
|
454
525
|
enhancements_applied.extend(workflow_enhancements)
|
|
@@ -504,6 +575,7 @@ class HybridLocustGenerator:
|
|
|
504
575
|
base_workflow: str,
|
|
505
576
|
grouped_enpoints: Dict[str, List[Endpoint]],
|
|
506
577
|
auth_endpoints: List[Endpoint],
|
|
578
|
+
db_type: str = "",
|
|
507
579
|
) -> Optional[str]:
|
|
508
580
|
try:
|
|
509
581
|
template = self.jinja_env.get_template("workflow.j2")
|
|
@@ -515,6 +587,7 @@ class HybridLocustGenerator:
|
|
|
515
587
|
base_workflow=base_workflow,
|
|
516
588
|
auth_endpoints=auth_endpoints,
|
|
517
589
|
base_content=base_content,
|
|
590
|
+
db_type=db_type,
|
|
518
591
|
)
|
|
519
592
|
enhanced_content = await self._call_ai_service(prompt)
|
|
520
593
|
return enhanced_content
|
|
@@ -524,7 +597,13 @@ class HybridLocustGenerator:
|
|
|
524
597
|
return ""
|
|
525
598
|
|
|
526
599
|
async def enhance_test_data_file(
|
|
527
|
-
self,
|
|
600
|
+
self,
|
|
601
|
+
base_content: str,
|
|
602
|
+
endpoints: List[Endpoint],
|
|
603
|
+
db_type: str = "",
|
|
604
|
+
data_provider: str = "",
|
|
605
|
+
db_config: str = "",
|
|
606
|
+
data_provider_path: str = "",
|
|
528
607
|
) -> Optional[str]:
|
|
529
608
|
"""Enhance test data generation with domain knowledge"""
|
|
530
609
|
|
|
@@ -539,11 +618,14 @@ class HybridLocustGenerator:
|
|
|
539
618
|
"base_content": base_content,
|
|
540
619
|
"schemas_info": schemas_info,
|
|
541
620
|
"endpoints": endpoints,
|
|
621
|
+
"db_type": db_type,
|
|
622
|
+
"data_provider_content": data_provider,
|
|
623
|
+
"db_config": db_config,
|
|
624
|
+
"data_provider_path": data_provider_path,
|
|
542
625
|
}
|
|
543
626
|
|
|
544
627
|
# Render enhanced content
|
|
545
628
|
prompt = template.render(**context)
|
|
546
|
-
|
|
547
629
|
enhanced_content = await self._call_ai_service(prompt)
|
|
548
630
|
if enhanced_content and self._validate_python_code(enhanced_content):
|
|
549
631
|
return enhanced_content
|
|
@@ -573,10 +655,8 @@ class HybridLocustGenerator:
|
|
|
573
655
|
|
|
574
656
|
return ""
|
|
575
657
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
messages = [
|
|
658
|
+
def _build_messages(self, prompt: str) -> list[dict]:
|
|
659
|
+
return [
|
|
580
660
|
{
|
|
581
661
|
"role": "system",
|
|
582
662
|
"content": "You are an expert Python developer specializing in Locust load testing. Generate clean, production-ready code with proper error handling. "
|
|
@@ -586,30 +666,42 @@ class HybridLocustGenerator:
|
|
|
586
666
|
{"role": "user", "content": prompt},
|
|
587
667
|
]
|
|
588
668
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
669
|
+
async def _make_api_call(self, messages: list[dict]) -> Optional[str]:
|
|
670
|
+
"""Make API call - ONE job"""
|
|
671
|
+
async with self._api_semaphore:
|
|
672
|
+
api_call = self.ai_client.chat.completions.create(
|
|
673
|
+
model=self.ai_config.model,
|
|
674
|
+
messages=messages,
|
|
675
|
+
max_tokens=self.ai_config.max_tokens,
|
|
676
|
+
temperature=self.ai_config.temperature,
|
|
677
|
+
top_p=0.9,
|
|
678
|
+
top_k=40,
|
|
679
|
+
repetition_penalty=1.1,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Wait for the API call with timeout
|
|
683
|
+
response = await asyncio.wait_for(
|
|
684
|
+
api_call,
|
|
685
|
+
timeout=self.ai_config.timeout,
|
|
686
|
+
)
|
|
687
|
+
if response.choices and response.choices[0].message:
|
|
688
|
+
content = response.choices[0].message.content.strip()
|
|
689
|
+
# Clean up the response
|
|
690
|
+
content = self._clean_ai_response(
|
|
691
|
+
self.extract_code_from_response(content)
|
|
603
692
|
)
|
|
693
|
+
return content
|
|
604
694
|
|
|
605
|
-
|
|
606
|
-
content = response.choices[0].message.content.strip()
|
|
695
|
+
return None
|
|
607
696
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
)
|
|
697
|
+
async def _call_ai_service(self, prompt: str) -> Optional[str]:
|
|
698
|
+
"""Call AI service with retry logic and validation"""
|
|
699
|
+
messages = self._build_messages(prompt)
|
|
612
700
|
|
|
701
|
+
for attempt in range(self.MAX_RETRIES): # Retry logic
|
|
702
|
+
try:
|
|
703
|
+
async with self._api_semaphore:
|
|
704
|
+
content = await self._make_api_call(messages)
|
|
613
705
|
if content:
|
|
614
706
|
return content
|
|
615
707
|
|
|
@@ -617,24 +709,41 @@ class HybridLocustGenerator:
|
|
|
617
709
|
logger.warning(f"AI service timeout on attempt {attempt + 1}")
|
|
618
710
|
|
|
619
711
|
except Exception as e:
|
|
620
|
-
|
|
712
|
+
classification = self._classify_error(e, attempt) # Helper 3
|
|
713
|
+
|
|
714
|
+
if not classification.is_retryable:
|
|
715
|
+
return ""
|
|
716
|
+
|
|
717
|
+
if attempt < self.MAX_RETRIES - 1:
|
|
718
|
+
await asyncio.sleep(classification.backoff_seconds)
|
|
621
719
|
|
|
622
|
-
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
if attempt < self.MAX_RETRIES - 1:
|
|
623
723
|
await asyncio.sleep(2**attempt)
|
|
624
724
|
|
|
625
725
|
return ""
|
|
626
726
|
|
|
627
727
|
def extract_code_from_response(self, response_text: str) -> str:
|
|
628
728
|
# Extract content between <code> tags
|
|
729
|
+
pattern = r"<code>(.*?)</code>"
|
|
730
|
+
matches = re.findall(pattern, response_text, re.DOTALL)
|
|
629
731
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
732
|
+
if not matches:
|
|
733
|
+
logger.warning("No <code> tags found, using full response")
|
|
734
|
+
return response_text.strip()
|
|
735
|
+
|
|
736
|
+
content = max(matches, key=len).strip()
|
|
737
|
+
|
|
738
|
+
# Content too short - use full response
|
|
739
|
+
if not content or len(content) <= 10:
|
|
740
|
+
logger.warning(
|
|
741
|
+
f"Code in tags too short ({len(content)} chars), using full response"
|
|
742
|
+
)
|
|
743
|
+
return response_text.strip()
|
|
636
744
|
|
|
637
|
-
|
|
745
|
+
logger.debug(f"Extracted {len(content)} chars from <code> tags")
|
|
746
|
+
return str(content)
|
|
638
747
|
|
|
639
748
|
def _clean_ai_response(self, content: str) -> str:
|
|
640
749
|
"""Clean and validate AI response"""
|