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.
Files changed (23) hide show
  1. jac_loadtest-0.3.2/PKG-INFO +183 -0
  2. jac_loadtest-0.3.2/README.md +165 -0
  3. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac.toml +1 -12
  4. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/bridge/auth.jac +8 -39
  5. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/cli.jac +39 -2
  6. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/config.jac +3 -5
  7. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/engine.jac +92 -2
  8. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/metrics.jac +39 -3
  9. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/core/process_runner.jac +11 -3
  10. jac_loadtest-0.3.2/jac_loadtest_cli/output/__init__.jac +0 -0
  11. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/output/reporter.jac +122 -58
  12. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/plugin.jac +2 -4
  13. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/templates/reporter_template.html +38 -7
  14. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/pyproject.toml +1 -14
  15. jac_loadtest-0.3.0/PKG-INFO +0 -107
  16. jac_loadtest-0.3.0/README.md +0 -80
  17. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/_jac_build.py +0 -0
  18. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/__init__.jac +0 -0
  19. /jac_loadtest-0.3.0/jac_loadtest_cli/bridge/__init__.jac → /jac_loadtest-0.3.2/jac_loadtest_cli/__init__.py +0 -0
  20. {jac_loadtest-0.3.0/jac_loadtest_cli/core → jac_loadtest-0.3.2/jac_loadtest_cli/bridge}/__init__.jac +0 -0
  21. {jac_loadtest-0.3.0 → jac_loadtest-0.3.2}/jac_loadtest_cli/bridge/topology.jac +0 -0
  22. {jac_loadtest-0.3.0/jac_loadtest_cli/output → jac_loadtest-0.3.2/jac_loadtest_cli/core}/__init__.jac +0 -0
  23. {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.0"
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 _credentials: list[Credential];
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.credentials_file {
18
- creds = _load_csv(config.credentials_file);
19
- } elif config.username and config.password {
20
- creds = [Credential(username=config.username, password=config.password)];
21
- } else {
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 AuthProvider(_credentials=creds, _login_path=config.login_path);
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.get_credential(vu_id);
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
  }