@testsmith/perfornium 0.1.0 → 0.3.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/dist/config/parser.d.ts +9 -0
- package/dist/config/parser.js +51 -0
- package/dist/config/types/step-types.d.ts +17 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -1
- package/dist/core/rendezvous.d.ts +70 -0
- package/dist/core/rendezvous.js +216 -0
- package/dist/core/step-executor.d.ts +1 -0
- package/dist/core/step-executor.js +52 -0
- package/dist/core/test-runner.js +5 -0
- package/dist/core/virtual-user.d.ts +9 -0
- package/dist/core/virtual-user.js +26 -10
- package/dist/dsl/test-builder.d.ts +18 -0
- package/dist/dsl/test-builder.js +24 -0
- package/package.json +10 -2
package/dist/config/parser.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ import { TestConfiguration } from './types';
|
|
|
2
2
|
export declare class ConfigParser {
|
|
3
3
|
private templateProcessor;
|
|
4
4
|
parse(configPath: string, environment?: string): Promise<TestConfiguration>;
|
|
5
|
+
/**
|
|
6
|
+
* Process includes - load scenarios from external files
|
|
7
|
+
*
|
|
8
|
+
* Supports:
|
|
9
|
+
* - includes: ["scenarios/login.yml", "scenarios/checkout.yml"]
|
|
10
|
+
* - Included files can contain single scenario or array of scenarios
|
|
11
|
+
* - Scenarios are merged with inline scenarios
|
|
12
|
+
*/
|
|
13
|
+
private processIncludes;
|
|
5
14
|
private parseTypeScript;
|
|
6
15
|
private evaluatePlainTypeScript;
|
|
7
16
|
private setupBaseDirectories;
|
package/dist/config/parser.js
CHANGED
|
@@ -62,6 +62,8 @@ class ConfigParser {
|
|
|
62
62
|
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
63
63
|
config = this.parseContent(configContent);
|
|
64
64
|
}
|
|
65
|
+
// Process includes (scenarios from external files)
|
|
66
|
+
config = await this.processIncludes(config, path.dirname(configPath));
|
|
65
67
|
logger_1.logger.debug(`Parsed config.global: ${JSON.stringify(config.global, null, 2)}`);
|
|
66
68
|
// Setup faker configuration if specified
|
|
67
69
|
logger_1.logger.debug('About to setup faker...');
|
|
@@ -77,6 +79,55 @@ class ConfigParser {
|
|
|
77
79
|
}
|
|
78
80
|
return config;
|
|
79
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Process includes - load scenarios from external files
|
|
84
|
+
*
|
|
85
|
+
* Supports:
|
|
86
|
+
* - includes: ["scenarios/login.yml", "scenarios/checkout.yml"]
|
|
87
|
+
* - Included files can contain single scenario or array of scenarios
|
|
88
|
+
* - Scenarios are merged with inline scenarios
|
|
89
|
+
*/
|
|
90
|
+
async processIncludes(config, baseDir) {
|
|
91
|
+
if (!config.includes || !Array.isArray(config.includes)) {
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
logger_1.logger.debug(`Processing ${config.includes.length} include(s)`);
|
|
95
|
+
const includedScenarios = [];
|
|
96
|
+
for (const includePath of config.includes) {
|
|
97
|
+
const fullPath = path.isAbsolute(includePath)
|
|
98
|
+
? includePath
|
|
99
|
+
: path.join(baseDir, includePath);
|
|
100
|
+
if (!fs.existsSync(fullPath)) {
|
|
101
|
+
throw new Error(`Included file not found: ${fullPath}`);
|
|
102
|
+
}
|
|
103
|
+
logger_1.logger.debug(`Loading included file: ${fullPath}`);
|
|
104
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
105
|
+
const parsed = this.parseContent(content);
|
|
106
|
+
// Support both single scenario object and array of scenarios
|
|
107
|
+
if (Array.isArray(parsed)) {
|
|
108
|
+
// File contains array of scenarios
|
|
109
|
+
includedScenarios.push(...parsed);
|
|
110
|
+
}
|
|
111
|
+
else if (parsed.scenarios && Array.isArray(parsed.scenarios)) {
|
|
112
|
+
// File contains scenarios property
|
|
113
|
+
includedScenarios.push(...parsed.scenarios);
|
|
114
|
+
}
|
|
115
|
+
else if (parsed.name && parsed.steps) {
|
|
116
|
+
// File contains single scenario
|
|
117
|
+
includedScenarios.push(parsed);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger_1.logger.warn(`Included file ${includePath} has no valid scenarios`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Merge included scenarios with inline scenarios
|
|
124
|
+
const inlineScenarios = config.scenarios || [];
|
|
125
|
+
config.scenarios = [...includedScenarios, ...inlineScenarios];
|
|
126
|
+
// Remove includes from config (no longer needed)
|
|
127
|
+
delete config.includes;
|
|
128
|
+
logger_1.logger.debug(`Total scenarios after includes: ${config.scenarios.length}`);
|
|
129
|
+
return config;
|
|
130
|
+
}
|
|
80
131
|
async parseTypeScript(configPath) {
|
|
81
132
|
try {
|
|
82
133
|
// Resolve to absolute path
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { StepHooks } from "./hooks";
|
|
2
|
-
export type Step = RESTStep | SOAPStep | WebStep | CustomStep | WaitStep | ScriptStep;
|
|
2
|
+
export type Step = RESTStep | SOAPStep | WebStep | CustomStep | WaitStep | ScriptStep | RendezvousStep;
|
|
3
3
|
export interface BaseStep {
|
|
4
4
|
name?: string;
|
|
5
|
-
type?: 'rest' | 'soap' | 'web' | 'custom' | 'wait' | 'script';
|
|
5
|
+
type?: 'rest' | 'soap' | 'web' | 'custom' | 'wait' | 'script' | 'rendezvous';
|
|
6
6
|
condition?: string;
|
|
7
7
|
continueOnError?: boolean;
|
|
8
8
|
hooks?: StepHooks;
|
|
@@ -79,6 +79,21 @@ export interface ScriptStep extends BaseStep {
|
|
|
79
79
|
returns?: string;
|
|
80
80
|
timeout?: number;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Rendezvous step for synchronized VU coordination
|
|
84
|
+
*
|
|
85
|
+
* Creates a synchronization point where VUs wait for each other before proceeding.
|
|
86
|
+
* Use this to create coordinated load spikes at specific points in the test.
|
|
87
|
+
*
|
|
88
|
+
* Example: 10 VUs simultaneously checking account balances
|
|
89
|
+
*/
|
|
90
|
+
export interface RendezvousStep extends BaseStep {
|
|
91
|
+
type: 'rendezvous';
|
|
92
|
+
rendezvous: string;
|
|
93
|
+
count: number;
|
|
94
|
+
timeout?: number | string;
|
|
95
|
+
policy?: 'all' | 'count';
|
|
96
|
+
}
|
|
82
97
|
export interface WebAction {
|
|
83
98
|
name?: string;
|
|
84
99
|
expected_text?: string;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export { VirtualUser } from './virtual-user';
|
|
|
3
3
|
export { StepExecutor } from './step-executor';
|
|
4
4
|
export { CSVDataProvider } from './csv-data-provider';
|
|
5
5
|
export type { CSVDataRow } from './csv-data-provider';
|
|
6
|
+
export { RendezvousManager } from './rendezvous';
|
|
7
|
+
export type { RendezvousConfig, RendezvousResult, RendezvousStats } from './rendezvous';
|
package/dist/core/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CSVDataProvider = exports.StepExecutor = exports.VirtualUser = exports.TestRunner = void 0;
|
|
3
|
+
exports.RendezvousManager = exports.CSVDataProvider = exports.StepExecutor = exports.VirtualUser = exports.TestRunner = void 0;
|
|
4
4
|
var test_runner_1 = require("./test-runner");
|
|
5
5
|
Object.defineProperty(exports, "TestRunner", { enumerable: true, get: function () { return test_runner_1.TestRunner; } });
|
|
6
6
|
var virtual_user_1 = require("./virtual-user");
|
|
@@ -9,3 +9,5 @@ var step_executor_1 = require("./step-executor");
|
|
|
9
9
|
Object.defineProperty(exports, "StepExecutor", { enumerable: true, get: function () { return step_executor_1.StepExecutor; } });
|
|
10
10
|
var csv_data_provider_1 = require("./csv-data-provider");
|
|
11
11
|
Object.defineProperty(exports, "CSVDataProvider", { enumerable: true, get: function () { return csv_data_provider_1.CSVDataProvider; } });
|
|
12
|
+
var rendezvous_1 = require("./rendezvous");
|
|
13
|
+
Object.defineProperty(exports, "RendezvousManager", { enumerable: true, get: function () { return rendezvous_1.RendezvousManager; } });
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendezvous Manager for synchronized VU coordination
|
|
3
|
+
*
|
|
4
|
+
* Rendezvous points allow Virtual Users to wait for each other at specific
|
|
5
|
+
* points in the test execution, creating coordinated load spikes.
|
|
6
|
+
*
|
|
7
|
+
* Like a fairground ride queue:
|
|
8
|
+
* - VUs enter the queue and wait
|
|
9
|
+
* - When enough VUs are waiting (release count), they all proceed together
|
|
10
|
+
* - If timeout expires, waiting VUs are released regardless of count
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
export interface RendezvousConfig {
|
|
14
|
+
name: string;
|
|
15
|
+
count: number;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
releasePolicy?: 'all' | 'count';
|
|
18
|
+
}
|
|
19
|
+
export declare class RendezvousManager extends EventEmitter {
|
|
20
|
+
private static instance;
|
|
21
|
+
private rendezvousPoints;
|
|
22
|
+
private isActive;
|
|
23
|
+
private constructor();
|
|
24
|
+
static getInstance(): RendezvousManager;
|
|
25
|
+
/**
|
|
26
|
+
* Reset the manager for a new test run
|
|
27
|
+
*/
|
|
28
|
+
reset(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Stop the manager and release all waiting VUs
|
|
31
|
+
*/
|
|
32
|
+
stop(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Wait at a rendezvous point
|
|
35
|
+
* Returns when either:
|
|
36
|
+
* - The required number of VUs are waiting (synchronized release)
|
|
37
|
+
* - The timeout expires
|
|
38
|
+
* - The test is stopped
|
|
39
|
+
*/
|
|
40
|
+
wait(config: RendezvousConfig, vuId: number): Promise<RendezvousResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Release VUs from a rendezvous point
|
|
43
|
+
*/
|
|
44
|
+
private releaseVUs;
|
|
45
|
+
/**
|
|
46
|
+
* Force release all waiting VUs (used during shutdown)
|
|
47
|
+
*/
|
|
48
|
+
private releaseWaitingVUs;
|
|
49
|
+
/**
|
|
50
|
+
* Get statistics for a rendezvous point
|
|
51
|
+
*/
|
|
52
|
+
getStats(name: string): RendezvousStats | null;
|
|
53
|
+
/**
|
|
54
|
+
* Get all rendezvous statistics
|
|
55
|
+
*/
|
|
56
|
+
getAllStats(): RendezvousStats[];
|
|
57
|
+
}
|
|
58
|
+
export interface RendezvousResult {
|
|
59
|
+
released: boolean;
|
|
60
|
+
reason: 'count_reached' | 'timeout' | 'manager_inactive' | 'error';
|
|
61
|
+
waitTime: number;
|
|
62
|
+
vuCount: number;
|
|
63
|
+
}
|
|
64
|
+
export interface RendezvousStats {
|
|
65
|
+
name: string;
|
|
66
|
+
requiredCount: number;
|
|
67
|
+
currentlyWaiting: number;
|
|
68
|
+
totalReleased: number;
|
|
69
|
+
timeout: number;
|
|
70
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rendezvous Manager for synchronized VU coordination
|
|
4
|
+
*
|
|
5
|
+
* Rendezvous points allow Virtual Users to wait for each other at specific
|
|
6
|
+
* points in the test execution, creating coordinated load spikes.
|
|
7
|
+
*
|
|
8
|
+
* Like a fairground ride queue:
|
|
9
|
+
* - VUs enter the queue and wait
|
|
10
|
+
* - When enough VUs are waiting (release count), they all proceed together
|
|
11
|
+
* - If timeout expires, waiting VUs are released regardless of count
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.RendezvousManager = void 0;
|
|
15
|
+
const logger_1 = require("../utils/logger");
|
|
16
|
+
const events_1 = require("events");
|
|
17
|
+
class RendezvousManager extends events_1.EventEmitter {
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.rendezvousPoints = new Map();
|
|
21
|
+
this.isActive = true;
|
|
22
|
+
}
|
|
23
|
+
static getInstance() {
|
|
24
|
+
if (!RendezvousManager.instance) {
|
|
25
|
+
RendezvousManager.instance = new RendezvousManager();
|
|
26
|
+
}
|
|
27
|
+
return RendezvousManager.instance;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reset the manager for a new test run
|
|
31
|
+
*/
|
|
32
|
+
reset() {
|
|
33
|
+
// Clear all timeout handles
|
|
34
|
+
for (const point of this.rendezvousPoints.values()) {
|
|
35
|
+
if (point.timeoutHandle) {
|
|
36
|
+
clearTimeout(point.timeoutHandle);
|
|
37
|
+
}
|
|
38
|
+
// Release any waiting VUs
|
|
39
|
+
for (const vu of point.waiting) {
|
|
40
|
+
vu.reject(new Error('Rendezvous manager reset'));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.rendezvousPoints.clear();
|
|
44
|
+
this.isActive = true;
|
|
45
|
+
logger_1.logger.debug('RendezvousManager reset');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop the manager and release all waiting VUs
|
|
49
|
+
*/
|
|
50
|
+
stop() {
|
|
51
|
+
this.isActive = false;
|
|
52
|
+
for (const point of this.rendezvousPoints.values()) {
|
|
53
|
+
if (point.timeoutHandle) {
|
|
54
|
+
clearTimeout(point.timeoutHandle);
|
|
55
|
+
}
|
|
56
|
+
// Release all waiting VUs
|
|
57
|
+
this.releaseWaitingVUs(point, 'Test stopped');
|
|
58
|
+
}
|
|
59
|
+
logger_1.logger.debug('RendezvousManager stopped');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Wait at a rendezvous point
|
|
63
|
+
* Returns when either:
|
|
64
|
+
* - The required number of VUs are waiting (synchronized release)
|
|
65
|
+
* - The timeout expires
|
|
66
|
+
* - The test is stopped
|
|
67
|
+
*/
|
|
68
|
+
async wait(config, vuId) {
|
|
69
|
+
if (!this.isActive) {
|
|
70
|
+
return {
|
|
71
|
+
released: true,
|
|
72
|
+
reason: 'manager_inactive',
|
|
73
|
+
waitTime: 0,
|
|
74
|
+
vuCount: 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const startTime = Date.now();
|
|
78
|
+
const pointName = config.name;
|
|
79
|
+
// Get or create rendezvous point
|
|
80
|
+
let point = this.rendezvousPoints.get(pointName);
|
|
81
|
+
if (!point) {
|
|
82
|
+
point = {
|
|
83
|
+
config: {
|
|
84
|
+
...config,
|
|
85
|
+
timeout: config.timeout ?? 30000,
|
|
86
|
+
releasePolicy: config.releasePolicy ?? 'all'
|
|
87
|
+
},
|
|
88
|
+
waiting: [],
|
|
89
|
+
releasedCount: 0
|
|
90
|
+
};
|
|
91
|
+
this.rendezvousPoints.set(pointName, point);
|
|
92
|
+
logger_1.logger.debug(`Rendezvous point '${pointName}' created (count: ${config.count}, timeout: ${point.config.timeout}ms)`);
|
|
93
|
+
}
|
|
94
|
+
logger_1.logger.debug(`VU${vuId} arriving at rendezvous '${pointName}' (${point.waiting.length + 1}/${config.count})`);
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const waitingVU = {
|
|
97
|
+
vuId,
|
|
98
|
+
resolve: () => {
|
|
99
|
+
const waitTime = Date.now() - startTime;
|
|
100
|
+
resolve({
|
|
101
|
+
released: true,
|
|
102
|
+
reason: 'count_reached',
|
|
103
|
+
waitTime,
|
|
104
|
+
vuCount: point.waiting.length + 1
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
reject,
|
|
108
|
+
arrivalTime: startTime
|
|
109
|
+
};
|
|
110
|
+
point.waiting.push(waitingVU);
|
|
111
|
+
// Emit event for monitoring
|
|
112
|
+
this.emit('vu_arrived', {
|
|
113
|
+
rendezvousName: pointName,
|
|
114
|
+
vuId,
|
|
115
|
+
waitingCount: point.waiting.length,
|
|
116
|
+
requiredCount: config.count
|
|
117
|
+
});
|
|
118
|
+
// Check if we've reached the required count
|
|
119
|
+
if (point.waiting.length >= config.count) {
|
|
120
|
+
logger_1.logger.debug(`Rendezvous '${pointName}' reached count (${point.waiting.length}/${config.count}) - releasing VUs`);
|
|
121
|
+
this.releaseVUs(point);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Set up timeout if not already set and timeout > 0
|
|
125
|
+
if (!point.timeoutHandle && point.config.timeout && point.config.timeout > 0) {
|
|
126
|
+
point.timeoutHandle = setTimeout(() => {
|
|
127
|
+
if (point.waiting.length > 0) {
|
|
128
|
+
logger_1.logger.debug(`Rendezvous '${pointName}' timeout - releasing ${point.waiting.length} VUs`);
|
|
129
|
+
this.releaseVUs(point, 'timeout');
|
|
130
|
+
}
|
|
131
|
+
}, point.config.timeout);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Release VUs from a rendezvous point
|
|
137
|
+
*/
|
|
138
|
+
releaseVUs(point, reason = 'count_reached') {
|
|
139
|
+
// Clear timeout
|
|
140
|
+
if (point.timeoutHandle) {
|
|
141
|
+
clearTimeout(point.timeoutHandle);
|
|
142
|
+
point.timeoutHandle = undefined;
|
|
143
|
+
}
|
|
144
|
+
const toRelease = point.config.releasePolicy === 'count'
|
|
145
|
+
? point.waiting.splice(0, point.config.count)
|
|
146
|
+
: point.waiting.splice(0);
|
|
147
|
+
const releaseTime = Date.now();
|
|
148
|
+
for (const vu of toRelease) {
|
|
149
|
+
const waitTime = releaseTime - vu.arrivalTime;
|
|
150
|
+
point.releasedCount++;
|
|
151
|
+
// Resolve the promise
|
|
152
|
+
vu.resolve();
|
|
153
|
+
logger_1.logger.debug(`VU${vu.vuId} released from rendezvous '${point.config.name}' (waited ${waitTime}ms, reason: ${reason})`);
|
|
154
|
+
}
|
|
155
|
+
// Emit release event
|
|
156
|
+
this.emit('vus_released', {
|
|
157
|
+
rendezvousName: point.config.name,
|
|
158
|
+
releasedCount: toRelease.length,
|
|
159
|
+
reason
|
|
160
|
+
});
|
|
161
|
+
// Reset timeout for next batch if there are remaining VUs
|
|
162
|
+
if (point.waiting.length > 0 && point.config.timeout && point.config.timeout > 0) {
|
|
163
|
+
point.timeoutHandle = setTimeout(() => {
|
|
164
|
+
if (point.waiting.length > 0) {
|
|
165
|
+
this.releaseVUs(point, 'timeout');
|
|
166
|
+
}
|
|
167
|
+
}, point.config.timeout);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Force release all waiting VUs (used during shutdown)
|
|
172
|
+
*/
|
|
173
|
+
releaseWaitingVUs(point, reason) {
|
|
174
|
+
const toRelease = point.waiting.splice(0);
|
|
175
|
+
for (const vu of toRelease) {
|
|
176
|
+
vu.resolve();
|
|
177
|
+
}
|
|
178
|
+
this.emit('vus_released', {
|
|
179
|
+
rendezvousName: point.config.name,
|
|
180
|
+
releasedCount: toRelease.length,
|
|
181
|
+
reason
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get statistics for a rendezvous point
|
|
186
|
+
*/
|
|
187
|
+
getStats(name) {
|
|
188
|
+
const point = this.rendezvousPoints.get(name);
|
|
189
|
+
if (!point)
|
|
190
|
+
return null;
|
|
191
|
+
return {
|
|
192
|
+
name: point.config.name,
|
|
193
|
+
requiredCount: point.config.count,
|
|
194
|
+
currentlyWaiting: point.waiting.length,
|
|
195
|
+
totalReleased: point.releasedCount,
|
|
196
|
+
timeout: point.config.timeout ?? 0
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get all rendezvous statistics
|
|
201
|
+
*/
|
|
202
|
+
getAllStats() {
|
|
203
|
+
const stats = [];
|
|
204
|
+
for (const point of this.rendezvousPoints.values()) {
|
|
205
|
+
stats.push({
|
|
206
|
+
name: point.config.name,
|
|
207
|
+
requiredCount: point.config.count,
|
|
208
|
+
currentlyWaiting: point.waiting.length,
|
|
209
|
+
totalReleased: point.releasedCount,
|
|
210
|
+
timeout: point.config.timeout ?? 0
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return stats;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
exports.RendezvousManager = RendezvousManager;
|
|
@@ -37,6 +37,7 @@ exports.StepExecutor = void 0;
|
|
|
37
37
|
const hooks_manager_1 = require("./hooks-manager");
|
|
38
38
|
const script_executor_1 = require("./script-executor");
|
|
39
39
|
const threshold_evaluator_1 = require("./threshold-evaluator");
|
|
40
|
+
const rendezvous_1 = require("./rendezvous");
|
|
40
41
|
const time_1 = require("../utils/time");
|
|
41
42
|
const template_1 = require("../utils/template");
|
|
42
43
|
const logger_1 = require("../utils/logger");
|
|
@@ -205,6 +206,9 @@ class StepExecutor {
|
|
|
205
206
|
case 'script':
|
|
206
207
|
result = await this.executeScriptStep(processedStep, context);
|
|
207
208
|
break;
|
|
209
|
+
case 'rendezvous':
|
|
210
|
+
result = await this.executeRendezvousStep(processedStep, context);
|
|
211
|
+
break;
|
|
208
212
|
default:
|
|
209
213
|
throw new Error(`Unsupported step type: ${step.type}`);
|
|
210
214
|
}
|
|
@@ -443,6 +447,54 @@ class StepExecutor {
|
|
|
443
447
|
custom_metrics: { wait_duration: duration }
|
|
444
448
|
};
|
|
445
449
|
}
|
|
450
|
+
async executeRendezvousStep(step, context) {
|
|
451
|
+
const rendezvousManager = rendezvous_1.RendezvousManager.getInstance();
|
|
452
|
+
// Parse timeout - can be number (ms) or string ("30s", "1m")
|
|
453
|
+
let timeoutMs = 30000; // Default 30 seconds
|
|
454
|
+
if (step.timeout !== undefined) {
|
|
455
|
+
if (typeof step.timeout === 'number') {
|
|
456
|
+
timeoutMs = step.timeout;
|
|
457
|
+
}
|
|
458
|
+
else if (typeof step.timeout === 'string') {
|
|
459
|
+
timeoutMs = (0, time_1.parseTime)(step.timeout);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const result = await rendezvousManager.wait({
|
|
464
|
+
name: step.rendezvous,
|
|
465
|
+
count: step.count,
|
|
466
|
+
timeout: timeoutMs,
|
|
467
|
+
releasePolicy: step.policy || 'all'
|
|
468
|
+
}, context.vu_id);
|
|
469
|
+
return {
|
|
470
|
+
success: true,
|
|
471
|
+
data: {
|
|
472
|
+
rendezvous: step.rendezvous,
|
|
473
|
+
released: result.released,
|
|
474
|
+
reason: result.reason,
|
|
475
|
+
vuCount: result.vuCount
|
|
476
|
+
},
|
|
477
|
+
response_time: result.waitTime,
|
|
478
|
+
custom_metrics: {
|
|
479
|
+
rendezvous_name: step.rendezvous,
|
|
480
|
+
rendezvous_wait_time: result.waitTime,
|
|
481
|
+
rendezvous_reason: result.reason,
|
|
482
|
+
rendezvous_vu_count: result.vuCount
|
|
483
|
+
},
|
|
484
|
+
shouldRecord: true // Always record rendezvous timing
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
error: error.message,
|
|
491
|
+
custom_metrics: {
|
|
492
|
+
rendezvous_name: step.rendezvous,
|
|
493
|
+
rendezvous_error: true
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
446
498
|
async executeScriptStep(step, context) {
|
|
447
499
|
const { file, function: funcName, params, returns, timeout = 30000 } = step;
|
|
448
500
|
const path = require('path');
|
package/dist/core/test-runner.js
CHANGED
|
@@ -50,6 +50,7 @@ const webhook_1 = require("../outputs/webhook");
|
|
|
50
50
|
const logger_1 = require("../utils/logger");
|
|
51
51
|
const time_1 = require("../utils/time");
|
|
52
52
|
const csv_data_provider_1 = require("./csv-data-provider");
|
|
53
|
+
const rendezvous_1 = require("./rendezvous");
|
|
53
54
|
const file_manager_1 = require("../utils/file-manager");
|
|
54
55
|
class TestRunner {
|
|
55
56
|
constructor(config) {
|
|
@@ -83,6 +84,8 @@ class TestRunner {
|
|
|
83
84
|
logger_1.logger.info(`🚀 Starting test: ${this.config.name}`);
|
|
84
85
|
this.isRunning = true;
|
|
85
86
|
this.startTime = Date.now();
|
|
87
|
+
// Reset rendezvous manager for this test run
|
|
88
|
+
rendezvous_1.RendezvousManager.getInstance().reset();
|
|
86
89
|
try {
|
|
87
90
|
await this.initialize();
|
|
88
91
|
// NO CSV termination callback setup needed anymore
|
|
@@ -116,6 +119,8 @@ class TestRunner {
|
|
|
116
119
|
async stop() {
|
|
117
120
|
logger_1.logger.info('⏹️ Stopping test...');
|
|
118
121
|
this.isRunning = false;
|
|
122
|
+
// Stop rendezvous manager - releases any waiting VUs
|
|
123
|
+
rendezvous_1.RendezvousManager.getInstance().stop();
|
|
119
124
|
// Stop all active VUs
|
|
120
125
|
this.activeVUs.forEach(vu => vu.stop());
|
|
121
126
|
// Wait for VUs to finish current operations
|
|
@@ -35,6 +35,15 @@ export declare class VirtualUser {
|
|
|
35
35
|
executeScenario(scenario: Scenario): Promise<void>;
|
|
36
36
|
private loadCSVDataIfNeeded;
|
|
37
37
|
private loadCSVDataForScenario;
|
|
38
|
+
/**
|
|
39
|
+
* Select scenarios based on weights using proportional distribution.
|
|
40
|
+
*
|
|
41
|
+
* Weights determine the probability of a scenario being selected:
|
|
42
|
+
* - scenario1 (weight: 50) + scenario2 (weight: 25) + scenario3 (weight: 25) = 100
|
|
43
|
+
* - 50% of VUs will run scenario1, 25% scenario2, 25% scenario3
|
|
44
|
+
*
|
|
45
|
+
* If weights don't sum to 100, they are normalized proportionally.
|
|
46
|
+
*/
|
|
38
47
|
private selectScenarios;
|
|
39
48
|
private executeSetup;
|
|
40
49
|
private executeTeardown;
|
|
@@ -366,20 +366,36 @@ class VirtualUser {
|
|
|
366
366
|
return await provider.getNextRow(this.id);
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Select scenarios based on weights using proportional distribution.
|
|
371
|
+
*
|
|
372
|
+
* Weights determine the probability of a scenario being selected:
|
|
373
|
+
* - scenario1 (weight: 50) + scenario2 (weight: 25) + scenario3 (weight: 25) = 100
|
|
374
|
+
* - 50% of VUs will run scenario1, 25% scenario2, 25% scenario3
|
|
375
|
+
*
|
|
376
|
+
* If weights don't sum to 100, they are normalized proportionally.
|
|
377
|
+
*/
|
|
369
378
|
selectScenarios(scenarios) {
|
|
370
|
-
|
|
379
|
+
if (scenarios.length === 0) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
if (scenarios.length === 1) {
|
|
383
|
+
return scenarios;
|
|
384
|
+
}
|
|
385
|
+
// Calculate total weight
|
|
386
|
+
const totalWeight = scenarios.reduce((sum, s) => sum + (s.weight ?? 100), 0);
|
|
387
|
+
// Generate random value between 0 and totalWeight
|
|
388
|
+
const random = Math.random() * totalWeight;
|
|
389
|
+
// Select scenario based on cumulative weight
|
|
390
|
+
let cumulative = 0;
|
|
371
391
|
for (const scenario of scenarios) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
selected.push(scenario);
|
|
392
|
+
cumulative += (scenario.weight ?? 100);
|
|
393
|
+
if (random < cumulative) {
|
|
394
|
+
return [scenario];
|
|
376
395
|
}
|
|
377
396
|
}
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
selected.push(scenarios[0]);
|
|
381
|
-
}
|
|
382
|
-
return selected;
|
|
397
|
+
// Fallback to last scenario (should not reach here)
|
|
398
|
+
return [scenarios[scenarios.length - 1]];
|
|
383
399
|
}
|
|
384
400
|
async executeSetup(setupScript) {
|
|
385
401
|
try {
|
|
@@ -27,6 +27,24 @@ export declare class ScenarioBuilder {
|
|
|
27
27
|
expectText(selector: string, text: string, name?: string): this;
|
|
28
28
|
expectNotVisible(selector: string, name?: string): this;
|
|
29
29
|
wait(duration: string | number): this;
|
|
30
|
+
/**
|
|
31
|
+
* Add a rendezvous point to synchronize Virtual Users
|
|
32
|
+
*
|
|
33
|
+
* @param name - Unique name for this rendezvous point
|
|
34
|
+
* @param count - Number of VUs to wait for before releasing
|
|
35
|
+
* @param options - Optional configuration (timeout, policy)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* scenario('Flash Sale')
|
|
39
|
+
* .post('/cart/add', { productId: 'LIMITED-001' })
|
|
40
|
+
* .rendezvous('checkout_sync', 10, { timeout: '30s' })
|
|
41
|
+
* .post('/checkout', { paymentMethod: 'card' })
|
|
42
|
+
* .done()
|
|
43
|
+
*/
|
|
44
|
+
rendezvous(name: string, count: number, options?: {
|
|
45
|
+
timeout?: string | number;
|
|
46
|
+
policy?: 'all' | 'count';
|
|
47
|
+
}): this;
|
|
30
48
|
get(path: string, options?: any): this;
|
|
31
49
|
post(path: string, body?: any, options?: any): this;
|
|
32
50
|
put(path: string, body?: any, options?: any): this;
|
package/dist/dsl/test-builder.js
CHANGED
|
@@ -163,6 +163,30 @@ class ScenarioBuilder {
|
|
|
163
163
|
duration: durationStr
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Add a rendezvous point to synchronize Virtual Users
|
|
168
|
+
*
|
|
169
|
+
* @param name - Unique name for this rendezvous point
|
|
170
|
+
* @param count - Number of VUs to wait for before releasing
|
|
171
|
+
* @param options - Optional configuration (timeout, policy)
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* scenario('Flash Sale')
|
|
175
|
+
* .post('/cart/add', { productId: 'LIMITED-001' })
|
|
176
|
+
* .rendezvous('checkout_sync', 10, { timeout: '30s' })
|
|
177
|
+
* .post('/checkout', { paymentMethod: 'card' })
|
|
178
|
+
* .done()
|
|
179
|
+
*/
|
|
180
|
+
rendezvous(name, count, options) {
|
|
181
|
+
return this.addStep({
|
|
182
|
+
name: `Rendezvous: ${name}`,
|
|
183
|
+
type: 'rendezvous',
|
|
184
|
+
rendezvous: name,
|
|
185
|
+
count,
|
|
186
|
+
...(options?.timeout && { timeout: options.timeout }),
|
|
187
|
+
...(options?.policy && { policy: options.policy })
|
|
188
|
+
});
|
|
189
|
+
}
|
|
166
190
|
// REST API step methods
|
|
167
191
|
get(path, options) {
|
|
168
192
|
return this.request('GET', path, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testsmith/perfornium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Flexible performance testing framework for REST, SOAP, and web applications",
|
|
5
5
|
"author": "TestSmith",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,15 @@
|
|
|
48
48
|
"dev": "tsc --watch",
|
|
49
49
|
"dev:link": "npm run build && npm link",
|
|
50
50
|
"dev:test": "npm run build && node dist/cli/cli.js",
|
|
51
|
-
"report:web-vitals": "node generate-web-vitals-report.js"
|
|
51
|
+
"report:web-vitals": "node generate-web-vitals-report.js",
|
|
52
|
+
"docker:build:controller": "docker build -t perfornium/controller -f docker/Dockerfile.controller .",
|
|
53
|
+
"docker:build:worker": "docker build -t perfornium/worker -f docker/Dockerfile.worker .",
|
|
54
|
+
"docker:build:worker-slim": "docker build -t perfornium/worker-slim -f docker/Dockerfile.worker-slim .",
|
|
55
|
+
"docker:build:all": "npm run docker:build:controller && npm run docker:build:worker && npm run docker:build:worker-slim",
|
|
56
|
+
"docker:up": "docker compose -f docker/docker-compose.yml up -d worker-1 worker-2 worker-3",
|
|
57
|
+
"docker:up:slim": "docker compose -f docker/docker-compose.slim.yml up -d worker-1 worker-2 worker-3",
|
|
58
|
+
"docker:down": "docker compose -f docker/docker-compose.yml down",
|
|
59
|
+
"docker:logs": "docker compose -f docker/docker-compose.yml logs -f"
|
|
52
60
|
},
|
|
53
61
|
"keywords": [
|
|
54
62
|
"performance",
|