@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.
Files changed (41) hide show
  1. package/README.md +52 -19
  2. package/dist/api.js +70 -18
  3. package/dist/cli.js +34 -6
  4. package/dist/commands/analyze.js +118 -0
  5. package/dist/commands/capacity.js +99 -0
  6. package/dist/commands/docs.js +57 -0
  7. package/dist/commands/files.js +18 -4
  8. package/dist/commands/plan.js +100 -0
  9. package/dist/commands/record.js +11 -1
  10. package/dist/commands/results.js +13 -7
  11. package/dist/commands/schema.js +86 -0
  12. package/dist/commands/skills.js +111 -0
  13. package/dist/commands/tests.js +73 -0
  14. package/dist/docs/agent-setup.md +109 -0
  15. package/dist/docs/agent-tasks.md +169 -0
  16. package/dist/docs/ai-agents.md +90 -0
  17. package/dist/docs/auth.md +74 -0
  18. package/dist/docs/automation.md +150 -0
  19. package/dist/docs/ci.md +105 -0
  20. package/dist/docs/clusters.md +48 -0
  21. package/dist/docs/commands.md +155 -0
  22. package/dist/docs/files.md +86 -0
  23. package/dist/docs/generators.md +52 -0
  24. package/dist/docs/getting-started.md +85 -0
  25. package/dist/docs/index.json +110 -0
  26. package/dist/docs/projects.md +50 -0
  27. package/dist/docs/recorder.md +143 -0
  28. package/dist/docs/results.md +79 -0
  29. package/dist/docs/running.md +80 -0
  30. package/dist/docs/skills.md +88 -0
  31. package/dist/docs/test-plans.md +92 -0
  32. package/dist/docs/troubleshooting.md +79 -0
  33. package/dist/plan.js +250 -0
  34. package/dist/skills/AGENTS.md +70 -0
  35. package/dist/skills/atlas-analyze/SKILL.md +104 -0
  36. package/dist/skills/atlas-correlate/SKILL.md +89 -0
  37. package/dist/skills/atlas-load-test/SKILL.md +154 -0
  38. package/dist/skills/atlas-record/SKILL.md +167 -0
  39. package/dist/skills/atlas-run/SKILL.md +109 -0
  40. package/dist/skills/atlas-test-plan/SKILL.md +281 -0
  41. 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