@cloudbeat/atlas-cli 0.3.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 +46 -19
- package/dist/api.js +20 -0
- 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,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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Atlas load testing
|
|
2
|
+
|
|
3
|
+
This project uses [Atlas](https://atlas.cloudbeat.io) for load testing. The
|
|
4
|
+
`atlas` CLI drives it; `atlas-rec` records scenarios from a browser.
|
|
5
|
+
|
|
6
|
+
Every command takes `--json`, and failures print JSON on standard error with a
|
|
7
|
+
meaningful exit code (`0` pass, `1` a threshold was breached, `2` a usage or
|
|
8
|
+
authentication problem, `3` the run failed or the server was unreachable).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
atlas schema --json # every command, argument and option
|
|
12
|
+
atlas docs # the documentation, offline
|
|
13
|
+
atlas docs troubleshooting # one topic
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## The flow
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
atlas whoami --json # who am I, which account
|
|
20
|
+
atlas projects --json # what can I see
|
|
21
|
+
atlas files list -p <project> --json # test plans and data files
|
|
22
|
+
atlas files download -p <project> <file> -o ./<file> # before editing anything
|
|
23
|
+
atlas plan check ./<file> -p <project> # will it run, and report?
|
|
24
|
+
atlas files upload -p <project> ./<file> # same name replaces, no version history
|
|
25
|
+
atlas capacity --json # machines, clusters, usage allowance
|
|
26
|
+
atlas run -p <project> --jmx <plan> --servers <n> --wait --gate "error_rate<1,p95<2000"
|
|
27
|
+
atlas results <testId> --json # aggregation report
|
|
28
|
+
atlas timeseries <testId> --json # the shape of the run
|
|
29
|
+
atlas errors <testId> --full # what failed, with request and response
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Recording a scenario, driven from the command line:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
atlas-rec start <url> -o flow.jmx # opens a browser, returns a session
|
|
36
|
+
atlas-rec snapshot # what is on the page
|
|
37
|
+
atlas-rec click --text "Sign in" # act on it
|
|
38
|
+
atlas-rec tx new "Checkout" # name the step that comes next
|
|
39
|
+
atlas-rec stop # write the plan
|
|
40
|
+
atlas-rec suggest flow.capture.json # values that change per session
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Rules that prevent real damage
|
|
44
|
+
|
|
45
|
+
- **A run costs machine time on someone's account.** Confirm before starting one
|
|
46
|
+
that is not a smoke test, and never stop a run you did not start.
|
|
47
|
+
- **Smoke-run first**: one machine, two users, a minute. Then check the results
|
|
48
|
+
actually contain data before scaling up.
|
|
49
|
+
- **Load multiplies by machines.** Each machine runs the whole plan, so total
|
|
50
|
+
users = users-in-the-plan x machines.
|
|
51
|
+
- **A test that measured nothing looks better than one that worked.** A missing
|
|
52
|
+
thread group with zero errors, a run that ended in seconds, or fast 200s that
|
|
53
|
+
return the login page all mean the test failed, not that the application is
|
|
54
|
+
fast.
|
|
55
|
+
- **Download before you edit or replace a stored file.** Uploading replaces it
|
|
56
|
+
and nothing keeps the old version.
|
|
57
|
+
- **Check a plan before uploading or running it.** A data file the project does
|
|
58
|
+
not have takes out a whole thread group with zero errors to explain it.
|
|
59
|
+
- **Think time is a setting, not a property.** Recorded plans pause between steps
|
|
60
|
+
using a `Settings - Sleeps` variables element; `-J` does not change it. Before
|
|
61
|
+
running a plan locally to debug it, check that element and disable it in your
|
|
62
|
+
working copy, or one user takes minutes instead of seconds. Put it back exactly
|
|
63
|
+
as it was before uploading.
|
|
64
|
+
- **A recording is a draft.** After stopping, report what was captured and ask
|
|
65
|
+
what to do next rather than uploading it. Offer to move the values every user
|
|
66
|
+
would otherwise repeat — credentials, search terms, ids — into a data file.
|
|
67
|
+
- **Report what happened**, including failed gates and uncertainty.
|
|
68
|
+
|
|
69
|
+
Full guidance is in the Atlas skills (`atlas skills install`) and at
|
|
70
|
+
https://atlas.cloudbeat.io/docs.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: atlas-analyze
|
|
3
|
+
description: Read and explain the results of an Atlas load test - the aggregation report, the shape of the run over time, the errors behind a failure - and recognise the failure modes where a broken test looks like a healthy one. Use when someone asks how a test went, what the numbers mean, why a test failed, or why results look wrong or missing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Reading results
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
atlas results <testId> # aggregation report, TOTAL first
|
|
10
|
+
atlas timeseries <testId> # users, response time, error rate over time
|
|
11
|
+
atlas errors <testId> # distinct errors, most common first
|
|
12
|
+
atlas errors <testId> --full # with the request and response behind each
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Add `--json` to any of them. `--label "<name>"` narrows timeseries and errors to
|
|
16
|
+
one transaction or request.
|
|
17
|
+
|
|
18
|
+
## Before interpreting anything, check the test was valid
|
|
19
|
+
|
|
20
|
+
A load test that measured nothing usually looks *better* than one that worked.
|
|
21
|
+
Rule these out first:
|
|
22
|
+
|
|
23
|
+
**Is every thread group present?** Compare the report against the plan. A missing
|
|
24
|
+
thread group with **zero errors** is the classic symptom of a data file the plan
|
|
25
|
+
could not open: every user in that group fails before sending a request, so there
|
|
26
|
+
is nothing to report as an error and the group simply vanishes while the others
|
|
27
|
+
look healthy. The cause is almost always a data file that was never uploaded to
|
|
28
|
+
the project. `atlas plan check -p <project> --jmx <plan>` names it in one line.
|
|
29
|
+
(A path from another machine, `C:\data\users.csv`, is not itself the problem —
|
|
30
|
+
Atlas resolves that to the uploaded file. Nothing uploaded under that name is.)
|
|
31
|
+
|
|
32
|
+
**Did it run for a plausible length of time?** A run that ends in a few seconds
|
|
33
|
+
with an empty dashboard did not run: the plan could not be started at all.
|
|
34
|
+
|
|
35
|
+
**Is the dashboard empty despite a full-length run?** Atlas attaches its
|
|
36
|
+
reporting to the plan when it deploys, and a file with an unusual structure can
|
|
37
|
+
defeat that — the test runs and reports nothing. `atlas plan check` says so
|
|
38
|
+
before you spend the time.
|
|
39
|
+
|
|
40
|
+
**Are the response codes real?** A wall of fast 200s that return the login page
|
|
41
|
+
is a failed test that passes every threshold. Check `atlas errors --full`, and be
|
|
42
|
+
suspicious of response times that are far better than the application's normal
|
|
43
|
+
behaviour.
|
|
44
|
+
|
|
45
|
+
**Did every machine report?** Users should add up to roughly what you asked for
|
|
46
|
+
(users-per-machine x machines). If the peak is a fraction of that, a machine was
|
|
47
|
+
not running.
|
|
48
|
+
|
|
49
|
+
## Then read the numbers
|
|
50
|
+
|
|
51
|
+
- **Percentiles, not averages.** p95 and p99 are what users feel; an average
|
|
52
|
+
hides the tail completely.
|
|
53
|
+
- **Error rate is errors over requests for the whole test**, not an average of
|
|
54
|
+
per-interval rates. The TOTAL row is authoritative.
|
|
55
|
+
- **Throughput is the result, not the goal.** If throughput flattens while users
|
|
56
|
+
keep climbing, you have found the saturation point — that is usually the
|
|
57
|
+
headline of the whole test.
|
|
58
|
+
- **Look at the shape.** `timeseries` shows where response time started to climb
|
|
59
|
+
and whether errors began at a particular user count. "It broke at about 300
|
|
60
|
+
users" is a more useful finding than any single number.
|
|
61
|
+
- **Per-transaction matters more than TOTAL.** One slow step in an otherwise fast
|
|
62
|
+
plan is invisible in the total and obvious per label.
|
|
63
|
+
|
|
64
|
+
## Explaining errors
|
|
65
|
+
|
|
66
|
+
`atlas errors --full` gives the actual request and response for each distinct
|
|
67
|
+
error. Sort out which kind you have:
|
|
68
|
+
|
|
69
|
+
- **4xx** — usually the test, not the application: a stale correlated value, an
|
|
70
|
+
expired session, data that does not exist in the target environment.
|
|
71
|
+
- **5xx** — usually the application, and worth reporting with the response body.
|
|
72
|
+
- **Timeouts / connection errors** — the application or something in front of it
|
|
73
|
+
giving up. Check when they started against the user count.
|
|
74
|
+
- **Assertion failures** — the request succeeded but returned the wrong thing,
|
|
75
|
+
such as a login page instead of the page you asked for.
|
|
76
|
+
|
|
77
|
+
A failure that appears only above a certain load is the interesting one. Report
|
|
78
|
+
the level it appeared at.
|
|
79
|
+
|
|
80
|
+
## Comparing runs
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
atlas tests -p <project> --json # recent runs with peak users, requests, error %
|
|
84
|
+
atlas results <previous> --json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Compare like with like: the same plan, the same number of machines, the same
|
|
88
|
+
user count. A regression is only a regression if the shape of the test was the
|
|
89
|
+
same.
|
|
90
|
+
|
|
91
|
+
## Writing it up
|
|
92
|
+
|
|
93
|
+
A good report says, in this order:
|
|
94
|
+
|
|
95
|
+
1. Whether it passed, against the thresholds that were set.
|
|
96
|
+
2. What load was actually applied — users, ramp, duration, machines.
|
|
97
|
+
3. Response time under that load: p95 per key transaction, not just the total.
|
|
98
|
+
4. Where it started to degrade, from the time series.
|
|
99
|
+
5. What failed, with real examples from the error detail.
|
|
100
|
+
6. What to do next.
|
|
101
|
+
|
|
102
|
+
State the uncertainty plainly where it exists. "The test never reached the target
|
|
103
|
+
load because one machine dropped out" is a far more useful sentence than a
|
|
104
|
+
confident summary of numbers that do not mean what they appear to.
|