@testsmith/perfornium 0.6.3 → 0.6.5
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/dist/cli/commands/distributed.js +2 -2
- package/dist/cli/commands/report.js +2 -2
- package/dist/cli/commands/run.js +2 -0
- package/dist/config/parser.js +2 -2
- package/dist/config/types/global-config.d.ts +82 -2
- package/dist/config/types/scenario-config.d.ts +2 -2
- package/dist/core/data/data-manager.d.ts +70 -0
- package/dist/core/data/data-manager.js +186 -0
- package/dist/core/data/data-provider.d.ts +85 -0
- package/dist/core/data/data-provider.js +468 -0
- package/dist/core/data/index.d.ts +8 -0
- package/dist/core/data/index.js +13 -0
- package/dist/core/execution/check-evaluator.d.ts +10 -0
- package/dist/core/execution/check-evaluator.js +79 -0
- package/dist/core/execution/data-extractor.d.ts +6 -0
- package/dist/core/execution/data-extractor.js +70 -0
- package/dist/core/execution/index.d.ts +3 -0
- package/dist/core/execution/index.js +9 -0
- package/dist/core/execution/json-payload-processor.d.ts +7 -0
- package/dist/core/execution/json-payload-processor.js +140 -0
- package/dist/core/factories/index.d.ts +2 -0
- package/dist/core/factories/index.js +7 -0
- package/dist/core/factories/output-handler-factory.d.ts +10 -0
- package/dist/core/factories/output-handler-factory.js +91 -0
- package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
- package/dist/core/factories/protocol-handler-factory.js +96 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +8 -3
- package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
- package/dist/core/reporting/dashboard-reporter.js +127 -0
- package/dist/core/reporting/index.d.ts +1 -0
- package/dist/core/reporting/index.js +5 -0
- package/dist/core/step-executor.d.ts +6 -20
- package/dist/core/step-executor.js +72 -366
- package/dist/core/strategies/index.d.ts +2 -0
- package/dist/core/strategies/index.js +7 -0
- package/dist/core/strategies/scenario-selector.d.ts +13 -0
- package/dist/core/strategies/scenario-selector.js +37 -0
- package/dist/core/strategies/think-time-strategy.d.ts +15 -0
- package/dist/core/strategies/think-time-strategy.js +71 -0
- package/dist/core/test-runner.d.ts +4 -11
- package/dist/core/test-runner.js +105 -312
- package/dist/core/virtual-user.d.ts +7 -37
- package/dist/core/virtual-user.js +29 -269
- package/dist/dashboard/routes/api.d.ts +64 -0
- package/dist/dashboard/routes/api.js +569 -0
- package/dist/dashboard/routes/index.d.ts +2 -0
- package/dist/dashboard/routes/index.js +7 -0
- package/dist/dashboard/routes/static.d.ts +6 -0
- package/dist/dashboard/routes/static.js +76 -0
- package/dist/dashboard/server.d.ts +8 -84
- package/dist/dashboard/server.js +76 -2007
- package/dist/dashboard/services/file-scanner.d.ts +7 -0
- package/dist/dashboard/services/file-scanner.js +114 -0
- package/dist/dashboard/services/index.d.ts +5 -0
- package/dist/dashboard/services/index.js +13 -0
- package/dist/dashboard/services/influxdb-service.d.ts +41 -0
- package/dist/dashboard/services/influxdb-service.js +329 -0
- package/dist/dashboard/services/metrics-parser.d.ts +12 -0
- package/dist/dashboard/services/metrics-parser.js +209 -0
- package/dist/dashboard/services/results-manager.d.ts +17 -0
- package/dist/dashboard/services/results-manager.js +311 -0
- package/dist/dashboard/services/test-executor.d.ts +41 -0
- package/dist/dashboard/services/test-executor.js +250 -0
- package/dist/dashboard/services/workers-manager.d.ts +13 -0
- package/dist/dashboard/services/workers-manager.js +81 -0
- package/dist/dashboard/templates/index.html +122 -0
- package/dist/dashboard/templates/scripts/main.js +3280 -0
- package/dist/dashboard/templates/styles.css +402 -0
- package/dist/dashboard/types.d.ts +168 -0
- package/dist/dashboard/types.js +2 -0
- package/dist/distributed/result-aggregator.js +1 -3
- package/dist/metrics/batch/batch-processor.d.ts +27 -0
- package/dist/metrics/batch/batch-processor.js +85 -0
- package/dist/metrics/batch/index.d.ts +1 -0
- package/dist/metrics/batch/index.js +5 -0
- package/dist/metrics/collector.d.ts +46 -45
- package/dist/metrics/collector.js +179 -640
- package/dist/metrics/core/error-tracker.d.ts +9 -0
- package/dist/metrics/core/error-tracker.js +52 -0
- package/dist/metrics/core/index.d.ts +3 -0
- package/dist/metrics/core/index.js +9 -0
- package/dist/metrics/core/result-storage.d.ts +19 -0
- package/dist/metrics/core/result-storage.js +56 -0
- package/dist/metrics/core/statistics-engine.d.ts +27 -0
- package/dist/metrics/core/statistics-engine.js +91 -0
- package/dist/metrics/output/file-writer.d.ts +19 -0
- package/dist/metrics/output/file-writer.js +129 -0
- package/dist/metrics/output/index.d.ts +2 -0
- package/dist/metrics/output/index.js +10 -0
- package/dist/metrics/output/influxdb-writer.d.ts +89 -0
- package/dist/metrics/output/influxdb-writer.js +404 -0
- package/dist/metrics/realtime/dispatcher.d.ts +18 -0
- package/dist/metrics/realtime/dispatcher.js +45 -0
- package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/graphite.js +61 -0
- package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
- package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/webhook.js +22 -0
- package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/websocket.js +25 -0
- package/dist/metrics/realtime/index.d.ts +5 -0
- package/dist/metrics/realtime/index.js +13 -0
- package/dist/metrics/reporting/index.d.ts +3 -0
- package/dist/metrics/reporting/index.js +9 -0
- package/dist/metrics/reporting/step-statistics.d.ts +6 -0
- package/dist/metrics/reporting/step-statistics.js +59 -0
- package/dist/metrics/reporting/summary-generator.d.ts +16 -0
- package/dist/metrics/reporting/summary-generator.js +46 -0
- package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
- package/dist/metrics/reporting/timeline-calculator.js +86 -0
- package/dist/metrics/types.d.ts +58 -0
- package/dist/outputs/csv.d.ts +2 -0
- package/dist/outputs/csv.js +21 -2
- package/dist/outputs/json.js +6 -2
- package/dist/protocols/rest/handler.d.ts +4 -53
- package/dist/protocols/rest/handler.js +73 -454
- package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
- package/dist/protocols/rest/request/auth-handler.js +30 -0
- package/dist/protocols/rest/request/body-processor.d.ts +11 -0
- package/dist/protocols/rest/request/body-processor.js +62 -0
- package/dist/protocols/rest/request/index.d.ts +2 -0
- package/dist/protocols/rest/request/index.js +7 -0
- package/dist/protocols/rest/response/checks.d.ts +6 -0
- package/dist/protocols/rest/response/checks.js +71 -0
- package/dist/protocols/rest/response/index.d.ts +2 -0
- package/dist/protocols/rest/response/index.js +7 -0
- package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
- package/dist/protocols/rest/response/size-calculator.js +64 -0
- package/dist/protocols/web/browser/highlight.d.ts +7 -0
- package/dist/protocols/web/browser/highlight.js +47 -0
- package/dist/protocols/web/browser/index.d.ts +4 -0
- package/dist/protocols/web/browser/index.js +11 -0
- package/dist/protocols/web/browser/manager.d.ts +20 -0
- package/dist/protocols/web/browser/manager.js +189 -0
- package/dist/protocols/web/browser/screenshot.d.ts +8 -0
- package/dist/protocols/web/browser/screenshot.js +69 -0
- package/dist/protocols/web/browser/storage.d.ts +5 -0
- package/dist/protocols/web/browser/storage.js +45 -0
- package/dist/protocols/web/commands/index.d.ts +5 -0
- package/dist/protocols/web/commands/index.js +11 -0
- package/dist/protocols/web/commands/interaction.d.ts +13 -0
- package/dist/protocols/web/commands/interaction.js +68 -0
- package/dist/protocols/web/commands/measurement.d.ts +16 -0
- package/dist/protocols/web/commands/measurement.js +33 -0
- package/dist/protocols/web/commands/navigation.d.ts +11 -0
- package/dist/protocols/web/commands/navigation.js +43 -0
- package/dist/protocols/web/commands/types.d.ts +12 -0
- package/dist/protocols/web/commands/types.js +2 -0
- package/dist/protocols/web/commands/verification.d.ts +11 -0
- package/dist/protocols/web/commands/verification.js +98 -0
- package/dist/protocols/web/handler.d.ts +19 -30
- package/dist/protocols/web/handler.js +160 -650
- package/dist/protocols/web/network/capture.d.ts +19 -0
- package/dist/protocols/web/network/capture.js +225 -0
- package/dist/protocols/web/network/filters.d.ts +5 -0
- package/dist/protocols/web/network/filters.js +49 -0
- package/dist/protocols/web/network/index.d.ts +4 -0
- package/dist/protocols/web/network/index.js +9 -0
- package/dist/protocols/web/network/types.d.ts +13 -0
- package/dist/protocols/web/network/types.js +2 -0
- package/dist/protocols/web/network/utils.d.ts +8 -0
- package/dist/protocols/web/network/utils.js +29 -0
- package/dist/recorder/native-recorder.js +2 -1
- package/dist/reporting/chart-data/index.d.ts +5 -0
- package/dist/reporting/chart-data/index.js +13 -0
- package/dist/reporting/chart-data/network.d.ts +25 -0
- package/dist/reporting/chart-data/network.js +78 -0
- package/dist/reporting/chart-data/scenario.d.ts +37 -0
- package/dist/reporting/chart-data/scenario.js +76 -0
- package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
- package/dist/reporting/chart-data/step-statistics.js +94 -0
- package/dist/reporting/chart-data/throughput.d.ts +16 -0
- package/dist/reporting/chart-data/throughput.js +24 -0
- package/dist/reporting/chart-data/timeline.d.ts +17 -0
- package/dist/reporting/chart-data/timeline.js +46 -0
- package/dist/reporting/handlebars-helpers.d.ts +1 -0
- package/dist/reporting/handlebars-helpers.js +63 -0
- package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
- package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
- package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
- package/dist/utils/data-utils.d.ts +17 -0
- package/dist/utils/data-utils.js +129 -0
- package/dist/utils/template.js +2 -2
- package/package.json +5 -2
- package/dist/core/csv-data-provider.d.ts +0 -47
- package/dist/core/csv-data-provider.js +0 -265
- package/dist/reporting/generator.d.ts +0 -42
- package/dist/reporting/generator.js +0 -1217
- package/dist/reporting/templates/html.hbs +0 -2453
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsParser = void 0;
|
|
4
|
+
class MetricsParser {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.influxEnabled = options?.influxEnabled ?? false;
|
|
7
|
+
}
|
|
8
|
+
parseOutputForMetrics(line, test) {
|
|
9
|
+
// Parse [RT] JSON data for individual response times
|
|
10
|
+
const rtMatch = line.match(/\[RT\]\s*(.+)/);
|
|
11
|
+
if (rtMatch) {
|
|
12
|
+
try {
|
|
13
|
+
const rtData = JSON.parse(rtMatch[1]);
|
|
14
|
+
const newRTs = rtData.map((r) => ({
|
|
15
|
+
timestamp: r.t,
|
|
16
|
+
value: r.v,
|
|
17
|
+
success: r.s === 1,
|
|
18
|
+
stepName: r.n || 'unknown'
|
|
19
|
+
}));
|
|
20
|
+
// When InfluxDB is enabled, keep all response times (data is persisted)
|
|
21
|
+
// Otherwise limit to 500 to prevent memory issues
|
|
22
|
+
if (this.influxEnabled) {
|
|
23
|
+
test.responseTimes = [...test.responseTimes, ...newRTs];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
test.responseTimes = [...test.responseTimes, ...newRTs].slice(-500);
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
// Ignore JSON parse errors
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// Parse [STEPS] JSON data for step statistics
|
|
36
|
+
const stepsMatch = line.match(/\[STEPS\]\s*(.+)/);
|
|
37
|
+
if (stepsMatch) {
|
|
38
|
+
try {
|
|
39
|
+
const stepData = JSON.parse(stepsMatch[1]);
|
|
40
|
+
test.stepStats = stepData.map((s) => ({
|
|
41
|
+
stepName: s.n,
|
|
42
|
+
scenario: s.s,
|
|
43
|
+
requests: s.r,
|
|
44
|
+
errors: s.e,
|
|
45
|
+
avgResponseTime: s.a,
|
|
46
|
+
p50: s.p50,
|
|
47
|
+
p95: s.p95,
|
|
48
|
+
p99: s.p99,
|
|
49
|
+
successRate: s.sr
|
|
50
|
+
}));
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// Ignore JSON parse errors
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Parse [ERRORS] JSON data for top errors
|
|
59
|
+
const topErrorsMatch = line.match(/\[ERRORS\]\s*(.+)/);
|
|
60
|
+
if (topErrorsMatch) {
|
|
61
|
+
try {
|
|
62
|
+
const errorData = JSON.parse(topErrorsMatch[1]);
|
|
63
|
+
test.topErrors = errorData.map((e) => ({
|
|
64
|
+
scenario: e.scenario,
|
|
65
|
+
action: e.action,
|
|
66
|
+
status: e.status,
|
|
67
|
+
error: e.error,
|
|
68
|
+
url: e.url,
|
|
69
|
+
count: e.count
|
|
70
|
+
}));
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// Ignore JSON parse errors
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// Parse [NETWORK] JSON data for captured HTTP calls
|
|
79
|
+
const networkMatch = line.match(/\[NETWORK\]\s*(.+)/);
|
|
80
|
+
if (networkMatch) {
|
|
81
|
+
try {
|
|
82
|
+
const networkData = JSON.parse(networkMatch[1]);
|
|
83
|
+
// Initialize network calls array if not present
|
|
84
|
+
if (!test.networkCalls) {
|
|
85
|
+
test.networkCalls = [];
|
|
86
|
+
}
|
|
87
|
+
test.networkCalls.push({
|
|
88
|
+
id: networkData.id,
|
|
89
|
+
vuId: networkData.vu,
|
|
90
|
+
url: networkData.url,
|
|
91
|
+
method: networkData.method,
|
|
92
|
+
status: networkData.status,
|
|
93
|
+
statusText: networkData.statusText,
|
|
94
|
+
duration: networkData.duration,
|
|
95
|
+
size: networkData.size,
|
|
96
|
+
type: networkData.type,
|
|
97
|
+
success: networkData.success,
|
|
98
|
+
error: networkData.error,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
requestHeaders: networkData.requestHeaders,
|
|
101
|
+
requestBody: networkData.requestBody,
|
|
102
|
+
responseHeaders: networkData.responseHeaders,
|
|
103
|
+
responseBody: networkData.responseBody
|
|
104
|
+
});
|
|
105
|
+
// Keep last 100 network calls to prevent memory issues
|
|
106
|
+
if (test.networkCalls.length > 100) {
|
|
107
|
+
test.networkCalls = test.networkCalls.slice(-100);
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
// Ignore JSON parse errors
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
// Parse the extended [PROGRESS] format with percentiles
|
|
117
|
+
// Format: [PROGRESS] VUs: 5 | Requests: 100 | Errors: 2 | Avg RT: 150ms | RPS: 10.5 | P50: 100ms | P90: 200ms | P95: 300ms | P99: 500ms | Success: 98.5%
|
|
118
|
+
const progressLineMatch = line.match(/\[PROGRESS\]\s*VUs:\s*(\d+)\s*\|\s*Requests:\s*(\d+)\s*\|\s*Errors:\s*(\d+)\s*\|\s*Avg RT:\s*(\d+(?:\.\d+)?)\s*ms\s*\|\s*RPS:\s*(\d+(?:\.\d+)?)/i);
|
|
119
|
+
if (progressLineMatch) {
|
|
120
|
+
test.metrics.currentVUs = parseInt(progressLineMatch[1]);
|
|
121
|
+
test.metrics.requests = parseInt(progressLineMatch[2]);
|
|
122
|
+
test.metrics.errors = parseInt(progressLineMatch[3]);
|
|
123
|
+
test.metrics.avgResponseTime = parseFloat(progressLineMatch[4]);
|
|
124
|
+
test.metrics.requestsPerSecond = parseFloat(progressLineMatch[5]);
|
|
125
|
+
// Parse percentiles if present
|
|
126
|
+
const p50Match = line.match(/P50:\s*(\d+(?:\.\d+)?)\s*ms/i);
|
|
127
|
+
const p90Match = line.match(/P90:\s*(\d+(?:\.\d+)?)\s*ms/i);
|
|
128
|
+
const p95Match = line.match(/P95:\s*(\d+(?:\.\d+)?)\s*ms/i);
|
|
129
|
+
const p99Match = line.match(/P99:\s*(\d+(?:\.\d+)?)\s*ms/i);
|
|
130
|
+
const successMatch = line.match(/Success:\s*(\d+(?:\.\d+)?)\s*%/i);
|
|
131
|
+
if (p50Match)
|
|
132
|
+
test.metrics.p50ResponseTime = parseFloat(p50Match[1]);
|
|
133
|
+
if (p90Match)
|
|
134
|
+
test.metrics.p90ResponseTime = parseFloat(p90Match[1]);
|
|
135
|
+
if (p95Match)
|
|
136
|
+
test.metrics.p95ResponseTime = parseFloat(p95Match[1]);
|
|
137
|
+
if (p99Match)
|
|
138
|
+
test.metrics.p99ResponseTime = parseFloat(p99Match[1]);
|
|
139
|
+
if (successMatch)
|
|
140
|
+
test.metrics.successRate = parseFloat(successMatch[1]);
|
|
141
|
+
// Add to history
|
|
142
|
+
this.addHistoryEntry(test);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
// Fallback: Parse various loose output formats for metrics
|
|
146
|
+
const vusMatch = line.match(/VUs?[:\s]+(\d+)/i);
|
|
147
|
+
const requestsMatch = line.match(/(?:total\s+)?requests?[:\s]+(\d+)/i);
|
|
148
|
+
const errorsMatch = line.match(/(?:failed|errors?)[:\s]+(\d+)/i);
|
|
149
|
+
const avgRtMatch = line.match(/(?:avg|average)\s*(?:rt|response\s*time)?[:\s]+(\d+(?:\.\d+)?)\s*ms/i);
|
|
150
|
+
const rpsMatch = line.match(/(?:rps|req\/s|requests\/s(?:ec)?)[:\s]+(\d+(?:\.\d+)?)/i);
|
|
151
|
+
let updated = false;
|
|
152
|
+
if (vusMatch) {
|
|
153
|
+
test.metrics.currentVUs = parseInt(vusMatch[1]);
|
|
154
|
+
updated = true;
|
|
155
|
+
}
|
|
156
|
+
if (requestsMatch) {
|
|
157
|
+
test.metrics.requests = parseInt(requestsMatch[1]);
|
|
158
|
+
updated = true;
|
|
159
|
+
}
|
|
160
|
+
if (errorsMatch) {
|
|
161
|
+
test.metrics.errors = parseInt(errorsMatch[1]);
|
|
162
|
+
updated = true;
|
|
163
|
+
}
|
|
164
|
+
if (avgRtMatch) {
|
|
165
|
+
test.metrics.avgResponseTime = parseFloat(avgRtMatch[1]);
|
|
166
|
+
updated = true;
|
|
167
|
+
}
|
|
168
|
+
if (rpsMatch) {
|
|
169
|
+
test.metrics.requestsPerSecond = parseFloat(rpsMatch[1]);
|
|
170
|
+
updated = true;
|
|
171
|
+
}
|
|
172
|
+
if (updated) {
|
|
173
|
+
this.addHistoryEntry(test);
|
|
174
|
+
}
|
|
175
|
+
return updated;
|
|
176
|
+
}
|
|
177
|
+
addHistoryEntry(test) {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const lastHistory = test.history[test.history.length - 1];
|
|
180
|
+
const rps = lastHistory && (now - lastHistory.timestamp) > 0
|
|
181
|
+
? (test.metrics.requests - lastHistory.requests) / ((now - lastHistory.timestamp) / 1000)
|
|
182
|
+
: (test.metrics.requestsPerSecond || 0);
|
|
183
|
+
test.history.push({
|
|
184
|
+
timestamp: now,
|
|
185
|
+
requests: test.metrics.requests,
|
|
186
|
+
errors: test.metrics.errors,
|
|
187
|
+
avgResponseTime: test.metrics.avgResponseTime,
|
|
188
|
+
p95ResponseTime: test.metrics.p95ResponseTime || 0,
|
|
189
|
+
p99ResponseTime: test.metrics.p99ResponseTime || 0,
|
|
190
|
+
vus: test.metrics.currentVUs,
|
|
191
|
+
rps: Math.max(0, rps)
|
|
192
|
+
});
|
|
193
|
+
if (test.history.length > 120)
|
|
194
|
+
test.history.shift();
|
|
195
|
+
}
|
|
196
|
+
parseNetworkData(line) {
|
|
197
|
+
const networkMatch = line.match(/\[NETWORK\]\s*(.+)/);
|
|
198
|
+
if (networkMatch) {
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(networkMatch[1]);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.MetricsParser = MetricsParser;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TestResult } from '../types';
|
|
2
|
+
export declare class ResultsManager {
|
|
3
|
+
private resultsDir;
|
|
4
|
+
constructor(resultsDir: string);
|
|
5
|
+
scanResults(): Promise<TestResult[]>;
|
|
6
|
+
loadFullResult(id: string): Promise<TestResult | null>;
|
|
7
|
+
deleteResult(id: string): Promise<void>;
|
|
8
|
+
saveResult(id: string, data: any): Promise<{
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}>;
|
|
12
|
+
generateComparison(results: (TestResult | null)[]): any;
|
|
13
|
+
private extractSummary;
|
|
14
|
+
private extractNetworkCalls;
|
|
15
|
+
private generateStepComparisons;
|
|
16
|
+
private calcDiff;
|
|
17
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ResultsManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const logger_1 = require("../../utils/logger");
|
|
40
|
+
class ResultsManager {
|
|
41
|
+
constructor(resultsDir) {
|
|
42
|
+
this.resultsDir = resultsDir;
|
|
43
|
+
}
|
|
44
|
+
async scanResults() {
|
|
45
|
+
const results = [];
|
|
46
|
+
try {
|
|
47
|
+
const files = await fs.readdir(this.resultsDir);
|
|
48
|
+
const excludePatterns = ['metrics', 'live-results', 'summary-incremental'];
|
|
49
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && !excludePatterns.some(p => f.includes(p)));
|
|
50
|
+
for (const file of jsonFiles) {
|
|
51
|
+
try {
|
|
52
|
+
const filePath = path.join(this.resultsDir, file);
|
|
53
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
54
|
+
const data = JSON.parse(content);
|
|
55
|
+
const stat = await fs.stat(filePath);
|
|
56
|
+
const fileId = file.replace('.json', '');
|
|
57
|
+
// Extract test name from filename if not in data
|
|
58
|
+
let testName = data.name || data.test_name;
|
|
59
|
+
if (!testName) {
|
|
60
|
+
const match = fileId.match(/^(.+)-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/);
|
|
61
|
+
testName = match ? match[1] : fileId;
|
|
62
|
+
}
|
|
63
|
+
// Extract timestamp from filename or metadata
|
|
64
|
+
let timestamp = data.timestamp || data.metadata?.generated_at;
|
|
65
|
+
if (!timestamp) {
|
|
66
|
+
const tsMatch = fileId.match(/(\d{4}-\d{2}-\d{2})_(\d{2})-(\d{2})-(\d{2})/);
|
|
67
|
+
if (tsMatch) {
|
|
68
|
+
timestamp = `${tsMatch[1]}T${tsMatch[2]}:${tsMatch[3]}:${tsMatch[4]}Z`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
timestamp = stat.mtime.toISOString();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
results.push({
|
|
75
|
+
id: fileId,
|
|
76
|
+
name: testName,
|
|
77
|
+
timestamp: timestamp,
|
|
78
|
+
duration: data.duration || data.total_duration || data.summary?.total_duration || 0,
|
|
79
|
+
summary: this.extractSummary(data),
|
|
80
|
+
scenarios: data.scenarios || [],
|
|
81
|
+
step_statistics: data.step_statistics || data.summary?.step_statistics || []
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
// Log skipped files so users know why results don't appear
|
|
86
|
+
logger_1.logger.warn(`Skipping invalid result file ${file}: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
// Results dir might not exist
|
|
92
|
+
}
|
|
93
|
+
results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
async loadFullResult(id) {
|
|
97
|
+
try {
|
|
98
|
+
const decodedId = decodeURIComponent(id);
|
|
99
|
+
const filePath = path.join(this.resultsDir, `${decodedId}.json`);
|
|
100
|
+
logger_1.logger.debug(`Loading result from: ${filePath}`);
|
|
101
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
102
|
+
const data = JSON.parse(content);
|
|
103
|
+
const stat = await fs.stat(filePath);
|
|
104
|
+
// Extract network calls from results
|
|
105
|
+
const networkCalls = this.extractNetworkCalls(data);
|
|
106
|
+
// Extract test name from filename if not in data
|
|
107
|
+
// Filename format: "Test Name-YYYY-MM-DD_HH-MM-SS-YYYYMMDD-HHMMSS-mmm.json"
|
|
108
|
+
let testName = data.name || data.test_name;
|
|
109
|
+
if (!testName) {
|
|
110
|
+
// Try to extract from filename by removing timestamp suffix
|
|
111
|
+
const match = decodedId.match(/^(.+)-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/);
|
|
112
|
+
testName = match ? match[1] : decodedId;
|
|
113
|
+
}
|
|
114
|
+
// Extract timestamp from filename or metadata
|
|
115
|
+
let timestamp = data.timestamp || data.metadata?.generated_at;
|
|
116
|
+
if (!timestamp) {
|
|
117
|
+
// Try to extract from filename
|
|
118
|
+
const tsMatch = decodedId.match(/(\d{4}-\d{2}-\d{2})_(\d{2})-(\d{2})-(\d{2})/);
|
|
119
|
+
if (tsMatch) {
|
|
120
|
+
timestamp = `${tsMatch[1]}T${tsMatch[2]}:${tsMatch[3]}:${tsMatch[4]}Z`;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
timestamp = stat.mtime.toISOString();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
id: decodedId,
|
|
128
|
+
name: testName,
|
|
129
|
+
timestamp: timestamp,
|
|
130
|
+
duration: data.duration || data.total_duration || data.summary?.total_duration || 0,
|
|
131
|
+
summary: this.extractSummary(data),
|
|
132
|
+
scenarios: data.scenarios || [],
|
|
133
|
+
step_statistics: data.step_statistics || data.summary?.step_statistics || [],
|
|
134
|
+
timeline_data: data.timeline_data || data.summary?.timeline_data || [],
|
|
135
|
+
vu_ramp_up: data.vu_ramp_up || data.summary?.vu_ramp_up || [],
|
|
136
|
+
response_time_distribution: data.response_time_distribution || [],
|
|
137
|
+
timeseries: data.timeseries || data.time_series || [],
|
|
138
|
+
error_details: data.error_details || data.summary?.error_details || [],
|
|
139
|
+
network_calls: networkCalls,
|
|
140
|
+
infrastructure_metrics: data.infrastructure_metrics || null,
|
|
141
|
+
raw: data
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
logger_1.logger.error(`Failed to load result ${id}:`, e.message);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async deleteResult(id) {
|
|
150
|
+
const decodedId = decodeURIComponent(id);
|
|
151
|
+
const filePath = path.join(this.resultsDir, `${decodedId}.json`);
|
|
152
|
+
logger_1.logger.debug(`Deleting result file: ${filePath}`);
|
|
153
|
+
await fs.unlink(filePath);
|
|
154
|
+
logger_1.logger.info(`Deleted result: ${decodedId}`);
|
|
155
|
+
}
|
|
156
|
+
async saveResult(id, data) {
|
|
157
|
+
// Ensure results directory exists
|
|
158
|
+
try {
|
|
159
|
+
await fs.access(this.resultsDir);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
await fs.mkdir(this.resultsDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
const sanitizedId = id.replace(/[<>:"/\\|?*]/g, '-');
|
|
165
|
+
const filePath = path.join(this.resultsDir, `${sanitizedId}.json`);
|
|
166
|
+
logger_1.logger.debug(`Saving result to: ${filePath}`);
|
|
167
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
168
|
+
logger_1.logger.info(`Saved result: ${sanitizedId}`);
|
|
169
|
+
return { id: sanitizedId, name: data.name || sanitizedId };
|
|
170
|
+
}
|
|
171
|
+
generateComparison(results) {
|
|
172
|
+
const valid = results.filter(r => r !== null);
|
|
173
|
+
if (valid.length < 2)
|
|
174
|
+
return null;
|
|
175
|
+
const baseline = valid[0];
|
|
176
|
+
const comparisons = valid.slice(1).map(result => ({
|
|
177
|
+
id: result.id,
|
|
178
|
+
name: result.name,
|
|
179
|
+
timestamp: result.timestamp,
|
|
180
|
+
diff: {
|
|
181
|
+
avg_response_time: this.calcDiff(baseline.summary.avg_response_time, result.summary.avg_response_time),
|
|
182
|
+
p50_response_time: this.calcDiff(baseline.summary.p50_response_time, result.summary.p50_response_time),
|
|
183
|
+
p95_response_time: this.calcDiff(baseline.summary.p95_response_time, result.summary.p95_response_time),
|
|
184
|
+
p99_response_time: this.calcDiff(baseline.summary.p99_response_time, result.summary.p99_response_time),
|
|
185
|
+
requests_per_second: this.calcDiff(baseline.summary.requests_per_second, result.summary.requests_per_second, true),
|
|
186
|
+
error_rate: {
|
|
187
|
+
value: result.summary.error_rate,
|
|
188
|
+
baseline: baseline.summary.error_rate,
|
|
189
|
+
change: (result.summary.error_rate - baseline.summary.error_rate).toFixed(2) + '%',
|
|
190
|
+
improved: result.summary.error_rate < baseline.summary.error_rate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}));
|
|
194
|
+
// Generate step-level comparisons
|
|
195
|
+
const stepComparisons = this.generateStepComparisons(valid);
|
|
196
|
+
// Get timeline data for line graphs
|
|
197
|
+
const timelineComparisons = valid.map(result => ({
|
|
198
|
+
id: result.id,
|
|
199
|
+
name: result.name,
|
|
200
|
+
timeline: result.timeline_data || []
|
|
201
|
+
}));
|
|
202
|
+
return {
|
|
203
|
+
baseline: { id: baseline.id, name: baseline.name, timestamp: baseline.timestamp },
|
|
204
|
+
comparisons,
|
|
205
|
+
stepComparisons,
|
|
206
|
+
timelineComparisons
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
extractSummary(data) {
|
|
210
|
+
const s = data.summary || data;
|
|
211
|
+
const percentiles = s.percentiles || {};
|
|
212
|
+
const totalReq = s.total_requests || 0;
|
|
213
|
+
const failedReq = s.failed_requests || 0;
|
|
214
|
+
return {
|
|
215
|
+
total_requests: totalReq,
|
|
216
|
+
successful_requests: s.successful_requests || (totalReq - failedReq),
|
|
217
|
+
failed_requests: failedReq,
|
|
218
|
+
avg_response_time: s.avg_response_time || s.mean_response_time || 0,
|
|
219
|
+
min_response_time: s.min_response_time || 0,
|
|
220
|
+
max_response_time: s.max_response_time || 0,
|
|
221
|
+
p50_response_time: percentiles['50'] || s.p50_response_time || s.median_response_time || 0,
|
|
222
|
+
p75_response_time: percentiles['75'] || s.p75_response_time || 0,
|
|
223
|
+
p90_response_time: percentiles['90'] || s.p90_response_time || 0,
|
|
224
|
+
p95_response_time: percentiles['95'] || s.p95_response_time || 0,
|
|
225
|
+
p99_response_time: percentiles['99'] || s.p99_response_time || 0,
|
|
226
|
+
requests_per_second: s.requests_per_second || s.throughput || 0,
|
|
227
|
+
error_rate: s.error_rate ?? (failedReq / Math.max(1, totalReq) * 100),
|
|
228
|
+
success_rate: s.success_rate ?? ((totalReq - failedReq) / Math.max(1, totalReq) * 100)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
extractNetworkCalls(data) {
|
|
232
|
+
const calls = [];
|
|
233
|
+
// Extract from results array (each result may have custom_metrics.network_calls)
|
|
234
|
+
const results = data.results || [];
|
|
235
|
+
for (const result of results) {
|
|
236
|
+
if (result.custom_metrics?.network_calls) {
|
|
237
|
+
calls.push(...result.custom_metrics.network_calls);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Also check for direct network_calls array
|
|
241
|
+
if (data.network_calls) {
|
|
242
|
+
calls.push(...data.network_calls);
|
|
243
|
+
}
|
|
244
|
+
return calls;
|
|
245
|
+
}
|
|
246
|
+
generateStepComparisons(results) {
|
|
247
|
+
// Collect all unique step names across all results
|
|
248
|
+
const allSteps = new Set();
|
|
249
|
+
results.forEach(result => {
|
|
250
|
+
(result.step_statistics || []).forEach((step) => {
|
|
251
|
+
allSteps.add(step.step_name);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
// For each step, gather metrics from all results
|
|
255
|
+
const stepComparisons = [];
|
|
256
|
+
allSteps.forEach(stepName => {
|
|
257
|
+
const stepData = {
|
|
258
|
+
step_name: stepName,
|
|
259
|
+
results: results.map(result => {
|
|
260
|
+
const step = (result.step_statistics || []).find((s) => s.step_name === stepName);
|
|
261
|
+
if (!step)
|
|
262
|
+
return null;
|
|
263
|
+
return {
|
|
264
|
+
testId: result.id,
|
|
265
|
+
testName: result.name,
|
|
266
|
+
total_requests: step.total_requests,
|
|
267
|
+
failed_requests: step.failed_requests,
|
|
268
|
+
success_rate: step.success_rate,
|
|
269
|
+
avg_response_time: step.avg_response_time,
|
|
270
|
+
min_response_time: step.min_response_time,
|
|
271
|
+
max_response_time: step.max_response_time,
|
|
272
|
+
p50: step.percentiles?.[50] || step.p50 || 0,
|
|
273
|
+
p95: step.percentiles?.[95] || step.p95 || 0,
|
|
274
|
+
p99: step.percentiles?.[99] || step.p99 || 0
|
|
275
|
+
};
|
|
276
|
+
})
|
|
277
|
+
};
|
|
278
|
+
// Calculate diffs from baseline (first result)
|
|
279
|
+
const baseline = stepData.results[0];
|
|
280
|
+
if (baseline) {
|
|
281
|
+
stepData.diffs = stepData.results.slice(1).map((current) => {
|
|
282
|
+
if (!current)
|
|
283
|
+
return null;
|
|
284
|
+
return {
|
|
285
|
+
avg_response_time: this.calcDiff(baseline.avg_response_time, current.avg_response_time),
|
|
286
|
+
p95: this.calcDiff(baseline.p95, current.p95),
|
|
287
|
+
p99: this.calcDiff(baseline.p99, current.p99),
|
|
288
|
+
success_rate: {
|
|
289
|
+
value: current.success_rate,
|
|
290
|
+
baseline: baseline.success_rate,
|
|
291
|
+
change: (current.success_rate - baseline.success_rate).toFixed(2) + '%',
|
|
292
|
+
improved: current.success_rate > baseline.success_rate
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
stepComparisons.push(stepData);
|
|
298
|
+
});
|
|
299
|
+
return stepComparisons;
|
|
300
|
+
}
|
|
301
|
+
calcDiff(baseline, current, higherIsBetter = false) {
|
|
302
|
+
const change = baseline ? ((current - baseline) / baseline * 100) : 0;
|
|
303
|
+
return {
|
|
304
|
+
value: current,
|
|
305
|
+
baseline,
|
|
306
|
+
change: change.toFixed(2) + '%',
|
|
307
|
+
improved: higherIsBetter ? current > baseline : current < baseline
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
exports.ResultsManager = ResultsManager;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { LiveTest, InfrastructureMetrics } from '../types';
|
|
2
|
+
export interface TestRunOptions {
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
report?: boolean;
|
|
5
|
+
output?: string;
|
|
6
|
+
vus?: number;
|
|
7
|
+
iterations?: number;
|
|
8
|
+
duration?: string;
|
|
9
|
+
rampUp?: string;
|
|
10
|
+
headless?: boolean;
|
|
11
|
+
workers?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TestExecutorCallbacks {
|
|
14
|
+
onOutput: (testId: string, data: string) => void;
|
|
15
|
+
onLiveUpdate: (test: LiveTest) => void;
|
|
16
|
+
onNetworkUpdate: (testId: string, data: any) => void;
|
|
17
|
+
onTestComplete: (test: LiveTest) => void;
|
|
18
|
+
onTestFinished: (testId: string, exitCode: number | null) => void;
|
|
19
|
+
getInfraSnapshot?: () => Record<string, InfrastructureMetrics[]>;
|
|
20
|
+
}
|
|
21
|
+
export interface TestExecutorOptions {
|
|
22
|
+
/** When InfluxDB is enabled, don't limit response times in memory */
|
|
23
|
+
influxEnabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare class TestExecutor {
|
|
26
|
+
private testsDir;
|
|
27
|
+
private resultsDir;
|
|
28
|
+
private runningProcesses;
|
|
29
|
+
private liveTests;
|
|
30
|
+
private metricsParser;
|
|
31
|
+
private callbacks;
|
|
32
|
+
constructor(testsDir: string, resultsDir: string, liveTests: Map<string, LiveTest>, callbacks: TestExecutorCallbacks, options?: TestExecutorOptions);
|
|
33
|
+
runTest(testPath: string, options: TestRunOptions): {
|
|
34
|
+
testId: string;
|
|
35
|
+
status: string;
|
|
36
|
+
};
|
|
37
|
+
stopTest(testId: string): boolean;
|
|
38
|
+
killAllProcesses(): void;
|
|
39
|
+
private injectInfraMetrics;
|
|
40
|
+
private parseOutputLine;
|
|
41
|
+
}
|