jac-loadtest 0.3.0__tar.gz → 0.3.2__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.
- jac_loadtest-0.3.2/PKG-INFO +183 -0
- jac_loadtest-0.3.2/README.md +165 -0
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac.toml +1 -12
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/bridge/auth.jac +8 -39
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/cli.jac +39 -2
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/config.jac +3 -5
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/engine.jac +92 -2
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/metrics.jac +39 -3
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/process_runner.jac +11 -3
- jac_loadtest-0.3.2/jac_loadtest_cli/output/__init__.jac +0 -0
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/output/reporter.jac +122 -58
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/plugin.jac +2 -4
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/templates/reporter_template.html +38 -7
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/pyproject.toml +1 -14
- jac_loadtest-0.3.0/PKG-INFO +0 -107
- jac_loadtest-0.3.0/README.md +0 -80
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/_jac_build.py +0 -0
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/__init__.jac +0 -0
- /jac_loadtest-0.3.0/jac_loadtest_cli/bridge/__init__.jac → /jac_loadtest-0.3.2/jac_loadtest_cli/__init__.py +0 -0
- {jac_loadtest-0.3.0/jac_loadtest_cli/core → jac_loadtest-0.3.2/jac_loadtest_cli/bridge}/__init__.jac +0 -0
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/bridge/topology.jac +0 -0
- {jac_loadtest-0.3.0/jac_loadtest_cli/output → jac_loadtest-0.3.2/jac_loadtest_cli/core}/__init__.jac +0 -0
- {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/har_parser.jac +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jac-loadtest
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: HAR-based load testing library for jac-scale applications
|
|
5
|
+
Author-email: Sahan Udayanga <sahanudayangaof@gmail.com>, Ravimal Ranathunga <ravimalranathunga@gmail.com>
|
|
6
|
+
Maintainer-email: Sahan Udayanga <sahanudayangaof@gmail.com>, Ravimal Ranathunga <ravimalranathunga@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: load-testing,har,jac,jaclang,jaseci,jac-scale,performance
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Project-URL: Repository, https://github.com/SahanUday/jac-loadtest
|
|
11
|
+
Project-URL: Source, https://github.com/SahanUday/jac-loadtest/main/jac-loadtest-cli
|
|
12
|
+
Project-URL: Issues, https://github.com/SahanUday/jac-loadtest/issues
|
|
13
|
+
Requires-Dist: jac-scale>=0.2.16
|
|
14
|
+
Requires-Dist: aiohttp>=3.9.0,<4.0.0
|
|
15
|
+
Requires-Dist: rich>=13.0.0
|
|
16
|
+
Requires-Dist: requests>=2.28.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# jac-loadtest-cli
|
|
20
|
+
|
|
21
|
+
HAR-based load testing CLI built on [Jac](https://github.com/jaseci-labs/jaseci). Capture real browser traffic via Chrome DevTools, export it as a `.har` file, and replay it under load — no scripting required.
|
|
22
|
+
|
|
23
|
+
The tool registers itself as a `jac` subcommand, so after installation you run `jac loadtest` alongside `jac start`, `jac deploy`, and the rest of the jac ecosystem.
|
|
24
|
+
|
|
25
|
+
> **Compatibility:** Works with any HTTP server — jac-scale, Django, FastAPI, Node.js, etc. The only jac-scale-specific feature is auth: if your app uses jac-scale's `/user/login` JWT flow, credentials are automatically handled. For other auth schemes the raw request from the HAR is replayed as-is.
|
|
26
|
+
|
|
27
|
+
## Testing Modes
|
|
28
|
+
|
|
29
|
+
**Monolith mode** (default) — all requests go through a single `--url`. Use this for production-realistic load testing: it measures what users actually experience end-to-end through the gateway.
|
|
30
|
+
|
|
31
|
+
**Microservice mode** — route requests directly to individual service processes by URL path prefix. Use this locally or inside your cluster to isolate per-service latency and identify which service is the bottleneck — without gateway overhead masking the signal.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Monolith: all traffic through the gateway
|
|
35
|
+
jac loadtest recording.har --url http://localhost:8000 --vus 10 --iterations 20
|
|
36
|
+
|
|
37
|
+
# Microservice: bypass gateway, route by path prefix to individual services
|
|
38
|
+
jac loadtest recording.har --mode microservice \
|
|
39
|
+
--url http://localhost:8000 \
|
|
40
|
+
--services-map '{"order_service":"http://localhost:18001","inventory_service":"http://localhost:18002"}' \
|
|
41
|
+
--vus 10 --iterations 20
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> **Note:** Microservice mode requires direct network access to service ports. This means it's only usable locally (`jac serve`) or from inside a Kubernetes cluster — not from outside production. For remote or production load testing, use monolith mode.
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Minimal: 1 VU, 1 HAR replay
|
|
50
|
+
jac loadtest recording.har --url http://localhost:8000
|
|
51
|
+
|
|
52
|
+
# 50 VUs, each replaying the HAR 100 times, with 10s ramp-up
|
|
53
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
54
|
+
--vus 50 --iterations 100 --ramp-up 10s
|
|
55
|
+
|
|
56
|
+
# Realistic pacing: replay at recorded think times, halved
|
|
57
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
58
|
+
--vus 10 --iterations 30 --think-time scaled --think-time-scale 0.5
|
|
59
|
+
|
|
60
|
+
# Rate-limited stress test: cap global throughput to 50 req/s
|
|
61
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
62
|
+
--vus 10 --iterations 50 --rps 50
|
|
63
|
+
|
|
64
|
+
# CI gate: fail if p95 > 500ms or error rate > 1%, stop early on first breach
|
|
65
|
+
jac loadtest recording.har --url http://staging:8000 \
|
|
66
|
+
--vus 20 --iterations 100 \
|
|
67
|
+
--fail-on-p95 500 --fail-on-error-rate 1 \
|
|
68
|
+
--threshold-start-delay 10s --abort-on-fail
|
|
69
|
+
|
|
70
|
+
# Per-request debug output
|
|
71
|
+
jac loadtest recording.har --url http://localhost:8000 --vus 2 --iterations 5 --debug
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Exit Codes
|
|
75
|
+
|
|
76
|
+
| Code | Meaning |
|
|
77
|
+
|------|---------|
|
|
78
|
+
| `0` | Test completed; all thresholds passed (or none configured) |
|
|
79
|
+
| `1` | At least one threshold failed (`--fail-on-error-rate`, `--fail-on-p95`, `--fail-on-p99`) |
|
|
80
|
+
| `2` | Config or tool error (missing flag, bad HAR file, auth failure) |
|
|
81
|
+
|
|
82
|
+
Failed thresholds are printed to stderr before exit:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
THRESHOLD FAILED: error_rate 3.2% > limit 1.0%
|
|
86
|
+
THRESHOLD FAILED: p95 612.4ms > limit 500.0ms
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Key Flags
|
|
90
|
+
|
|
91
|
+
| Flag | Default | Description |
|
|
92
|
+
|------|---------|-------------|
|
|
93
|
+
| `--vus` | `1` | Concurrent virtual users (asyncio coroutines per worker) |
|
|
94
|
+
| `--iterations` | `1` | Stop each VU after N full HAR replays |
|
|
95
|
+
| `--ramp-up` | `0s` | Stagger VU startup — prevents thundering herd at test start |
|
|
96
|
+
| `--workers` | CPU count | Worker processes; each runs its own event loop (capped at `--vus` and CPU count) |
|
|
97
|
+
| `--rps` | `0` (unlimited) | Global requests-per-second cap across all VUs |
|
|
98
|
+
| `--timeout` | `30s` | Per-request timeout; exceeded requests are recorded as `TIMEOUT` errors |
|
|
99
|
+
| `--think-time` | `none` | `none` / `real` / `scaled` — inter-request delay from HAR timings |
|
|
100
|
+
| `--think-time-scale` | `1.0` | Multiplier on think times (`0.5` = half speed, `2.0` = double) |
|
|
101
|
+
| `--fail-on-error-rate` | — | Fail (exit 1) if global error rate exceeds N% |
|
|
102
|
+
| `--fail-on-p95` | — | Fail (exit 1) if global p95 latency exceeds N ms |
|
|
103
|
+
| `--fail-on-p99` | — | Fail (exit 1) if global p99 latency exceeds N ms |
|
|
104
|
+
| `--threshold-start-delay` | `0s` | Ignore threshold checks for this long after the test starts |
|
|
105
|
+
| `--abort-on-fail` | `false` | Stop the test immediately when a threshold is first breached |
|
|
106
|
+
| `--debug` | `false` | Print one line per request to stderr: VU ID, endpoint, status, latency |
|
|
107
|
+
| `--report-format` | `console` | `console` / `json` / `html` |
|
|
108
|
+
| `--report-out` | — | Write report to file (required for `--report-format html`) |
|
|
109
|
+
|
|
110
|
+
See [docs/COMMANDS.md](docs/COMMANDS.md) for the full flag reference including microservice mode, auth, and `jac.toml` configuration.
|
|
111
|
+
|
|
112
|
+
## Authentication
|
|
113
|
+
|
|
114
|
+
The tool auto-detects the login request in the HAR by matching `--login-path` (default `/user/login`). At test start each VU logs in once and injects the returned JWT into all subsequent requests.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# All VUs log in with the same account used during HAR recording
|
|
118
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
119
|
+
--username admin@example.com --password secret
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Developer Setup
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# 1. Navigate to the CLI directory
|
|
126
|
+
cd jac_loadtest_cli
|
|
127
|
+
|
|
128
|
+
# 2. Install the package in editable mode (also installs runtime deps)
|
|
129
|
+
jac install -e .
|
|
130
|
+
|
|
131
|
+
# 3. Verify the command is registered
|
|
132
|
+
jac loadtest --help
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Running tests
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cd jac_loadtest_cli
|
|
139
|
+
jac test tests/ # all 148 tests
|
|
140
|
+
jac test tests/unit/ # unit tests only
|
|
141
|
+
jac test tests/integration/ # integration tests (needs aiohttp servers)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Mock service for local testing
|
|
145
|
+
|
|
146
|
+
The `scripts/mock_service.jac` script spins up lightweight HTTP servers to test against without a real backend:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Start two fake services on ports 8001 and 8002
|
|
150
|
+
jac run scripts/mock_service.jac -- order_service:8001 inventory_service:8002
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Project Layout
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
jac_loadtest_cli/ ← Python package (importable as jac_loadtest_cli)
|
|
157
|
+
├── plugin.jac ← registers `jac loadtest` via jaclang entry-points
|
|
158
|
+
├── cli.jac ← argument wiring, run orchestration, exit codes
|
|
159
|
+
├── config.jac ← LoadTestConfig + three-layer config resolution
|
|
160
|
+
├── core/
|
|
161
|
+
│ ├── har_parser.jac ← parse HAR 1.2, filter, URL rewrite
|
|
162
|
+
│ ├── engine.jac ← asyncio VU coroutines, RPS cap, threshold watcher
|
|
163
|
+
│ ├── metrics.jac ← RequestResult, MetricsCollector, p50/p95/p99
|
|
164
|
+
│ └── process_runner.jac ← multi-process worker orchestration
|
|
165
|
+
├── bridge/
|
|
166
|
+
│ ├── auth.jac ← login detection, JWT injection, credential rotation
|
|
167
|
+
│ └── topology.jac ← TopologyRouter, longest-prefix matching
|
|
168
|
+
├── output/
|
|
169
|
+
│ └── reporter.jac ← Rich console, JSON, HTML reporters
|
|
170
|
+
└── scripts/
|
|
171
|
+
└── mock_service.jac ← lightweight fake HTTP servers for local testing
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## HAR Compatibility
|
|
175
|
+
|
|
176
|
+
Tested with HAR **1.1** and **1.2** (the format exported by Chrome DevTools, Firefox, Postman, and Insomnia). Files from other versions are parsed with a warning — open an issue if something breaks.
|
|
177
|
+
|
|
178
|
+
## Documentation
|
|
179
|
+
|
|
180
|
+
- [Architecture](docs/ARCHITECTURE.md) — module map, data flow, design decisions
|
|
181
|
+
- [Commands](docs/COMMANDS.md) — full CLI flag reference
|
|
182
|
+
- [Roadmap](docs/COMBINED_ROADMAP.md) — delivery phases for CLI and web UI
|
|
183
|
+
- [Testing](docs/TESTING.md) — test strategy and coverage guide
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# jac-loadtest-cli
|
|
2
|
+
|
|
3
|
+
HAR-based load testing CLI built on [Jac](https://github.com/jaseci-labs/jaseci). Capture real browser traffic via Chrome DevTools, export it as a `.har` file, and replay it under load — no scripting required.
|
|
4
|
+
|
|
5
|
+
The tool registers itself as a `jac` subcommand, so after installation you run `jac loadtest` alongside `jac start`, `jac deploy`, and the rest of the jac ecosystem.
|
|
6
|
+
|
|
7
|
+
> **Compatibility:** Works with any HTTP server — jac-scale, Django, FastAPI, Node.js, etc. The only jac-scale-specific feature is auth: if your app uses jac-scale's `/user/login` JWT flow, credentials are automatically handled. For other auth schemes the raw request from the HAR is replayed as-is.
|
|
8
|
+
|
|
9
|
+
## Testing Modes
|
|
10
|
+
|
|
11
|
+
**Monolith mode** (default) — all requests go through a single `--url`. Use this for production-realistic load testing: it measures what users actually experience end-to-end through the gateway.
|
|
12
|
+
|
|
13
|
+
**Microservice mode** — route requests directly to individual service processes by URL path prefix. Use this locally or inside your cluster to isolate per-service latency and identify which service is the bottleneck — without gateway overhead masking the signal.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Monolith: all traffic through the gateway
|
|
17
|
+
jac loadtest recording.har --url http://localhost:8000 --vus 10 --iterations 20
|
|
18
|
+
|
|
19
|
+
# Microservice: bypass gateway, route by path prefix to individual services
|
|
20
|
+
jac loadtest recording.har --mode microservice \
|
|
21
|
+
--url http://localhost:8000 \
|
|
22
|
+
--services-map '{"order_service":"http://localhost:18001","inventory_service":"http://localhost:18002"}' \
|
|
23
|
+
--vus 10 --iterations 20
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Note:** Microservice mode requires direct network access to service ports. This means it's only usable locally (`jac serve`) or from inside a Kubernetes cluster — not from outside production. For remote or production load testing, use monolith mode.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Minimal: 1 VU, 1 HAR replay
|
|
32
|
+
jac loadtest recording.har --url http://localhost:8000
|
|
33
|
+
|
|
34
|
+
# 50 VUs, each replaying the HAR 100 times, with 10s ramp-up
|
|
35
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
36
|
+
--vus 50 --iterations 100 --ramp-up 10s
|
|
37
|
+
|
|
38
|
+
# Realistic pacing: replay at recorded think times, halved
|
|
39
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
40
|
+
--vus 10 --iterations 30 --think-time scaled --think-time-scale 0.5
|
|
41
|
+
|
|
42
|
+
# Rate-limited stress test: cap global throughput to 50 req/s
|
|
43
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
44
|
+
--vus 10 --iterations 50 --rps 50
|
|
45
|
+
|
|
46
|
+
# CI gate: fail if p95 > 500ms or error rate > 1%, stop early on first breach
|
|
47
|
+
jac loadtest recording.har --url http://staging:8000 \
|
|
48
|
+
--vus 20 --iterations 100 \
|
|
49
|
+
--fail-on-p95 500 --fail-on-error-rate 1 \
|
|
50
|
+
--threshold-start-delay 10s --abort-on-fail
|
|
51
|
+
|
|
52
|
+
# Per-request debug output
|
|
53
|
+
jac loadtest recording.har --url http://localhost:8000 --vus 2 --iterations 5 --debug
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Exit Codes
|
|
57
|
+
|
|
58
|
+
| Code | Meaning |
|
|
59
|
+
|------|---------|
|
|
60
|
+
| `0` | Test completed; all thresholds passed (or none configured) |
|
|
61
|
+
| `1` | At least one threshold failed (`--fail-on-error-rate`, `--fail-on-p95`, `--fail-on-p99`) |
|
|
62
|
+
| `2` | Config or tool error (missing flag, bad HAR file, auth failure) |
|
|
63
|
+
|
|
64
|
+
Failed thresholds are printed to stderr before exit:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
THRESHOLD FAILED: error_rate 3.2% > limit 1.0%
|
|
68
|
+
THRESHOLD FAILED: p95 612.4ms > limit 500.0ms
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Key Flags
|
|
72
|
+
|
|
73
|
+
| Flag | Default | Description |
|
|
74
|
+
|------|---------|-------------|
|
|
75
|
+
| `--vus` | `1` | Concurrent virtual users (asyncio coroutines per worker) |
|
|
76
|
+
| `--iterations` | `1` | Stop each VU after N full HAR replays |
|
|
77
|
+
| `--ramp-up` | `0s` | Stagger VU startup — prevents thundering herd at test start |
|
|
78
|
+
| `--workers` | CPU count | Worker processes; each runs its own event loop (capped at `--vus` and CPU count) |
|
|
79
|
+
| `--rps` | `0` (unlimited) | Global requests-per-second cap across all VUs |
|
|
80
|
+
| `--timeout` | `30s` | Per-request timeout; exceeded requests are recorded as `TIMEOUT` errors |
|
|
81
|
+
| `--think-time` | `none` | `none` / `real` / `scaled` — inter-request delay from HAR timings |
|
|
82
|
+
| `--think-time-scale` | `1.0` | Multiplier on think times (`0.5` = half speed, `2.0` = double) |
|
|
83
|
+
| `--fail-on-error-rate` | — | Fail (exit 1) if global error rate exceeds N% |
|
|
84
|
+
| `--fail-on-p95` | — | Fail (exit 1) if global p95 latency exceeds N ms |
|
|
85
|
+
| `--fail-on-p99` | — | Fail (exit 1) if global p99 latency exceeds N ms |
|
|
86
|
+
| `--threshold-start-delay` | `0s` | Ignore threshold checks for this long after the test starts |
|
|
87
|
+
| `--abort-on-fail` | `false` | Stop the test immediately when a threshold is first breached |
|
|
88
|
+
| `--debug` | `false` | Print one line per request to stderr: VU ID, endpoint, status, latency |
|
|
89
|
+
| `--report-format` | `console` | `console` / `json` / `html` |
|
|
90
|
+
| `--report-out` | — | Write report to file (required for `--report-format html`) |
|
|
91
|
+
|
|
92
|
+
See [docs/COMMANDS.md](docs/COMMANDS.md) for the full flag reference including microservice mode, auth, and `jac.toml` configuration.
|
|
93
|
+
|
|
94
|
+
## Authentication
|
|
95
|
+
|
|
96
|
+
The tool auto-detects the login request in the HAR by matching `--login-path` (default `/user/login`). At test start each VU logs in once and injects the returned JWT into all subsequent requests.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# All VUs log in with the same account used during HAR recording
|
|
100
|
+
jac loadtest recording.har --url http://localhost:8000 \
|
|
101
|
+
--username admin@example.com --password secret
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Developer Setup
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# 1. Navigate to the CLI directory
|
|
108
|
+
cd jac_loadtest_cli
|
|
109
|
+
|
|
110
|
+
# 2. Install the package in editable mode (also installs runtime deps)
|
|
111
|
+
jac install -e .
|
|
112
|
+
|
|
113
|
+
# 3. Verify the command is registered
|
|
114
|
+
jac loadtest --help
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Running tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd jac_loadtest_cli
|
|
121
|
+
jac test tests/ # all 148 tests
|
|
122
|
+
jac test tests/unit/ # unit tests only
|
|
123
|
+
jac test tests/integration/ # integration tests (needs aiohttp servers)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Mock service for local testing
|
|
127
|
+
|
|
128
|
+
The `scripts/mock_service.jac` script spins up lightweight HTTP servers to test against without a real backend:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Start two fake services on ports 8001 and 8002
|
|
132
|
+
jac run scripts/mock_service.jac -- order_service:8001 inventory_service:8002
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Project Layout
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
jac_loadtest_cli/ ← Python package (importable as jac_loadtest_cli)
|
|
139
|
+
├── plugin.jac ← registers `jac loadtest` via jaclang entry-points
|
|
140
|
+
├── cli.jac ← argument wiring, run orchestration, exit codes
|
|
141
|
+
├── config.jac ← LoadTestConfig + three-layer config resolution
|
|
142
|
+
├── core/
|
|
143
|
+
│ ├── har_parser.jac ← parse HAR 1.2, filter, URL rewrite
|
|
144
|
+
│ ├── engine.jac ← asyncio VU coroutines, RPS cap, threshold watcher
|
|
145
|
+
│ ├── metrics.jac ← RequestResult, MetricsCollector, p50/p95/p99
|
|
146
|
+
│ └── process_runner.jac ← multi-process worker orchestration
|
|
147
|
+
├── bridge/
|
|
148
|
+
│ ├── auth.jac ← login detection, JWT injection, credential rotation
|
|
149
|
+
│ └── topology.jac ← TopologyRouter, longest-prefix matching
|
|
150
|
+
├── output/
|
|
151
|
+
│ └── reporter.jac ← Rich console, JSON, HTML reporters
|
|
152
|
+
└── scripts/
|
|
153
|
+
└── mock_service.jac ← lightweight fake HTTP servers for local testing
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## HAR Compatibility
|
|
157
|
+
|
|
158
|
+
Tested with HAR **1.1** and **1.2** (the format exported by Chrome DevTools, Firefox, Postman, and Insomnia). Files from other versions are parsed with a warning — open an issue if something breaks.
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
161
|
+
|
|
162
|
+
- [Architecture](docs/ARCHITECTURE.md) — module map, data flow, design decisions
|
|
163
|
+
- [Commands](docs/COMMANDS.md) — full CLI flag reference
|
|
164
|
+
- [Roadmap](docs/COMBINED_ROADMAP.md) — delivery phases for CLI and web UI
|
|
165
|
+
- [Testing](docs/TESTING.md) — test strategy and coverage guide
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "jac-loadtest"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "HAR-based load testing library for jac-scale applications"
|
|
5
5
|
authors = [{ name = "Sahan Udayanga", email = "sahanudayangaof@gmail.com" }, { name = "Ravimal Ranathunga", email = "ravimalranathunga@gmail.com" }]
|
|
6
6
|
maintainers = [{ name = "Sahan Udayanga", email = "sahanudayangaof@gmail.com" }, { name = "Ravimal Ranathunga", email = "ravimalranathunga@gmail.com" }]
|
|
@@ -26,16 +26,5 @@ aiohttp = ">=3.9.0,<4.0.0"
|
|
|
26
26
|
rich = ">=13.0.0"
|
|
27
27
|
requests = ">=2.28.0"
|
|
28
28
|
|
|
29
|
-
[dev-dependencies]
|
|
30
|
-
pytest = ">=8.0"
|
|
31
|
-
pytest-asyncio = ">=0.23"
|
|
32
|
-
pytest-mock = ">=3.12"
|
|
33
|
-
|
|
34
|
-
[optional-dependencies.test]
|
|
35
|
-
pytest = ">=8.0"
|
|
36
|
-
pytest-asyncio = ">=0.23"
|
|
37
|
-
pytest-mock = ">=3.12"
|
|
38
|
-
"aiohttp[speedups]" = ">=3.9.0,<4.0.0"
|
|
39
|
-
|
|
40
29
|
[entrypoints.jac]
|
|
41
30
|
loadtest = "jac_loadtest_cli.plugin:loadtest"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import csv;
|
|
2
1
|
import aiohttp;
|
|
3
2
|
import from ..config { LoadTestConfig }
|
|
4
3
|
|
|
@@ -10,26 +9,21 @@ obj Credential {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
obj AuthProvider {
|
|
13
|
-
has
|
|
12
|
+
has _credential: Credential;
|
|
14
13
|
has _login_path: str = "/user/login";
|
|
15
14
|
|
|
16
15
|
class def from_config(config: LoadTestConfig) -> AuthProvider | None {
|
|
17
|
-
if config.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return None;
|
|
16
|
+
if config.username and config.password {
|
|
17
|
+
return AuthProvider(
|
|
18
|
+
_credential=Credential(username=config.username, password=config.password),
|
|
19
|
+
_login_path=config.login_path
|
|
20
|
+
);
|
|
23
21
|
}
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
def get_credential(vu_id: int) -> Credential {
|
|
28
|
-
return self._credentials[vu_id % len(self._credentials)];
|
|
22
|
+
return None;
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
async def authenticate(vu_id: int, session: aiohttp.ClientSession, base_url: str) -> str {
|
|
32
|
-
cred = self.
|
|
26
|
+
cred = self._credential;
|
|
33
27
|
identity_type = "email" if "@" in cred.username else "username";
|
|
34
28
|
payload = {
|
|
35
29
|
"identity": {"type": identity_type, "value": cred.username},
|
|
@@ -67,28 +61,3 @@ obj AuthProvider {
|
|
|
67
61
|
}
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
def _load_csv(path: str) -> list[Credential] {
|
|
71
|
-
credentials = [];
|
|
72
|
-
with open(path, newline="") as f {
|
|
73
|
-
reader = csv.reader(f);
|
|
74
|
-
i = 0;
|
|
75
|
-
for row in reader {
|
|
76
|
-
if len(row) < 2 {
|
|
77
|
-
i += 1;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
username = row[0].strip();
|
|
81
|
-
password = row[1].strip();
|
|
82
|
-
if i == 0 and username.lower() == "username" {
|
|
83
|
-
i += 1;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
credentials.append(Credential(username=username, password=password));
|
|
87
|
-
i += 1;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if not credentials {
|
|
91
|
-
raise ValueError(f"No credentials found in {path!r}");
|
|
92
|
-
}
|
|
93
|
-
return credentials;
|
|
94
|
-
}
|
|
@@ -4,7 +4,7 @@ import time;
|
|
|
4
4
|
import from .config { from_args, LoadTestConfig }
|
|
5
5
|
import from .core.har_parser { parse_har, HarEntry }
|
|
6
6
|
import from .core.engine { run_all_vus }
|
|
7
|
-
import from .core.metrics { MetricsCollector }
|
|
7
|
+
import from .core.metrics { MetricsCollector, percentile }
|
|
8
8
|
import from .core.process_runner { run_multiprocess }
|
|
9
9
|
import from .output.reporter { render_console, render_json, render_html }
|
|
10
10
|
import from .bridge.auth { AuthProvider, AuthenticationError }
|
|
@@ -89,7 +89,7 @@ def run(args: object) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
duration_s = time.time() - t_start;
|
|
92
|
-
stats = metrics.compute_endpoint_stats(t_start=t_start);
|
|
92
|
+
stats = metrics.compute_endpoint_stats(t_start=t_start, apdex_t=config.apdex_t, actual_duration_s=duration_s);
|
|
93
93
|
snapshots = metrics.generate_timeseries(t_start);
|
|
94
94
|
_comp = metrics.completion_percentiles(t_start);
|
|
95
95
|
completion_p50 = _comp[0];
|
|
@@ -147,6 +147,43 @@ def run(args: object) {
|
|
|
147
147
|
completion_p99_s=completion_p99,
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
threshold_failures: list[str] = [];
|
|
152
|
+
if metrics.total_count > 0 {
|
|
153
|
+
if config.fail_on_error_rate is not None {
|
|
154
|
+
actual_rate = metrics.error_count / metrics.total_count * 100.0;
|
|
155
|
+
if actual_rate > config.fail_on_error_rate {
|
|
156
|
+
threshold_failures.append(
|
|
157
|
+
f"error_rate {actual_rate:.1f}% > limit {config.fail_on_error_rate:.1f}%"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
all_latencies = [r.latency_ms for r in metrics._samples];
|
|
162
|
+
if all_latencies {
|
|
163
|
+
if config.fail_on_p95 is not None {
|
|
164
|
+
p95_val = percentile(all_latencies, 95);
|
|
165
|
+
if p95_val > config.fail_on_p95 {
|
|
166
|
+
threshold_failures.append(
|
|
167
|
+
f"p95 {p95_val:.1f}ms > limit {config.fail_on_p95:.1f}ms"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if config.fail_on_p99 is not None {
|
|
172
|
+
p99_val = percentile(all_latencies, 99);
|
|
173
|
+
if p99_val > config.fail_on_p99 {
|
|
174
|
+
threshold_failures.append(
|
|
175
|
+
f"p99 {p99_val:.1f}ms > limit {config.fail_on_p99:.1f}ms"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if threshold_failures {
|
|
182
|
+
for msg in threshold_failures {
|
|
183
|
+
print(f"THRESHOLD FAILED: {msg}", file=sys.stderr);
|
|
184
|
+
}
|
|
185
|
+
sys.exit(1);
|
|
186
|
+
}
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
|
|
@@ -2,7 +2,6 @@ import os;
|
|
|
2
2
|
|
|
3
3
|
glob BUILT_IN_DEFAULTS: dict[str, object] = {
|
|
4
4
|
"vus": 1,
|
|
5
|
-
"duration": "30s",
|
|
6
5
|
"iterations": 1,
|
|
7
6
|
"ramp_up": "0s",
|
|
8
7
|
"timeout": "30s",
|
|
@@ -22,11 +21,11 @@ glob BUILT_IN_DEFAULTS: dict[str, object] = {
|
|
|
22
21
|
"max_samples": 1_000_000,
|
|
23
22
|
"csrf": False,
|
|
24
23
|
"debug": False,
|
|
24
|
+
"apdex_t": 500.0,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
obj LoadTestConfig {
|
|
28
28
|
has vus: int = 1;
|
|
29
|
-
has duration: str = "30s";
|
|
30
29
|
has iterations: int = 1;
|
|
31
30
|
has ramp_up: str = "0s";
|
|
32
31
|
has timeout: str = "30s";
|
|
@@ -46,11 +45,11 @@ obj LoadTestConfig {
|
|
|
46
45
|
has report_format: str = "console";
|
|
47
46
|
has max_samples: int = 1_000_000;
|
|
48
47
|
has debug: bool = False;
|
|
48
|
+
has apdex_t: float = 500.0;
|
|
49
49
|
has har_file: str = "";
|
|
50
50
|
has url: str | None = None;
|
|
51
51
|
has username: str | None = None;
|
|
52
52
|
has password: str | None = None;
|
|
53
|
-
has credentials_file: str | None = None;
|
|
54
53
|
has services_map: str | None = None;
|
|
55
54
|
has report_out: str | None = None;
|
|
56
55
|
}
|
|
@@ -167,14 +166,12 @@ def from_args(args: object) -> LoadTestConfig {
|
|
|
167
166
|
url=_getattr_opt_str(args, "url"),
|
|
168
167
|
username=_getattr_opt_str(args, "username"),
|
|
169
168
|
password=_getattr_opt_str(args, "password"),
|
|
170
|
-
credentials_file=_getattr_opt_str(args, "credentials_file"),
|
|
171
169
|
services_map=_getattr_opt_str(args, "services_map"),
|
|
172
170
|
report_out=_getattr_opt_str(args, "report_out"),
|
|
173
171
|
iterations=_resolve_int("iterations", args, toml),
|
|
174
172
|
mode=_resolve_str("mode", args, toml),
|
|
175
173
|
vus=_resolve_int("vus", args, toml),
|
|
176
174
|
workers=_resolve_int("workers", args, toml),
|
|
177
|
-
duration=_resolve_str("duration", args, toml),
|
|
178
175
|
ramp_up=_resolve_str("ramp_up", args, toml),
|
|
179
176
|
timeout=_resolve_str("timeout", args, toml),
|
|
180
177
|
think_time=_resolve_str("think_time", args, toml),
|
|
@@ -191,5 +188,6 @@ def from_args(args: object) -> LoadTestConfig {
|
|
|
191
188
|
threshold_start_delay=_resolve_str("threshold_start_delay", args, toml),
|
|
192
189
|
report_format=_resolve_str("report_format", args, toml),
|
|
193
190
|
debug=_resolve_bool("debug", args, toml),
|
|
191
|
+
apdex_t=_resolve_float("apdex_t", args, toml),
|
|
194
192
|
);
|
|
195
193
|
}
|