rubric-load-tester 0.2.0__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.
- rubric_load_tester-0.2.0/LICENSE +21 -0
- rubric_load_tester-0.2.0/PKG-INFO +292 -0
- rubric_load_tester-0.2.0/README.md +244 -0
- rubric_load_tester-0.2.0/pyproject.toml +44 -0
- rubric_load_tester-0.2.0/rubric/__init__.py +0 -0
- rubric_load_tester-0.2.0/rubric/auth/__init__.py +8 -0
- rubric_load_tester-0.2.0/rubric/auth/interviewer.py +852 -0
- rubric_load_tester-0.2.0/rubric/auth/resolver.py +96 -0
- rubric_load_tester-0.2.0/rubric/auth/store.py +113 -0
- rubric_load_tester-0.2.0/rubric/cli/__init__.py +1 -0
- rubric_load_tester-0.2.0/rubric/cli/commands/forecast.py +228 -0
- rubric_load_tester-0.2.0/rubric/cli/commands/inspect.py +76 -0
- rubric_load_tester-0.2.0/rubric/cli/commands/run.py +529 -0
- rubric_load_tester-0.2.0/rubric/cli/commands/settings.py +44 -0
- rubric_load_tester-0.2.0/rubric/cli/commands/trace.py +186 -0
- rubric_load_tester-0.2.0/rubric/cli/helpers.py +518 -0
- rubric_load_tester-0.2.0/rubric/cli/main.py +127 -0
- rubric_load_tester-0.2.0/rubric/cli/tracer.py +104 -0
- rubric_load_tester-0.2.0/rubric/config/__init__.py +5 -0
- rubric_load_tester-0.2.0/rubric/config/loader.py +103 -0
- rubric_load_tester-0.2.0/rubric/core/__init__.py +1 -0
- rubric_load_tester-0.2.0/rubric/core/assertions.py +152 -0
- rubric_load_tester-0.2.0/rubric/core/llm.py +376 -0
- rubric_load_tester-0.2.0/rubric/core/schema.py +967 -0
- rubric_load_tester-0.2.0/rubric/core/synthesizer.py +490 -0
- rubric_load_tester-0.2.0/rubric/engine/__init__.py +1 -0
- rubric_load_tester-0.2.0/rubric/engine/aggregator.py +208 -0
- rubric_load_tester-0.2.0/rubric/engine/auth_pool.py +106 -0
- rubric_load_tester-0.2.0/rubric/engine/journey.py +228 -0
- rubric_load_tester-0.2.0/rubric/engine/rate_limit.py +56 -0
- rubric_load_tester-0.2.0/rubric/engine/registry.py +124 -0
- rubric_load_tester-0.2.0/rubric/engine/runner.py +5 -0
- rubric_load_tester-0.2.0/rubric/engine/scheduler.py +1064 -0
- rubric_load_tester-0.2.0/rubric/engine/types.py +54 -0
- rubric_load_tester-0.2.0/rubric/engine/utils.py +191 -0
- rubric_load_tester-0.2.0/rubric/forecast/__init__.py +0 -0
- rubric_load_tester-0.2.0/rubric/forecast/analyzer.py +350 -0
- rubric_load_tester-0.2.0/rubric/forecast/reporter.py +1354 -0
- rubric_load_tester-0.2.0/rubric/report/__init__.py +1 -0
- rubric_load_tester-0.2.0/rubric/report/diagnostics.py +97 -0
- rubric_load_tester-0.2.0/rubric/report/export.py +664 -0
- rubric_load_tester-0.2.0/rubric/report/junit.py +100 -0
- rubric_load_tester-0.2.0/rubric/report/metrics.py +522 -0
- rubric_load_tester-0.2.0/rubric/report/reporter.py +8 -0
- rubric_load_tester-0.2.0/rubric/report/terminal.py +295 -0
- rubric_load_tester-0.2.0/rubric/sample_api.py +323 -0
- rubric_load_tester-0.2.0/rubric/settings.py +199 -0
- rubric_load_tester-0.2.0/rubric_load_tester.egg-info/PKG-INFO +292 -0
- rubric_load_tester-0.2.0/rubric_load_tester.egg-info/SOURCES.txt +67 -0
- rubric_load_tester-0.2.0/rubric_load_tester.egg-info/dependency_links.txt +1 -0
- rubric_load_tester-0.2.0/rubric_load_tester.egg-info/entry_points.txt +2 -0
- rubric_load_tester-0.2.0/rubric_load_tester.egg-info/requires.txt +9 -0
- rubric_load_tester-0.2.0/rubric_load_tester.egg-info/top_level.txt +1 -0
- rubric_load_tester-0.2.0/setup.cfg +4 -0
- rubric_load_tester-0.2.0/tests/test_aggregator.py +146 -0
- rubric_load_tester-0.2.0/tests/test_assertions_and_junit.py +128 -0
- rubric_load_tester-0.2.0/tests/test_auth_resolver.py +84 -0
- rubric_load_tester-0.2.0/tests/test_config_loader.py +77 -0
- rubric_load_tester-0.2.0/tests/test_error_attribution.py +74 -0
- rubric_load_tester-0.2.0/tests/test_journey.py +42 -0
- rubric_load_tester-0.2.0/tests/test_oob_classification.py +64 -0
- rubric_load_tester-0.2.0/tests/test_registry.py +31 -0
- rubric_load_tester-0.2.0/tests/test_reporter.py +60 -0
- rubric_load_tester-0.2.0/tests/test_scheduler_phases.py +41 -0
- rubric_load_tester-0.2.0/tests/test_schema.py +76 -0
- rubric_load_tester-0.2.0/tests/test_schema_constraints.py +246 -0
- rubric_load_tester-0.2.0/tests/test_settings.py +13 -0
- rubric_load_tester-0.2.0/tests/test_synthesizer.py +40 -0
- rubric_load_tester-0.2.0/tests/test_synthesizer_constraints.py +210 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rubric-load-tester
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Zero-config, LLM-powered API load tester.
|
|
5
|
+
Author: Rubric Team
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/adetu-siyan/rubric_lt
|
|
29
|
+
Classifier: Development Status :: 4 - Beta
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: Topic :: Software Development :: Testing
|
|
34
|
+
Classifier: Topic :: Software Development :: Testing :: Traffic Generation
|
|
35
|
+
Requires-Python: >=3.11
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: click
|
|
39
|
+
Requires-Dist: rich
|
|
40
|
+
Requires-Dist: aiohttp
|
|
41
|
+
Requires-Dist: numpy
|
|
42
|
+
Requires-Dist: groq
|
|
43
|
+
Requires-Dist: python-dotenv
|
|
44
|
+
Requires-Dist: pyyaml
|
|
45
|
+
Requires-Dist: pydantic
|
|
46
|
+
Requires-Dist: prompt_toolkit
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Rubric
|
|
50
|
+
|
|
51
|
+
Rubric points at your API, reads its OpenAPI schema, and hits it with realistic load — no manual test scripts to write. It uses an LLM to generate the actual request payloads (valid data, edge cases, and garbage inputs), fires them concurrently, and gives you latency percentiles plus AI notes on what broke and why.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
1. **Schema ingestion** — pulls `/openapi.json`, sorts routes into buckets (does it need a body? does it depend on an ID from an earlier request?)
|
|
58
|
+
2. **Payload synthesis** — the LLM writes ~50 payloads per endpoint across three tiers, generated in parallel so this doesn't become the slow part
|
|
59
|
+
3. **Load engine** — async workers fire the requests and stream live numbers to your terminal as it runs
|
|
60
|
+
4. **Report** — latency percentiles, pass/fail per endpoint, and a short AI writeup of likely root causes for anything that failed
|
|
61
|
+
|
|
62
|
+
### Route buckets
|
|
63
|
+
|
|
64
|
+
| Bucket | Methods | What it means |
|
|
65
|
+
|--------|---------|----------------|
|
|
66
|
+
| A | GET, DELETE | No body — just path/query params |
|
|
67
|
+
| B | POST, PUT, PATCH | Has a request body |
|
|
68
|
+
| C | Any | Depends on a resource ID from an earlier POST in the run |
|
|
69
|
+
|
|
70
|
+
### Payload tiers
|
|
71
|
+
|
|
72
|
+
| Tier | Count | What it's testing |
|
|
73
|
+
|------|-------|--------------------|
|
|
74
|
+
| `happy_path` | 17 | Normal, valid data — should just return 2xx |
|
|
75
|
+
| `edge_cases` | 17 | Boundaries: empty strings, huge ints, unicode, very long strings |
|
|
76
|
+
| `malformed` | 16 | Wrong types, missing fields, broken structure |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Getting set up
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cd rubric
|
|
84
|
+
pip install -r requirements.txt
|
|
85
|
+
# or install as a package so you get the `rubric` command:
|
|
86
|
+
pip install -e .
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then copy the env file and fill in your provider:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cp .env.example .env
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```env
|
|
96
|
+
LLM_PROVIDER=groq # or openai
|
|
97
|
+
|
|
98
|
+
# Groq — fast, has a free tier, this is what most people use
|
|
99
|
+
GROQ_API_KEY=your_groq_key_here
|
|
100
|
+
GROQ_MODEL=meta-llama/llama-4-scout-17b-16e-instruct
|
|
101
|
+
|
|
102
|
+
# OpenAI, if you'd rather use that
|
|
103
|
+
OPENAI_API_KEY=your_openai_key_here
|
|
104
|
+
OPENAI_MODEL=gpt-4o
|
|
105
|
+
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Using it
|
|
111
|
+
|
|
112
|
+
The simplest case:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
rubric run http://localhost:8000
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Tuning the run:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
rubric run http://localhost:8000 \
|
|
122
|
+
--requests 500 \
|
|
123
|
+
--concurrency 50 \
|
|
124
|
+
--tier happy_path \
|
|
125
|
+
--p95-threshold 200 \
|
|
126
|
+
--error-rate 0.01
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Run every tier in one go:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
rubric run http://localhost:8000 --tier all --requests 1000
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Just want to see what Rubric found without actually load testing:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
rubric inspect http://localhost:8000
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Skip the AI diagnostics step if you just want raw numbers fast:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
rubric run http://localhost:8000 --no-diagnostics
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Switch providers per run instead of editing `.env`:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
rubric run http://localhost:8000 --provider groq
|
|
151
|
+
rubric run http://localhost:8000 --provider openai
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Testing against a self-signed cert (local/dev)
|
|
155
|
+
|
|
156
|
+
Rubric checks the target's TLS certificate before sending login credentials, the same way a browser would — if it can't verify the cert, it won't send anything. That's fine for a real API, but it'll block you if you're testing against `localhost` or a dev server on a self-signed cert. In that case, opt in explicitly:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
rubric run https://localhost:8443 --allow-self-signed
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Only do this against something you control. It turns off the check that stops your credentials and tokens from being sent to an unverified server — don't use it against a real target over a network you don't fully trust.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Flags
|
|
167
|
+
|
|
168
|
+
| Flag | Default | What it does |
|
|
169
|
+
|------|---------|---------------|
|
|
170
|
+
| `--requests` | 100 | Total requests spread across endpoints |
|
|
171
|
+
| `--concurrency` | 20 | Max concurrent connections |
|
|
172
|
+
| `--tier` | `happy_path` | `happy_path`, `edge_cases`, `malformed`, or `all` |
|
|
173
|
+
| `--p95-threshold` | 200 | P95 latency (ms) — above this, endpoint is marked DEGRADED |
|
|
174
|
+
| `--error-rate` | 0.01 | 5xx rate above this marks the endpoint FAILED |
|
|
175
|
+
| `--no-diagnostics` | False | Skip the AI failure writeup |
|
|
176
|
+
| `--save` | True | Write the JSON report to disk |
|
|
177
|
+
| `--provider` | from `.env` | Override the LLM provider for this run |
|
|
178
|
+
| `--allow-self-signed` | False | Skip TLS verification for login/token requests — local/self-signed dev servers only |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## What you get back
|
|
183
|
+
|
|
184
|
+
While it's running, you'll see something like this update live:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
RUBRIC · 847 requests · 142.3 RPS · 5.9s
|
|
188
|
+
┌────────────────────────┬──────┬──────┬──────┬──────┬──────┬───────┐
|
|
189
|
+
│ Endpoint │ RPS │ P95 │ 2xx │ 4xx │ 5xx │ Total │
|
|
190
|
+
├────────────────────────┼──────┼──────┼──────┼──────┼──────┼───────┤
|
|
191
|
+
│ POST /users │ 142 │ 84ms│ 891 │ 23 │ 6 │ 920 │
|
|
192
|
+
│ GET /users/{user_id} │ 89 │ 31ms│ 412 │ 0 │ 1 │ 413 │
|
|
193
|
+
│ POST /products │ 201 │ 19ms│ 1204 │ 11 │ 0 │ 1215 │
|
|
194
|
+
└────────────────────────┴──────┴──────┴──────┴──────┴──────┴───────┘
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
And a summary once it's done:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
┌────────────────────────┬────────────┬─────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
|
|
201
|
+
│ Endpoint │ Status │ RPS │ P50 │ P95 │ P99 │ 2xx │ 4xx │ 5xx │ Err% │
|
|
202
|
+
├────────────────────────┼────────────┼─────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
|
|
203
|
+
│ POST /users │ PASSED │ 142 │ 34ms │ 84ms │201ms │ 891 │ 23 │ 6 │0.67% │
|
|
204
|
+
│ GET /users/{user_id} │ DEGRADED │ 89 │ 12ms │312ms │ 67ms │ 412 │ 0 │ 1 │0.24% │
|
|
205
|
+
│ POST /products │ FAILED │ 201 │ 18ms │ 44ms │ 98ms │ 980 │ 11 │ 24 │2.10% │
|
|
206
|
+
└────────────────────────┴────────────┴─────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
|
|
207
|
+
|
|
208
|
+
SUITE FAILED
|
|
209
|
+
Passed: 1 Degraded: 1 Failed: 1
|
|
210
|
+
Total: 2507 requests Duration: 17.6s RPS: 142.4
|
|
211
|
+
Thresholds — P95 < 200ms, error rate < 1%
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
For anything that failed, you'll get a short explanation of the likely cause, not just a status code:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
1. POST /users
|
|
218
|
+
Payload: {"name": "", "email": "x@y.com", "role": ""}
|
|
219
|
+
Cause: empty 'role' string hits a NOT NULL constraint at the DB layer.
|
|
220
|
+
Fix: add a validator to reject empty strings for that field.
|
|
221
|
+
|
|
222
|
+
2. POST /products
|
|
223
|
+
Payload: {"price": -1, "name": "Widget", "category": "tools"}
|
|
224
|
+
Cause: negative price trips a CHECK constraint in Postgres.
|
|
225
|
+
Fix: add Field(gt=0) to the price field in the schema.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Trying it against the sample API
|
|
231
|
+
|
|
232
|
+
There's a small FastAPI app included if you want to try Rubric without pointing it at a real project yet:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
pip install fastapi uvicorn
|
|
236
|
+
uvicorn sample_api:app --reload
|
|
237
|
+
|
|
238
|
+
# then, in another terminal:
|
|
239
|
+
rubric run http://localhost:8000
|
|
240
|
+
rubric inspect http://localhost:8000
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## The JSON report
|
|
246
|
+
|
|
247
|
+
Every run writes `rubric_report_<url>_<timestamp>.json` to disk:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"suite_result": "SUITE FAILED",
|
|
252
|
+
"thresholds": { "p95_ms": 200, "error_rate_pct": 1.0 },
|
|
253
|
+
"summary": {
|
|
254
|
+
"total_requests": 2507,
|
|
255
|
+
"duration_seconds": 17.6,
|
|
256
|
+
"overall_rps": 142.4,
|
|
257
|
+
"connection_drops": 0,
|
|
258
|
+
"passed": 1,
|
|
259
|
+
"degraded": 1,
|
|
260
|
+
"failed": 1
|
|
261
|
+
},
|
|
262
|
+
"endpoints": [ ... ],
|
|
263
|
+
"failure_diagnostics": [ ... ]
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`suite_result` is meant to be read by CI — Rubric exits `1` on a failed suite.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Layout
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
rubric/
|
|
275
|
+
├── core/
|
|
276
|
+
│ ├── schema.py # OpenAPI ingestion & route categorization
|
|
277
|
+
│ ├── synthesizer.py # LLM payload synthesis
|
|
278
|
+
│ └── llm.py # Groq / OpenAI client wrapper
|
|
279
|
+
├── engine/
|
|
280
|
+
│ └── runner.py # Async load engine & live streaming
|
|
281
|
+
├── report/
|
|
282
|
+
│ └── reporter.py # Reports & AI diagnostics
|
|
283
|
+
├── cli/
|
|
284
|
+
│ └── main.py # Click entry point
|
|
285
|
+
├── sample_api.py # Demo FastAPI server
|
|
286
|
+
├── requirements.txt
|
|
287
|
+
├── setup.py
|
|
288
|
+
└── .env.example
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Rubric
|
|
2
|
+
|
|
3
|
+
Rubric points at your API, reads its OpenAPI schema, and hits it with realistic load — no manual test scripts to write. It uses an LLM to generate the actual request payloads (valid data, edge cases, and garbage inputs), fires them concurrently, and gives you latency percentiles plus AI notes on what broke and why.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
1. **Schema ingestion** — pulls `/openapi.json`, sorts routes into buckets (does it need a body? does it depend on an ID from an earlier request?)
|
|
10
|
+
2. **Payload synthesis** — the LLM writes ~50 payloads per endpoint across three tiers, generated in parallel so this doesn't become the slow part
|
|
11
|
+
3. **Load engine** — async workers fire the requests and stream live numbers to your terminal as it runs
|
|
12
|
+
4. **Report** — latency percentiles, pass/fail per endpoint, and a short AI writeup of likely root causes for anything that failed
|
|
13
|
+
|
|
14
|
+
### Route buckets
|
|
15
|
+
|
|
16
|
+
| Bucket | Methods | What it means |
|
|
17
|
+
|--------|---------|----------------|
|
|
18
|
+
| A | GET, DELETE | No body — just path/query params |
|
|
19
|
+
| B | POST, PUT, PATCH | Has a request body |
|
|
20
|
+
| C | Any | Depends on a resource ID from an earlier POST in the run |
|
|
21
|
+
|
|
22
|
+
### Payload tiers
|
|
23
|
+
|
|
24
|
+
| Tier | Count | What it's testing |
|
|
25
|
+
|------|-------|--------------------|
|
|
26
|
+
| `happy_path` | 17 | Normal, valid data — should just return 2xx |
|
|
27
|
+
| `edge_cases` | 17 | Boundaries: empty strings, huge ints, unicode, very long strings |
|
|
28
|
+
| `malformed` | 16 | Wrong types, missing fields, broken structure |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Getting set up
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd rubric
|
|
36
|
+
pip install -r requirements.txt
|
|
37
|
+
# or install as a package so you get the `rubric` command:
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then copy the env file and fill in your provider:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cp .env.example .env
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```env
|
|
48
|
+
LLM_PROVIDER=groq # or openai
|
|
49
|
+
|
|
50
|
+
# Groq — fast, has a free tier, this is what most people use
|
|
51
|
+
GROQ_API_KEY=your_groq_key_here
|
|
52
|
+
GROQ_MODEL=meta-llama/llama-4-scout-17b-16e-instruct
|
|
53
|
+
|
|
54
|
+
# OpenAI, if you'd rather use that
|
|
55
|
+
OPENAI_API_KEY=your_openai_key_here
|
|
56
|
+
OPENAI_MODEL=gpt-4o
|
|
57
|
+
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Using it
|
|
63
|
+
|
|
64
|
+
The simplest case:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
rubric run http://localhost:8000
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Tuning the run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
rubric run http://localhost:8000 \
|
|
74
|
+
--requests 500 \
|
|
75
|
+
--concurrency 50 \
|
|
76
|
+
--tier happy_path \
|
|
77
|
+
--p95-threshold 200 \
|
|
78
|
+
--error-rate 0.01
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Run every tier in one go:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
rubric run http://localhost:8000 --tier all --requests 1000
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Just want to see what Rubric found without actually load testing:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
rubric inspect http://localhost:8000
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Skip the AI diagnostics step if you just want raw numbers fast:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
rubric run http://localhost:8000 --no-diagnostics
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Switch providers per run instead of editing `.env`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
rubric run http://localhost:8000 --provider groq
|
|
103
|
+
rubric run http://localhost:8000 --provider openai
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Testing against a self-signed cert (local/dev)
|
|
107
|
+
|
|
108
|
+
Rubric checks the target's TLS certificate before sending login credentials, the same way a browser would — if it can't verify the cert, it won't send anything. That's fine for a real API, but it'll block you if you're testing against `localhost` or a dev server on a self-signed cert. In that case, opt in explicitly:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rubric run https://localhost:8443 --allow-self-signed
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Only do this against something you control. It turns off the check that stops your credentials and tokens from being sent to an unverified server — don't use it against a real target over a network you don't fully trust.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Flags
|
|
119
|
+
|
|
120
|
+
| Flag | Default | What it does |
|
|
121
|
+
|------|---------|---------------|
|
|
122
|
+
| `--requests` | 100 | Total requests spread across endpoints |
|
|
123
|
+
| `--concurrency` | 20 | Max concurrent connections |
|
|
124
|
+
| `--tier` | `happy_path` | `happy_path`, `edge_cases`, `malformed`, or `all` |
|
|
125
|
+
| `--p95-threshold` | 200 | P95 latency (ms) — above this, endpoint is marked DEGRADED |
|
|
126
|
+
| `--error-rate` | 0.01 | 5xx rate above this marks the endpoint FAILED |
|
|
127
|
+
| `--no-diagnostics` | False | Skip the AI failure writeup |
|
|
128
|
+
| `--save` | True | Write the JSON report to disk |
|
|
129
|
+
| `--provider` | from `.env` | Override the LLM provider for this run |
|
|
130
|
+
| `--allow-self-signed` | False | Skip TLS verification for login/token requests — local/self-signed dev servers only |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## What you get back
|
|
135
|
+
|
|
136
|
+
While it's running, you'll see something like this update live:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
RUBRIC · 847 requests · 142.3 RPS · 5.9s
|
|
140
|
+
┌────────────────────────┬──────┬──────┬──────┬──────┬──────┬───────┐
|
|
141
|
+
│ Endpoint │ RPS │ P95 │ 2xx │ 4xx │ 5xx │ Total │
|
|
142
|
+
├────────────────────────┼──────┼──────┼──────┼──────┼──────┼───────┤
|
|
143
|
+
│ POST /users │ 142 │ 84ms│ 891 │ 23 │ 6 │ 920 │
|
|
144
|
+
│ GET /users/{user_id} │ 89 │ 31ms│ 412 │ 0 │ 1 │ 413 │
|
|
145
|
+
│ POST /products │ 201 │ 19ms│ 1204 │ 11 │ 0 │ 1215 │
|
|
146
|
+
└────────────────────────┴──────┴──────┴──────┴──────┴──────┴───────┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
And a summary once it's done:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
┌────────────────────────┬────────────┬─────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
|
|
153
|
+
│ Endpoint │ Status │ RPS │ P50 │ P95 │ P99 │ 2xx │ 4xx │ 5xx │ Err% │
|
|
154
|
+
├────────────────────────┼────────────┼─────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
|
|
155
|
+
│ POST /users │ PASSED │ 142 │ 34ms │ 84ms │201ms │ 891 │ 23 │ 6 │0.67% │
|
|
156
|
+
│ GET /users/{user_id} │ DEGRADED │ 89 │ 12ms │312ms │ 67ms │ 412 │ 0 │ 1 │0.24% │
|
|
157
|
+
│ POST /products │ FAILED │ 201 │ 18ms │ 44ms │ 98ms │ 980 │ 11 │ 24 │2.10% │
|
|
158
|
+
└────────────────────────┴────────────┴─────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
|
|
159
|
+
|
|
160
|
+
SUITE FAILED
|
|
161
|
+
Passed: 1 Degraded: 1 Failed: 1
|
|
162
|
+
Total: 2507 requests Duration: 17.6s RPS: 142.4
|
|
163
|
+
Thresholds — P95 < 200ms, error rate < 1%
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
For anything that failed, you'll get a short explanation of the likely cause, not just a status code:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
1. POST /users
|
|
170
|
+
Payload: {"name": "", "email": "x@y.com", "role": ""}
|
|
171
|
+
Cause: empty 'role' string hits a NOT NULL constraint at the DB layer.
|
|
172
|
+
Fix: add a validator to reject empty strings for that field.
|
|
173
|
+
|
|
174
|
+
2. POST /products
|
|
175
|
+
Payload: {"price": -1, "name": "Widget", "category": "tools"}
|
|
176
|
+
Cause: negative price trips a CHECK constraint in Postgres.
|
|
177
|
+
Fix: add Field(gt=0) to the price field in the schema.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Trying it against the sample API
|
|
183
|
+
|
|
184
|
+
There's a small FastAPI app included if you want to try Rubric without pointing it at a real project yet:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pip install fastapi uvicorn
|
|
188
|
+
uvicorn sample_api:app --reload
|
|
189
|
+
|
|
190
|
+
# then, in another terminal:
|
|
191
|
+
rubric run http://localhost:8000
|
|
192
|
+
rubric inspect http://localhost:8000
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## The JSON report
|
|
198
|
+
|
|
199
|
+
Every run writes `rubric_report_<url>_<timestamp>.json` to disk:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"suite_result": "SUITE FAILED",
|
|
204
|
+
"thresholds": { "p95_ms": 200, "error_rate_pct": 1.0 },
|
|
205
|
+
"summary": {
|
|
206
|
+
"total_requests": 2507,
|
|
207
|
+
"duration_seconds": 17.6,
|
|
208
|
+
"overall_rps": 142.4,
|
|
209
|
+
"connection_drops": 0,
|
|
210
|
+
"passed": 1,
|
|
211
|
+
"degraded": 1,
|
|
212
|
+
"failed": 1
|
|
213
|
+
},
|
|
214
|
+
"endpoints": [ ... ],
|
|
215
|
+
"failure_diagnostics": [ ... ]
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`suite_result` is meant to be read by CI — Rubric exits `1` on a failed suite.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Layout
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
rubric/
|
|
227
|
+
├── core/
|
|
228
|
+
│ ├── schema.py # OpenAPI ingestion & route categorization
|
|
229
|
+
│ ├── synthesizer.py # LLM payload synthesis
|
|
230
|
+
│ └── llm.py # Groq / OpenAI client wrapper
|
|
231
|
+
├── engine/
|
|
232
|
+
│ └── runner.py # Async load engine & live streaming
|
|
233
|
+
├── report/
|
|
234
|
+
│ └── reporter.py # Reports & AI diagnostics
|
|
235
|
+
├── cli/
|
|
236
|
+
│ └── main.py # Click entry point
|
|
237
|
+
├── sample_api.py # Demo FastAPI server
|
|
238
|
+
├── requirements.txt
|
|
239
|
+
├── setup.py
|
|
240
|
+
└── .env.example
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rubric-load-tester"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Zero-config, LLM-powered API load tester."
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Rubric Team"}
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = {file = "LICENSE"}
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Topic :: Software Development :: Testing",
|
|
21
|
+
"Topic :: Software Development :: Testing :: Traffic Generation",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"click",
|
|
25
|
+
"rich",
|
|
26
|
+
"aiohttp",
|
|
27
|
+
"numpy",
|
|
28
|
+
"groq",
|
|
29
|
+
"python-dotenv",
|
|
30
|
+
"pyyaml",
|
|
31
|
+
"pydantic",
|
|
32
|
+
"prompt_toolkit",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
rubric = "rubric.cli.main:main"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/adetu-siyan/rubric_lt"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["."]
|
|
43
|
+
include = ["rubric*"]
|
|
44
|
+
exclude = ["rubric_traces*", "tests*"]
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .store import load_auth, save_auth, delete_credentials, auth_file_exists
|
|
2
|
+
from .interviewer import run_auth_interview
|
|
3
|
+
from .resolver import resolve_headers
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"load_auth", "save_auth", "delete_credentials", "auth_file_exists",
|
|
7
|
+
"run_auth_interview", "resolve_headers",
|
|
8
|
+
]
|