@testsmith/perfornium 0.1.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 +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,281 @@
|
|
|
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.mockCommand = mockCommand;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const logger_1 = require("../../utils/logger");
|
|
39
|
+
// Sample data for mock endpoints
|
|
40
|
+
const users = [
|
|
41
|
+
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'admin' },
|
|
42
|
+
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'user' },
|
|
43
|
+
{ id: 3, name: 'Charlie Brown', email: 'charlie@example.com', role: 'user' },
|
|
44
|
+
{ id: 4, name: 'Diana Prince', email: 'diana@example.com', role: 'moderator' },
|
|
45
|
+
{ id: 5, name: 'Eve Wilson', email: 'eve@example.com', role: 'user' },
|
|
46
|
+
];
|
|
47
|
+
const products = [
|
|
48
|
+
{ id: 'P001', name: 'Laptop', price: 999.99, category: 'Electronics', inStock: true },
|
|
49
|
+
{ id: 'P002', name: 'Smartphone', price: 599.99, category: 'Electronics', inStock: true },
|
|
50
|
+
{ id: 'P003', name: 'Headphones', price: 149.99, category: 'Electronics', inStock: false },
|
|
51
|
+
{ id: 'P004', name: 'Keyboard', price: 79.99, category: 'Accessories', inStock: true },
|
|
52
|
+
{ id: 'P005', name: 'Mouse', price: 39.99, category: 'Accessories', inStock: true },
|
|
53
|
+
];
|
|
54
|
+
let requestCount = 0;
|
|
55
|
+
const startTime = Date.now();
|
|
56
|
+
async function mockCommand(options) {
|
|
57
|
+
const port = parseInt(options.port || '3000');
|
|
58
|
+
const host = options.host || 'localhost';
|
|
59
|
+
const delay = parseInt(options.delay || '0');
|
|
60
|
+
const server = http.createServer(async (req, res) => {
|
|
61
|
+
requestCount++;
|
|
62
|
+
const requestId = requestCount;
|
|
63
|
+
// Add artificial delay if configured
|
|
64
|
+
if (delay > 0) {
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
66
|
+
}
|
|
67
|
+
// CORS headers
|
|
68
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
69
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
70
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
71
|
+
res.setHeader('Content-Type', 'application/json');
|
|
72
|
+
// Handle OPTIONS (preflight)
|
|
73
|
+
if (req.method === 'OPTIONS') {
|
|
74
|
+
res.writeHead(204);
|
|
75
|
+
res.end();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const url = new URL(req.url || '/', `http://${host}:${port}`);
|
|
79
|
+
const path = url.pathname;
|
|
80
|
+
logger_1.logger.debug(`[${requestId}] ${req.method} ${path}`);
|
|
81
|
+
try {
|
|
82
|
+
// Route handling
|
|
83
|
+
if (path === '/status' || path === '/health') {
|
|
84
|
+
// Health check endpoint
|
|
85
|
+
res.writeHead(200);
|
|
86
|
+
res.end(JSON.stringify({
|
|
87
|
+
status: 'ok',
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
90
|
+
requests: requestCount
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
else if (path === '/users' && req.method === 'GET') {
|
|
94
|
+
// List users
|
|
95
|
+
res.writeHead(200);
|
|
96
|
+
res.end(JSON.stringify(users));
|
|
97
|
+
}
|
|
98
|
+
else if (path.match(/^\/users\/\d+$/) && req.method === 'GET') {
|
|
99
|
+
// Get single user
|
|
100
|
+
const id = parseInt(path.split('/')[2]);
|
|
101
|
+
const user = users.find(u => u.id === id);
|
|
102
|
+
if (user) {
|
|
103
|
+
res.writeHead(200);
|
|
104
|
+
res.end(JSON.stringify(user));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
res.writeHead(404);
|
|
108
|
+
res.end(JSON.stringify({ error: 'User not found', id }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (path === '/users' && req.method === 'POST') {
|
|
112
|
+
// Create user
|
|
113
|
+
let body = '';
|
|
114
|
+
req.on('data', chunk => body += chunk);
|
|
115
|
+
req.on('end', () => {
|
|
116
|
+
try {
|
|
117
|
+
const data = JSON.parse(body);
|
|
118
|
+
const newUser = {
|
|
119
|
+
id: users.length + 1,
|
|
120
|
+
name: data.name || 'New User',
|
|
121
|
+
email: data.email || 'new@example.com',
|
|
122
|
+
role: data.role || 'user'
|
|
123
|
+
};
|
|
124
|
+
users.push(newUser);
|
|
125
|
+
res.writeHead(201);
|
|
126
|
+
res.end(JSON.stringify(newUser));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
res.writeHead(400);
|
|
130
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
else if (path === '/products' && req.method === 'GET') {
|
|
136
|
+
// List products with optional filtering
|
|
137
|
+
const category = url.searchParams.get('category');
|
|
138
|
+
const inStock = url.searchParams.get('inStock');
|
|
139
|
+
let filtered = [...products];
|
|
140
|
+
if (category) {
|
|
141
|
+
filtered = filtered.filter(p => p.category.toLowerCase() === category.toLowerCase());
|
|
142
|
+
}
|
|
143
|
+
if (inStock !== null) {
|
|
144
|
+
filtered = filtered.filter(p => p.inStock === (inStock === 'true'));
|
|
145
|
+
}
|
|
146
|
+
res.writeHead(200);
|
|
147
|
+
res.end(JSON.stringify(filtered));
|
|
148
|
+
}
|
|
149
|
+
else if (path.match(/^\/products\/P\d+$/) && req.method === 'GET') {
|
|
150
|
+
// Get single product
|
|
151
|
+
const id = path.split('/')[2];
|
|
152
|
+
const product = products.find(p => p.id === id);
|
|
153
|
+
if (product) {
|
|
154
|
+
res.writeHead(200);
|
|
155
|
+
res.end(JSON.stringify(product));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
res.writeHead(404);
|
|
159
|
+
res.end(JSON.stringify({ error: 'Product not found', id }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (path === '/auth/login' && req.method === 'POST') {
|
|
163
|
+
// Login endpoint
|
|
164
|
+
let body = '';
|
|
165
|
+
req.on('data', chunk => body += chunk);
|
|
166
|
+
req.on('end', () => {
|
|
167
|
+
try {
|
|
168
|
+
const data = JSON.parse(body);
|
|
169
|
+
if (data.username && data.password) {
|
|
170
|
+
res.writeHead(200);
|
|
171
|
+
res.end(JSON.stringify({
|
|
172
|
+
token: `mock-token-${Date.now()}`,
|
|
173
|
+
user: { username: data.username, role: 'user' },
|
|
174
|
+
expiresIn: 3600
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
res.writeHead(401);
|
|
179
|
+
res.end(JSON.stringify({ error: 'Invalid credentials' }));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
res.writeHead(400);
|
|
184
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
else if (path === '/echo' && req.method === 'POST') {
|
|
190
|
+
// Echo back the request body
|
|
191
|
+
let body = '';
|
|
192
|
+
req.on('data', chunk => body += chunk);
|
|
193
|
+
req.on('end', () => {
|
|
194
|
+
res.writeHead(200);
|
|
195
|
+
res.end(JSON.stringify({
|
|
196
|
+
method: req.method,
|
|
197
|
+
path: path,
|
|
198
|
+
headers: req.headers,
|
|
199
|
+
body: body ? JSON.parse(body) : null,
|
|
200
|
+
timestamp: new Date().toISOString()
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
else if (path === '/delay' && req.method === 'GET') {
|
|
206
|
+
// Configurable delay endpoint
|
|
207
|
+
const ms = parseInt(url.searchParams.get('ms') || '1000');
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
209
|
+
res.writeHead(200);
|
|
210
|
+
res.end(JSON.stringify({ delayed: ms, timestamp: new Date().toISOString() }));
|
|
211
|
+
}
|
|
212
|
+
else if (path === '/error' && req.method === 'GET') {
|
|
213
|
+
// Simulate error responses
|
|
214
|
+
const code = parseInt(url.searchParams.get('code') || '500');
|
|
215
|
+
res.writeHead(code);
|
|
216
|
+
res.end(JSON.stringify({ error: `Simulated ${code} error`, code }));
|
|
217
|
+
}
|
|
218
|
+
else if (path === '/random' && req.method === 'GET') {
|
|
219
|
+
// Random data endpoint
|
|
220
|
+
res.writeHead(200);
|
|
221
|
+
res.end(JSON.stringify({
|
|
222
|
+
id: Math.floor(Math.random() * 10000),
|
|
223
|
+
uuid: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
random: Math.random()
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// 404 for unknown routes
|
|
230
|
+
res.writeHead(404);
|
|
231
|
+
res.end(JSON.stringify({
|
|
232
|
+
error: 'Not found',
|
|
233
|
+
path,
|
|
234
|
+
availableEndpoints: [
|
|
235
|
+
'GET /status',
|
|
236
|
+
'GET /health',
|
|
237
|
+
'GET /users',
|
|
238
|
+
'GET /users/:id',
|
|
239
|
+
'POST /users',
|
|
240
|
+
'GET /products',
|
|
241
|
+
'GET /products/:id',
|
|
242
|
+
'POST /auth/login',
|
|
243
|
+
'POST /echo',
|
|
244
|
+
'GET /delay?ms=1000',
|
|
245
|
+
'GET /error?code=500',
|
|
246
|
+
'GET /random'
|
|
247
|
+
]
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
res.writeHead(500);
|
|
253
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
server.listen(port, host, () => {
|
|
257
|
+
logger_1.logger.success(`\n🚀 Mock server running at http://${host}:${port}\n`);
|
|
258
|
+
logger_1.logger.info('Available endpoints:');
|
|
259
|
+
logger_1.logger.info(' GET /status - Health check');
|
|
260
|
+
logger_1.logger.info(' GET /health - Health check (alias)');
|
|
261
|
+
logger_1.logger.info(' GET /users - List all users');
|
|
262
|
+
logger_1.logger.info(' GET /users/:id - Get user by ID');
|
|
263
|
+
logger_1.logger.info(' POST /users - Create user');
|
|
264
|
+
logger_1.logger.info(' GET /products - List products (?category=X&inStock=true)');
|
|
265
|
+
logger_1.logger.info(' GET /products/:id - Get product by ID');
|
|
266
|
+
logger_1.logger.info(' POST /auth/login - Login (returns token)');
|
|
267
|
+
logger_1.logger.info(' POST /echo - Echo request body');
|
|
268
|
+
logger_1.logger.info(' GET /delay?ms=1000 - Delayed response');
|
|
269
|
+
logger_1.logger.info(' GET /error?code=500 - Simulate error');
|
|
270
|
+
logger_1.logger.info(' GET /random - Random data');
|
|
271
|
+
logger_1.logger.info('\nPress Ctrl+C to stop\n');
|
|
272
|
+
});
|
|
273
|
+
// Handle graceful shutdown
|
|
274
|
+
process.on('SIGINT', () => {
|
|
275
|
+
logger_1.logger.info('\nShutting down mock server...');
|
|
276
|
+
server.close(() => {
|
|
277
|
+
logger_1.logger.success('Mock server stopped');
|
|
278
|
+
process.exit(0);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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.reportCommand = reportCommand;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const enhanced_html_generator_1 = require("../../reporting/enhanced-html-generator");
|
|
40
|
+
const logger_1 = require("../../utils/logger");
|
|
41
|
+
async function reportCommand(resultsPath, options) {
|
|
42
|
+
try {
|
|
43
|
+
logger_1.logger.info(`📊 Generating report from: ${resultsPath}`);
|
|
44
|
+
if (!fs.existsSync(resultsPath)) {
|
|
45
|
+
logger_1.logger.error(`❌ Results file not found: ${resultsPath}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const resultsContent = fs.readFileSync(resultsPath, 'utf8');
|
|
49
|
+
let resultsData;
|
|
50
|
+
if (resultsPath.endsWith('.json')) {
|
|
51
|
+
resultsData = JSON.parse(resultsContent);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
logger_1.logger.error('❌ Only JSON results files are currently supported');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const generator = new enhanced_html_generator_1.EnhancedHTMLReportGenerator();
|
|
58
|
+
const outputPath = options.output || 'report.html';
|
|
59
|
+
await generator.generate({
|
|
60
|
+
testName: options.title || resultsData.testName || 'Performance Test',
|
|
61
|
+
summary: resultsData.summary,
|
|
62
|
+
results: resultsData.results || []
|
|
63
|
+
}, outputPath);
|
|
64
|
+
logger_1.logger.success(`✅ Report generated: ${path.resolve(outputPath)}`);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
logger_1.logger.error(`❌ Report generation failed: ${error.message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RunOptions {
|
|
2
|
+
env?: string;
|
|
3
|
+
workers?: string;
|
|
4
|
+
output?: string;
|
|
5
|
+
report?: boolean;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
verbose?: boolean;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
maxUsers?: string;
|
|
10
|
+
global?: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function runCommand(configPath: string, options: RunOptions): Promise<void>;
|
|
@@ -0,0 +1,260 @@
|
|
|
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.runCommand = runCommand;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const http = __importStar(require("http"));
|
|
39
|
+
const parser_1 = require("../../config/parser");
|
|
40
|
+
const validator_1 = require("../../config/validator");
|
|
41
|
+
const test_runner_1 = require("../../core/test-runner");
|
|
42
|
+
const logger_1 = require("../../utils/logger");
|
|
43
|
+
// Lightweight mock server for testing
|
|
44
|
+
let mockServer = null;
|
|
45
|
+
let mockRequestCount = 0;
|
|
46
|
+
const mockStartTime = Date.now();
|
|
47
|
+
function startMockServer(port = 3000) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
mockServer = http.createServer((req, res) => {
|
|
50
|
+
mockRequestCount++;
|
|
51
|
+
res.setHeader('Content-Type', 'application/json');
|
|
52
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
53
|
+
if (req.method === 'OPTIONS') {
|
|
54
|
+
res.writeHead(204);
|
|
55
|
+
res.end();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Simple /status endpoint
|
|
59
|
+
res.writeHead(200);
|
|
60
|
+
res.end(JSON.stringify({
|
|
61
|
+
status: 'ok',
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
uptime: Math.floor((Date.now() - mockStartTime) / 1000),
|
|
64
|
+
requests: mockRequestCount
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
mockServer.on('error', (err) => {
|
|
68
|
+
if (err.code === 'EADDRINUSE') {
|
|
69
|
+
logger_1.logger.warn(`Port ${port} in use, mock server not started`);
|
|
70
|
+
resolve(); // Continue without mock server
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
mockServer.listen(port, 'localhost', () => {
|
|
77
|
+
logger_1.logger.info(`🎯 Mock server started at http://localhost:${port}/status`);
|
|
78
|
+
resolve();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function stopMockServer() {
|
|
83
|
+
if (mockServer) {
|
|
84
|
+
mockServer.close();
|
|
85
|
+
mockServer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function runCommand(configPath, options) {
|
|
89
|
+
try {
|
|
90
|
+
// Set log level: default=WARN, --verbose=INFO, --debug=DEBUG
|
|
91
|
+
if (options.debug) {
|
|
92
|
+
logger_1.logger.setLevel(logger_1.LogLevel.DEBUG);
|
|
93
|
+
}
|
|
94
|
+
else if (options.verbose) {
|
|
95
|
+
logger_1.logger.setLevel(logger_1.LogLevel.INFO);
|
|
96
|
+
}
|
|
97
|
+
logger_1.logger.info(`Loading configuration: ${configPath}`);
|
|
98
|
+
const parser = new parser_1.ConfigParser();
|
|
99
|
+
const config = await parser.parse(configPath, options.env);
|
|
100
|
+
// Apply CLI global variable overrides
|
|
101
|
+
applyGlobalOverrides(config, options);
|
|
102
|
+
// Auto-start mock server if base_url targets localhost:3000
|
|
103
|
+
const baseUrl = config.global?.base_url || '';
|
|
104
|
+
if (baseUrl.match(/^https?:\/\/localhost:3000/)) {
|
|
105
|
+
await startMockServer(3000);
|
|
106
|
+
}
|
|
107
|
+
// Apply max users override
|
|
108
|
+
if (options.maxUsers) {
|
|
109
|
+
const maxUsers = parseInt(options.maxUsers);
|
|
110
|
+
const { getPrimaryLoadPhase } = await Promise.resolve().then(() => __importStar(require('../../config/types/load-config')));
|
|
111
|
+
const primaryPhase = getPrimaryLoadPhase(config.load);
|
|
112
|
+
const currentUsers = primaryPhase.virtual_users || primaryPhase.vus;
|
|
113
|
+
if (currentUsers && currentUsers > maxUsers) {
|
|
114
|
+
logger_1.logger.warn(`Limiting virtual users from ${currentUsers} to ${maxUsers}`);
|
|
115
|
+
primaryPhase.virtual_users = maxUsers;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Process templates with environment variables
|
|
119
|
+
const processedConfig = parser.processTemplates(config, {
|
|
120
|
+
env: process.env,
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
datetime: new Date().toISOString()
|
|
123
|
+
});
|
|
124
|
+
logger_1.logger.info(`Validating configuration...`);
|
|
125
|
+
const validator = new validator_1.ConfigValidator();
|
|
126
|
+
const validation = validator.validate(processedConfig);
|
|
127
|
+
if (!validation.valid) {
|
|
128
|
+
logger_1.logger.error('Configuration validation failed:');
|
|
129
|
+
validation.errors.forEach(error => logger_1.logger.error(` - ${error}`));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (validation.warnings.length > 0) {
|
|
133
|
+
logger_1.logger.warn('Configuration warnings:');
|
|
134
|
+
validation.warnings.forEach(warning => logger_1.logger.warn(` - ${warning}`));
|
|
135
|
+
}
|
|
136
|
+
if (options.dryRun) {
|
|
137
|
+
logger_1.logger.success('Configuration is valid');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Override output directory if specified
|
|
141
|
+
if (options.output && processedConfig.outputs) {
|
|
142
|
+
processedConfig.outputs.forEach(output => {
|
|
143
|
+
if (output.file) {
|
|
144
|
+
output.file = path.join(options.output, path.basename(output.file));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Enable report generation if requested
|
|
149
|
+
if (options.report) {
|
|
150
|
+
processedConfig.report = {
|
|
151
|
+
generate: true,
|
|
152
|
+
output: options.output ? path.join(options.output, 'report.html') : 'report.html',
|
|
153
|
+
...processedConfig.report
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (options.workers) {
|
|
157
|
+
const { WorkerManager } = await Promise.resolve().then(() => __importStar(require('../../workers/manager')));
|
|
158
|
+
const manager = new WorkerManager();
|
|
159
|
+
const workerAddresses = options.workers.split(',').map(w => w.trim());
|
|
160
|
+
for (const address of workerAddresses) {
|
|
161
|
+
await manager.addWorker(address);
|
|
162
|
+
}
|
|
163
|
+
await manager.distributeTest(processedConfig);
|
|
164
|
+
await manager.waitForCompletion();
|
|
165
|
+
const metrics = manager.getAggregatedMetrics();
|
|
166
|
+
const summary = metrics.getSummary();
|
|
167
|
+
logger_1.logger.success(`Distributed test completed: ${summary.success_rate.toFixed(2)}% success rate`);
|
|
168
|
+
await manager.cleanup();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const runner = new test_runner_1.TestRunner(processedConfig);
|
|
172
|
+
// Set up graceful shutdown
|
|
173
|
+
process.on('SIGINT', async () => {
|
|
174
|
+
logger_1.logger.info('Received SIGINT, stopping test...');
|
|
175
|
+
await runner.stop();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
await runner.run();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
logger_1.logger.error(`Test execution failed: ${error.message}`);
|
|
183
|
+
if (options.verbose || options.debug) {
|
|
184
|
+
console.error(error.stack);
|
|
185
|
+
}
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
// Stop mock server if it was started
|
|
190
|
+
stopMockServer();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Apply CLI global variable overrides to the config
|
|
195
|
+
* Supports dot notation for nested properties: browser.headless=false
|
|
196
|
+
*/
|
|
197
|
+
function applyGlobalOverrides(config, options) {
|
|
198
|
+
if (!options.global || options.global.length === 0) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Ensure global config exists
|
|
202
|
+
if (!config.global) {
|
|
203
|
+
config.global = {};
|
|
204
|
+
}
|
|
205
|
+
for (const override of options.global) {
|
|
206
|
+
const eqIndex = override.indexOf('=');
|
|
207
|
+
if (eqIndex === -1) {
|
|
208
|
+
logger_1.logger.warn(`Invalid global override format: ${override} (expected key=value)`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const key = override.substring(0, eqIndex);
|
|
212
|
+
const rawValue = override.substring(eqIndex + 1);
|
|
213
|
+
const value = parseValue(rawValue);
|
|
214
|
+
logger_1.logger.info(`Overriding global.${key} = ${JSON.stringify(value)}`);
|
|
215
|
+
setNestedValue(config.global, key, value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Parse a string value to its appropriate type
|
|
220
|
+
*/
|
|
221
|
+
function parseValue(value) {
|
|
222
|
+
// Boolean
|
|
223
|
+
if (value.toLowerCase() === 'true')
|
|
224
|
+
return true;
|
|
225
|
+
if (value.toLowerCase() === 'false')
|
|
226
|
+
return false;
|
|
227
|
+
// Number (integer or float)
|
|
228
|
+
if (/^-?\d+$/.test(value))
|
|
229
|
+
return parseInt(value, 10);
|
|
230
|
+
if (/^-?\d+\.\d+$/.test(value))
|
|
231
|
+
return parseFloat(value);
|
|
232
|
+
// JSON object or array
|
|
233
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
234
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(value);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Not valid JSON, return as string
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// String (keep as-is)
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Set a nested value using dot notation
|
|
247
|
+
* Example: setNestedValue(obj, 'browser.headless', false)
|
|
248
|
+
*/
|
|
249
|
+
function setNestedValue(obj, path, value) {
|
|
250
|
+
const parts = path.split('.');
|
|
251
|
+
let current = obj;
|
|
252
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
253
|
+
const part = parts[i];
|
|
254
|
+
if (!(part in current) || typeof current[part] !== 'object') {
|
|
255
|
+
current[part] = {};
|
|
256
|
+
}
|
|
257
|
+
current = current[part];
|
|
258
|
+
}
|
|
259
|
+
current[parts[parts.length - 1]] = value;
|
|
260
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCommand = validateCommand;
|
|
4
|
+
const parser_1 = require("../../config/parser");
|
|
5
|
+
const validator_1 = require("../../config/validator");
|
|
6
|
+
const logger_1 = require("../../utils/logger");
|
|
7
|
+
async function validateCommand(configPath, options) {
|
|
8
|
+
try {
|
|
9
|
+
logger_1.logger.info(`🔍 Validating configuration: ${configPath}`);
|
|
10
|
+
const parser = new parser_1.ConfigParser();
|
|
11
|
+
const config = await parser.parse(configPath, options.env);
|
|
12
|
+
const validator = new validator_1.ConfigValidator();
|
|
13
|
+
const result = validator.validate(config);
|
|
14
|
+
if (result.valid) {
|
|
15
|
+
logger_1.logger.success('✅ Configuration is valid');
|
|
16
|
+
if (result.warnings.length > 0) {
|
|
17
|
+
logger_1.logger.warn('⚠️ Warnings:');
|
|
18
|
+
result.warnings.forEach(warning => logger_1.logger.warn(` - ${warning}`));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
logger_1.logger.error('❌ Configuration validation failed:');
|
|
23
|
+
result.errors.forEach(error => logger_1.logger.error(` - ${error}`));
|
|
24
|
+
if (result.warnings.length > 0) {
|
|
25
|
+
logger_1.logger.warn('⚠️ Warnings:');
|
|
26
|
+
result.warnings.forEach(warning => logger_1.logger.warn(` - ${warning}`));
|
|
27
|
+
}
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger_1.logger.error(`❌ Validation failed: ${error.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|