@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,179 @@
|
|
|
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.distributedCommand = distributedCommand;
|
|
37
|
+
const coordinator_1 = require("../../distributed/coordinator");
|
|
38
|
+
const config_1 = require("../../config");
|
|
39
|
+
const validator_1 = require("../../config/validator");
|
|
40
|
+
const logger_1 = require("../../utils/logger");
|
|
41
|
+
const timestamp_helper_1 = require("../../utils/timestamp-helper");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
async function distributedCommand(configPath, options) {
|
|
44
|
+
try {
|
|
45
|
+
// Set log level: default=WARN, --verbose=INFO, --debug=DEBUG
|
|
46
|
+
if (options.debug) {
|
|
47
|
+
logger_1.logger.setLevel(logger_1.LogLevel.DEBUG);
|
|
48
|
+
}
|
|
49
|
+
else if (options.verbose) {
|
|
50
|
+
logger_1.logger.setLevel(logger_1.LogLevel.INFO);
|
|
51
|
+
}
|
|
52
|
+
// Parse test configuration
|
|
53
|
+
const parser = new config_1.ConfigParser();
|
|
54
|
+
const testConfig = await parser.parse(configPath, options.env);
|
|
55
|
+
// Validate configuration
|
|
56
|
+
const validator = new validator_1.ConfigValidator();
|
|
57
|
+
const validation = validator.validate(testConfig);
|
|
58
|
+
if (!validation.valid) {
|
|
59
|
+
logger_1.logger.error('❌ Configuration validation failed:');
|
|
60
|
+
validation.errors.forEach(error => logger_1.logger.error(` - ${error}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// Parse worker configurations
|
|
64
|
+
const workers = parseWorkerConfigs(options.workers, options.workersFile);
|
|
65
|
+
if (workers.length === 0) {
|
|
66
|
+
logger_1.logger.error('❌ No workers specified');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Create distributed test configuration
|
|
70
|
+
const distributedConfig = {
|
|
71
|
+
workers,
|
|
72
|
+
strategy: options.strategy || 'capacity_based',
|
|
73
|
+
sync_start: options.syncStart || false,
|
|
74
|
+
heartbeat_interval: 30000,
|
|
75
|
+
timeout: 300000,
|
|
76
|
+
retry_failed: true
|
|
77
|
+
};
|
|
78
|
+
// Initialize and run distributed test
|
|
79
|
+
const coordinator = new coordinator_1.DistributedCoordinator(distributedConfig);
|
|
80
|
+
// Set up graceful shutdown
|
|
81
|
+
process.on('SIGINT', async () => {
|
|
82
|
+
logger_1.logger.info('🛑 Received SIGINT, stopping distributed test...');
|
|
83
|
+
await coordinator.stop();
|
|
84
|
+
await coordinator.cleanup();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
87
|
+
await coordinator.initialize();
|
|
88
|
+
logger_1.logger.info(`🚀 Starting distributed test with ${workers.length} workers`);
|
|
89
|
+
logger_1.logger.info(`📊 Strategy: ${distributedConfig.strategy}`);
|
|
90
|
+
logger_1.logger.info(`🔄 Sync start: ${distributedConfig.sync_start ? 'Yes' : 'No'}`);
|
|
91
|
+
await coordinator.executeTest(testConfig);
|
|
92
|
+
// Get aggregated results
|
|
93
|
+
const results = coordinator.getAggregatedResults();
|
|
94
|
+
logger_1.logger.info('📊 Distributed Test Results:');
|
|
95
|
+
logger_1.logger.info(` Total Requests: ${results.summary.total_requests}`);
|
|
96
|
+
logger_1.logger.info(` Success Rate: ${results.summary.success_rate.toFixed(2)}%`);
|
|
97
|
+
logger_1.logger.info(` Avg Response Time: ${results.summary.avg_response_time.toFixed(2)}ms`);
|
|
98
|
+
logger_1.logger.info(` Requests/sec: ${results.summary.requests_per_second.toFixed(2)}`);
|
|
99
|
+
// Output results if specified
|
|
100
|
+
if (options.output) {
|
|
101
|
+
const outputPath = options.output.endsWith('.json') ? options.output : `${options.output}/distributed-results.json`;
|
|
102
|
+
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
103
|
+
logger_1.logger.success(`📄 Results written to: ${outputPath}`);
|
|
104
|
+
}
|
|
105
|
+
// Generate report if requested
|
|
106
|
+
if (options.report || testConfig.report?.generate) {
|
|
107
|
+
try {
|
|
108
|
+
const { EnhancedHTMLReportGenerator } = await Promise.resolve().then(() => __importStar(require('../../reporting/enhanced-html-generator')));
|
|
109
|
+
const generator = new EnhancedHTMLReportGenerator();
|
|
110
|
+
const timestamp = timestamp_helper_1.TimestampHelper.getTimestamp('file');
|
|
111
|
+
const reportFilename = `distributed-report-${timestamp}.html`;
|
|
112
|
+
// Always use timestamped filename for distributed reports in reports folder
|
|
113
|
+
const reportPath = options.output ?
|
|
114
|
+
`${options.output}/${reportFilename}` :
|
|
115
|
+
`reports/${reportFilename}`;
|
|
116
|
+
await generator.generate({
|
|
117
|
+
testName: `${testConfig.name} (Distributed)`,
|
|
118
|
+
summary: results.summary,
|
|
119
|
+
results: results.results
|
|
120
|
+
}, reportPath);
|
|
121
|
+
logger_1.logger.success(`📋 Report generated: ${reportPath}`);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
logger_1.logger.error(`❌ Report generation failed: ${error}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await coordinator.cleanup();
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
logger_1.logger.error(`❌ Distributed test failed: ${error.message}`);
|
|
131
|
+
if (options.verbose || options.debug) {
|
|
132
|
+
console.error(error.stack);
|
|
133
|
+
}
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function parseWorkerConfigs(workersString, workersFile) {
|
|
138
|
+
const workers = [];
|
|
139
|
+
// Parse from command line
|
|
140
|
+
if (workersString) {
|
|
141
|
+
const workerAddresses = workersString.split(',').map(w => w.trim());
|
|
142
|
+
workerAddresses.forEach(address => {
|
|
143
|
+
const [host, portStr] = address.split(':');
|
|
144
|
+
const port = parseInt(portStr || '8080');
|
|
145
|
+
workers.push({
|
|
146
|
+
host: host.trim(),
|
|
147
|
+
port,
|
|
148
|
+
capacity: 100, // Default capacity
|
|
149
|
+
region: 'default'
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Parse from file
|
|
154
|
+
if (workersFile) {
|
|
155
|
+
try {
|
|
156
|
+
const fileContent = fs.readFileSync(workersFile, 'utf8');
|
|
157
|
+
const fileWorkers = JSON.parse(fileContent);
|
|
158
|
+
if (Array.isArray(fileWorkers)) {
|
|
159
|
+
// Apply defaults to workers from file
|
|
160
|
+
fileWorkers.forEach((w) => {
|
|
161
|
+
workers.push({
|
|
162
|
+
host: w.host || 'localhost',
|
|
163
|
+
port: w.port || 8080,
|
|
164
|
+
capacity: w.capacity ?? 100, // Default capacity if not specified
|
|
165
|
+
region: w.region || 'default'
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
throw new Error('Workers file must contain an array of worker configurations');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger_1.logger.error(`❌ Failed to parse workers file ${workersFile}:`, error);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return workers;
|
|
179
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface ImportOptions {
|
|
2
|
+
output: string;
|
|
3
|
+
format: 'yaml' | 'typescript';
|
|
4
|
+
verbose?: boolean;
|
|
5
|
+
interactive?: boolean;
|
|
6
|
+
autoCorrelate?: boolean;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
sourceFile?: string;
|
|
9
|
+
tags?: string;
|
|
10
|
+
excludeTags?: string;
|
|
11
|
+
paths?: string;
|
|
12
|
+
methods?: string;
|
|
13
|
+
filterDomains?: string;
|
|
14
|
+
excludeDomains?: string;
|
|
15
|
+
services?: string;
|
|
16
|
+
operations?: string;
|
|
17
|
+
folders?: string;
|
|
18
|
+
scenariosPerFile?: string;
|
|
19
|
+
confidence?: string;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
export declare function importCommand(type: 'openapi' | 'wsdl' | 'har' | 'postman', sourceFile: string, options: ImportOptions): Promise<void>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/commands/import.ts
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.importCommand = importCommand;
|
|
41
|
+
const fs = __importStar(require("fs/promises"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const yaml = __importStar(require("js-yaml"));
|
|
44
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const ora_1 = __importDefault(require("ora"));
|
|
47
|
+
const open_api_importer_1 = require("../../importers/open-api-importer");
|
|
48
|
+
const wsdl_importer_1 = require("../../importers/wsdl-importer");
|
|
49
|
+
const har_importer_1 = require("../../importers/har-importer");
|
|
50
|
+
const test_output_writer_1 = require("../../utils/test-output-writer");
|
|
51
|
+
async function importCommand(type, sourceFile, options) {
|
|
52
|
+
const spinner = (0, ora_1.default)(`Loading ${type.toUpperCase()} from ${sourceFile}`).start();
|
|
53
|
+
try {
|
|
54
|
+
// Validate source file exists
|
|
55
|
+
await fs.access(sourceFile);
|
|
56
|
+
let importer;
|
|
57
|
+
let endpoints = [];
|
|
58
|
+
// Create appropriate importer
|
|
59
|
+
switch (type) {
|
|
60
|
+
case 'openapi':
|
|
61
|
+
importer = await createOpenAPIImporter(sourceFile, options);
|
|
62
|
+
endpoints = importer.extractEndpoints();
|
|
63
|
+
break;
|
|
64
|
+
case 'wsdl':
|
|
65
|
+
importer = await createWSDLImporter(sourceFile, options);
|
|
66
|
+
endpoints = importer.extractServices();
|
|
67
|
+
break;
|
|
68
|
+
case 'har':
|
|
69
|
+
importer = await createHARImporter(sourceFile, options);
|
|
70
|
+
endpoints = importer.extractEndpoints();
|
|
71
|
+
break;
|
|
72
|
+
// case 'postman':
|
|
73
|
+
// importer = await createPostmanImporter(sourceFile, options);
|
|
74
|
+
// endpoints = importer.extractRequests();
|
|
75
|
+
// break;
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Unsupported import type: ${type}`);
|
|
78
|
+
}
|
|
79
|
+
spinner.succeed(`Found ${endpoints.length} ${type === 'wsdl' ? 'services' : 'endpoints'}`);
|
|
80
|
+
if (endpoints.length === 0) {
|
|
81
|
+
console.log(chalk_1.default.yellow('No endpoints found to import'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Apply filters
|
|
85
|
+
endpoints = applyFilters(endpoints, type, options);
|
|
86
|
+
if (endpoints.length === 0) {
|
|
87
|
+
console.log(chalk_1.default.yellow('No endpoints match the specified filters'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(chalk_1.default.blue(`${endpoints.length} endpoints after filtering`));
|
|
91
|
+
// Interactive selection if enabled
|
|
92
|
+
let selectedEndpoints = endpoints;
|
|
93
|
+
if (options.interactive) {
|
|
94
|
+
selectedEndpoints = await interactiveEndpointSelection(endpoints, type);
|
|
95
|
+
}
|
|
96
|
+
if (selectedEndpoints.length === 0) {
|
|
97
|
+
console.log(chalk_1.default.yellow('No endpoints selected'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Generate scenarios
|
|
101
|
+
console.log(chalk_1.default.blue(`Generating scenarios for ${selectedEndpoints.length} endpoints...`));
|
|
102
|
+
let scenarios = importer.generateScenarios(selectedEndpoints);
|
|
103
|
+
// Apply correlations if enabled
|
|
104
|
+
if (options.autoCorrelate) {
|
|
105
|
+
console.log(chalk_1.default.blue('Analyzing data correlations...'));
|
|
106
|
+
scenarios = await applyCorrelations(scenarios, options);
|
|
107
|
+
}
|
|
108
|
+
// Generate output files
|
|
109
|
+
await generateOutputFiles(scenarios, type, options);
|
|
110
|
+
console.log(chalk_1.default.green(`Successfully imported ${scenarios.length} scenarios to ${options.output}/`));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
114
|
+
spinner.fail(`Import failed: ${errorMessage}`);
|
|
115
|
+
if (options.verbose && error instanceof Error) {
|
|
116
|
+
console.error(error.stack);
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function createOpenAPIImporter(sourceFile, options) {
|
|
122
|
+
const content = await fs.readFile(sourceFile, 'utf8');
|
|
123
|
+
let spec;
|
|
124
|
+
if (sourceFile.endsWith('.json')) {
|
|
125
|
+
spec = JSON.parse(content);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
spec = yaml.load(content);
|
|
129
|
+
}
|
|
130
|
+
// Apply base URL override
|
|
131
|
+
if (options.baseUrl && spec.servers) {
|
|
132
|
+
spec.servers[0].url = options.baseUrl;
|
|
133
|
+
}
|
|
134
|
+
return new open_api_importer_1.OpenAPIImporter(spec);
|
|
135
|
+
}
|
|
136
|
+
async function createWSDLImporter(sourceFile, _options) {
|
|
137
|
+
const content = await fs.readFile(sourceFile, 'utf8');
|
|
138
|
+
return new wsdl_importer_1.WSDLImporter(content);
|
|
139
|
+
}
|
|
140
|
+
async function createHARImporter(sourceFile, _options) {
|
|
141
|
+
const content = await fs.readFile(sourceFile, 'utf8');
|
|
142
|
+
const har = JSON.parse(content);
|
|
143
|
+
return new har_importer_1.HARImporter(har);
|
|
144
|
+
}
|
|
145
|
+
// async function createPostmanImporter(sourceFile: string, options: ImportOptions): Promise<PostmanImporter> {
|
|
146
|
+
// const content = await fs.readFile(sourceFile, 'utf8');
|
|
147
|
+
// const collection = JSON.parse(content);
|
|
148
|
+
//
|
|
149
|
+
// let environment = {};
|
|
150
|
+
// if (options.environment) {
|
|
151
|
+
// const envContent = await fs.readFile(options.environment, 'utf8');
|
|
152
|
+
// environment = JSON.parse(envContent);
|
|
153
|
+
// }
|
|
154
|
+
//
|
|
155
|
+
// return new PostmanImporter(collection, environment);
|
|
156
|
+
// }
|
|
157
|
+
function applyFilters(endpoints, type, options) {
|
|
158
|
+
let filtered = [...endpoints];
|
|
159
|
+
// Apply type-specific filters
|
|
160
|
+
switch (type) {
|
|
161
|
+
case 'openapi':
|
|
162
|
+
filtered = applyOpenAPIFilters(filtered, options);
|
|
163
|
+
break;
|
|
164
|
+
case 'har':
|
|
165
|
+
filtered = applyHARFilters(filtered, options);
|
|
166
|
+
break;
|
|
167
|
+
case 'wsdl':
|
|
168
|
+
filtered = applyWSDLFilters(filtered, options);
|
|
169
|
+
break;
|
|
170
|
+
case 'postman':
|
|
171
|
+
filtered = applyPostmanFilters(filtered, options);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
return filtered;
|
|
175
|
+
}
|
|
176
|
+
function applyOpenAPIFilters(endpoints, options) {
|
|
177
|
+
let filtered = endpoints;
|
|
178
|
+
// Filter by tags
|
|
179
|
+
if (options.tags) {
|
|
180
|
+
const includeTags = options.tags.split(',').map((t) => t.trim());
|
|
181
|
+
filtered = filtered.filter(ep => ep.tags && ep.tags.some((tag) => includeTags.includes(tag)));
|
|
182
|
+
}
|
|
183
|
+
if (options.excludeTags) {
|
|
184
|
+
const excludeTags = options.excludeTags.split(',').map((t) => t.trim());
|
|
185
|
+
filtered = filtered.filter(ep => !ep.tags || !ep.tags.some((tag) => excludeTags.includes(tag)));
|
|
186
|
+
}
|
|
187
|
+
// Filter by paths
|
|
188
|
+
if (options.paths) {
|
|
189
|
+
const pathPatterns = options.paths.split(',').map((p) => new RegExp(p.trim()));
|
|
190
|
+
filtered = filtered.filter(ep => pathPatterns.some((pattern) => pattern.test(ep.path)));
|
|
191
|
+
}
|
|
192
|
+
// Filter by methods
|
|
193
|
+
if (options.methods) {
|
|
194
|
+
const methods = options.methods.split(',').map((m) => m.trim().toUpperCase());
|
|
195
|
+
filtered = filtered.filter(ep => methods.includes(ep.method));
|
|
196
|
+
}
|
|
197
|
+
return filtered;
|
|
198
|
+
}
|
|
199
|
+
function applyHARFilters(endpoints, options) {
|
|
200
|
+
let filtered = endpoints;
|
|
201
|
+
// Filter by domains
|
|
202
|
+
if (options.filterDomains) {
|
|
203
|
+
const domains = options.filterDomains.split(',').map((d) => d.trim());
|
|
204
|
+
filtered = filtered.filter(ep => domains.some((domain) => ep.path.includes(domain) || (ep.description && ep.description.includes(domain))));
|
|
205
|
+
}
|
|
206
|
+
if (options.excludeDomains) {
|
|
207
|
+
const excludeDomains = options.excludeDomains.split(',').map((d) => d.trim());
|
|
208
|
+
filtered = filtered.filter(ep => !excludeDomains.some((domain) => ep.path.includes(domain) || (ep.description && ep.description.includes(domain))));
|
|
209
|
+
}
|
|
210
|
+
// Filter by methods
|
|
211
|
+
if (options.methods) {
|
|
212
|
+
const methods = options.methods.split(',').map((m) => m.trim().toUpperCase());
|
|
213
|
+
filtered = filtered.filter(ep => methods.includes(ep.method));
|
|
214
|
+
}
|
|
215
|
+
return filtered;
|
|
216
|
+
}
|
|
217
|
+
function applyWSDLFilters(services, options) {
|
|
218
|
+
let filtered = services;
|
|
219
|
+
if (options.services) {
|
|
220
|
+
const serviceNames = options.services.split(',').map((s) => s.trim());
|
|
221
|
+
filtered = filtered.filter(svc => svc.name && serviceNames.some((name) => svc.name.includes(name)));
|
|
222
|
+
}
|
|
223
|
+
if (options.operations) {
|
|
224
|
+
const operationNames = options.operations.split(',').map((op) => op.trim());
|
|
225
|
+
filtered = filtered.filter(svc => svc.name && operationNames.some((name) => svc.name.includes(name)));
|
|
226
|
+
}
|
|
227
|
+
return filtered;
|
|
228
|
+
}
|
|
229
|
+
function applyPostmanFilters(requests, options) {
|
|
230
|
+
let filtered = requests;
|
|
231
|
+
if (options.folders) {
|
|
232
|
+
const folderNames = options.folders.split(',').map((f) => f.trim());
|
|
233
|
+
filtered = filtered.filter(req => req.folder && folderNames.includes(req.folder));
|
|
234
|
+
}
|
|
235
|
+
return filtered;
|
|
236
|
+
}
|
|
237
|
+
async function interactiveEndpointSelection(endpoints, type) {
|
|
238
|
+
// Group endpoints for better presentation
|
|
239
|
+
const grouped = groupEndpoints(endpoints, type);
|
|
240
|
+
const selected = [];
|
|
241
|
+
for (const [groupName, groupEndpoints] of Object.entries(grouped)) {
|
|
242
|
+
console.log(chalk_1.default.blue(`\n📁 ${groupName} (${groupEndpoints.length} ${type === 'wsdl' ? 'services' : 'endpoints'})`));
|
|
243
|
+
// Show endpoints in the group
|
|
244
|
+
groupEndpoints.forEach((ep, index) => {
|
|
245
|
+
const method = type === 'wsdl' ? 'SOAP' : ep.method;
|
|
246
|
+
console.log(` ${index + 1}. ${chalk_1.default.cyan(method.padEnd(6))} ${ep.path}`);
|
|
247
|
+
if (ep.description) {
|
|
248
|
+
console.log(` ${chalk_1.default.gray(ep.description.substring(0, 80))}${ep.description.length > 80 ? '...' : ''}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// Prompt for selection
|
|
252
|
+
const { selection } = await inquirer_1.default.prompt([{
|
|
253
|
+
type: 'input',
|
|
254
|
+
name: 'selection',
|
|
255
|
+
message: `Select from ${groupName} (numbers separated by commas, 'all', or 'skip'):`,
|
|
256
|
+
default: 'skip'
|
|
257
|
+
}]);
|
|
258
|
+
if (selection.toLowerCase() === 'skip')
|
|
259
|
+
continue;
|
|
260
|
+
if (selection.toLowerCase() === 'all') {
|
|
261
|
+
selected.push(...groupEndpoints);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Parse selection
|
|
265
|
+
const indices = selection.split(',')
|
|
266
|
+
.map((s) => parseInt(s.trim()) - 1)
|
|
267
|
+
.filter((i) => i >= 0 && i < groupEndpoints.length);
|
|
268
|
+
for (const index of indices) {
|
|
269
|
+
selected.push(groupEndpoints[index]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return selected;
|
|
273
|
+
}
|
|
274
|
+
function groupEndpoints(endpoints, type) {
|
|
275
|
+
const groups = {};
|
|
276
|
+
endpoints.forEach(endpoint => {
|
|
277
|
+
let groupName = 'Other';
|
|
278
|
+
switch (type) {
|
|
279
|
+
case 'openapi':
|
|
280
|
+
groupName = endpoint.tags && endpoint.tags.length > 0
|
|
281
|
+
? endpoint.tags[0]
|
|
282
|
+
: extractPathGroup(endpoint.path);
|
|
283
|
+
break;
|
|
284
|
+
case 'har':
|
|
285
|
+
groupName = extractDomainFromEndpoint(endpoint);
|
|
286
|
+
break;
|
|
287
|
+
case 'wsdl':
|
|
288
|
+
groupName = endpoint.name ? extractServiceGroup(endpoint.name) : 'Unknown';
|
|
289
|
+
break;
|
|
290
|
+
case 'postman':
|
|
291
|
+
groupName = endpoint.folder || 'Root';
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
if (!groups[groupName]) {
|
|
295
|
+
groups[groupName] = [];
|
|
296
|
+
}
|
|
297
|
+
groups[groupName].push(endpoint);
|
|
298
|
+
});
|
|
299
|
+
return groups;
|
|
300
|
+
}
|
|
301
|
+
function extractPathGroup(path) {
|
|
302
|
+
const parts = path.split('/').filter(p => p && !p.startsWith('{'));
|
|
303
|
+
return parts.length > 0 ? parts[0] : 'Root';
|
|
304
|
+
}
|
|
305
|
+
function extractDomainFromEndpoint(endpoint) {
|
|
306
|
+
if (endpoint.description && endpoint.description.includes('://')) {
|
|
307
|
+
try {
|
|
308
|
+
const url = new URL(endpoint.description.split(' ').find((part) => part.includes('://')) || '');
|
|
309
|
+
return url.hostname;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return 'Unknown';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return extractPathGroup(endpoint.path);
|
|
316
|
+
}
|
|
317
|
+
function extractServiceGroup(serviceName) {
|
|
318
|
+
const parts = serviceName.split('.');
|
|
319
|
+
return parts.length > 1 ? parts[0] : serviceName;
|
|
320
|
+
}
|
|
321
|
+
async function applyCorrelations(scenarios, _options) {
|
|
322
|
+
// Note: DataCorrelationAnalyzer is not available, so we'll skip this functionality for now
|
|
323
|
+
// You'll need to import and implement this class or remove this feature
|
|
324
|
+
console.log(chalk_1.default.yellow('Data correlation analysis is not available - skipping'));
|
|
325
|
+
return scenarios;
|
|
326
|
+
/* Commented out until DataCorrelationAnalyzer is available:
|
|
327
|
+
const analyzer = new DataCorrelationAnalyzer();
|
|
328
|
+
const correlatedScenarios: Scenario[] = [];
|
|
329
|
+
|
|
330
|
+
for (const scenario of scenarios) {
|
|
331
|
+
console.log(`Analyzing correlations for: ${scenario.name}`);
|
|
332
|
+
|
|
333
|
+
const dependencies = analyzer.analyzeSteps(scenario.steps);
|
|
334
|
+
|
|
335
|
+
if (dependencies.length > 0) {
|
|
336
|
+
console.log(` Found ${dependencies.length} potential correlations`);
|
|
337
|
+
|
|
338
|
+
if (options.interactive) {
|
|
339
|
+
// Interactive correlation review
|
|
340
|
+
const approvedDeps = await reviewCorrelations(dependencies, scenario.name);
|
|
341
|
+
scenario.steps = analyzer.applyCorrelations(scenario.steps, approvedDeps);
|
|
342
|
+
} else {
|
|
343
|
+
// Auto-apply with confidence threshold
|
|
344
|
+
const threshold = parseFloat(options.confidence || '0.6');
|
|
345
|
+
const highConfidenceDeps = dependencies.filter((dep: Dependency) =>
|
|
346
|
+
dep.required || (dep.description && dep.description.includes('confidence:') &&
|
|
347
|
+
parseFloat(dep.description.match(/confidence:\s*(\d*\.?\d+)/)?.[1] || '0') >= threshold)
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
if (highConfidenceDeps.length > 0) {
|
|
351
|
+
console.log(` Applying ${highConfidenceDeps.length} high-confidence correlations`);
|
|
352
|
+
scenario.steps = analyzer.applyCorrelations(scenario.steps, highConfidenceDeps);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Generate correlation report
|
|
357
|
+
if (dependencies.length > 0 && options.output) {
|
|
358
|
+
const report = analyzer.generateCorrelationReport(dependencies);
|
|
359
|
+
const reportPath = path.join(options.output, `${scenario.name}-correlations.md`);
|
|
360
|
+
await ensureDirectory(path.dirname(reportPath));
|
|
361
|
+
await fs.writeFile(reportPath, report, 'utf8');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
correlatedScenarios.push(scenario);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return correlatedScenarios;
|
|
369
|
+
*/
|
|
370
|
+
}
|
|
371
|
+
async function reviewCorrelations(dependencies, scenarioName) {
|
|
372
|
+
console.log(chalk_1.default.blue(`\nReviewing correlations for: ${scenarioName}`));
|
|
373
|
+
const approved = [];
|
|
374
|
+
for (const dep of dependencies) {
|
|
375
|
+
console.log(chalk_1.default.yellow(`\nCorrelation: ${dep.sourceStep} → ${dep.targetStep}`));
|
|
376
|
+
console.log(` Extract: ${dep.sourceField}`);
|
|
377
|
+
console.log(` Use in: ${dep.targetLocation} as ${dep.targetField}`);
|
|
378
|
+
console.log(` ${dep.description}`);
|
|
379
|
+
const { apply } = await inquirer_1.default.prompt([{
|
|
380
|
+
type: 'confirm',
|
|
381
|
+
name: 'apply',
|
|
382
|
+
message: 'Apply this correlation?',
|
|
383
|
+
default: dep.required
|
|
384
|
+
}]);
|
|
385
|
+
if (apply) {
|
|
386
|
+
approved.push(dep);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return approved;
|
|
390
|
+
}
|
|
391
|
+
async function generateOutputFiles(scenarios, type, options) {
|
|
392
|
+
await ensureDirectory(options.output);
|
|
393
|
+
// Group scenarios into files based on scenariosPerFile option
|
|
394
|
+
const scenariosPerFile = parseInt(options.scenariosPerFile || '10');
|
|
395
|
+
const groups = [];
|
|
396
|
+
for (let i = 0; i < scenarios.length; i += scenariosPerFile) {
|
|
397
|
+
groups.push(scenarios.slice(i, i + scenariosPerFile));
|
|
398
|
+
}
|
|
399
|
+
// Determine output format - now supports typescript
|
|
400
|
+
const format = options.format === 'typescript'
|
|
401
|
+
? 'typescript'
|
|
402
|
+
: options.format || 'yaml';
|
|
403
|
+
for (let i = 0; i < groups.length; i++) {
|
|
404
|
+
const group = groups[i];
|
|
405
|
+
const testName = groups.length > 1
|
|
406
|
+
? `${type.toUpperCase()} Import (Part ${i + 1})`
|
|
407
|
+
: `${type.toUpperCase()} Import`;
|
|
408
|
+
// Determine file extension based on format
|
|
409
|
+
const extension = format === 'typescript' ? '.spec.ts' : `.${format}`;
|
|
410
|
+
const filename = groups.length > 1
|
|
411
|
+
? `${type}-import-${i + 1}${extension}`
|
|
412
|
+
: `${type}-import${extension}`;
|
|
413
|
+
const initialPath = path.join(options.output, filename);
|
|
414
|
+
const filepath = await test_output_writer_1.TestOutputWriter.getSafeFilename(initialPath);
|
|
415
|
+
// Use the generic TestOutputWriter
|
|
416
|
+
const writer = new test_output_writer_1.TestOutputWriter({
|
|
417
|
+
name: testName,
|
|
418
|
+
description: `Generated test configuration from ${type.toUpperCase()} import`,
|
|
419
|
+
baseUrl: options.baseUrl || '{{env.BASE_URL}}',
|
|
420
|
+
scenarios: group,
|
|
421
|
+
format: format,
|
|
422
|
+
outputPath: filepath,
|
|
423
|
+
sourceType: type,
|
|
424
|
+
metadata: {
|
|
425
|
+
importedFrom: options.sourceFile,
|
|
426
|
+
importedAt: new Date().toISOString(),
|
|
427
|
+
totalEndpoints: scenarios.length,
|
|
428
|
+
groupIndex: i + 1,
|
|
429
|
+
totalGroups: groups.length,
|
|
430
|
+
filters: {
|
|
431
|
+
tags: options.tags,
|
|
432
|
+
excludeTags: options.excludeTags,
|
|
433
|
+
paths: options.paths,
|
|
434
|
+
methods: options.methods,
|
|
435
|
+
domains: options.filterDomains,
|
|
436
|
+
excludeDomains: options.excludeDomains,
|
|
437
|
+
services: options.services,
|
|
438
|
+
operations: options.operations,
|
|
439
|
+
folders: options.folders
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}, {
|
|
443
|
+
// DSL options for importers
|
|
444
|
+
includeDataGeneration: true,
|
|
445
|
+
includeCSVSupport: true,
|
|
446
|
+
includeHooks: true,
|
|
447
|
+
includeCustomLogic: type !== 'wsdl', // Less custom logic for SOAP
|
|
448
|
+
includeMultipleLoadPatterns: true
|
|
449
|
+
});
|
|
450
|
+
const savedPath = await writer.write();
|
|
451
|
+
console.log(chalk_1.default.green(`✅ Generated: ${savedPath}`));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function ensureDirectory(dirPath) {
|
|
455
|
+
try {
|
|
456
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Directory might already exist
|
|
460
|
+
}
|
|
461
|
+
}
|