@cloudbeat/atlas-cli 0.2.0 → 0.4.0
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.
- package/README.md +52 -19
- package/dist/api.js +70 -18
- package/dist/cli.js +34 -6
- package/dist/commands/analyze.js +118 -0
- package/dist/commands/capacity.js +99 -0
- package/dist/commands/docs.js +57 -0
- package/dist/commands/files.js +18 -4
- package/dist/commands/plan.js +100 -0
- package/dist/commands/record.js +11 -1
- package/dist/commands/results.js +13 -7
- package/dist/commands/schema.js +86 -0
- package/dist/commands/skills.js +111 -0
- package/dist/commands/tests.js +73 -0
- package/dist/docs/agent-setup.md +109 -0
- package/dist/docs/agent-tasks.md +169 -0
- package/dist/docs/ai-agents.md +90 -0
- package/dist/docs/auth.md +74 -0
- package/dist/docs/automation.md +150 -0
- package/dist/docs/ci.md +105 -0
- package/dist/docs/clusters.md +48 -0
- package/dist/docs/commands.md +155 -0
- package/dist/docs/files.md +86 -0
- package/dist/docs/generators.md +52 -0
- package/dist/docs/getting-started.md +85 -0
- package/dist/docs/index.json +110 -0
- package/dist/docs/projects.md +50 -0
- package/dist/docs/recorder.md +143 -0
- package/dist/docs/results.md +79 -0
- package/dist/docs/running.md +80 -0
- package/dist/docs/skills.md +88 -0
- package/dist/docs/test-plans.md +92 -0
- package/dist/docs/troubleshooting.md +79 -0
- package/dist/plan.js +250 -0
- package/dist/skills/AGENTS.md +70 -0
- package/dist/skills/atlas-analyze/SKILL.md +104 -0
- package/dist/skills/atlas-correlate/SKILL.md +89 -0
- package/dist/skills/atlas-load-test/SKILL.md +154 -0
- package/dist/skills/atlas-record/SKILL.md +167 -0
- package/dist/skills/atlas-run/SKILL.md +109 -0
- package/dist/skills/atlas-test-plan/SKILL.md +281 -0
- package/package.json +5 -3
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Reading Results
|
|
2
|
+
|
|
3
|
+
Every run has a dashboard. Open it from a project's **Results** tab, or from the
|
|
4
|
+
test list at `/<project>`.
|
|
5
|
+
|
|
6
|
+
The dashboard refreshes itself while a test is running, so you can leave it open
|
|
7
|
+
from start to finish.
|
|
8
|
+
|
|
9
|
+
## Headline numbers
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **Status** | Whether the test is still running |
|
|
14
|
+
| **Test Started** | When it began |
|
|
15
|
+
| **Duration** | How long it has been running, or ran for |
|
|
16
|
+
| **Max Users** | The peak number of concurrent users, added up across every machine |
|
|
17
|
+
| **Req/sec** | Throughput |
|
|
18
|
+
| **Avg Response Time** | Average across the whole test |
|
|
19
|
+
| **Error Rate** | Failed requests as a percentage of all requests, across the whole test |
|
|
20
|
+
|
|
21
|
+
Each load generator reports its own thread count, and Atlas adds them up. If you
|
|
22
|
+
ran a 100-thread plan on 5 machines, expect roughly 500 — if it shows 100,
|
|
23
|
+
something stopped four of your generators from producing load.
|
|
24
|
+
|
|
25
|
+
Average response time and error rate are **whole-test** figures, not the last
|
|
26
|
+
few seconds, so they won't jump around as the test finishes.
|
|
27
|
+
|
|
28
|
+
## The chart
|
|
29
|
+
|
|
30
|
+
Three series on one timeline:
|
|
31
|
+
|
|
32
|
+
- **Active Users** (green, right axis) — concurrency over time
|
|
33
|
+
- **Avg. Response Time** (blue, left axis) — how the system responded as load grew
|
|
34
|
+
- **Error Rate %** (red) — when things started failing
|
|
35
|
+
|
|
36
|
+
Read together, these tell you where your system's limit is: the point where
|
|
37
|
+
response time climbs and errors begin while users are still increasing.
|
|
38
|
+
|
|
39
|
+
## The aggregation report
|
|
40
|
+
|
|
41
|
+
One row per request, grouped by thread group, with count, response times
|
|
42
|
+
(average, median, percentiles, min/max), throughput and error percentage.
|
|
43
|
+
|
|
44
|
+
Every column sorts. Rows are colour-coded:
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **TOTAL** | The whole test |
|
|
49
|
+
| **Thread group** | One per thread group in your plan |
|
|
50
|
+
| **Transaction** | A named step, if your plan uses Transaction Controllers |
|
|
51
|
+
| **Request** | An individual HTTP request |
|
|
52
|
+
|
|
53
|
+
The same sampler name in two different thread groups stays two separate rows,
|
|
54
|
+
attributed to the right group — so a shared "login" step used by several groups
|
|
55
|
+
doesn't get merged into one misleading number.
|
|
56
|
+
|
|
57
|
+
### Drilling in
|
|
58
|
+
|
|
59
|
+
- **Click an Error %** — opens a panel listing the actual errors for that
|
|
60
|
+
request: response codes and messages, with the full request and response for
|
|
61
|
+
each distinct failure.
|
|
62
|
+
- **Click a Label** — opens the same chart filtered to that one request, so you
|
|
63
|
+
can see whether a single endpoint degraded while the rest stayed healthy.
|
|
64
|
+
There's an error-type dropdown to isolate one specific failure.
|
|
65
|
+
|
|
66
|
+
## Exporting
|
|
67
|
+
|
|
68
|
+
**↓ Export Excel** downloads the run as a spreadsheet with the chart and the full
|
|
69
|
+
table, colour-coded to match. Useful for attaching to a report.
|
|
70
|
+
|
|
71
|
+
## Dark mode
|
|
72
|
+
|
|
73
|
+
Add `?theme=dark` to any dashboard URL.
|
|
74
|
+
|
|
75
|
+
## Comparing runs
|
|
76
|
+
|
|
77
|
+
There is no side-by-side comparison view yet. Name your runs (in the Results
|
|
78
|
+
tab) so you can tell variations apart, and export to Excel to compare them
|
|
79
|
+
outside Atlas.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Running a Test
|
|
2
|
+
|
|
3
|
+
## From the web interface
|
|
4
|
+
|
|
5
|
+
Open a project, go to **Run Test**, choose where to run, and press **Run**.
|
|
6
|
+
|
|
7
|
+
There is no separate deploy step. Atlas reserves the machines, powers on any
|
|
8
|
+
that are switched off, copies the files, and starts JMeter.
|
|
9
|
+
|
|
10
|
+
## Choosing where it runs
|
|
11
|
+
|
|
12
|
+
| | |
|
|
13
|
+
|---|---|
|
|
14
|
+
| **Servers** | Pick individual load generators by name. Switched-off machines can still be selected — they are powered on for you. |
|
|
15
|
+
| **Cluster** | Run on a cluster and say how many units you want. Units are created for the test and destroyed afterwards. |
|
|
16
|
+
|
|
17
|
+
The number of machines multiplies your load: a plan with 100 threads run on 5
|
|
18
|
+
generators applies 500 concurrent users to your system.
|
|
19
|
+
|
|
20
|
+
Atlas does not divide your thread count across machines — each one runs the plan
|
|
21
|
+
as written. Five generators running a 100-thread plan is 500 users, not 100.
|
|
22
|
+
|
|
23
|
+
## From the CLI
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
atlas run -p SHOP --jmx checkout.jmx --servers 5 --wait
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| | |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `--servers 5` | use any 5 available load generators |
|
|
32
|
+
| `--server gen-01 --server gen-02` | pick specific ones by name |
|
|
33
|
+
| `--cluster my-cluster --units 10` | run on 10 units in a cluster |
|
|
34
|
+
| `--jmx <filename>` | which uploaded plan to run (needed only if the project holds more than one) |
|
|
35
|
+
| `--args "<jmeter args>"` | extra JMeter arguments |
|
|
36
|
+
| `--no-split` | send whole data files to every machine |
|
|
37
|
+
| `--wait` | follow the run and print the report when it finishes |
|
|
38
|
+
| `--timeout 30m` | how long `--wait` waits before giving up (default `2h`) |
|
|
39
|
+
|
|
40
|
+
## While it runs
|
|
41
|
+
|
|
42
|
+
The Run Test tab becomes a live view showing each stage — powering on,
|
|
43
|
+
deploying, running — with a **Stop** button.
|
|
44
|
+
|
|
45
|
+
**Watch** in the sidebar shows live per-machine statistics while a test is in
|
|
46
|
+
progress, which is the quickest way to confirm every generator is actually
|
|
47
|
+
producing load.
|
|
48
|
+
|
|
49
|
+
From the CLI:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
atlas status SHOP-20260818-194201
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## If a machine is busy
|
|
56
|
+
|
|
57
|
+
A load generator can only run one test at a time. If you ask for machines that
|
|
58
|
+
are already in use, your run is **queued** and starts when they free up. You
|
|
59
|
+
don't need to retry.
|
|
60
|
+
|
|
61
|
+
## Stopping a test
|
|
62
|
+
|
|
63
|
+
Press **Stop**, or:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
atlas stop SHOP-20260818-194201
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Atlas asks JMeter to shut down cleanly so the final results are recorded, and
|
|
70
|
+
only reports the machine as stopped once the process has actually ended.
|
|
71
|
+
|
|
72
|
+
If a machine can't be reached when you stop a test, Atlas tells you rather than
|
|
73
|
+
pretending it worked, and the run stays open. Retry once the machine is
|
|
74
|
+
reachable — this prevents a test carrying on unnoticed in the background.
|
|
75
|
+
|
|
76
|
+
## When a test ends
|
|
77
|
+
|
|
78
|
+
A test ends by itself when the plan says so — a thread group's duration or loop
|
|
79
|
+
count running out. When it does, the machines are released and any that Atlas
|
|
80
|
+
powered on are switched off.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# The Skills
|
|
2
|
+
|
|
3
|
+
`atlas skills install` writes six skills plus a vendor-neutral `AGENTS.md`. Each
|
|
4
|
+
skill is a short document covering one part of load testing: what to do, in what
|
|
5
|
+
order, and which mistakes to avoid.
|
|
6
|
+
|
|
7
|
+
They are plain Markdown. Read them, edit them for your project, commit them —
|
|
8
|
+
they are yours once installed.
|
|
9
|
+
|
|
10
|
+
## How they load
|
|
11
|
+
|
|
12
|
+
Claude Code reads each skill's description and loads the one that fits your
|
|
13
|
+
request, so you never name a skill yourself. Ask *"why did the test fail?"* and
|
|
14
|
+
it loads the results skill; ask *"record a checkout"* and it loads the recording
|
|
15
|
+
one. Several can load in one conversation as the work moves on.
|
|
16
|
+
|
|
17
|
+
Assistants without skill support read the same guidance, condensed, from
|
|
18
|
+
`AGENTS.md`.
|
|
19
|
+
|
|
20
|
+
## What each one covers
|
|
21
|
+
|
|
22
|
+
### `atlas-load-test`
|
|
23
|
+
|
|
24
|
+
The end-to-end flow, and the one that usually loads first for *"load test our
|
|
25
|
+
checkout"*. Picking or creating a plan, uploading data, sizing, the mandatory
|
|
26
|
+
smoke run, then the real run and the report. Mostly it sequences the others.
|
|
27
|
+
|
|
28
|
+
### `atlas-record`
|
|
29
|
+
|
|
30
|
+
Recording a scenario from a real browser. Choosing who clicks — you or the
|
|
31
|
+
assistant — driving the browser through the accessibility tree, naming
|
|
32
|
+
transactions before performing them, signing in without handing over a password,
|
|
33
|
+
and the traps: native dialogs that freeze a session, one click recording two
|
|
34
|
+
requests, everything before the first action landing in one transaction.
|
|
35
|
+
|
|
36
|
+
### `atlas-test-plan`
|
|
37
|
+
|
|
38
|
+
Plans and data files, and the longest of the six. Downloading before editing,
|
|
39
|
+
the three kinds of change in order of preference, and how these plans are laid
|
|
40
|
+
out — think-time settings, host settings, the numbered transactions that make a
|
|
41
|
+
report readable, and sizing through run-time properties.
|
|
42
|
+
|
|
43
|
+
It also carries the two things that save the most time in practice: the stages
|
|
44
|
+
for turning a fresh recording into a real test (flow → test data → dynamic
|
|
45
|
+
values → checks → upload), and a local debugging recipe that switches think time
|
|
46
|
+
off in a throwaway copy, runs one user, and restores the original — seconds
|
|
47
|
+
instead of minutes per attempt.
|
|
48
|
+
|
|
49
|
+
### `atlas-correlate`
|
|
50
|
+
|
|
51
|
+
Values the server issues per session — CSRF tokens, session ids, cart ids — and
|
|
52
|
+
why a replayed one produces a fast, clean, meaningless test. Reviewing what the
|
|
53
|
+
recorder detected with its evidence, pinning what it missed, and telling those
|
|
54
|
+
apart from values that belong in a data file instead.
|
|
55
|
+
|
|
56
|
+
### `atlas-run`
|
|
57
|
+
|
|
58
|
+
Starting and sizing a run: how users map onto machines, ramp-up, fixed machines
|
|
59
|
+
versus a cluster, pass/fail thresholds, exit codes and CI usage, and what to do
|
|
60
|
+
while a run is queued behind someone else's.
|
|
61
|
+
|
|
62
|
+
### `atlas-analyze`
|
|
63
|
+
|
|
64
|
+
Reading a result: the aggregation report, the shape of a run over time, the
|
|
65
|
+
errors behind a failure — and recognising the failure modes where a broken test
|
|
66
|
+
looks like a healthy one. If you only read one, read this one.
|
|
67
|
+
|
|
68
|
+
## Editing them
|
|
69
|
+
|
|
70
|
+
Installed skills are ordinary files in your repository. Adding your own
|
|
71
|
+
conventions is normal and encouraged:
|
|
72
|
+
|
|
73
|
+
- which project maps to which service
|
|
74
|
+
- the thresholds your team gates on
|
|
75
|
+
- environments that must never be load tested
|
|
76
|
+
- who to ask before a run above a certain size
|
|
77
|
+
|
|
78
|
+
`atlas skills install` will not overwrite a file you have edited unless you pass
|
|
79
|
+
`--force`, so your changes survive an upgrade. When you do upgrade, diff first.
|
|
80
|
+
|
|
81
|
+
## Listing what you have
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
atlas skills list # what this CLI version carries
|
|
85
|
+
atlas skills install --force # refresh after upgrading the CLI
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
[Build your own integration →](/agents/automation)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Test Plan Requirements
|
|
2
|
+
|
|
3
|
+
Atlas runs standard JMeter test plans, but a plan written on a desktop needs a
|
|
4
|
+
few changes before it runs correctly on a fleet of Linux load generators. These
|
|
5
|
+
are the things that actually catch people out.
|
|
6
|
+
|
|
7
|
+
## Data file paths
|
|
8
|
+
|
|
9
|
+
Reference data files by a plain relative path:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
./users.csv ✅
|
|
13
|
+
users.csv ✅
|
|
14
|
+
C:\Users\me\users.csv ❌
|
|
15
|
+
/home/me/data/users.csv ❌
|
|
16
|
+
./data/users.csv ❌
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A project's files live in one flat folder — see [Test Files](/guide/test-files).
|
|
20
|
+
|
|
21
|
+
If a **CSV Data Set Config** path can't be resolved, every thread in that thread
|
|
22
|
+
group throws before issuing a single request. There are no failed samples: the
|
|
23
|
+
summary reports `0.00%` errors and the dashboard **omits that thread group**
|
|
24
|
+
entirely while the others look healthy.
|
|
25
|
+
|
|
26
|
+
If a thread group is missing from your results, check its data file paths first.
|
|
27
|
+
|
|
28
|
+
Every file the plan references must also actually be uploaded.
|
|
29
|
+
|
|
30
|
+
## Disable GUI listeners
|
|
31
|
+
|
|
32
|
+
Switch these off before uploading:
|
|
33
|
+
|
|
34
|
+
- **View Results Tree**
|
|
35
|
+
- **Aggregate Report**
|
|
36
|
+
- **Simple Data Writer**
|
|
37
|
+
|
|
38
|
+
They consume a lot of memory during a headless run — enough to destabilise a
|
|
39
|
+
long test — and Atlas collects its own results regardless. A **Simple Data
|
|
40
|
+
Writer** pointing at a `C:\...` path is doubly useless.
|
|
41
|
+
|
|
42
|
+
## Thread counts are per machine
|
|
43
|
+
|
|
44
|
+
Atlas does not divide your threads across machines. A 100-thread plan on 5
|
|
45
|
+
generators is **500** concurrent users.
|
|
46
|
+
|
|
47
|
+
Decide the number you want in total, then divide by how many generators you plan
|
|
48
|
+
to use.
|
|
49
|
+
|
|
50
|
+
## Header rows in CSV files
|
|
51
|
+
|
|
52
|
+
Whether line 1 of a data file is a header is decided by your plan's **CSV Data
|
|
53
|
+
Set Config**, not guessed:
|
|
54
|
+
|
|
55
|
+
- If **Variable Names** is filled in and **Ignore first line** is true, line 1 is
|
|
56
|
+
a header.
|
|
57
|
+
- If **Variable Names** is empty, JMeter reads line 1 as the names — so it counts
|
|
58
|
+
as a header too.
|
|
59
|
+
|
|
60
|
+
Get this right and each machine's slice keeps the header while the data rows are
|
|
61
|
+
divided. Get it wrong and you either inject a bogus row or lose a real one.
|
|
62
|
+
|
|
63
|
+
## Timers and pacing
|
|
64
|
+
|
|
65
|
+
Think time matters more in a distributed test, not less. Without it, every
|
|
66
|
+
virtual user hammers as fast as the network allows, which produces a number that
|
|
67
|
+
looks impressive and means nothing.
|
|
68
|
+
|
|
69
|
+
## Parameterising the target
|
|
70
|
+
|
|
71
|
+
If you test more than one environment, drive the host from a variable rather
|
|
72
|
+
than hardcoding it, and pass it at run time:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
atlas run -p SHOP --args "-Jhost=staging.example.com"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## A checklist
|
|
79
|
+
|
|
80
|
+
Before your first run:
|
|
81
|
+
|
|
82
|
+
- [ ] Data file paths are relative (`./file.csv`)
|
|
83
|
+
- [ ] Every referenced file is uploaded
|
|
84
|
+
- [ ] GUI listeners disabled
|
|
85
|
+
- [ ] Thread count is per-machine, and you've multiplied by your generator count
|
|
86
|
+
- [ ] Header handling matches your files
|
|
87
|
+
- [ ] Timers present
|
|
88
|
+
- [ ] The plan runs locally in headless mode first: `jmeter -n -t plan.jmx`
|
|
89
|
+
|
|
90
|
+
That last one is the highest-value check. `jmeter -n -t plan.jmx` on your own
|
|
91
|
+
machine catches most problems in seconds, without waiting for machines to power
|
|
92
|
+
on.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## A thread group is missing from my results
|
|
4
|
+
|
|
5
|
+
The usual cause is a **data file path that can't be resolved**. Every thread in
|
|
6
|
+
that group fails before sending a request, so there are no errors to report — the
|
|
7
|
+
group simply isn't there.
|
|
8
|
+
|
|
9
|
+
Check the **CSV Data Set Config** entries in that thread group and make each one
|
|
10
|
+
a relative path (`./data.csv`), then confirm the file is uploaded. See
|
|
11
|
+
[Test Plan Requirements](/reference/test-plans).
|
|
12
|
+
|
|
13
|
+
## The test finished in seconds with no results
|
|
14
|
+
|
|
15
|
+
Usually the plan didn't start properly. Check:
|
|
16
|
+
|
|
17
|
+
- The plan runs locally: `jmeter -n -t plan.jmx`
|
|
18
|
+
- Every referenced file is uploaded
|
|
19
|
+
- The machine is big enough — an undersized generator can fail to start JMeter
|
|
20
|
+
at all rather than running slowly
|
|
21
|
+
|
|
22
|
+
## Max Users is lower than I expected
|
|
23
|
+
|
|
24
|
+
Max Users adds up the concurrent users reported by every machine. If you ran on
|
|
25
|
+
5 generators and it shows roughly one machine's worth, the others weren't
|
|
26
|
+
producing load.
|
|
27
|
+
|
|
28
|
+
Check the Fleet page: were all the machines connected? Open **Watch** during a
|
|
29
|
+
run to see per-machine activity live.
|
|
30
|
+
|
|
31
|
+
## A machine shows as on, but not connected
|
|
32
|
+
|
|
33
|
+
The machine is running but its agent isn't reaching Atlas. On the machine:
|
|
34
|
+
|
|
35
|
+
- Confirm the Atlas agent service is running
|
|
36
|
+
- Confirm it can reach your Atlas instance over outbound HTTPS
|
|
37
|
+
- Check for a proxy or firewall rule blocking outbound connections
|
|
38
|
+
|
|
39
|
+
The agent reconnects on its own once the connection is restored.
|
|
40
|
+
|
|
41
|
+
## My test is queued and hasn't started
|
|
42
|
+
|
|
43
|
+
A load generator runs one test at a time. If the machines you asked for are
|
|
44
|
+
busy, your run waits and starts automatically when they free up. Either wait, or
|
|
45
|
+
choose different machines.
|
|
46
|
+
|
|
47
|
+
## Stop said it couldn't reach a machine
|
|
48
|
+
|
|
49
|
+
Atlas only marks a machine stopped once it has confirmed the process actually
|
|
50
|
+
ended. If it can't reach one, it tells you and the run stays open — deliberately,
|
|
51
|
+
so a test can't keep running unnoticed while the record says it stopped.
|
|
52
|
+
|
|
53
|
+
Retry the stop when the machine is reachable again.
|
|
54
|
+
|
|
55
|
+
## Error rate is high but I can't see why
|
|
56
|
+
|
|
57
|
+
Click the **Error %** cell for a request in the aggregation report. That opens
|
|
58
|
+
the actual errors — response codes, messages, and the full request and response
|
|
59
|
+
for each distinct failure.
|
|
60
|
+
|
|
61
|
+
If errors are concentrated in one endpoint, click its **Label** to chart that
|
|
62
|
+
request on its own and see when it started failing.
|
|
63
|
+
|
|
64
|
+
## Results look wrong for one request
|
|
65
|
+
|
|
66
|
+
Check whether the same sampler name exists in more than one thread group. Atlas
|
|
67
|
+
keeps them as separate rows attributed to the right group — if you expected them
|
|
68
|
+
combined, that's why the numbers differ from a single-machine JMeter run.
|
|
69
|
+
|
|
70
|
+
## A recorded plan fails on the second run
|
|
71
|
+
|
|
72
|
+
Usually a dynamic value that wasn't correlated — a token that was valid when you
|
|
73
|
+
recorded and isn't now. Re-record with correlation enabled (the default), and
|
|
74
|
+
check that the step which issues the token is part of the recording.
|
|
75
|
+
|
|
76
|
+
## Getting help
|
|
77
|
+
|
|
78
|
+
When reporting a problem, include the **test ID** (`SHOP-20260818-194201`). It
|
|
79
|
+
identifies the run and everything recorded about it.
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading a JMeter test plan well enough to say whether it will work here.
|
|
4
|
+
*
|
|
5
|
+
* This is an advisory scan, not the deploy path: the control plane does its own
|
|
6
|
+
* work on the copy it sends out (flattening data-file paths it recognises,
|
|
7
|
+
* switching off GUI listeners, attaching Atlas reporting). What it CANNOT fix is
|
|
8
|
+
* a data file that was never uploaded, a plan with no enabled thread group, or a
|
|
9
|
+
* file whose structure it cannot attach reporting to — and every one of those
|
|
10
|
+
* fails quietly at run time, which is what this exists to catch beforehand.
|
|
11
|
+
*
|
|
12
|
+
* Regex over the XML, deliberately: the same shape the control plane uses, no
|
|
13
|
+
* parser dependency, and it never rewrites anything.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.checkPlan = checkPlan;
|
|
17
|
+
/** Lower-cased basename, tolerant of Windows separators — matches how the
|
|
18
|
+
* control plane decides whether a reference means an uploaded file. */
|
|
19
|
+
function basenameKey(filename) {
|
|
20
|
+
const parts = filename.replace(/\\/g, '/').split('/');
|
|
21
|
+
return (parts[parts.length - 1] ?? filename).trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function basename(filename) {
|
|
24
|
+
const parts = filename.replace(/\\/g, '/').split('/');
|
|
25
|
+
return (parts[parts.length - 1] ?? filename).trim();
|
|
26
|
+
}
|
|
27
|
+
function stringProp(block, name) {
|
|
28
|
+
const m = block.match(new RegExp(`<stringProp name="${name}">([\\s\\S]*?)</stringProp>`));
|
|
29
|
+
return m ? m[1].trim() : '';
|
|
30
|
+
}
|
|
31
|
+
function intProp(block, name) {
|
|
32
|
+
const m = block.match(new RegExp(`<(?:string|int)Prop name="${name}">([\\s\\S]*?)</(?:string|int)Prop>`));
|
|
33
|
+
return m ? m[1].trim() : '';
|
|
34
|
+
}
|
|
35
|
+
function attr(tag, name) {
|
|
36
|
+
return new RegExp(`${name}="([^"]*)"`).exec(tag)?.[1] ?? '';
|
|
37
|
+
}
|
|
38
|
+
/** JMeter treats a missing `enabled` attribute as true. */
|
|
39
|
+
function isEnabled(tag) {
|
|
40
|
+
return attr(tag, 'enabled') !== 'false';
|
|
41
|
+
}
|
|
42
|
+
const THREAD_GROUP_TAGS = [
|
|
43
|
+
'ThreadGroup',
|
|
44
|
+
'SetupThreadGroup',
|
|
45
|
+
'PostThreadGroup',
|
|
46
|
+
'com\\.blazemeter\\.jmeter\\.threads\\.concurrency\\.ConcurrencyThreadGroup',
|
|
47
|
+
'kg\\.apc\\.jmeter\\.threads\\.UltimateThreadGroup',
|
|
48
|
+
];
|
|
49
|
+
function readThreadGroups(jmx) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const tag of THREAD_GROUP_TAGS) {
|
|
52
|
+
const re = new RegExp(`<(${tag})\\b([^>]*)>([\\s\\S]*?)</\\1>`, 'g');
|
|
53
|
+
let m;
|
|
54
|
+
while ((m = re.exec(jmx)) !== null) {
|
|
55
|
+
const [, kind, attrs, body] = m;
|
|
56
|
+
const threads = intProp(body, 'ThreadGroup.num_threads') || intProp(body, 'TargetLevel');
|
|
57
|
+
const property = /\$\{__P\(([^,)]+)/.exec(threads)?.[1]?.trim();
|
|
58
|
+
out.push({
|
|
59
|
+
name: attr(`<x ${attrs}>`, 'testname'),
|
|
60
|
+
kind,
|
|
61
|
+
enabled: isEnabled(`<x ${attrs}>`),
|
|
62
|
+
threads,
|
|
63
|
+
loops: intProp(body, 'LoopController.loops'),
|
|
64
|
+
...(property ? { sizeProperty: property } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function readDataFiles(jmx, uploaded) {
|
|
71
|
+
const known = uploaded ? new Set(uploaded.map(basenameKey)) : undefined;
|
|
72
|
+
// Keyed by the file it resolves to: a plan commonly reads one file from
|
|
73
|
+
// several data sets, and reporting it four times helps nobody.
|
|
74
|
+
const byFile = new Map();
|
|
75
|
+
const re = /<CSVDataSet\b([^>]*)>([\s\S]*?)<\/CSVDataSet>/g;
|
|
76
|
+
let m;
|
|
77
|
+
while ((m = re.exec(jmx)) !== null) {
|
|
78
|
+
const [, attrs, body] = m;
|
|
79
|
+
const reference = stringProp(body, 'filename');
|
|
80
|
+
if (!reference)
|
|
81
|
+
continue;
|
|
82
|
+
// A path built at run time can't be judged now, and the control plane leaves
|
|
83
|
+
// it alone for the same reason.
|
|
84
|
+
if (reference.includes('${'))
|
|
85
|
+
continue;
|
|
86
|
+
const key = basenameKey(reference);
|
|
87
|
+
const enabled = isEnabled(`<x ${attrs}>`);
|
|
88
|
+
const hasPath = /[\\/]/.test(reference);
|
|
89
|
+
const existing = byFile.get(key);
|
|
90
|
+
if (existing) {
|
|
91
|
+
if (!existing.references.includes(reference))
|
|
92
|
+
existing.references.push(reference);
|
|
93
|
+
existing.enabled ||= enabled;
|
|
94
|
+
existing.hasPath ||= hasPath;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
byFile.set(key, {
|
|
98
|
+
filename: basename(reference),
|
|
99
|
+
references: [reference],
|
|
100
|
+
enabled,
|
|
101
|
+
hasPath,
|
|
102
|
+
...(known ? { uploaded: known.has(key) } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return [...byFile.values()];
|
|
106
|
+
}
|
|
107
|
+
/** Listeners the control plane switches off in the deployed copy. */
|
|
108
|
+
const DISABLED_AT_DEPLOY = {
|
|
109
|
+
ViewResultsFullVisualizer: 'View Results Tree',
|
|
110
|
+
SimpleDataWriter: 'Simple Data Writer',
|
|
111
|
+
};
|
|
112
|
+
function readListeners(jmx) {
|
|
113
|
+
const out = [];
|
|
114
|
+
const re = /<ResultCollector\b([^>]*)>/g;
|
|
115
|
+
let m;
|
|
116
|
+
while ((m = re.exec(jmx)) !== null) {
|
|
117
|
+
const tag = m[0];
|
|
118
|
+
const guiclass = attr(tag, 'guiclass');
|
|
119
|
+
if (!(guiclass in DISABLED_AT_DEPLOY))
|
|
120
|
+
continue;
|
|
121
|
+
out.push({
|
|
122
|
+
guiclass,
|
|
123
|
+
name: attr(tag, 'testname') || DISABLED_AT_DEPLOY[guiclass],
|
|
124
|
+
enabled: isEnabled(tag),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Whether Atlas can attach its reporting.
|
|
131
|
+
*
|
|
132
|
+
* It is inserted as the last child of the Test Plan's hashTree, which needs the
|
|
133
|
+
* file to end in the standard way. A plan that fails this runs and produces
|
|
134
|
+
* nothing — no dashboard, no results — so it is an error, not a nicety.
|
|
135
|
+
*/
|
|
136
|
+
function reportingCanAttach(jmx) {
|
|
137
|
+
return /<\/hashTree>(\s*)<\/hashTree>(\s*)<\/jmeterTestPlan>(\s*)$/.test(jmx);
|
|
138
|
+
}
|
|
139
|
+
function checkPlan(jmx, uploaded) {
|
|
140
|
+
const issues = [];
|
|
141
|
+
const threadGroups = readThreadGroups(jmx);
|
|
142
|
+
const dataFiles = readDataFiles(jmx, uploaded);
|
|
143
|
+
const listeners = readListeners(jmx);
|
|
144
|
+
const canAttach = reportingCanAttach(jmx);
|
|
145
|
+
if (!/<jmeterTestPlan\b/.test(jmx)) {
|
|
146
|
+
issues.push({
|
|
147
|
+
severity: 'error',
|
|
148
|
+
code: 'not-a-test-plan',
|
|
149
|
+
message: 'This file is not a JMeter test plan.',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const opens = (jmx.match(/<hashTree>/g) ?? []).length;
|
|
153
|
+
const closes = (jmx.match(/<\/hashTree>/g) ?? []).length;
|
|
154
|
+
if (opens !== closes) {
|
|
155
|
+
issues.push({
|
|
156
|
+
severity: 'error',
|
|
157
|
+
code: 'unbalanced',
|
|
158
|
+
message: `The plan's structure is broken: ${opens} <hashTree> against ${closes} closing tags.`,
|
|
159
|
+
fix: 'Restore the file and redo the edit — JMeter will not open it in this state.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!canAttach) {
|
|
163
|
+
issues.push({
|
|
164
|
+
severity: 'error',
|
|
165
|
+
code: 'reporting-cannot-attach',
|
|
166
|
+
message: 'Atlas cannot attach its reporting to this plan, so a run would produce no results.',
|
|
167
|
+
fix: 'Open and re-save the plan in JMeter so it ends with the standard structure.',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const enabledGroups = threadGroups.filter(g => g.enabled);
|
|
171
|
+
if (threadGroups.length === 0) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: 'error',
|
|
174
|
+
code: 'no-thread-groups',
|
|
175
|
+
message: 'The plan has no thread groups, so it would run nothing.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else if (enabledGroups.length === 0) {
|
|
179
|
+
issues.push({
|
|
180
|
+
severity: 'error',
|
|
181
|
+
code: 'all-thread-groups-disabled',
|
|
182
|
+
message: `All ${threadGroups.length} thread group(s) are disabled, so the run would do nothing.`,
|
|
183
|
+
fix: 'Enable the one you mean to run.',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const file of dataFiles.filter(f => f.enabled)) {
|
|
187
|
+
const shown = file.references[0];
|
|
188
|
+
if (file.uploaded === false) {
|
|
189
|
+
// The one that hurts: every thread in the group fails before sampling, so
|
|
190
|
+
// the group disappears from the results with no errors to explain it.
|
|
191
|
+
issues.push({
|
|
192
|
+
severity: 'error',
|
|
193
|
+
code: 'data-file-missing',
|
|
194
|
+
message: `"${file.filename}" is read by the plan but is not in the project.`,
|
|
195
|
+
fix: `Upload it: atlas files upload ${file.filename}`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else if (file.uploaded && file.hasPath) {
|
|
199
|
+
issues.push({
|
|
200
|
+
severity: 'info',
|
|
201
|
+
code: 'data-file-path',
|
|
202
|
+
message: `"${shown}" is a path; Atlas will use the uploaded "${file.filename}".`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else if (file.uploaded === undefined && file.hasPath) {
|
|
206
|
+
issues.push({
|
|
207
|
+
severity: 'warning',
|
|
208
|
+
code: 'data-file-path-unchecked',
|
|
209
|
+
message: `"${shown}" is a path from another machine.`,
|
|
210
|
+
fix: `Make sure "${file.filename}" is uploaded to the project; then Atlas resolves it for you.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const liveListeners = listeners.filter(l => l.enabled);
|
|
215
|
+
if (liveListeners.length) {
|
|
216
|
+
issues.push({
|
|
217
|
+
severity: 'info',
|
|
218
|
+
code: 'gui-listeners',
|
|
219
|
+
message: `${liveListeners.map(l => `"${l.name}"`).join(', ')} will be switched off for the run.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (enabledGroups.length && enabledGroups.every(g => !g.sizeProperty)) {
|
|
223
|
+
issues.push({
|
|
224
|
+
severity: 'warning',
|
|
225
|
+
code: 'fixed-thread-count',
|
|
226
|
+
message: 'User counts are fixed inside the plan, so the only way to scale it is more machines.',
|
|
227
|
+
fix: 'Use ${__P(threads,N)} for the thread count to size the run with -Jthreads.',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Say which property, because it is whatever the plan's author chose.
|
|
232
|
+
const props = [...new Set(enabledGroups.map(g => g.sizeProperty).filter(Boolean))];
|
|
233
|
+
if (props.length) {
|
|
234
|
+
issues.push({
|
|
235
|
+
severity: 'info',
|
|
236
|
+
code: 'sizable',
|
|
237
|
+
message: `Size this run with ${props.map(p => `-J${p}=<n>`).join(' ')}.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
threadGroups,
|
|
243
|
+
dataFiles,
|
|
244
|
+
listeners,
|
|
245
|
+
reportingCanAttach: canAttach,
|
|
246
|
+
issues,
|
|
247
|
+
ok: !issues.some(i => i.severity === 'error'),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=plan.js.map
|