@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,488 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VirtualUser = void 0;
|
|
4
|
+
const step_executor_1 = require("./step-executor");
|
|
5
|
+
const csv_data_provider_1 = require("./csv-data-provider");
|
|
6
|
+
const hooks_manager_1 = require("./hooks-manager");
|
|
7
|
+
const time_1 = require("../utils/time");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
class VirtualUser {
|
|
10
|
+
constructor(id, metrics, handlers, testName = 'Load Test', vuHooks, globalThinkTime, globalCSV) {
|
|
11
|
+
this.isActive = true;
|
|
12
|
+
this.scenarios = [];
|
|
13
|
+
this.csvProviders = new Map();
|
|
14
|
+
logger_1.logger.debug(`VirtualUser ${id} created`);
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.metrics = metrics;
|
|
17
|
+
this.handlers = handlers;
|
|
18
|
+
this.testName = testName;
|
|
19
|
+
this.globalThinkTime = globalThinkTime; // Store global think time
|
|
20
|
+
this.stepExecutor = new step_executor_1.StepExecutor(handlers, testName); // Pass testName to StepExecutor
|
|
21
|
+
this.vuHooksManager = new hooks_manager_1.VUHooksManager(testName, id, vuHooks);
|
|
22
|
+
// Initialize global CSV if configured
|
|
23
|
+
if (globalCSV?.config) {
|
|
24
|
+
this.globalCSVMode = globalCSV.mode || 'next';
|
|
25
|
+
this.globalCSVProvider = csv_data_provider_1.CSVDataProvider.getInstance(globalCSV.config);
|
|
26
|
+
logger_1.logger.debug(`VU${id}: Global CSV configured (mode: ${this.globalCSVMode})`);
|
|
27
|
+
}
|
|
28
|
+
this.context = {
|
|
29
|
+
vu_id: id,
|
|
30
|
+
iteration: 0,
|
|
31
|
+
variables: {},
|
|
32
|
+
extracted_data: {}
|
|
33
|
+
// csv_data and global_csv_data will be added only when needed
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// FIXED: Now async to support CSV initialization
|
|
37
|
+
async setScenarios(scenarios) {
|
|
38
|
+
logger_1.logger.debug(`VU${this.id}: setScenarios called with ${scenarios.length} scenarios`);
|
|
39
|
+
this.scenarios = scenarios;
|
|
40
|
+
// Debug: Let's see what the scenarios look like
|
|
41
|
+
for (const scenario of scenarios) {
|
|
42
|
+
logger_1.logger.debug(`VU${this.id}: Scenario "${scenario.name}" config: ${JSON.stringify(scenario, null, 2)}`);
|
|
43
|
+
}
|
|
44
|
+
// Initialize CSV providers only if needed
|
|
45
|
+
await this.initializeCSVProvidersIfNeeded();
|
|
46
|
+
logger_1.logger.debug(`VU${this.id}: setScenarios completed`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Initialize CSV providers only for scenarios that need them
|
|
50
|
+
*/
|
|
51
|
+
async initializeCSVProvidersIfNeeded() {
|
|
52
|
+
const csvScenarios = this.scenarios.filter(s => s.csv_data);
|
|
53
|
+
if (csvScenarios.length === 0) {
|
|
54
|
+
// No CSV scenarios - skip initialization entirely
|
|
55
|
+
logger_1.logger.debug(`VU${this.id}: No CSV scenarios found, skipping CSV initialization`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
logger_1.logger.debug(`VU${this.id}: Found ${csvScenarios.length} scenarios with CSV data`);
|
|
59
|
+
for (const scenario of csvScenarios) {
|
|
60
|
+
const csvScenario = scenario; // Cast to access CSV properties
|
|
61
|
+
logger_1.logger.debug(`VU${this.id}: Processing CSV for scenario "${scenario.name}": ${JSON.stringify(csvScenario.csv_data)}`);
|
|
62
|
+
if (csvScenario.csv_data) {
|
|
63
|
+
try {
|
|
64
|
+
const provider = csv_data_provider_1.CSVDataProvider.getInstance(csvScenario.csv_data);
|
|
65
|
+
await provider.loadData();
|
|
66
|
+
this.csvProviders.set(scenario.name, provider);
|
|
67
|
+
logger_1.logger.debug(`VU${this.id}: Initialized CSV provider for scenario "${scenario.name}"`);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
logger_1.logger.warn(`VU${this.id}: Failed to initialize CSV for scenario "${scenario.name}":`, error);
|
|
71
|
+
// Don't fail the entire VU - just log the warning and continue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
logger_1.logger.debug(`VU${this.id}: CSV initialization completed. Providers: ${this.csvProviders.size}`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load global CSV data and merge into context variables
|
|
79
|
+
* Called once at the start of each VU execution cycle
|
|
80
|
+
*/
|
|
81
|
+
async loadGlobalCSVData() {
|
|
82
|
+
if (!this.globalCSVProvider) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await this.globalCSVProvider.loadData();
|
|
87
|
+
let csvData = null;
|
|
88
|
+
switch (this.globalCSVMode) {
|
|
89
|
+
case 'unique':
|
|
90
|
+
csvData = await this.globalCSVProvider.getUniqueRow(this.id);
|
|
91
|
+
break;
|
|
92
|
+
case 'random':
|
|
93
|
+
csvData = await this.globalCSVProvider.getRandomRow(this.id);
|
|
94
|
+
break;
|
|
95
|
+
case 'next':
|
|
96
|
+
default:
|
|
97
|
+
csvData = await this.globalCSVProvider.getNextRow(this.id);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (csvData) {
|
|
101
|
+
this.context.global_csv_data = csvData;
|
|
102
|
+
// Merge global CSV data into variables (can be overridden by scenario CSV)
|
|
103
|
+
logger_1.logger.debug(`VU${this.id}: Adding global CSV columns to variables: ${Object.keys(csvData).join(', ')}`);
|
|
104
|
+
for (const [key, value] of Object.entries(csvData)) {
|
|
105
|
+
this.context.variables[key] = value;
|
|
106
|
+
logger_1.logger.debug(`VU${this.id}: Set global CSV variable: ${key} = ${value}`);
|
|
107
|
+
}
|
|
108
|
+
logger_1.logger.debug(`VU ${this.id}: Loaded global CSV data: ${Object.keys(csvData).join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
logger_1.logger.debug(`VU${this.id}: Global CSV data exhausted - stopping VU`);
|
|
112
|
+
delete this.context.global_csv_data;
|
|
113
|
+
await this.stop();
|
|
114
|
+
throw new Error(`VU${this.id} terminated due to global CSV data exhaustion`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof Error && error.message.includes('terminated due to global CSV')) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
logger_1.logger.warn(`๐ VU ${this.id}: Failed to load global CSV data:`, error);
|
|
122
|
+
delete this.context.global_csv_data;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// This method executes scenarios once (called repeatedly by load patterns)
|
|
126
|
+
async executeScenarios() {
|
|
127
|
+
if (!this.isActive || this.scenarios.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Load global CSV data first (available to all scenarios)
|
|
131
|
+
try {
|
|
132
|
+
await this.loadGlobalCSVData();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof Error && error.message.includes('terminated due to global CSV')) {
|
|
136
|
+
logger_1.logger.warn(`๐ VU ${this.id}: Stopping due to global CSV exhaustion`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Log but continue if global CSV fails for other reasons
|
|
140
|
+
logger_1.logger.warn(`๐ VU ${this.id}: Global CSV loading failed, continuing without:`, error);
|
|
141
|
+
}
|
|
142
|
+
// Execute beforeVU hook
|
|
143
|
+
try {
|
|
144
|
+
const beforeVUResult = await this.vuHooksManager.executeBeforeVU(this.context.variables, this.context.extracted_data);
|
|
145
|
+
// Merge any variables returned by beforeVU hook
|
|
146
|
+
if (beforeVUResult?.variables) {
|
|
147
|
+
Object.assign(this.context.variables, beforeVUResult.variables);
|
|
148
|
+
logger_1.logger.debug(`VU${this.id}: beforeVU hook set variables: ${Object.keys(beforeVUResult.variables).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
logger_1.logger.error(`โ VU ${this.id} beforeVU hook failed:`, error);
|
|
153
|
+
// Continue execution even if beforeVU fails
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const selectedScenarios = this.selectScenarios(this.scenarios);
|
|
157
|
+
for (const scenario of selectedScenarios) {
|
|
158
|
+
if (!this.isActive)
|
|
159
|
+
break;
|
|
160
|
+
try {
|
|
161
|
+
await this.executeScenario(scenario);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
logger_1.logger.error(`โ VU ${this.id} failed executing scenario ${scenario.name}:`, error);
|
|
165
|
+
this.metrics.recordError(this.id, scenario.name, 'scenario', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
// Execute teardownVU hook
|
|
171
|
+
try {
|
|
172
|
+
await this.vuHooksManager.executeTeardownVU(this.context.variables, this.context.extracted_data);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
logger_1.logger.error(`โ VU ${this.id} teardownVU hook failed:`, error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async executeScenario(scenario) {
|
|
180
|
+
// Handle both number and LoopConfig for backwards compatibility
|
|
181
|
+
const loops = typeof scenario.loop === 'number' ? scenario.loop : (scenario.loop?.count || 1);
|
|
182
|
+
logger_1.logger.debug(`VU ${this.id} executing scenario: ${scenario.name} (${loops} loops)`);
|
|
183
|
+
logger_1.logger.debug(`Scenario variables: ${JSON.stringify(scenario.variables)}`);
|
|
184
|
+
// Process scenario variables and store in context
|
|
185
|
+
if (scenario.variables) {
|
|
186
|
+
logger_1.logger.debug(`Processing ${Object.keys(scenario.variables).length} scenario variables`);
|
|
187
|
+
for (const [key, value] of Object.entries(scenario.variables)) {
|
|
188
|
+
this.context.variables[key] = value;
|
|
189
|
+
logger_1.logger.debug(`Set variable: ${key} = ${value}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Load CSV data if this scenario uses it (completely optional)
|
|
193
|
+
logger_1.logger.debug(`About to load CSV data if needed...`);
|
|
194
|
+
await this.loadCSVDataIfNeeded(scenario);
|
|
195
|
+
logger_1.logger.debug(`CSV data loading completed`);
|
|
196
|
+
logger_1.logger.debug(`Context variables after CSV setup: ${JSON.stringify(this.context.variables)}`);
|
|
197
|
+
// Create scenario hooks manager
|
|
198
|
+
const scenarioHooksManager = new hooks_manager_1.ScenarioHooksManager(this.testName, this.id, scenario.name, scenario.hooks);
|
|
199
|
+
// Execute beforeScenario hook
|
|
200
|
+
try {
|
|
201
|
+
const beforeScenarioResult = await scenarioHooksManager.executeBeforeScenario(this.context.variables, this.context.extracted_data, this.context.csv_data);
|
|
202
|
+
// CRITICAL FIX: The hook manager should have updated the objects by reference
|
|
203
|
+
// But let's also merge any returned variables to be safe
|
|
204
|
+
if (beforeScenarioResult?.variables) {
|
|
205
|
+
Object.assign(this.context.variables, beforeScenarioResult.variables);
|
|
206
|
+
logger_1.logger.debug(`VU${this.id}: beforeScenario hook merged additional variables: ${Object.keys(beforeScenarioResult.variables).join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
// Debug: Show what's in context after beforeScenario
|
|
209
|
+
logger_1.logger.debug(`VU${this.id}: Variables after beforeScenario: ${Object.keys(this.context.variables).join(', ')}`);
|
|
210
|
+
logger_1.logger.debug(`VU${this.id}: Extracted data after beforeScenario: ${Object.keys(this.context.extracted_data).join(', ')}`);
|
|
211
|
+
logger_1.logger.debug(`VU${this.id}: Extracted data values: ${JSON.stringify(this.context.extracted_data)}`);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logger_1.logger.error(`โ VU ${this.id} beforeScenario hook failed:`, error);
|
|
215
|
+
// You might want to return here if authentication is critical
|
|
216
|
+
if (scenario.hooks?.beforeScenario?.continueOnError === false) {
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Legacy support: Run setup if configured (deprecated - use hooks.beforeScenario)
|
|
221
|
+
if (scenario.setup && !scenario.hooks?.beforeScenario) {
|
|
222
|
+
await this.executeSetup(scenario.setup);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
for (let iteration = 0; iteration < loops; iteration++) {
|
|
226
|
+
if (!this.isActive)
|
|
227
|
+
break;
|
|
228
|
+
this.context.iteration = iteration;
|
|
229
|
+
logger_1.logger.debug(`๐ VU ${this.id} scenario ${scenario.name} iteration ${iteration + 1}/${loops}`);
|
|
230
|
+
// Execute beforeLoop hook
|
|
231
|
+
try {
|
|
232
|
+
const beforeLoopResult = await scenarioHooksManager.executeBeforeLoop(iteration, this.context.variables, this.context.extracted_data, this.context.csv_data);
|
|
233
|
+
if (beforeLoopResult?.variables) {
|
|
234
|
+
Object.assign(this.context.variables, beforeLoopResult.variables);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
logger_1.logger.error(`โ VU ${this.id} beforeLoop hook failed:`, error);
|
|
239
|
+
}
|
|
240
|
+
// For unique CSV mode, get new CSV data each iteration
|
|
241
|
+
const csvScenario = scenario;
|
|
242
|
+
if (csvScenario.csv_mode === 'unique' && iteration > 0 && this.csvProviders.has(scenario.name)) {
|
|
243
|
+
await this.loadCSVDataIfNeeded(scenario);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Execute all steps in sequence
|
|
247
|
+
for (let stepIndex = 0; stepIndex < scenario.steps.length; stepIndex++) {
|
|
248
|
+
if (!this.isActive)
|
|
249
|
+
break;
|
|
250
|
+
const step = scenario.steps[stepIndex];
|
|
251
|
+
// Debug: Show context before each step
|
|
252
|
+
logger_1.logger.debug(`VU${this.id}: About to execute step "${step.name || step.type}"`);
|
|
253
|
+
logger_1.logger.debug(`VU${this.id}: Available variables: ${Object.keys(this.context.variables).join(', ')}`);
|
|
254
|
+
logger_1.logger.debug(`VU${this.id}: Available extracted_data: ${Object.keys(this.context.extracted_data).join(', ')}`);
|
|
255
|
+
try {
|
|
256
|
+
const result = await this.stepExecutor.executeStep(step, this.context, scenario.name);
|
|
257
|
+
if (result.shouldRecord) {
|
|
258
|
+
this.metrics.recordResult(result);
|
|
259
|
+
}
|
|
260
|
+
// Apply hierarchical think time: step > scenario > global
|
|
261
|
+
const effectiveThinkTime = this.getEffectiveThinkTime(step, scenario);
|
|
262
|
+
if (effectiveThinkTime !== undefined) {
|
|
263
|
+
await this.applyThinkTime(effectiveThinkTime);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
logger_1.logger.error(`โ VU ${this.id} step failed:`, error);
|
|
268
|
+
this.metrics.recordError(this.id, scenario.name, step.name || step.type || 'rest', error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Execute afterLoop hook
|
|
272
|
+
try {
|
|
273
|
+
await scenarioHooksManager.executeAfterLoop(iteration, this.context.variables, this.context.extracted_data, this.context.csv_data);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
logger_1.logger.error(`โ VU ${this.id} afterLoop hook failed:`, error);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
// Execute afterLoop hook even on error
|
|
281
|
+
try {
|
|
282
|
+
await scenarioHooksManager.executeAfterLoop(iteration, this.context.variables, this.context.extracted_data, this.context.csv_data);
|
|
283
|
+
}
|
|
284
|
+
catch (hookError) {
|
|
285
|
+
logger_1.logger.error(`โ VU ${this.id} afterLoop hook failed during error handling:`, hookError);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
// Think time between iterations (except after last iteration)
|
|
290
|
+
if (iteration < loops - 1) {
|
|
291
|
+
await this.applyThinkTime(scenario.think_time);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
// Execute teardownScenario hook
|
|
297
|
+
try {
|
|
298
|
+
await scenarioHooksManager.executeTeardownScenario(this.context.variables, this.context.extracted_data, this.context.csv_data);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
logger_1.logger.error(`โ VU ${this.id} teardownScenario hook failed:`, error);
|
|
302
|
+
}
|
|
303
|
+
// Legacy support: Run teardown if configured (deprecated - use hooks.teardownScenario)
|
|
304
|
+
if (scenario.teardown && !scenario.hooks?.teardownScenario) {
|
|
305
|
+
await this.executeTeardown(scenario.teardown);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// ... rest of your existing methods remain exactly the same
|
|
310
|
+
async loadCSVDataIfNeeded(scenario) {
|
|
311
|
+
const csvScenario = scenario;
|
|
312
|
+
logger_1.logger.debug(`VU${this.id}: Checking CSV need for scenario "${scenario.name}"`);
|
|
313
|
+
logger_1.logger.debug(`VU${this.id}: Has csv_data config: ${!!csvScenario.csv_data}`);
|
|
314
|
+
logger_1.logger.debug(`VU${this.id}: Has CSV provider: ${this.csvProviders.has(scenario.name)}`);
|
|
315
|
+
if (!csvScenario.csv_data || !this.csvProviders.has(scenario.name)) {
|
|
316
|
+
logger_1.logger.debug(`VU${this.id}: No CSV data needed for scenario "${scenario.name}"`);
|
|
317
|
+
delete this.context.csv_data;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
logger_1.logger.debug(`VU${this.id}: Loading CSV data for scenario "${scenario.name}"...`);
|
|
322
|
+
const csvData = await this.loadCSVDataForScenario(csvScenario);
|
|
323
|
+
if (csvData) {
|
|
324
|
+
this.context.csv_data = csvData;
|
|
325
|
+
logger_1.logger.debug(`VU${this.id}: Adding CSV columns to variables: ${Object.keys(csvData).join(', ')}`);
|
|
326
|
+
for (const [key, value] of Object.entries(csvData)) {
|
|
327
|
+
if (!(key in this.context.variables)) {
|
|
328
|
+
this.context.variables[key] = value;
|
|
329
|
+
logger_1.logger.debug(`VU${this.id}: Added CSV variable: ${key} = ${value}`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
logger_1.logger.debug(`VU${this.id}: Skipped CSV variable ${key} (already in variables)`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
logger_1.logger.debug(`VU ${this.id}: Loaded CSV data for scenario "${scenario.name}": ${Object.keys(csvData).join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
logger_1.logger.debug(`VU${this.id}: No CSV data available - terminating this VU`);
|
|
339
|
+
delete this.context.csv_data;
|
|
340
|
+
this.stop();
|
|
341
|
+
throw new Error(`VU${this.id} terminated due to CSV data exhaustion in scenario "${scenario.name}"`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
if (error instanceof Error && error.message.includes('terminated due to CSV data exhaustion')) {
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
logger_1.logger.warn(`VU ${this.id}: Failed to load CSV data for scenario "${scenario.name}":`, error);
|
|
349
|
+
delete this.context.csv_data;
|
|
350
|
+
logger_1.logger.debug(`VU${this.id}: Continuing with fallback variables after CSV error`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async loadCSVDataForScenario(scenario) {
|
|
354
|
+
const provider = this.csvProviders.get(scenario.name);
|
|
355
|
+
if (!provider) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const mode = scenario.csv_mode || 'next';
|
|
359
|
+
switch (mode) {
|
|
360
|
+
case 'unique':
|
|
361
|
+
return await provider.getUniqueRow(this.id);
|
|
362
|
+
case 'random':
|
|
363
|
+
return await provider.getRandomRow();
|
|
364
|
+
case 'next':
|
|
365
|
+
default:
|
|
366
|
+
return await provider.getNextRow(this.id);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
selectScenarios(scenarios) {
|
|
370
|
+
const selected = [];
|
|
371
|
+
for (const scenario of scenarios) {
|
|
372
|
+
const weight = scenario.weight || 100;
|
|
373
|
+
const random = Math.random() * 100;
|
|
374
|
+
if (random < weight) {
|
|
375
|
+
selected.push(scenario);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Ensure at least one scenario is selected
|
|
379
|
+
if (selected.length === 0 && scenarios.length > 0) {
|
|
380
|
+
selected.push(scenarios[0]);
|
|
381
|
+
}
|
|
382
|
+
return selected;
|
|
383
|
+
}
|
|
384
|
+
async executeSetup(setupScript) {
|
|
385
|
+
try {
|
|
386
|
+
await this.executeScript(setupScript, 'setup');
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
logger_1.logger.warn(`โ ๏ธ VU ${this.id} setup script failed:`, error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async executeTeardown(teardownScript) {
|
|
393
|
+
try {
|
|
394
|
+
await this.executeScript(teardownScript, 'teardown');
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
logger_1.logger.warn(`โ ๏ธ VU ${this.id} teardown script failed:`, error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async executeScript(script, type) {
|
|
401
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
402
|
+
const fn = new AsyncFunction('context', 'require', 'console', script);
|
|
403
|
+
const timeout = 30000; // 30 seconds timeout
|
|
404
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
405
|
+
setTimeout(() => reject(new Error(`${type} script timeout`)), timeout);
|
|
406
|
+
});
|
|
407
|
+
return Promise.race([
|
|
408
|
+
fn(this.context, require, console),
|
|
409
|
+
timeoutPromise
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get effective think time using hierarchical override:
|
|
414
|
+
* Step think_time > Scenario think_time > Global think_time
|
|
415
|
+
*/
|
|
416
|
+
getEffectiveThinkTime(step, scenario) {
|
|
417
|
+
// Step level has highest priority
|
|
418
|
+
if (step.think_time !== undefined) {
|
|
419
|
+
return step.think_time;
|
|
420
|
+
}
|
|
421
|
+
// Scenario level is next
|
|
422
|
+
if (scenario.think_time !== undefined) {
|
|
423
|
+
return scenario.think_time;
|
|
424
|
+
}
|
|
425
|
+
// Global level is fallback
|
|
426
|
+
return this.globalThinkTime;
|
|
427
|
+
}
|
|
428
|
+
async applyThinkTime(thinkTime) {
|
|
429
|
+
if (!thinkTime) {
|
|
430
|
+
// No think time specified at any level - skip
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (typeof thinkTime === 'number') {
|
|
434
|
+
logger_1.logger.debug(`Applying thinktime: ${thinkTime} seconds`);
|
|
435
|
+
await (0, time_1.sleep)(thinkTime * 1000);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const rangeMatch = thinkTime.match(/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)([sm])?$/);
|
|
439
|
+
if (rangeMatch) {
|
|
440
|
+
const [, minStr, maxStr, unit] = rangeMatch;
|
|
441
|
+
let min = parseFloat(minStr);
|
|
442
|
+
let max = parseFloat(maxStr);
|
|
443
|
+
if (unit === 's' || !unit) {
|
|
444
|
+
min *= 1000;
|
|
445
|
+
max *= 1000;
|
|
446
|
+
}
|
|
447
|
+
const thinkTimeMs = (0, time_1.randomBetween)(min, max);
|
|
448
|
+
await (0, time_1.sleep)(thinkTimeMs);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
try {
|
|
452
|
+
const thinkTimeMs = (0, time_1.parseTime)(thinkTime);
|
|
453
|
+
await (0, time_1.sleep)(thinkTimeMs);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
logger_1.logger.warn(`โ ๏ธ Invalid think time format: ${thinkTime}`);
|
|
457
|
+
await (0, time_1.sleep)((0, time_1.randomBetween)(1000, 3000));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// stop(): void {
|
|
462
|
+
// this.isActive = false;
|
|
463
|
+
// logger.debug(`โน๏ธ VU ${this.id} stopped`);
|
|
464
|
+
// }
|
|
465
|
+
async stop() {
|
|
466
|
+
this.isActive = false;
|
|
467
|
+
logger_1.logger.debug(`โน๏ธ VU ${this.id} stopping...`);
|
|
468
|
+
// Clean up browser resources if WebHandler exists
|
|
469
|
+
const webHandler = this.handlers.get('web');
|
|
470
|
+
if (webHandler && typeof webHandler.cleanupVU === 'function') {
|
|
471
|
+
try {
|
|
472
|
+
await webHandler.cleanupVU(this.id);
|
|
473
|
+
logger_1.logger.debug(`๐งน VU ${this.id}: Browser cleanup completed`);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
logger_1.logger.warn(`โ ๏ธ VU ${this.id}: Error during browser cleanup:`, error);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
logger_1.logger.debug(`โน๏ธ VU ${this.id} stopped`);
|
|
480
|
+
}
|
|
481
|
+
getId() {
|
|
482
|
+
return this.id;
|
|
483
|
+
}
|
|
484
|
+
isRunning() {
|
|
485
|
+
return this.isActive;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
exports.VirtualUser = VirtualUser;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { TestConfiguration } from '../config';
|
|
3
|
+
import { RemoteWorker, RemoteWorkerConfig } from './remote-worker';
|
|
4
|
+
export interface DistributedTestConfig {
|
|
5
|
+
workers: RemoteWorkerConfig[];
|
|
6
|
+
strategy: 'even' | 'capacity_based' | 'round_robin' | 'geographic';
|
|
7
|
+
sync_start: boolean;
|
|
8
|
+
heartbeat_interval: number;
|
|
9
|
+
timeout: number;
|
|
10
|
+
retry_failed: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface WorkAssignment {
|
|
13
|
+
worker: RemoteWorker;
|
|
14
|
+
config: TestConfiguration;
|
|
15
|
+
virtualUsers: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class DistributedCoordinator extends EventEmitter {
|
|
18
|
+
private workers;
|
|
19
|
+
private config;
|
|
20
|
+
private testConfig;
|
|
21
|
+
private loadDistributor;
|
|
22
|
+
private resultAggregator;
|
|
23
|
+
private healthMonitor;
|
|
24
|
+
private isRunning;
|
|
25
|
+
constructor(config: DistributedTestConfig);
|
|
26
|
+
initialize(): Promise<void>;
|
|
27
|
+
executeTest(testConfig: TestConfiguration): Promise<void>;
|
|
28
|
+
private synchronizedStart;
|
|
29
|
+
private rollingStart;
|
|
30
|
+
private waitForCompletion;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
cleanup(): Promise<void>;
|
|
33
|
+
getAggregatedResults(): any;
|
|
34
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DistributedCoordinator = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const remote_worker_1 = require("./remote-worker"); // Fixed import
|
|
6
|
+
const load_distributor_1 = require("./load-distributor");
|
|
7
|
+
const result_aggregator_1 = require("./result-aggregator");
|
|
8
|
+
const health_monitor_1 = require("./health-monitor");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
class DistributedCoordinator extends events_1.EventEmitter {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super();
|
|
13
|
+
this.workers = [];
|
|
14
|
+
this.testConfig = null;
|
|
15
|
+
this.isRunning = false;
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.loadDistributor = new load_distributor_1.LoadDistributor();
|
|
18
|
+
this.resultAggregator = new result_aggregator_1.ResultAggregator();
|
|
19
|
+
this.healthMonitor = new health_monitor_1.HealthMonitor();
|
|
20
|
+
}
|
|
21
|
+
async initialize() {
|
|
22
|
+
logger_1.logger.info(`๐ Initializing distributed test with ${this.config.workers.length} workers`);
|
|
23
|
+
for (const workerConfig of this.config.workers) {
|
|
24
|
+
try {
|
|
25
|
+
const worker = new remote_worker_1.RemoteWorker(workerConfig);
|
|
26
|
+
worker.on('result', (result) => {
|
|
27
|
+
this.resultAggregator.addResult(result);
|
|
28
|
+
this.emit('result', result);
|
|
29
|
+
});
|
|
30
|
+
worker.on('error', (error) => {
|
|
31
|
+
logger_1.logger.error(`โ Worker ${worker.getAddress()} error:`, error);
|
|
32
|
+
this.emit('worker-error', { worker: worker.getAddress(), error });
|
|
33
|
+
});
|
|
34
|
+
worker.on('status', (status) => {
|
|
35
|
+
this.healthMonitor.updateWorkerStatus(worker.getAddress(), status);
|
|
36
|
+
});
|
|
37
|
+
await worker.connect();
|
|
38
|
+
this.workers.push(worker);
|
|
39
|
+
logger_1.logger.info(`โ
Connected to worker: ${worker.getAddress()}`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger_1.logger.error(`โ Failed to connect to worker ${workerConfig.host}:${workerConfig.port}:`, error);
|
|
43
|
+
if (!this.config.retry_failed) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (this.workers.length === 0) {
|
|
49
|
+
throw new Error('No workers available for distributed testing');
|
|
50
|
+
}
|
|
51
|
+
this.healthMonitor.start(this.workers, this.config.heartbeat_interval);
|
|
52
|
+
logger_1.logger.info(`๐ฏ ${this.workers.length} workers ready for distributed testing`);
|
|
53
|
+
}
|
|
54
|
+
async executeTest(testConfig) {
|
|
55
|
+
this.testConfig = testConfig;
|
|
56
|
+
this.isRunning = true;
|
|
57
|
+
try {
|
|
58
|
+
const workAssignments = this.loadDistributor.distribute(testConfig, this.workers, this.config.strategy);
|
|
59
|
+
logger_1.logger.info(`๐ Load distribution:`);
|
|
60
|
+
workAssignments.forEach(assignment => {
|
|
61
|
+
logger_1.logger.info(` ${assignment.worker.getAddress()}: ${assignment.virtualUsers} VUs`);
|
|
62
|
+
});
|
|
63
|
+
this.resultAggregator.start();
|
|
64
|
+
if (this.config.sync_start) {
|
|
65
|
+
await this.synchronizedStart(workAssignments);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
await this.rollingStart(workAssignments);
|
|
69
|
+
}
|
|
70
|
+
await this.waitForCompletion();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
this.isRunning = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async synchronizedStart(assignments) {
|
|
77
|
+
logger_1.logger.info('๐ Starting synchronized distributed test...');
|
|
78
|
+
const preparations = assignments.map(async (assignment) => {
|
|
79
|
+
await assignment.worker.prepareTest(assignment.config);
|
|
80
|
+
});
|
|
81
|
+
await Promise.all(preparations);
|
|
82
|
+
logger_1.logger.info('โ
All workers prepared');
|
|
83
|
+
const startTime = Date.now() + 5000;
|
|
84
|
+
const starts = assignments.map(async (assignment) => {
|
|
85
|
+
await assignment.worker.startTest(startTime);
|
|
86
|
+
});
|
|
87
|
+
await Promise.all(starts);
|
|
88
|
+
logger_1.logger.info('๐ฏ All workers started');
|
|
89
|
+
}
|
|
90
|
+
async rollingStart(assignments) {
|
|
91
|
+
logger_1.logger.info('๐ Starting rolling distributed test...');
|
|
92
|
+
for (const assignment of assignments) {
|
|
93
|
+
try {
|
|
94
|
+
await assignment.worker.executeTest(assignment.config);
|
|
95
|
+
logger_1.logger.info(`โ
Started worker ${assignment.worker.getAddress()}`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger_1.logger.error(`โ Failed to start worker ${assignment.worker.getAddress()}:`, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async waitForCompletion() {
|
|
103
|
+
logger_1.logger.info('โณ Waiting for all workers to complete...');
|
|
104
|
+
const completionPromises = this.workers.map(worker => worker.waitForCompletion().catch((error) => {
|
|
105
|
+
logger_1.logger.error(`โ Worker ${worker.getAddress()} completion error:`, error);
|
|
106
|
+
return null;
|
|
107
|
+
}));
|
|
108
|
+
await Promise.all(completionPromises);
|
|
109
|
+
// Collect results from all workers
|
|
110
|
+
logger_1.logger.info('๐ Collecting results from workers...');
|
|
111
|
+
for (const worker of this.workers) {
|
|
112
|
+
try {
|
|
113
|
+
const workerResults = await worker.getResults();
|
|
114
|
+
if (workerResults) {
|
|
115
|
+
// Add worker results to aggregator
|
|
116
|
+
if (workerResults.results) {
|
|
117
|
+
workerResults.results.forEach((result) => {
|
|
118
|
+
this.resultAggregator.addResult(result, worker.getAddress());
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Add VU ramp-up events from worker's summary
|
|
122
|
+
if (workerResults.summary?.vu_ramp_up && Array.isArray(workerResults.summary.vu_ramp_up)) {
|
|
123
|
+
this.resultAggregator.addVURampUpEvents(workerResults.summary.vu_ramp_up, worker.getAddress());
|
|
124
|
+
logger_1.logger.debug(`๐ Collected ${workerResults.summary.vu_ramp_up.length} VU events from ${worker.getAddress()}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logger_1.logger.warn(`โ ๏ธ Failed to get results from worker ${worker.getAddress()}:`, error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.resultAggregator.stop();
|
|
133
|
+
logger_1.logger.info('โ
All workers completed');
|
|
134
|
+
}
|
|
135
|
+
async stop() {
|
|
136
|
+
logger_1.logger.info('โน๏ธ Stopping distributed test...');
|
|
137
|
+
this.isRunning = false;
|
|
138
|
+
const stopPromises = this.workers.map(worker => worker.stop().catch((error) => {
|
|
139
|
+
logger_1.logger.warn(`โ ๏ธ Error stopping worker ${worker.getAddress()}:`, error);
|
|
140
|
+
}));
|
|
141
|
+
await Promise.all(stopPromises);
|
|
142
|
+
logger_1.logger.info('๐ All workers stopped');
|
|
143
|
+
}
|
|
144
|
+
async cleanup() {
|
|
145
|
+
logger_1.logger.info('๐งน Cleaning up distributed test...');
|
|
146
|
+
this.healthMonitor.stop();
|
|
147
|
+
const cleanupPromises = this.workers.map(worker => worker.disconnect().catch((error) => {
|
|
148
|
+
logger_1.logger.warn(`โ ๏ธ Error cleaning up worker ${worker.getAddress()}:`, error);
|
|
149
|
+
}));
|
|
150
|
+
await Promise.all(cleanupPromises);
|
|
151
|
+
this.workers = [];
|
|
152
|
+
logger_1.logger.info('โ
Cleanup completed');
|
|
153
|
+
}
|
|
154
|
+
getAggregatedResults() {
|
|
155
|
+
return this.resultAggregator.getAggregatedResults();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.DistributedCoordinator = DistributedCoordinator;
|