@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,440 @@
|
|
|
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.WSDLImporter = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class WSDLImporter {
|
|
40
|
+
constructor(wsdlContent) {
|
|
41
|
+
this.selectedSoapVersions = [];
|
|
42
|
+
this.wsdlContent = wsdlContent;
|
|
43
|
+
}
|
|
44
|
+
detectSoapVersions() {
|
|
45
|
+
const versions = [];
|
|
46
|
+
// Find SOAP 1.1 bindings
|
|
47
|
+
const soap11BindingRegex = /<wsdl:binding[^>]*name="([^"]*)"[^>]*>[\s\S]*?<soap:binding[^>]*>[\s\S]*?<\/wsdl:binding>/g;
|
|
48
|
+
let soap11Match;
|
|
49
|
+
while ((soap11Match = soap11BindingRegex.exec(this.wsdlContent)) !== null) {
|
|
50
|
+
if (!versions.some(v => v.bindingName === soap11Match[1])) {
|
|
51
|
+
versions.push({
|
|
52
|
+
version: '1.1',
|
|
53
|
+
bindingName: soap11Match[1],
|
|
54
|
+
description: 'SOAP 1.1 (Legacy, widely supported)'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Find SOAP 1.2 bindings
|
|
59
|
+
const soap12BindingRegex = /<wsdl:binding[^>]*name="([^"]*)"[^>]*>[\s\S]*?<soap12:binding[^>]*>[\s\S]*?<\/wsdl:binding>/g;
|
|
60
|
+
let soap12Match;
|
|
61
|
+
while ((soap12Match = soap12BindingRegex.exec(this.wsdlContent)) !== null) {
|
|
62
|
+
if (!versions.some(v => v.bindingName === soap12Match[1])) {
|
|
63
|
+
versions.push({
|
|
64
|
+
version: '1.2',
|
|
65
|
+
bindingName: soap12Match[1],
|
|
66
|
+
description: 'SOAP 1.2 (Modern, more features)'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return versions;
|
|
71
|
+
}
|
|
72
|
+
setSoapVersions(versions) {
|
|
73
|
+
this.selectedSoapVersions = versions;
|
|
74
|
+
}
|
|
75
|
+
extractServices() {
|
|
76
|
+
const services = [];
|
|
77
|
+
try {
|
|
78
|
+
const serviceElements = this.extractServiceElements();
|
|
79
|
+
for (const serviceElement of serviceElements) {
|
|
80
|
+
const serviceName = this.extractAttribute(serviceElement.content, 'name') || 'UnknownService';
|
|
81
|
+
const ports = this.extractPortElements(serviceElement.content);
|
|
82
|
+
for (const port of ports) {
|
|
83
|
+
const bindingName = this.extractAttribute(port.content, 'binding')?.replace('tns:', '') || '';
|
|
84
|
+
if (!this.isBindingVersionSelected(bindingName)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const endpoint = this.extractEndpointFromPort(port.content);
|
|
88
|
+
const soapVersion = this.getSoapVersionFromBinding(bindingName);
|
|
89
|
+
const operations = this.extractOperationsFromBinding(bindingName);
|
|
90
|
+
for (const operation of operations) {
|
|
91
|
+
services.push(this.createImportableEndpoint({
|
|
92
|
+
serviceName,
|
|
93
|
+
bindingName,
|
|
94
|
+
endpoint,
|
|
95
|
+
soapVersion,
|
|
96
|
+
operation
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error('Error parsing WSDL:', error);
|
|
104
|
+
}
|
|
105
|
+
return services;
|
|
106
|
+
}
|
|
107
|
+
generateOutputFilename(baseFilename) {
|
|
108
|
+
if (!fs.existsSync(baseFilename)) {
|
|
109
|
+
return baseFilename;
|
|
110
|
+
}
|
|
111
|
+
const ext = path.extname(baseFilename);
|
|
112
|
+
const nameWithoutExt = path.basename(baseFilename, ext);
|
|
113
|
+
const dir = path.dirname(baseFilename);
|
|
114
|
+
let counter = 1;
|
|
115
|
+
let newFilename;
|
|
116
|
+
do {
|
|
117
|
+
newFilename = path.join(dir, `${nameWithoutExt}_${counter}${ext}`);
|
|
118
|
+
counter++;
|
|
119
|
+
} while (fs.existsSync(newFilename));
|
|
120
|
+
return newFilename;
|
|
121
|
+
}
|
|
122
|
+
generateScenarios(selectedServices) {
|
|
123
|
+
return selectedServices.map(service => {
|
|
124
|
+
const operationName = service.name.split('.').pop() || 'Unknown';
|
|
125
|
+
const args = this.generateArgsFromSchema(service.requestBody?.schema);
|
|
126
|
+
const soapVersion = service.tags?.find(tag => tag.startsWith('SOAP-'))?.replace('SOAP-', '') || '1.1';
|
|
127
|
+
return {
|
|
128
|
+
name: `test_${service.name.toLowerCase().replace(/\./g, '_')}`,
|
|
129
|
+
steps: [{
|
|
130
|
+
name: service.name,
|
|
131
|
+
type: 'soap',
|
|
132
|
+
operation: operationName,
|
|
133
|
+
args: args,
|
|
134
|
+
body: service.requestBody?.example || this.generateSOAPExample(operationName, service.requestBody?.schema, soapVersion),
|
|
135
|
+
extract: this.generateSOAPExtractions(operationName)
|
|
136
|
+
}]
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
getWsdlUrl(selectedServices) {
|
|
141
|
+
return selectedServices.length > 0 ? this.extractWsdlUrl(selectedServices[0]) : '';
|
|
142
|
+
}
|
|
143
|
+
// Private helper methods
|
|
144
|
+
extractServiceElements() {
|
|
145
|
+
const serviceRegex = /<wsdl:service[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/wsdl:service>/g;
|
|
146
|
+
const services = [];
|
|
147
|
+
let serviceMatch;
|
|
148
|
+
while ((serviceMatch = serviceRegex.exec(this.wsdlContent)) !== null) {
|
|
149
|
+
services.push({
|
|
150
|
+
name: serviceMatch[1],
|
|
151
|
+
content: serviceMatch[2]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return services;
|
|
155
|
+
}
|
|
156
|
+
extractPortElements(serviceContent) {
|
|
157
|
+
const portRegex = /<wsdl:port[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/wsdl:port>/g;
|
|
158
|
+
const ports = [];
|
|
159
|
+
let portMatch;
|
|
160
|
+
while ((portMatch = portRegex.exec(serviceContent)) !== null) {
|
|
161
|
+
ports.push({
|
|
162
|
+
name: portMatch[1],
|
|
163
|
+
content: portMatch[0] // Include the full port element
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return ports;
|
|
167
|
+
}
|
|
168
|
+
extractEndpointFromPort(portContent) {
|
|
169
|
+
const soapAddressMatch = portContent.match(/location="([^"]*)"/);
|
|
170
|
+
return soapAddressMatch ? soapAddressMatch[1] : '/soap';
|
|
171
|
+
}
|
|
172
|
+
isBindingVersionSelected(bindingName) {
|
|
173
|
+
if (this.selectedSoapVersions.length === 0) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
const soapVersion = this.getSoapVersionFromBinding(bindingName);
|
|
177
|
+
return this.selectedSoapVersions.includes(soapVersion);
|
|
178
|
+
}
|
|
179
|
+
getSoapVersionFromBinding(bindingName) {
|
|
180
|
+
const bindingRegex = new RegExp(`<wsdl:binding[^>]*name="${bindingName}"[^>]*>([\\s\\S]*?)<\\/wsdl:binding>`, 'i');
|
|
181
|
+
const bindingMatch = this.wsdlContent.match(bindingRegex);
|
|
182
|
+
if (bindingMatch && bindingMatch[1].includes('soap12:binding')) {
|
|
183
|
+
return '1.2';
|
|
184
|
+
}
|
|
185
|
+
return '1.1';
|
|
186
|
+
}
|
|
187
|
+
extractOperationsFromBinding(bindingName) {
|
|
188
|
+
const bindingRegex = new RegExp(`<wsdl:binding[^>]*name="${bindingName}"[^>]*>([\\s\\S]*?)<\\/wsdl:binding>`, 'i');
|
|
189
|
+
const bindingMatch = this.wsdlContent.match(bindingRegex);
|
|
190
|
+
if (!bindingMatch)
|
|
191
|
+
return [];
|
|
192
|
+
const operations = [];
|
|
193
|
+
const operationRegex = /<wsdl:operation[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/wsdl:operation>/g;
|
|
194
|
+
let operationMatch;
|
|
195
|
+
while ((operationMatch = operationRegex.exec(bindingMatch[1])) !== null) {
|
|
196
|
+
const operationName = operationMatch[1];
|
|
197
|
+
const operationContent = operationMatch[2];
|
|
198
|
+
const soapAction = this.extractSoapAction(operationContent);
|
|
199
|
+
const documentation = this.getOperationDocumentation(operationName);
|
|
200
|
+
const inputSchema = this.parseSchemaElement(operationName);
|
|
201
|
+
const outputSchema = this.parseSchemaElement(`${operationName}Response`);
|
|
202
|
+
operations.push({
|
|
203
|
+
name: operationName,
|
|
204
|
+
soapAction,
|
|
205
|
+
documentation,
|
|
206
|
+
inputSchema,
|
|
207
|
+
outputSchema
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return operations;
|
|
211
|
+
}
|
|
212
|
+
extractSoapAction(operationContent) {
|
|
213
|
+
const soapActionMatch = operationContent.match(/soapAction="([^"]*)"/);
|
|
214
|
+
return soapActionMatch ? soapActionMatch[1] : '';
|
|
215
|
+
}
|
|
216
|
+
getOperationDocumentation(operationName) {
|
|
217
|
+
const operationRegex = new RegExp(`<wsdl:operation[^>]*name="${operationName}"[^>]*>([\\s\\S]*?)<\\/wsdl:operation>`, 'i');
|
|
218
|
+
const operationMatch = this.wsdlContent.match(operationRegex);
|
|
219
|
+
if (operationMatch) {
|
|
220
|
+
const docMatch = operationMatch[1].match(/<wsdl:documentation[^>]*>([\s\S]*?)<\/wsdl:documentation>/i);
|
|
221
|
+
return docMatch ? docMatch[1].trim() : undefined;
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
parseSchemaElement(elementName) {
|
|
226
|
+
const elementRegex = new RegExp(`<s:element[^>]*name="${elementName}"[^>]*>([\\s\\S]*?)<\\/s:element>`, 'i');
|
|
227
|
+
const elementMatch = this.wsdlContent.match(elementRegex);
|
|
228
|
+
if (!elementMatch)
|
|
229
|
+
return { type: 'object', properties: {} };
|
|
230
|
+
const sequenceMatch = elementMatch[1].match(/<s:sequence[^>]*>([\s\S]*?)<\/s:sequence>/i);
|
|
231
|
+
if (!sequenceMatch)
|
|
232
|
+
return { type: 'object', properties: {} };
|
|
233
|
+
const fields = {};
|
|
234
|
+
const fieldRegex = /<s:element[^>]*name="([^"]*)"[^>]*type="([^"]*)"[^>]*minOccurs="([^"]*)"[^>]*\/?>|<s:element[^>]*name="([^"]*)"[^>]*type="([^"]*)"[^>]*>/g;
|
|
235
|
+
let fieldMatch;
|
|
236
|
+
while ((fieldMatch = fieldRegex.exec(sequenceMatch[1])) !== null) {
|
|
237
|
+
const name = fieldMatch[1] || fieldMatch[4];
|
|
238
|
+
const type = fieldMatch[2] || fieldMatch[5];
|
|
239
|
+
const minOccurs = fieldMatch[3];
|
|
240
|
+
if (name && type) {
|
|
241
|
+
fields[name] = {
|
|
242
|
+
type: this.mapXsdType(type),
|
|
243
|
+
required: minOccurs === '1',
|
|
244
|
+
description: `${name} parameter`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { type: 'object', properties: fields };
|
|
249
|
+
}
|
|
250
|
+
mapXsdType(xsdType) {
|
|
251
|
+
const typeMap = {
|
|
252
|
+
's:int': 'integer',
|
|
253
|
+
's:string': 'string',
|
|
254
|
+
's:double': 'number',
|
|
255
|
+
's:float': 'number',
|
|
256
|
+
's:boolean': 'boolean',
|
|
257
|
+
's:dateTime': 'string'
|
|
258
|
+
};
|
|
259
|
+
return typeMap[xsdType] || 'string';
|
|
260
|
+
}
|
|
261
|
+
createImportableEndpoint(config) {
|
|
262
|
+
const { serviceName, bindingName, endpoint, soapVersion, operation } = config;
|
|
263
|
+
return {
|
|
264
|
+
id: `${serviceName}_${bindingName}_${operation.name}_${soapVersion}`,
|
|
265
|
+
name: `${serviceName}.${operation.name}`,
|
|
266
|
+
method: 'POST',
|
|
267
|
+
path: this.getPathFromUrl(endpoint),
|
|
268
|
+
description: `${operation.documentation || `${operation.name} operation`} (SOAP ${soapVersion})`,
|
|
269
|
+
requestBody: {
|
|
270
|
+
contentType: this.getContentType(soapVersion),
|
|
271
|
+
schema: operation.inputSchema,
|
|
272
|
+
example: this.generateSOAPExample(operation.name, operation.inputSchema, soapVersion),
|
|
273
|
+
required: true
|
|
274
|
+
},
|
|
275
|
+
responses: [{
|
|
276
|
+
statusCode: 200,
|
|
277
|
+
contentType: this.getResponseContentType(soapVersion),
|
|
278
|
+
schema: operation.outputSchema,
|
|
279
|
+
example: this.generateSOAPResponseExample(operation.name, operation.outputSchema, soapVersion)
|
|
280
|
+
}],
|
|
281
|
+
parameters: this.generateSOAPParameters(operation.soapAction, soapVersion),
|
|
282
|
+
tags: ['SOAP', serviceName, `SOAP-${soapVersion}`],
|
|
283
|
+
selected: false
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
getContentType(soapVersion) {
|
|
287
|
+
return soapVersion === '1.2'
|
|
288
|
+
? 'application/soap+xml; charset=utf-8'
|
|
289
|
+
: 'text/xml; charset=utf-8';
|
|
290
|
+
}
|
|
291
|
+
getResponseContentType(soapVersion) {
|
|
292
|
+
return soapVersion === '1.2' ? 'application/soap+xml' : 'text/xml';
|
|
293
|
+
}
|
|
294
|
+
generateSOAPParameters(soapAction, soapVersion) {
|
|
295
|
+
if (soapVersion === '1.2') {
|
|
296
|
+
return [{
|
|
297
|
+
name: 'Content-Type',
|
|
298
|
+
in: 'header',
|
|
299
|
+
required: true,
|
|
300
|
+
type: 'string',
|
|
301
|
+
description: 'SOAP 1.2 Content Type with action',
|
|
302
|
+
example: `application/soap+xml; charset=utf-8; action="${soapAction}"`
|
|
303
|
+
}];
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
return [{
|
|
307
|
+
name: 'SOAPAction',
|
|
308
|
+
in: 'header',
|
|
309
|
+
required: true,
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'SOAP 1.1 Action header',
|
|
312
|
+
example: `"${soapAction}"`
|
|
313
|
+
}];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
generateSOAPExample(operationName, schema, soapVersion) {
|
|
317
|
+
const targetNamespace = this.getTargetNamespace();
|
|
318
|
+
const params = this.generateSampleParameters(schema);
|
|
319
|
+
if (soapVersion === '1.2') {
|
|
320
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
321
|
+
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
322
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
323
|
+
xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
|
|
324
|
+
<soap12:Body>
|
|
325
|
+
<${operationName} xmlns="${targetNamespace}">
|
|
326
|
+
${params}
|
|
327
|
+
</${operationName}>
|
|
328
|
+
</soap12:Body>
|
|
329
|
+
</soap12:Envelope>`;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
333
|
+
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
334
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
335
|
+
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
336
|
+
<soap:Body>
|
|
337
|
+
<${operationName} xmlns="${targetNamespace}">
|
|
338
|
+
${params}
|
|
339
|
+
</${operationName}>
|
|
340
|
+
</soap:Body>
|
|
341
|
+
</soap:Envelope>`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
generateSOAPResponseExample(operationName, schema, soapVersion) {
|
|
345
|
+
const targetNamespace = this.getTargetNamespace();
|
|
346
|
+
const responseParams = this.generateSampleParameters(schema);
|
|
347
|
+
const responseElementName = `${operationName}Response`;
|
|
348
|
+
if (soapVersion === '1.2') {
|
|
349
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
350
|
+
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
351
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
352
|
+
xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
|
|
353
|
+
<soap12:Body>
|
|
354
|
+
<${responseElementName} xmlns="${targetNamespace}">
|
|
355
|
+
${responseParams}
|
|
356
|
+
</${responseElementName}>
|
|
357
|
+
</soap12:Body>
|
|
358
|
+
</soap12:Envelope>`;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
362
|
+
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
363
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
364
|
+
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
365
|
+
<soap:Body>
|
|
366
|
+
<${responseElementName} xmlns="${targetNamespace}">
|
|
367
|
+
${responseParams}
|
|
368
|
+
</${responseElementName}>
|
|
369
|
+
</soap:Body>
|
|
370
|
+
</soap:Envelope>`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
generateSampleParameters(schema) {
|
|
374
|
+
if (!schema?.properties)
|
|
375
|
+
return '';
|
|
376
|
+
return Object.entries(schema.properties)
|
|
377
|
+
.map(([name, prop]) => {
|
|
378
|
+
const sampleValue = this.getSampleValue(prop.type);
|
|
379
|
+
return `<${name}>${sampleValue}</${name}>`;
|
|
380
|
+
})
|
|
381
|
+
.join('\n ');
|
|
382
|
+
}
|
|
383
|
+
getSampleValue(type) {
|
|
384
|
+
switch (type) {
|
|
385
|
+
case 'integer': return 1;
|
|
386
|
+
case 'number': return 1.0;
|
|
387
|
+
case 'boolean': return 'true';
|
|
388
|
+
default: return 'sample';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
generateArgsFromSchema(schema) {
|
|
392
|
+
if (!schema?.properties)
|
|
393
|
+
return {};
|
|
394
|
+
const args = {};
|
|
395
|
+
Object.entries(schema.properties).forEach(([name, prop]) => {
|
|
396
|
+
args[name] = this.getSampleValue(prop.type);
|
|
397
|
+
});
|
|
398
|
+
return args;
|
|
399
|
+
}
|
|
400
|
+
extractWsdlUrl(service) {
|
|
401
|
+
// Extract base URL from path and construct WSDL URL
|
|
402
|
+
const basePath = service.path.replace('.asmx', '');
|
|
403
|
+
return `http://www.dneonline.com${basePath}.asmx?wsdl`;
|
|
404
|
+
}
|
|
405
|
+
generateSOAPExtractions(operationName) {
|
|
406
|
+
return [
|
|
407
|
+
{
|
|
408
|
+
name: 'soap_fault',
|
|
409
|
+
type: 'regex',
|
|
410
|
+
expression: '<soap:Fault[^>]*>([\\s\\S]*?)</soap:Fault>',
|
|
411
|
+
optional: true
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: `${operationName.toLowerCase()}_result`,
|
|
415
|
+
type: 'regex',
|
|
416
|
+
expression: `<${operationName}Result[^>]*>([^<]*)</${operationName}Result>`
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
}
|
|
420
|
+
getPathFromUrl(url) {
|
|
421
|
+
try {
|
|
422
|
+
const urlObj = new URL(url);
|
|
423
|
+
return urlObj.pathname;
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
const pathMatch = url.match(/^https?:\/\/[^\/]+(.*)$/);
|
|
427
|
+
return pathMatch ? pathMatch[1] : '/calculator.asmx';
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
getTargetNamespace() {
|
|
431
|
+
const namespaceMatch = this.wsdlContent.match(/targetNamespace="([^"]*)"/);
|
|
432
|
+
return namespaceMatch ? namespaceMatch[1] : 'http://tempuri.org/';
|
|
433
|
+
}
|
|
434
|
+
extractAttribute(xmlString, attributeName) {
|
|
435
|
+
const regex = new RegExp(`${attributeName}="([^"]*)"`, 'i');
|
|
436
|
+
const match = xmlString.match(regex);
|
|
437
|
+
return match ? match[1] : null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
exports.WSDLImporter = WSDLImporter;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { test, testData, TestBuilder, ScenarioBuilder, LoadBuilder, load } from './dsl/test-builder';
|
|
2
|
+
export type { ScenarioContext } from './dsl/test-builder';
|
|
3
|
+
export { TestRunner } from './core/test-runner';
|
|
4
|
+
export { ConfigParser } from './config/parser';
|
|
5
|
+
export type { TestConfiguration, Scenario, Step, VUContext } from './config/types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/index.ts (builds to dist/index.js)
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConfigParser = exports.TestRunner = exports.load = exports.LoadBuilder = exports.ScenarioBuilder = exports.TestBuilder = exports.testData = exports.test = void 0;
|
|
5
|
+
// Export everything from test-builder
|
|
6
|
+
var test_builder_1 = require("./dsl/test-builder");
|
|
7
|
+
Object.defineProperty(exports, "test", { enumerable: true, get: function () { return test_builder_1.test; } });
|
|
8
|
+
Object.defineProperty(exports, "testData", { enumerable: true, get: function () { return test_builder_1.testData; } });
|
|
9
|
+
Object.defineProperty(exports, "TestBuilder", { enumerable: true, get: function () { return test_builder_1.TestBuilder; } });
|
|
10
|
+
Object.defineProperty(exports, "ScenarioBuilder", { enumerable: true, get: function () { return test_builder_1.ScenarioBuilder; } });
|
|
11
|
+
Object.defineProperty(exports, "LoadBuilder", { enumerable: true, get: function () { return test_builder_1.LoadBuilder; } });
|
|
12
|
+
Object.defineProperty(exports, "load", { enumerable: true, get: function () { return test_builder_1.load; } });
|
|
13
|
+
// Export TestRunner and ConfigParser
|
|
14
|
+
var test_runner_1 = require("./core/test-runner");
|
|
15
|
+
Object.defineProperty(exports, "TestRunner", { enumerable: true, get: function () { return test_runner_1.TestRunner; } });
|
|
16
|
+
var parser_1 = require("./config/parser");
|
|
17
|
+
Object.defineProperty(exports, "ConfigParser", { enumerable: true, get: function () { return parser_1.ConfigParser; } });
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ArrivalsPattern = void 0;
|
|
4
|
+
const time_1 = require("../utils/time");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
class ArrivalsPattern {
|
|
7
|
+
async execute(config, vuFactory) {
|
|
8
|
+
const rate = config.rate; // users per second
|
|
9
|
+
const duration = (0, time_1.parseTime)(config.duration || '5m');
|
|
10
|
+
const rampUp = (0, time_1.parseTime)(config.ramp_up || '0s');
|
|
11
|
+
const vuDuration = (0, time_1.parseTime)(config.vu_duration || '30s'); // How long each VU runs
|
|
12
|
+
if (!rate || rate <= 0) {
|
|
13
|
+
throw new Error('Arrivals pattern requires a positive rate');
|
|
14
|
+
}
|
|
15
|
+
logger_1.logger.info(`đ¯ Arrivals: ${rate} users/sec, test duration: ${(duration / 1000).toFixed(1)}s, VU duration: ${(vuDuration / 1000).toFixed(1)}s`);
|
|
16
|
+
const targetIntervalMs = 1000 / rate;
|
|
17
|
+
let vuId = 0;
|
|
18
|
+
let currentRate = 0;
|
|
19
|
+
const testStartTime = Date.now();
|
|
20
|
+
const testEndTime = testStartTime + duration;
|
|
21
|
+
// Gradually ramp up to target rate
|
|
22
|
+
if (rampUp > 0) {
|
|
23
|
+
const rampSteps = Math.ceil(rampUp / 1000); // 1 second intervals
|
|
24
|
+
const rateIncrement = rate / rampSteps;
|
|
25
|
+
for (let step = 1; step <= rampSteps && Date.now() < testEndTime; step++) {
|
|
26
|
+
currentRate = Math.min(rate, step * rateIncrement);
|
|
27
|
+
const stepInterval = 1000 / currentRate;
|
|
28
|
+
logger_1.logger.debug(`đ Ramp-up step ${step}/${rampSteps}: ${currentRate.toFixed(2)} users/sec`);
|
|
29
|
+
const stepEndTime = Math.min(Date.now() + 1000, testEndTime);
|
|
30
|
+
while (Date.now() < stepEndTime) {
|
|
31
|
+
const vuStartTime = Date.now();
|
|
32
|
+
// CRITICAL: Create and run VU but don't block the arrivals rate
|
|
33
|
+
this.createAndRunVU(vuFactory, ++vuId, vuDuration, vuStartTime);
|
|
34
|
+
await (0, time_1.sleep)(stepInterval);
|
|
35
|
+
if (Date.now() >= testEndTime) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Run at target rate for remaining duration
|
|
42
|
+
const remainingTestTime = testEndTime - Date.now();
|
|
43
|
+
if (remainingTestTime > 0) {
|
|
44
|
+
logger_1.logger.debug(`đ¯ Running at target rate: ${rate} users/sec for ${(remainingTestTime / 1000).toFixed(1)}s`);
|
|
45
|
+
while (Date.now() < testEndTime) {
|
|
46
|
+
const vuStartTime = Date.now();
|
|
47
|
+
// CRITICAL: Create and run VU but don't block the arrivals rate
|
|
48
|
+
this.createAndRunVU(vuFactory, ++vuId, vuDuration, vuStartTime);
|
|
49
|
+
await (0, time_1.sleep)(targetIntervalMs);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Wait for the last VUs to complete (they might run beyond test end time)
|
|
53
|
+
const lastVUEndTime = Date.now() + vuDuration;
|
|
54
|
+
const finalWaitTime = Math.max(lastVUEndTime - Date.now(), 0);
|
|
55
|
+
if (finalWaitTime > 0) {
|
|
56
|
+
logger_1.logger.debug(`âŗ Waiting ${(finalWaitTime / 1000).toFixed(1)}s for last VUs to complete...`);
|
|
57
|
+
await (0, time_1.sleep)(finalWaitTime);
|
|
58
|
+
}
|
|
59
|
+
logger_1.logger.debug('â
Arrivals pattern completed');
|
|
60
|
+
}
|
|
61
|
+
async createAndRunVU(vuFactory, vuId, durationMs, startTime) {
|
|
62
|
+
// Run VU creation and execution asynchronously to not block arrivals rate
|
|
63
|
+
(async () => {
|
|
64
|
+
try {
|
|
65
|
+
logger_1.logger.debug(`Creating VU ${vuId}...`);
|
|
66
|
+
const vu = await this.createVU(vuFactory, vuId);
|
|
67
|
+
logger_1.logger.debug(`VU ${vuId} ready`);
|
|
68
|
+
// Record VU start for metrics and reporting
|
|
69
|
+
const metrics = vuFactory.getMetrics();
|
|
70
|
+
metrics.recordVUStart(vu.getId());
|
|
71
|
+
logger_1.logger.debug(`đ¤ Started VU ${vu.getId()}`);
|
|
72
|
+
// Run VU for its individual duration
|
|
73
|
+
await this.runVUForDuration(vu, durationMs, startTime);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
77
|
+
if (errorMessage.includes('terminated due to CSV data exhaustion')) {
|
|
78
|
+
logger_1.logger.info(`âšī¸ VU ${vuId} terminated due to CSV data exhaustion`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
logger_1.logger.error(`â Failed to create/run VU ${vuId}:`, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
}
|
|
86
|
+
async createVU(vuFactory, id) {
|
|
87
|
+
// Handle both sync and async factories
|
|
88
|
+
const vu = vuFactory.create(id);
|
|
89
|
+
if (vu instanceof Promise) {
|
|
90
|
+
return await vu;
|
|
91
|
+
}
|
|
92
|
+
return vu;
|
|
93
|
+
}
|
|
94
|
+
async runVUForDuration(vu, durationMs, startTime) {
|
|
95
|
+
const endTime = startTime + durationMs;
|
|
96
|
+
while (Date.now() < endTime && vu.isRunning()) {
|
|
97
|
+
try {
|
|
98
|
+
await vu.executeScenarios();
|
|
99
|
+
if (Date.now() >= endTime) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
await (0, time_1.sleep)(100); // Small pause between iterations
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
106
|
+
if (errorMessage.includes('terminated due to CSV data exhaustion')) {
|
|
107
|
+
logger_1.logger.info(`âšī¸ VU ${vu.getId()} terminated due to CSV data exhaustion`);
|
|
108
|
+
break; // Exit the loop, VU is already stopped
|
|
109
|
+
}
|
|
110
|
+
logger_1.logger.error(`â VU ${vu.getId()} error:`, error);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// VU cleanup happens automatically when it goes out of scope
|
|
115
|
+
logger_1.logger.debug(`đ VU ${vu.getId()} completed after ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.ArrivalsPattern = ArrivalsPattern;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { VirtualUser } from '../core';
|
|
2
|
+
import { MetricsCollector } from '../metrics/collector';
|
|
3
|
+
export interface LoadPattern {
|
|
4
|
+
execute(config: any, vuFactory: VUFactory): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface VUFactory {
|
|
7
|
+
create: (id: number) => VirtualUser | Promise<VirtualUser>;
|
|
8
|
+
getMetrics: () => MetricsCollector;
|
|
9
|
+
}
|