loadpilot 0.1.3__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.
- loadpilot-0.1.3/.env.example +1 -0
- loadpilot-0.1.3/.gitignore +39 -0
- loadpilot-0.1.3/PKG-INFO +235 -0
- loadpilot-0.1.3/README.md +216 -0
- loadpilot-0.1.3/hatch_build.py +73 -0
- loadpilot-0.1.3/loadpilot/__init__.py +4 -0
- loadpilot-0.1.3/loadpilot/_bridge.py +152 -0
- loadpilot-0.1.3/loadpilot/cli.py +759 -0
- loadpilot-0.1.3/loadpilot/client.py +79 -0
- loadpilot-0.1.3/loadpilot/dsl.py +127 -0
- loadpilot-0.1.3/loadpilot/models.py +101 -0
- loadpilot-0.1.3/loadpilot/report.py +475 -0
- loadpilot-0.1.3/loadpilot/templates/monitoring/docker-compose.yml +70 -0
- loadpilot-0.1.3/loadpilot/templates/monitoring/grafana-dashboard.json +262 -0
- loadpilot-0.1.3/pyproject.toml +58 -0
- loadpilot-0.1.3/scenarios/example.py +26 -0
- loadpilot-0.1.3/tests/__init__.py +0 -0
- loadpilot-0.1.3/tests/test_bridge.py +197 -0
- loadpilot-0.1.3/tests/test_cli_plan.py +220 -0
- loadpilot-0.1.3/tests/test_dsl.py +155 -0
- loadpilot-0.1.3/tests/test_integration.py +648 -0
- loadpilot-0.1.3/tests/test_models.py +117 -0
- loadpilot-0.1.3/tests/test_report.py +174 -0
- loadpilot-0.1.3/uv.lock +612 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TARGET_URL=http://localhost:8000
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
|
|
12
|
+
# pytest / coverage
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
|
|
17
|
+
# Rust
|
|
18
|
+
engine/target/
|
|
19
|
+
**/*.rs.bk
|
|
20
|
+
|
|
21
|
+
# Cargo.lock — library crates omit it; binary crates keep it.
|
|
22
|
+
# LoadPilot ships binaries, so Cargo.lock is committed.
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
.DS_Store
|
|
30
|
+
|
|
31
|
+
# Bundled binaries (added by hatch_build.py — never commit)
|
|
32
|
+
cli/loadpilot/coordinator
|
|
33
|
+
cli/loadpilot/coordinator.exe
|
|
34
|
+
cli/loadpilot/agent
|
|
35
|
+
cli/loadpilot/agent.exe
|
|
36
|
+
|
|
37
|
+
# LoadPilot generated outputs
|
|
38
|
+
report*.html
|
|
39
|
+
*.html
|
loadpilot-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loadpilot
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Write load tests in Python. Run them at Rust speed.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: httpx
|
|
8
|
+
Requires-Dist: pydantic
|
|
9
|
+
Requires-Dist: questionary
|
|
10
|
+
Requires-Dist: rich
|
|
11
|
+
Requires-Dist: typer
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: anyio[trio]; extra == 'dev'
|
|
14
|
+
Requires-Dist: coverage>=7; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# LoadPilot
|
|
21
|
+
|
|
22
|
+
[](https://github.com/VladislavAkulich/loadpilot/actions)
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
|
|
25
|
+
> Write load tests in Python. Run them at Rust speed.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
LoadPilot — CheckoutFlow [01:42] steady: 100/100 RPS
|
|
29
|
+
|
|
30
|
+
Requests/sec: 100.0 Total: 9,812
|
|
31
|
+
Errors: 0.0% Failed: 0
|
|
32
|
+
|
|
33
|
+
Latency:
|
|
34
|
+
p50: 12ms
|
|
35
|
+
p95: 28ms
|
|
36
|
+
p99: 41ms
|
|
37
|
+
max: 203ms
|
|
38
|
+
|
|
39
|
+
[████████████████████] 100%
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
LoadPilot is a load testing tool for teams that want to write scenarios in Python
|
|
43
|
+
without the throughput ceiling of a pure-Python engine.
|
|
44
|
+
|
|
45
|
+
Scenarios are plain Python classes. HTTP execution runs in async Rust.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install loadpilot
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No Rust required — the coordinator binary is bundled in the wheel.
|
|
56
|
+
|
|
57
|
+
**Supported platforms:** Linux x86_64 · macOS Intel · macOS Apple Silicon · Windows x86_64
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# scaffold a project
|
|
65
|
+
loadpilot init my-load-tests
|
|
66
|
+
cd my-load-tests
|
|
67
|
+
|
|
68
|
+
# run a scenario
|
|
69
|
+
loadpilot run scenarios/example.py --target https://your-api.example.com
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or run without arguments to pick a scenario interactively:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
loadpilot run --target https://your-api.example.com
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
→ [Getting Started in 5 minutes](docs/getting-started.md)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Write a Scenario
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from loadpilot import VUser, scenario, task, LoadClient
|
|
86
|
+
|
|
87
|
+
@scenario(rps=100, duration="2m", ramp_up="30s")
|
|
88
|
+
class CheckoutFlow(VUser):
|
|
89
|
+
|
|
90
|
+
def on_start(self, client: LoadClient):
|
|
91
|
+
resp = client.post("/auth/login", json={"user": "test", "pass": "secret"})
|
|
92
|
+
self.token = resp.json()["access_token"]
|
|
93
|
+
|
|
94
|
+
@task(weight=5)
|
|
95
|
+
def browse(self, client: LoadClient):
|
|
96
|
+
client.get("/api/products", headers={"Authorization": f"Bearer {self.token}"})
|
|
97
|
+
|
|
98
|
+
def check_browse(self, status_code: int, body) -> None:
|
|
99
|
+
assert status_code == 200
|
|
100
|
+
assert isinstance(body, list)
|
|
101
|
+
|
|
102
|
+
@task(weight=1)
|
|
103
|
+
def purchase(self, client: LoadClient):
|
|
104
|
+
client.post("/api/orders", json={"product_id": 42, "qty": 1},
|
|
105
|
+
headers={"Authorization": f"Bearer {self.token}"})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `on_start` — runs once per virtual user before tasks start (login, setup)
|
|
109
|
+
- `@task` — weighted HTTP task; multiple tasks run in proportion
|
|
110
|
+
- `check_{task}` — assertion after each request; fail = error counted
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Features
|
|
115
|
+
|
|
116
|
+
| | |
|
|
117
|
+
|---|---|
|
|
118
|
+
| **Python DSL** | `@scenario`, `@task`, `on_start`, `check_*` — no new syntax to learn |
|
|
119
|
+
| **Load profiles** | ramp (default), constant, step, spike |
|
|
120
|
+
| **SLA thresholds** | fail CI with exit code 1 on p99/p95/error-rate breach |
|
|
121
|
+
| **Distributed mode** | coordinator + N agents over NATS; local or remote, free |
|
|
122
|
+
| **HTML report** | self-contained, open in any browser, no server required |
|
|
123
|
+
| **Prometheus metrics** | live Grafana dashboards while the test runs |
|
|
124
|
+
| **Interactive TUI** | `loadpilot run` with no args opens a scenario browser |
|
|
125
|
+
| **`pip install`** | coordinator binary bundled in the wheel, no Rust needed |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Performance
|
|
130
|
+
|
|
131
|
+
LoadPilot is benchmarked against k6 and Locust against a Rust/axum echo server in Docker.
|
|
132
|
+
LoadPilot runs in **PyO3 mode** — with `on_start` (login) and `check_*` (assertion per task) — a realistic workload with Python callbacks active.
|
|
133
|
+
|
|
134
|
+
### Precision — 500 RPS target
|
|
135
|
+
|
|
136
|
+
| Tool | RPS actual | p50 | p99 | Errors | CPU avg |
|
|
137
|
+
|------|-----------|-----|-----|--------|---------|
|
|
138
|
+
| **LoadPilot (PyO3)** | **478** | 4ms | 15ms | 0% | **14%** |
|
|
139
|
+
| k6 | 491 | 8ms | 118ms | 0% | 129% |
|
|
140
|
+
| Locust | 498 | 150ms | 1500ms | 0% | 88% |
|
|
141
|
+
|
|
142
|
+
LoadPilot uses **9× less CPU** than k6 at the same load. Locust reaches the RPS target but its Python/GIL scheduler adds significant latency (p99 ≥ 1500ms at only 500 RPS).
|
|
143
|
+
|
|
144
|
+
### Max throughput — 30s, no cap
|
|
145
|
+
|
|
146
|
+
| Tool | RPS | p50 | p99 | Errors | CPU avg |
|
|
147
|
+
|------|-----|-----|-----|--------|---------|
|
|
148
|
+
| **LoadPilot (PyO3)** | **2205** | 11ms | 38ms | 0% | **165%** |
|
|
149
|
+
| k6 | 1799 | 14ms | 175ms | 0% | 212% |
|
|
150
|
+
| Locust | 677 | 100ms | 170ms | 0% | 117% |
|
|
151
|
+
|
|
152
|
+
**1.2× k6** and **3.3× Locust** at max throughput. **1.6× better CPU efficiency per request** vs k6.
|
|
153
|
+
|
|
154
|
+
→ [Full benchmark details and methodology](docs/benchmark.md)
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## SLA Thresholds
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
@scenario(
|
|
162
|
+
rps=100, duration="2m",
|
|
163
|
+
thresholds={"p99_ms": 500, "p95_ms": 300, "error_rate": 1.0},
|
|
164
|
+
)
|
|
165
|
+
class CheckoutFlow(VUser): ...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Thresholds
|
|
170
|
+
✓ p99 latency 41ms < 500ms
|
|
171
|
+
✓ p95 latency 28ms < 300ms
|
|
172
|
+
✓ error rate 0% < 1%
|
|
173
|
+
|
|
174
|
+
All thresholds passed.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Exit code `1` on breach — drop into any CI pipeline. Override without editing the file:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
loadpilot run scenarios/checkout.py --target https://staging.example.com \
|
|
181
|
+
--threshold p99_ms=800 --threshold error_rate=2
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Distributed Mode
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# 4 local processes
|
|
190
|
+
loadpilot run scenarios/checkout.py --target https://api.example.com --agents 4
|
|
191
|
+
|
|
192
|
+
# external agents on remote machines
|
|
193
|
+
loadpilot run scenarios/checkout.py \
|
|
194
|
+
--target https://api.example.com \
|
|
195
|
+
--external-agents 4
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The CLI output is identical to single-machine mode — the coordinator aggregates
|
|
199
|
+
everything transparently. `on_start` (login, setup) runs on the coordinator and
|
|
200
|
+
ships per-user auth headers to agents, so distributed tests can authenticate
|
|
201
|
+
without Python running on each agent.
|
|
202
|
+
|
|
203
|
+
→ [Distributed mode guide](docs/distributed.md)
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Live Monitoring
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# scaffolded by `loadpilot init`
|
|
211
|
+
docker compose -f monitoring/docker-compose.yml up -d
|
|
212
|
+
# Grafana → http://localhost:3000 (LoadPilot dashboard auto-imported)
|
|
213
|
+
# Prometheus → http://localhost:9091
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use Grafana to correlate load test metrics with your service's own metrics
|
|
217
|
+
(CPU, DB latency, error rates) on the same timeline.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Documentation
|
|
222
|
+
|
|
223
|
+
- [Getting Started](docs/getting-started.md)
|
|
224
|
+
- [DSL Reference](docs/dsl-reference.md) — `@scenario`, `@task`, `on_start`, `check_*`, `client.batch()`
|
|
225
|
+
- [CLI Reference](docs/cli-reference.md) — all flags for `loadpilot run` and `loadpilot init`
|
|
226
|
+
- [Distributed Mode](docs/distributed.md) — local agents, remote agents, Railway, NATS
|
|
227
|
+
- [Benchmark](docs/benchmark.md) — methodology, full results, how to reproduce
|
|
228
|
+
- [Architecture](docs/architecture.md) — how the Python DSL and Rust engine interact
|
|
229
|
+
- [Development](docs/development.md) — building from source, running tests
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# LoadPilot
|
|
2
|
+
|
|
3
|
+
[](https://github.com/VladislavAkulich/loadpilot/actions)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
> Write load tests in Python. Run them at Rust speed.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
LoadPilot — CheckoutFlow [01:42] steady: 100/100 RPS
|
|
10
|
+
|
|
11
|
+
Requests/sec: 100.0 Total: 9,812
|
|
12
|
+
Errors: 0.0% Failed: 0
|
|
13
|
+
|
|
14
|
+
Latency:
|
|
15
|
+
p50: 12ms
|
|
16
|
+
p95: 28ms
|
|
17
|
+
p99: 41ms
|
|
18
|
+
max: 203ms
|
|
19
|
+
|
|
20
|
+
[████████████████████] 100%
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
LoadPilot is a load testing tool for teams that want to write scenarios in Python
|
|
24
|
+
without the throughput ceiling of a pure-Python engine.
|
|
25
|
+
|
|
26
|
+
Scenarios are plain Python classes. HTTP execution runs in async Rust.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install loadpilot
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
No Rust required — the coordinator binary is bundled in the wheel.
|
|
37
|
+
|
|
38
|
+
**Supported platforms:** Linux x86_64 · macOS Intel · macOS Apple Silicon · Windows x86_64
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# scaffold a project
|
|
46
|
+
loadpilot init my-load-tests
|
|
47
|
+
cd my-load-tests
|
|
48
|
+
|
|
49
|
+
# run a scenario
|
|
50
|
+
loadpilot run scenarios/example.py --target https://your-api.example.com
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or run without arguments to pick a scenario interactively:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
loadpilot run --target https://your-api.example.com
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
→ [Getting Started in 5 minutes](docs/getting-started.md)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Write a Scenario
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from loadpilot import VUser, scenario, task, LoadClient
|
|
67
|
+
|
|
68
|
+
@scenario(rps=100, duration="2m", ramp_up="30s")
|
|
69
|
+
class CheckoutFlow(VUser):
|
|
70
|
+
|
|
71
|
+
def on_start(self, client: LoadClient):
|
|
72
|
+
resp = client.post("/auth/login", json={"user": "test", "pass": "secret"})
|
|
73
|
+
self.token = resp.json()["access_token"]
|
|
74
|
+
|
|
75
|
+
@task(weight=5)
|
|
76
|
+
def browse(self, client: LoadClient):
|
|
77
|
+
client.get("/api/products", headers={"Authorization": f"Bearer {self.token}"})
|
|
78
|
+
|
|
79
|
+
def check_browse(self, status_code: int, body) -> None:
|
|
80
|
+
assert status_code == 200
|
|
81
|
+
assert isinstance(body, list)
|
|
82
|
+
|
|
83
|
+
@task(weight=1)
|
|
84
|
+
def purchase(self, client: LoadClient):
|
|
85
|
+
client.post("/api/orders", json={"product_id": 42, "qty": 1},
|
|
86
|
+
headers={"Authorization": f"Bearer {self.token}"})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `on_start` — runs once per virtual user before tasks start (login, setup)
|
|
90
|
+
- `@task` — weighted HTTP task; multiple tasks run in proportion
|
|
91
|
+
- `check_{task}` — assertion after each request; fail = error counted
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Features
|
|
96
|
+
|
|
97
|
+
| | |
|
|
98
|
+
|---|---|
|
|
99
|
+
| **Python DSL** | `@scenario`, `@task`, `on_start`, `check_*` — no new syntax to learn |
|
|
100
|
+
| **Load profiles** | ramp (default), constant, step, spike |
|
|
101
|
+
| **SLA thresholds** | fail CI with exit code 1 on p99/p95/error-rate breach |
|
|
102
|
+
| **Distributed mode** | coordinator + N agents over NATS; local or remote, free |
|
|
103
|
+
| **HTML report** | self-contained, open in any browser, no server required |
|
|
104
|
+
| **Prometheus metrics** | live Grafana dashboards while the test runs |
|
|
105
|
+
| **Interactive TUI** | `loadpilot run` with no args opens a scenario browser |
|
|
106
|
+
| **`pip install`** | coordinator binary bundled in the wheel, no Rust needed |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Performance
|
|
111
|
+
|
|
112
|
+
LoadPilot is benchmarked against k6 and Locust against a Rust/axum echo server in Docker.
|
|
113
|
+
LoadPilot runs in **PyO3 mode** — with `on_start` (login) and `check_*` (assertion per task) — a realistic workload with Python callbacks active.
|
|
114
|
+
|
|
115
|
+
### Precision — 500 RPS target
|
|
116
|
+
|
|
117
|
+
| Tool | RPS actual | p50 | p99 | Errors | CPU avg |
|
|
118
|
+
|------|-----------|-----|-----|--------|---------|
|
|
119
|
+
| **LoadPilot (PyO3)** | **478** | 4ms | 15ms | 0% | **14%** |
|
|
120
|
+
| k6 | 491 | 8ms | 118ms | 0% | 129% |
|
|
121
|
+
| Locust | 498 | 150ms | 1500ms | 0% | 88% |
|
|
122
|
+
|
|
123
|
+
LoadPilot uses **9× less CPU** than k6 at the same load. Locust reaches the RPS target but its Python/GIL scheduler adds significant latency (p99 ≥ 1500ms at only 500 RPS).
|
|
124
|
+
|
|
125
|
+
### Max throughput — 30s, no cap
|
|
126
|
+
|
|
127
|
+
| Tool | RPS | p50 | p99 | Errors | CPU avg |
|
|
128
|
+
|------|-----|-----|-----|--------|---------|
|
|
129
|
+
| **LoadPilot (PyO3)** | **2205** | 11ms | 38ms | 0% | **165%** |
|
|
130
|
+
| k6 | 1799 | 14ms | 175ms | 0% | 212% |
|
|
131
|
+
| Locust | 677 | 100ms | 170ms | 0% | 117% |
|
|
132
|
+
|
|
133
|
+
**1.2× k6** and **3.3× Locust** at max throughput. **1.6× better CPU efficiency per request** vs k6.
|
|
134
|
+
|
|
135
|
+
→ [Full benchmark details and methodology](docs/benchmark.md)
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## SLA Thresholds
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
@scenario(
|
|
143
|
+
rps=100, duration="2m",
|
|
144
|
+
thresholds={"p99_ms": 500, "p95_ms": 300, "error_rate": 1.0},
|
|
145
|
+
)
|
|
146
|
+
class CheckoutFlow(VUser): ...
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Thresholds
|
|
151
|
+
✓ p99 latency 41ms < 500ms
|
|
152
|
+
✓ p95 latency 28ms < 300ms
|
|
153
|
+
✓ error rate 0% < 1%
|
|
154
|
+
|
|
155
|
+
All thresholds passed.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Exit code `1` on breach — drop into any CI pipeline. Override without editing the file:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
loadpilot run scenarios/checkout.py --target https://staging.example.com \
|
|
162
|
+
--threshold p99_ms=800 --threshold error_rate=2
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Distributed Mode
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# 4 local processes
|
|
171
|
+
loadpilot run scenarios/checkout.py --target https://api.example.com --agents 4
|
|
172
|
+
|
|
173
|
+
# external agents on remote machines
|
|
174
|
+
loadpilot run scenarios/checkout.py \
|
|
175
|
+
--target https://api.example.com \
|
|
176
|
+
--external-agents 4
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The CLI output is identical to single-machine mode — the coordinator aggregates
|
|
180
|
+
everything transparently. `on_start` (login, setup) runs on the coordinator and
|
|
181
|
+
ships per-user auth headers to agents, so distributed tests can authenticate
|
|
182
|
+
without Python running on each agent.
|
|
183
|
+
|
|
184
|
+
→ [Distributed mode guide](docs/distributed.md)
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Live Monitoring
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# scaffolded by `loadpilot init`
|
|
192
|
+
docker compose -f monitoring/docker-compose.yml up -d
|
|
193
|
+
# Grafana → http://localhost:3000 (LoadPilot dashboard auto-imported)
|
|
194
|
+
# Prometheus → http://localhost:9091
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use Grafana to correlate load test metrics with your service's own metrics
|
|
198
|
+
(CPU, DB latency, error rates) on the same timeline.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Documentation
|
|
203
|
+
|
|
204
|
+
- [Getting Started](docs/getting-started.md)
|
|
205
|
+
- [DSL Reference](docs/dsl-reference.md) — `@scenario`, `@task`, `on_start`, `check_*`, `client.batch()`
|
|
206
|
+
- [CLI Reference](docs/cli-reference.md) — all flags for `loadpilot run` and `loadpilot init`
|
|
207
|
+
- [Distributed Mode](docs/distributed.md) — local agents, remote agents, Railway, NATS
|
|
208
|
+
- [Benchmark](docs/benchmark.md) — methodology, full results, how to reproduce
|
|
209
|
+
- [Architecture](docs/architecture.md) — how the Python DSL and Rust engine interact
|
|
210
|
+
- [Development](docs/development.md) — building from source, running tests
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Hatchling build hook — compiles the Rust coordinator and bundles it into the wheel.
|
|
2
|
+
|
|
3
|
+
During `pip install` from source (or `hatch build` in CI) this hook:
|
|
4
|
+
1. Runs `cargo build --release` for the coordinator crate.
|
|
5
|
+
2. Copies the resulting binary into `loadpilot/` so it is packaged as data.
|
|
6
|
+
3. Tells hatchling that the wheel is platform-specific (not pure Python).
|
|
7
|
+
|
|
8
|
+
When installing a pre-built wheel from PyPI the hook is NOT invoked — the binary
|
|
9
|
+
is already present inside the wheel (baked in by CI).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
21
|
+
|
|
22
|
+
# Path from cli/ (where pyproject.toml lives) up to the engine workspace root.
|
|
23
|
+
_ENGINE_DIR = Path(__file__).parent.parent / "engine"
|
|
24
|
+
_PKG_DIR = Path(__file__).parent / "loadpilot"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _binary_name() -> str:
|
|
28
|
+
return "coordinator.exe" if sys.platform == "win32" else "coordinator"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CustomBuildHook(BuildHookInterface):
|
|
32
|
+
"""Hatchling hook: build + bundle Rust coordinator binary."""
|
|
33
|
+
|
|
34
|
+
PLUGIN_NAME = "custom"
|
|
35
|
+
|
|
36
|
+
def initialize(self, version: str, build_data: dict) -> None:
|
|
37
|
+
if self.target_name not in ("wheel", "editable"):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
binary_name = _binary_name()
|
|
41
|
+
dst = _PKG_DIR / binary_name
|
|
42
|
+
|
|
43
|
+
# In CI the binary may already be built and copied by the workflow.
|
|
44
|
+
# Skip compilation if it's already there and SKIP_CARGO env var is set.
|
|
45
|
+
if dst.exists() and os.environ.get("LOADPILOT_SKIP_CARGO"):
|
|
46
|
+
self.app.display_info(f"[loadpilot] Skipping cargo build — {dst} already exists.")
|
|
47
|
+
else:
|
|
48
|
+
self.app.display_info("[loadpilot] Building Rust coordinator (cargo build --release)…")
|
|
49
|
+
env = os.environ.copy()
|
|
50
|
+
# PyO3 0.23 officially supports up to Python 3.13.
|
|
51
|
+
# This flag enables forward-compatibility with newer Python versions
|
|
52
|
+
# via the stable ABI — safe for binary-only use.
|
|
53
|
+
env.setdefault("PYO3_USE_ABI3_FORWARD_COMPATIBILITY", "1")
|
|
54
|
+
subprocess.run(
|
|
55
|
+
["cargo", "build", "--release", "--package", "coordinator", "--package", "agent"],
|
|
56
|
+
cwd=str(_ENGINE_DIR),
|
|
57
|
+
env=env,
|
|
58
|
+
check=True,
|
|
59
|
+
)
|
|
60
|
+
for name in [binary_name, "agent" + ("" if sys.platform != "win32" else ".exe")]:
|
|
61
|
+
src = _ENGINE_DIR / "target" / "release" / name
|
|
62
|
+
dst_bin = _PKG_DIR / name
|
|
63
|
+
shutil.copy2(src, dst_bin)
|
|
64
|
+
dst_bin.chmod(dst_bin.stat().st_mode | 0o111)
|
|
65
|
+
self.app.display_info(f"[loadpilot] Bundled {name} → {dst_bin}")
|
|
66
|
+
|
|
67
|
+
# Tell hatchling to include the binary in the wheel as package data.
|
|
68
|
+
build_data["shared_data"] = {}
|
|
69
|
+
build_data["artifacts"] = [str(dst)]
|
|
70
|
+
|
|
71
|
+
# Mark the wheel as platform-specific so pip picks the right one.
|
|
72
|
+
build_data["pure_python"] = False
|
|
73
|
+
build_data["infer_tag"] = True
|