@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,34 @@
|
|
|
1
|
+
import { TestConfiguration } from '../config';
|
|
2
|
+
import { MetricsCollector } from '../metrics/collector';
|
|
3
|
+
export declare class TestRunner {
|
|
4
|
+
private config;
|
|
5
|
+
private metrics;
|
|
6
|
+
private handlers;
|
|
7
|
+
private outputs;
|
|
8
|
+
private activeVUs;
|
|
9
|
+
private isRunning;
|
|
10
|
+
private startTime;
|
|
11
|
+
constructor(config: TestConfiguration);
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
private setupCSVBaseDirectory;
|
|
14
|
+
stop(): Promise<void>;
|
|
15
|
+
private initialize;
|
|
16
|
+
private initializeProtocolHandlers;
|
|
17
|
+
private getRequiredProtocols;
|
|
18
|
+
private findWSDLUrl;
|
|
19
|
+
private initializeOutputs;
|
|
20
|
+
/**
|
|
21
|
+
* Process template variables in file paths and automatically add timestamp if not present
|
|
22
|
+
*/
|
|
23
|
+
private processTemplateFilePath;
|
|
24
|
+
private executeLoadPattern;
|
|
25
|
+
private getLoadPatternForPhase;
|
|
26
|
+
private createVUFactory;
|
|
27
|
+
private waitForVUsToComplete;
|
|
28
|
+
private cleanup;
|
|
29
|
+
private finalize;
|
|
30
|
+
private logSummary;
|
|
31
|
+
private logErrorDetails;
|
|
32
|
+
private generateReport;
|
|
33
|
+
getMetrics(): MetricsCollector;
|
|
34
|
+
}
|
|
@@ -0,0 +1,465 @@
|
|
|
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.TestRunner = void 0;
|
|
37
|
+
const collector_1 = require("../metrics/collector");
|
|
38
|
+
const virtual_user_1 = require("./virtual-user");
|
|
39
|
+
const handler_1 = require("../protocols/rest/handler");
|
|
40
|
+
const handler_2 = require("../protocols/soap/handler");
|
|
41
|
+
const handler_3 = require("../protocols/web/handler");
|
|
42
|
+
const basic_1 = require("../load-patterns/basic");
|
|
43
|
+
const stepping_1 = require("../load-patterns/stepping");
|
|
44
|
+
const arrivals_1 = require("../load-patterns/arrivals");
|
|
45
|
+
const csv_1 = require("../outputs/csv");
|
|
46
|
+
const json_1 = require("../outputs/json");
|
|
47
|
+
const influxdb_1 = require("../outputs/influxdb");
|
|
48
|
+
const graphite_1 = require("../outputs/graphite");
|
|
49
|
+
const webhook_1 = require("../outputs/webhook");
|
|
50
|
+
const logger_1 = require("../utils/logger");
|
|
51
|
+
const time_1 = require("../utils/time");
|
|
52
|
+
const csv_data_provider_1 = require("./csv-data-provider");
|
|
53
|
+
const file_manager_1 = require("../utils/file-manager");
|
|
54
|
+
class TestRunner {
|
|
55
|
+
constructor(config) {
|
|
56
|
+
this.handlers = new Map();
|
|
57
|
+
this.outputs = [];
|
|
58
|
+
this.activeVUs = [];
|
|
59
|
+
this.isRunning = false;
|
|
60
|
+
this.startTime = 0;
|
|
61
|
+
this.config = config;
|
|
62
|
+
this.metrics = new collector_1.MetricsCollector();
|
|
63
|
+
// Set log level based on debug config
|
|
64
|
+
if (config.debug?.log_level || config.global?.debug?.log_level) {
|
|
65
|
+
const logLevel = config.debug?.log_level || config.global?.debug?.log_level;
|
|
66
|
+
switch (logLevel) {
|
|
67
|
+
case 'debug':
|
|
68
|
+
logger_1.logger.setLevel(logger_1.LogLevel.DEBUG);
|
|
69
|
+
break;
|
|
70
|
+
case 'info':
|
|
71
|
+
logger_1.logger.setLevel(logger_1.LogLevel.INFO);
|
|
72
|
+
break;
|
|
73
|
+
case 'warn':
|
|
74
|
+
logger_1.logger.setLevel(logger_1.LogLevel.WARN);
|
|
75
|
+
break;
|
|
76
|
+
case 'error':
|
|
77
|
+
logger_1.logger.setLevel(logger_1.LogLevel.ERROR);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async run() {
|
|
83
|
+
logger_1.logger.info(`๐ Starting test: ${this.config.name}`);
|
|
84
|
+
this.isRunning = true;
|
|
85
|
+
this.startTime = Date.now();
|
|
86
|
+
try {
|
|
87
|
+
await this.initialize();
|
|
88
|
+
// NO CSV termination callback setup needed anymore
|
|
89
|
+
await this.executeLoadPattern();
|
|
90
|
+
await this.finalize();
|
|
91
|
+
const duration = Date.now() - this.startTime;
|
|
92
|
+
logger_1.logger.success(`โ
Test completed successfully in ${(duration / 1000).toFixed(1)}s`);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger_1.logger.error('โ Test failed:', error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.isRunning = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
setupCSVBaseDirectory() {
|
|
103
|
+
const hasCSVScenarios = this.config.scenarios?.some((s) => s.csv_data);
|
|
104
|
+
if (hasCSVScenarios) {
|
|
105
|
+
const baseDir = process.cwd();
|
|
106
|
+
csv_data_provider_1.CSVDataProvider.setBaseDir(baseDir);
|
|
107
|
+
// Check if any scenario has cycleOnExhaustion: false
|
|
108
|
+
const hasStopOnExhaustion = this.config.scenarios?.some((s) => s.csv_data && s.csv_data.cycleOnExhaustion === false);
|
|
109
|
+
if (hasStopOnExhaustion) {
|
|
110
|
+
logger_1.logger.info('โน๏ธ CSV exhaustion handling enabled - individual VUs will stop when CSV data is exhausted');
|
|
111
|
+
}
|
|
112
|
+
logger_1.logger.debug(`Set CSV base directory: ${baseDir}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// FIXED: Added stop method that was missing
|
|
116
|
+
async stop() {
|
|
117
|
+
logger_1.logger.info('โน๏ธ Stopping test...');
|
|
118
|
+
this.isRunning = false;
|
|
119
|
+
// Stop all active VUs
|
|
120
|
+
this.activeVUs.forEach(vu => vu.stop());
|
|
121
|
+
// Wait for VUs to finish current operations
|
|
122
|
+
await this.waitForVUsToComplete(10000); // 10 second timeout
|
|
123
|
+
// Cleanup handlers
|
|
124
|
+
await this.cleanup();
|
|
125
|
+
}
|
|
126
|
+
async initialize() {
|
|
127
|
+
logger_1.logger.debug('๐ง Initializing test runner...');
|
|
128
|
+
// Initialize protocol handlers
|
|
129
|
+
await this.initializeProtocolHandlers();
|
|
130
|
+
// Initialize outputs
|
|
131
|
+
await this.initializeOutputs();
|
|
132
|
+
// Setup metrics collection
|
|
133
|
+
this.metrics.start();
|
|
134
|
+
this.metrics.on('result', (result) => {
|
|
135
|
+
this.outputs.forEach(output => {
|
|
136
|
+
if (output && typeof output.writeResult === 'function') {
|
|
137
|
+
output.writeResult(result).catch(err => logger_1.logger.warn('โ ๏ธ Output write failed:', err));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
logger_1.logger.debug('โ
Test runner initialized');
|
|
142
|
+
}
|
|
143
|
+
async initializeProtocolHandlers() {
|
|
144
|
+
// Check which protocols are needed based on scenarios
|
|
145
|
+
const protocolsNeeded = this.getRequiredProtocols();
|
|
146
|
+
const debugConfig = this.config.debug || this.config.global?.debug;
|
|
147
|
+
// Initialize REST handler if needed
|
|
148
|
+
if (protocolsNeeded.has('rest')) {
|
|
149
|
+
const handler = new handler_1.RESTHandler(this.config.global?.base_url, this.config.global?.headers || {}, this.config.global?.timeout, debugConfig // Pass debug config to handler
|
|
150
|
+
);
|
|
151
|
+
this.handlers.set('rest', handler);
|
|
152
|
+
logger_1.logger.debug('๐ REST handler initialized');
|
|
153
|
+
}
|
|
154
|
+
// Initialize SOAP handler if needed
|
|
155
|
+
if (protocolsNeeded.has('soap')) {
|
|
156
|
+
const wsdlUrl = this.findWSDLUrl();
|
|
157
|
+
if (wsdlUrl) {
|
|
158
|
+
const handler = new handler_2.SOAPHandler(wsdlUrl);
|
|
159
|
+
await handler.initialize();
|
|
160
|
+
this.handlers.set('soap', handler);
|
|
161
|
+
logger_1.logger.debug('๐งผ SOAP handler initialized');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Initialize Web handler if needed
|
|
165
|
+
if (protocolsNeeded.has('web')) {
|
|
166
|
+
const browserConfig = this.config.global?.web || this.config.global?.browser || {};
|
|
167
|
+
const webConfig = {
|
|
168
|
+
type: browserConfig.type || 'chromium',
|
|
169
|
+
headless: browserConfig.headless ?? true,
|
|
170
|
+
base_url: browserConfig.base_url || this.config.global?.base_url,
|
|
171
|
+
viewport: browserConfig.viewport,
|
|
172
|
+
slow_mo: browserConfig.slow_mo,
|
|
173
|
+
highlight: browserConfig.highlight,
|
|
174
|
+
clear_storage: browserConfig.clear_storage
|
|
175
|
+
};
|
|
176
|
+
const handler = new handler_3.WebHandler(webConfig);
|
|
177
|
+
await handler.initialize();
|
|
178
|
+
this.handlers.set('web', handler);
|
|
179
|
+
logger_1.logger.debug('๐ Web handler initialized');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
getRequiredProtocols() {
|
|
183
|
+
const protocols = new Set();
|
|
184
|
+
for (const scenario of this.config.scenarios) {
|
|
185
|
+
for (const step of scenario.steps) {
|
|
186
|
+
protocols.add(step.type || 'rest');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return protocols;
|
|
190
|
+
}
|
|
191
|
+
findWSDLUrl() {
|
|
192
|
+
// First check global config
|
|
193
|
+
if (this.config.global?.wsdl_url) {
|
|
194
|
+
return this.config.global.wsdl_url;
|
|
195
|
+
}
|
|
196
|
+
// Fallback: check individual steps (for backward compatibility)
|
|
197
|
+
for (const scenario of this.config.scenarios) {
|
|
198
|
+
for (const step of scenario.steps) {
|
|
199
|
+
if (step.type === 'soap' && 'wsdl' in step && step.wsdl) {
|
|
200
|
+
return step.wsdl;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
async initializeOutputs() {
|
|
207
|
+
if (!this.config.outputs)
|
|
208
|
+
return;
|
|
209
|
+
for (const outputConfig of this.config.outputs) {
|
|
210
|
+
// Skip disabled outputs
|
|
211
|
+
if (outputConfig.enabled === false) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
let output;
|
|
215
|
+
try {
|
|
216
|
+
// Process timestamp templates in file paths
|
|
217
|
+
const processedFilePath = this.processTemplateFilePath(outputConfig.file);
|
|
218
|
+
switch (outputConfig.type) {
|
|
219
|
+
case 'csv':
|
|
220
|
+
output = new csv_1.CSVOutput(processedFilePath);
|
|
221
|
+
break;
|
|
222
|
+
case 'json':
|
|
223
|
+
output = new json_1.JSONOutput(processedFilePath);
|
|
224
|
+
break;
|
|
225
|
+
case 'influxdb':
|
|
226
|
+
output = new influxdb_1.InfluxDBOutput(outputConfig.url, outputConfig.database, outputConfig.tags);
|
|
227
|
+
break;
|
|
228
|
+
case 'graphite':
|
|
229
|
+
const [host, port] = (outputConfig.url || 'localhost:2003').split(':');
|
|
230
|
+
output = new graphite_1.GraphiteOutput(host, parseInt(port || '2003'), 'perfornium');
|
|
231
|
+
break;
|
|
232
|
+
case 'webhook':
|
|
233
|
+
output = new webhook_1.WebhookOutput(outputConfig.url, outputConfig.headers || {}, 'json', outputConfig.template);
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
logger_1.logger.warn(`โ ๏ธ Unsupported output type: ${outputConfig.type}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
await output.initialize();
|
|
240
|
+
this.outputs.push(output);
|
|
241
|
+
logger_1.logger.debug(`๐ ${outputConfig.type} output initialized`);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
logger_1.logger.warn(`โ ๏ธ Failed to initialize ${outputConfig.type} output:`, error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Process template variables in file paths and automatically add timestamp if not present
|
|
250
|
+
*/
|
|
251
|
+
processTemplateFilePath(filePath) {
|
|
252
|
+
if (!filePath) {
|
|
253
|
+
return `results/${this.config.name}-{{timestamp}}.csv`;
|
|
254
|
+
}
|
|
255
|
+
// If no timestamp placeholder exists, automatically add one before the extension
|
|
256
|
+
let processedPath = filePath;
|
|
257
|
+
if (!filePath.includes('{{timestamp}}')) {
|
|
258
|
+
const lastDot = filePath.lastIndexOf('.');
|
|
259
|
+
if (lastDot > 0) {
|
|
260
|
+
const name = filePath.substring(0, lastDot);
|
|
261
|
+
const ext = filePath.substring(lastDot);
|
|
262
|
+
processedPath = `${name}-{{timestamp}}${ext}`;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
processedPath = `${filePath}-{{timestamp}}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Use FileManager to process timestamp templates
|
|
269
|
+
return file_manager_1.FileManager.processFilePath(processedPath);
|
|
270
|
+
}
|
|
271
|
+
async executeLoadPattern() {
|
|
272
|
+
// Setup base directory for CSV files BEFORE creating VUs
|
|
273
|
+
this.setupCSVBaseDirectory();
|
|
274
|
+
const vuFactory = this.createVUFactory();
|
|
275
|
+
// Support both single load config and array of phases
|
|
276
|
+
const loadPhases = Array.isArray(this.config.load) ? this.config.load : [this.config.load];
|
|
277
|
+
logger_1.logger.info(`๐ Executing ${loadPhases.length} load phase(s)`);
|
|
278
|
+
// Execute each load phase sequentially
|
|
279
|
+
for (let i = 0; i < loadPhases.length; i++) {
|
|
280
|
+
const phase = loadPhases[i];
|
|
281
|
+
const phaseName = phase.name || `Phase ${i + 1}`;
|
|
282
|
+
logger_1.logger.info(`\n๐ Starting ${phaseName}: ${phase.pattern} pattern`);
|
|
283
|
+
if (phase.virtual_users || phase.vus) {
|
|
284
|
+
logger_1.logger.info(` Users: ${phase.virtual_users || phase.vus}, Duration: ${phase.duration}`);
|
|
285
|
+
}
|
|
286
|
+
const pattern = this.getLoadPatternForPhase(phase);
|
|
287
|
+
await pattern.execute(phase, vuFactory);
|
|
288
|
+
// Wait for phase to complete
|
|
289
|
+
if (phase.duration) {
|
|
290
|
+
await this.waitForVUsToComplete();
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// For "run once" mode, clear activeVUs
|
|
294
|
+
this.activeVUs.forEach(vu => vu.stop());
|
|
295
|
+
this.activeVUs.length = 0;
|
|
296
|
+
}
|
|
297
|
+
logger_1.logger.success(`โ
Completed ${phaseName}`);
|
|
298
|
+
// Small gap between phases
|
|
299
|
+
if (i < loadPhases.length - 1) {
|
|
300
|
+
logger_1.logger.info('โธ๏ธ Pausing 2s between phases...');
|
|
301
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
logger_1.logger.success(`\n๐ All ${loadPhases.length} load phase(s) completed`);
|
|
305
|
+
}
|
|
306
|
+
getLoadPatternForPhase(phase) {
|
|
307
|
+
let pattern;
|
|
308
|
+
switch (phase.pattern) {
|
|
309
|
+
case 'basic':
|
|
310
|
+
pattern = new basic_1.BasicPattern();
|
|
311
|
+
if ('setTestRunningChecker' in pattern) {
|
|
312
|
+
pattern.setTestRunningChecker(() => this.isRunning);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
case 'stepping':
|
|
316
|
+
pattern = new stepping_1.SteppingPattern();
|
|
317
|
+
break;
|
|
318
|
+
case 'arrivals':
|
|
319
|
+
pattern = new arrivals_1.ArrivalsPattern();
|
|
320
|
+
break;
|
|
321
|
+
default:
|
|
322
|
+
throw new Error(`Unsupported load pattern: ${phase.pattern}`);
|
|
323
|
+
}
|
|
324
|
+
return pattern;
|
|
325
|
+
}
|
|
326
|
+
createVUFactory() {
|
|
327
|
+
return {
|
|
328
|
+
create: async (id) => {
|
|
329
|
+
// Pass global think time to VU
|
|
330
|
+
const globalThinkTime = this.config.global?.think_time;
|
|
331
|
+
// Build global CSV options if configured
|
|
332
|
+
let globalCSV;
|
|
333
|
+
if (this.config.global?.csv_data) {
|
|
334
|
+
globalCSV = {
|
|
335
|
+
config: this.config.global.csv_data,
|
|
336
|
+
mode: this.config.global.csv_mode || 'next'
|
|
337
|
+
};
|
|
338
|
+
logger_1.logger.debug(`VU ${id}: Global CSV configured from test config`);
|
|
339
|
+
}
|
|
340
|
+
const vu = new virtual_user_1.VirtualUser(id, this.metrics, this.handlers, this.config.name, undefined, // vuHooks
|
|
341
|
+
globalThinkTime, globalCSV);
|
|
342
|
+
// CRITICAL: Must await the CSV initialization
|
|
343
|
+
logger_1.logger.debug(`Initializing VU ${id} with scenarios (including CSV)...`);
|
|
344
|
+
await vu.setScenarios(this.config.scenarios);
|
|
345
|
+
logger_1.logger.debug(`VU ${id} fully initialized`);
|
|
346
|
+
this.activeVUs.push(vu);
|
|
347
|
+
return vu;
|
|
348
|
+
},
|
|
349
|
+
getMetrics: () => this.metrics
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async waitForVUsToComplete(timeoutMs = 60000) {
|
|
353
|
+
const startTime = Date.now();
|
|
354
|
+
while (this.activeVUs.length > 0 && this.isRunning) {
|
|
355
|
+
const elapsed = Date.now() - startTime;
|
|
356
|
+
if (elapsed > timeoutMs) {
|
|
357
|
+
logger_1.logger.warn(`โ ๏ธ Timeout waiting for ${this.activeVUs.length} VUs to complete`);
|
|
358
|
+
// Force stop remaining VUs
|
|
359
|
+
this.activeVUs.forEach(vu => vu.stop());
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
await (0, time_1.sleep)(1000);
|
|
363
|
+
if (this.activeVUs.length > 0 && elapsed % 5000 === 0) { // Log every 5 seconds
|
|
364
|
+
logger_1.logger.debug(`โณ Waiting for ${this.activeVUs.length} VUs to complete...`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
logger_1.logger.debug('โ
All VUs completed');
|
|
368
|
+
}
|
|
369
|
+
async cleanup() {
|
|
370
|
+
logger_1.logger.debug('๐งน Cleaning up handlers...');
|
|
371
|
+
// Cleanup protocol handlers
|
|
372
|
+
for (const [name, handler] of this.handlers) {
|
|
373
|
+
try {
|
|
374
|
+
if (handler.cleanup) {
|
|
375
|
+
await handler.cleanup();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
logger_1.logger.warn(`โ ๏ธ Error cleaning up ${name} handler:`, error);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async finalize() {
|
|
384
|
+
logger_1.logger.debug('๐ Finalizing test...');
|
|
385
|
+
// Cleanup handlers
|
|
386
|
+
await this.cleanup();
|
|
387
|
+
// Generate summary and write to outputs
|
|
388
|
+
const summary = this.metrics.getSummary();
|
|
389
|
+
this.logSummary(summary);
|
|
390
|
+
this.logErrorDetails(summary);
|
|
391
|
+
// Write summary to outputs
|
|
392
|
+
for (const output of this.outputs) {
|
|
393
|
+
try {
|
|
394
|
+
await output.writeSummary(summary);
|
|
395
|
+
await output.finalize();
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
logger_1.logger.warn('โ ๏ธ Output finalization failed:', error);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Generate HTML report if configured
|
|
402
|
+
if (this.config.report?.generate) {
|
|
403
|
+
await this.generateReport(summary);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
logSummary(summary) {
|
|
407
|
+
logger_1.logger.info('๐ Test Summary:');
|
|
408
|
+
logger_1.logger.info(` Total Requests: ${summary.total_requests}`);
|
|
409
|
+
logger_1.logger.info(` Success Rate: ${summary.success_rate.toFixed(2)}%`);
|
|
410
|
+
logger_1.logger.info(` Avg Response Time: ${summary.avg_response_time.toFixed(2)}ms`);
|
|
411
|
+
logger_1.logger.info(` Requests/sec: ${summary.requests_per_second.toFixed(2)}`);
|
|
412
|
+
logger_1.logger.info(` Duration: ${(summary.total_duration / 1000).toFixed(1)}s`);
|
|
413
|
+
}
|
|
414
|
+
logErrorDetails(summary) {
|
|
415
|
+
if (summary.failed_requests > 0) {
|
|
416
|
+
logger_1.logger.warn(`โ ${summary.failed_requests} requests failed`);
|
|
417
|
+
// Log status code distribution
|
|
418
|
+
logger_1.logger.info('๐ Status Code Distribution:');
|
|
419
|
+
Object.entries(summary.status_distribution)
|
|
420
|
+
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
|
421
|
+
.forEach(([status, count]) => {
|
|
422
|
+
const statusNum = parseInt(status);
|
|
423
|
+
const isError = statusNum >= 400;
|
|
424
|
+
const emoji = isError ? 'โ' : 'โ
';
|
|
425
|
+
logger_1.logger.info(` ${emoji} ${status}: ${count} requests`);
|
|
426
|
+
});
|
|
427
|
+
// Log top errors
|
|
428
|
+
if (summary.error_details && summary.error_details.length > 0) {
|
|
429
|
+
logger_1.logger.warn('๐ Top Error Details:');
|
|
430
|
+
summary.error_details.slice(0, 5).forEach((error, index) => {
|
|
431
|
+
logger_1.logger.warn(` ${index + 1}. ${error.error} (${error.count}x)`);
|
|
432
|
+
if (error.status) {
|
|
433
|
+
logger_1.logger.warn(` Status: ${error.status}`);
|
|
434
|
+
}
|
|
435
|
+
if (error.request_url) {
|
|
436
|
+
logger_1.logger.warn(` URL: ${error.request_url}`);
|
|
437
|
+
}
|
|
438
|
+
if (error.response_body && error.response_body.length < 200) {
|
|
439
|
+
logger_1.logger.warn(` Response: ${error.response_body}`);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async generateReport(summary) {
|
|
446
|
+
try {
|
|
447
|
+
const { EnhancedHTMLReportGenerator } = await Promise.resolve().then(() => __importStar(require('../reporting/enhanced-html-generator')));
|
|
448
|
+
const generator = new EnhancedHTMLReportGenerator();
|
|
449
|
+
// Process report path with automatic timestamp
|
|
450
|
+
const reportPath = this.processTemplateFilePath(this.config.report.output);
|
|
451
|
+
await generator.generate({
|
|
452
|
+
testName: this.config.name,
|
|
453
|
+
summary,
|
|
454
|
+
results: this.metrics.getResults()
|
|
455
|
+
}, reportPath);
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
logger_1.logger.error('โ Report generation failed:', error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
getMetrics() {
|
|
462
|
+
return this.metrics;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
exports.TestRunner = TestRunner;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ThresholdConfig } from '../config';
|
|
2
|
+
import { TestResult } from '../metrics/types';
|
|
3
|
+
export interface ThresholdEvaluationResult {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
failures: ThresholdFailure[];
|
|
6
|
+
}
|
|
7
|
+
export interface ThresholdFailure {
|
|
8
|
+
threshold: ThresholdConfig;
|
|
9
|
+
actualValue: number | string;
|
|
10
|
+
expectedValue: number | string;
|
|
11
|
+
message: string;
|
|
12
|
+
severity: 'warning' | 'error' | 'critical';
|
|
13
|
+
}
|
|
14
|
+
export declare class ThresholdEvaluator {
|
|
15
|
+
/**
|
|
16
|
+
* Evaluate all thresholds against a test result
|
|
17
|
+
*/
|
|
18
|
+
static evaluate(thresholds: ThresholdConfig[], result: TestResult, stepName?: string): ThresholdEvaluationResult;
|
|
19
|
+
/**
|
|
20
|
+
* Evaluate a single threshold
|
|
21
|
+
*/
|
|
22
|
+
private static evaluateThreshold;
|
|
23
|
+
/**
|
|
24
|
+
* Extract the actual metric value from test result
|
|
25
|
+
*/
|
|
26
|
+
private static extractMetricValue;
|
|
27
|
+
/**
|
|
28
|
+
* Parse expected value, handling units like "1s", "500ms", etc.
|
|
29
|
+
*/
|
|
30
|
+
private static parseExpectedValue;
|
|
31
|
+
/**
|
|
32
|
+
* Compare actual vs expected values using the specified operator
|
|
33
|
+
*/
|
|
34
|
+
private static compareValues;
|
|
35
|
+
/**
|
|
36
|
+
* Get human-readable operator symbol
|
|
37
|
+
*/
|
|
38
|
+
private static getOperatorSymbol;
|
|
39
|
+
/**
|
|
40
|
+
* Execute threshold action based on severity and configuration
|
|
41
|
+
*/
|
|
42
|
+
static executeThresholdActions(evaluationResult: ThresholdEvaluationResult, stepName?: string): Promise<void>;
|
|
43
|
+
}
|