@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,80 @@
|
|
|
1
|
+
import { FakerConfig } from '../config/types';
|
|
2
|
+
export interface TemplateConfig {
|
|
3
|
+
file: string;
|
|
4
|
+
data?: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
export declare class TemplateProcessor {
|
|
7
|
+
private static templateCache;
|
|
8
|
+
private static handlebarHelpersRegistered;
|
|
9
|
+
private static baseDir;
|
|
10
|
+
private get fakerInstance();
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Configure faker settings globally (affects all instances)
|
|
14
|
+
*/
|
|
15
|
+
configureFaker(config: FakerConfig): void;
|
|
16
|
+
/**
|
|
17
|
+
* Set base directory for template files
|
|
18
|
+
*/
|
|
19
|
+
static setBaseDir(dir: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Ensure Handlebars helpers are registered (only once)
|
|
22
|
+
*/
|
|
23
|
+
private ensureHandlebarsHelpers;
|
|
24
|
+
/**
|
|
25
|
+
* Set seed for reproducible data
|
|
26
|
+
*/
|
|
27
|
+
setSeed(seed: number): void;
|
|
28
|
+
/**
|
|
29
|
+
* Get available locales
|
|
30
|
+
*/
|
|
31
|
+
getAvailableLocales(): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Process template string with faker support (maintains backward compatibility)
|
|
34
|
+
*/
|
|
35
|
+
process(template: string, context: Record<string, any>): string;
|
|
36
|
+
/**
|
|
37
|
+
* Process faker expressions and variables in a string
|
|
38
|
+
*/
|
|
39
|
+
private processFakerAndVariables;
|
|
40
|
+
/**
|
|
41
|
+
* Evaluate faker expressions like faker.person.firstName or faker.person.firstName()
|
|
42
|
+
*/
|
|
43
|
+
private evaluateFakerExpression;
|
|
44
|
+
/**
|
|
45
|
+
* Load and compile a Handlebars template file
|
|
46
|
+
*/
|
|
47
|
+
private loadTemplate;
|
|
48
|
+
/**
|
|
49
|
+
* Process Handlebars template specification
|
|
50
|
+
*/
|
|
51
|
+
private processHandlebarsTemplate;
|
|
52
|
+
/**
|
|
53
|
+
* Process CSV data specification
|
|
54
|
+
*/
|
|
55
|
+
private processCSVData;
|
|
56
|
+
/**
|
|
57
|
+
* Synchronous CSV data processing (cached approach)
|
|
58
|
+
*/
|
|
59
|
+
private processCSVDataSync;
|
|
60
|
+
/**
|
|
61
|
+
* Process a template with data
|
|
62
|
+
*/
|
|
63
|
+
private processTemplate;
|
|
64
|
+
/**
|
|
65
|
+
* Clear template cache (useful for development)
|
|
66
|
+
*/
|
|
67
|
+
static clearCache(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Evaluate helper functions like randomInt(1, 100)
|
|
70
|
+
*/
|
|
71
|
+
private evaluateHelperFunction;
|
|
72
|
+
/**
|
|
73
|
+
* Parse function arguments
|
|
74
|
+
*/
|
|
75
|
+
private parseArguments;
|
|
76
|
+
/**
|
|
77
|
+
* Get nested property from object
|
|
78
|
+
*/
|
|
79
|
+
private getNestedProperty;
|
|
80
|
+
}
|
|
@@ -0,0 +1,513 @@
|
|
|
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.TemplateProcessor = void 0;
|
|
37
|
+
const csv_data_provider_1 = require("../core/csv-data-provider");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const Handlebars = __importStar(require("handlebars"));
|
|
41
|
+
const logger_1 = require("./logger");
|
|
42
|
+
const timestamp_helper_1 = require("./timestamp-helper");
|
|
43
|
+
const faker_manager_1 = require("./faker-manager");
|
|
44
|
+
class TemplateProcessor {
|
|
45
|
+
// Instance gets faker from the lazy-loading manager
|
|
46
|
+
get fakerInstance() {
|
|
47
|
+
return faker_manager_1.fakerManager.getFaker();
|
|
48
|
+
}
|
|
49
|
+
constructor() {
|
|
50
|
+
this.ensureHandlebarsHelpers();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Configure faker settings globally (affects all instances)
|
|
54
|
+
*/
|
|
55
|
+
configureFaker(config) {
|
|
56
|
+
logger_1.logger.debug(`configureFaker called with: ${JSON.stringify(config)}`);
|
|
57
|
+
logger_1.logger.debug(`Current global locale: ${faker_manager_1.fakerManager.currentLocale}`);
|
|
58
|
+
const availableLocales = faker_manager_1.fakerManager.getAvailableLocales();
|
|
59
|
+
if (config.locale && availableLocales.includes(config.locale)) {
|
|
60
|
+
faker_manager_1.fakerManager.setLocale(config.locale);
|
|
61
|
+
logger_1.logger.debug(`Global faker locale set to: ${config.locale}`);
|
|
62
|
+
}
|
|
63
|
+
else if (config.locale) {
|
|
64
|
+
logger_1.logger.warn(`Locale "${config.locale}" not available. Available: ${availableLocales.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
if (config.seed !== undefined) {
|
|
67
|
+
faker_manager_1.fakerManager.setSeed(config.seed);
|
|
68
|
+
logger_1.logger.debug(`Global seed set to: ${config.seed}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Set base directory for template files
|
|
73
|
+
*/
|
|
74
|
+
static setBaseDir(dir) {
|
|
75
|
+
TemplateProcessor.baseDir = dir;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Ensure Handlebars helpers are registered (only once)
|
|
79
|
+
*/
|
|
80
|
+
ensureHandlebarsHelpers() {
|
|
81
|
+
if (TemplateProcessor.handlebarHelpersRegistered) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Faker helpers
|
|
85
|
+
Handlebars.registerHelper('faker', (path) => {
|
|
86
|
+
return this.getNestedProperty(this.fakerInstance, path)();
|
|
87
|
+
});
|
|
88
|
+
// Random helpers
|
|
89
|
+
Handlebars.registerHelper('randomInt', (min, max) => {
|
|
90
|
+
return this.fakerInstance.number.int({ min, max });
|
|
91
|
+
});
|
|
92
|
+
Handlebars.registerHelper('randomChoice', (...args) => {
|
|
93
|
+
// Remove the last argument (Handlebars options)
|
|
94
|
+
const choices = args.slice(0, -1);
|
|
95
|
+
return this.fakerInstance.helpers.arrayElement(choices);
|
|
96
|
+
});
|
|
97
|
+
Handlebars.registerHelper('uuid', () => {
|
|
98
|
+
return this.fakerInstance.string.uuid();
|
|
99
|
+
});
|
|
100
|
+
Handlebars.registerHelper('timestamp', (format) => {
|
|
101
|
+
const fmt = (format || 'unix');
|
|
102
|
+
return timestamp_helper_1.TimestampHelper.getTimestamp(fmt);
|
|
103
|
+
});
|
|
104
|
+
Handlebars.registerHelper('isoDate', (days = 0) => {
|
|
105
|
+
const date = new Date();
|
|
106
|
+
date.setDate(date.getDate() + days);
|
|
107
|
+
return date.toISOString();
|
|
108
|
+
});
|
|
109
|
+
// Conditional helpers
|
|
110
|
+
Handlebars.registerHelper('eq', (a, b) => a === b);
|
|
111
|
+
Handlebars.registerHelper('ne', (a, b) => a !== b);
|
|
112
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
113
|
+
Handlebars.registerHelper('lt', (a, b) => a < b);
|
|
114
|
+
// Array helpers
|
|
115
|
+
Handlebars.registerHelper('length', (array) => array ? array.length : 0);
|
|
116
|
+
Handlebars.registerHelper('first', (array) => array && array.length > 0 ? array[0] : null);
|
|
117
|
+
Handlebars.registerHelper('last', (array) => array && array.length > 0 ? array[array.length - 1] : null);
|
|
118
|
+
TemplateProcessor.handlebarHelpersRegistered = true;
|
|
119
|
+
logger_1.logger.debug('Handlebars helpers registered');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Set seed for reproducible data
|
|
123
|
+
*/
|
|
124
|
+
setSeed(seed) {
|
|
125
|
+
this.fakerInstance.seed(seed);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get available locales
|
|
129
|
+
*/
|
|
130
|
+
getAvailableLocales() {
|
|
131
|
+
return faker_manager_1.fakerManager.getAvailableLocales();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Process template string with faker support (maintains backward compatibility)
|
|
135
|
+
*/
|
|
136
|
+
process(template, context) {
|
|
137
|
+
// ALWAYS set a unique seed before processing ANY templates
|
|
138
|
+
const vuId = context.vu_id || context.__VU || 1;
|
|
139
|
+
const iteration = context.iteration || context.__ITER || 0;
|
|
140
|
+
// Create a truly unique seed for each call
|
|
141
|
+
const timestamp = Date.now();
|
|
142
|
+
const randomComponent = Math.floor(Math.random() * 10000);
|
|
143
|
+
const uniqueSeed = timestamp + (vuId * 100000) + (iteration * 1000) + randomComponent;
|
|
144
|
+
this.fakerInstance.seed(uniqueSeed);
|
|
145
|
+
// Test the seed immediately
|
|
146
|
+
const testName = this.fakerInstance.person.firstName();
|
|
147
|
+
logger_1.logger.debug(`VU${vuId} Iter${iteration}: Seed ${uniqueSeed} (locale: ${faker_manager_1.fakerManager.currentLocale}) -> Test: "${testName}"`);
|
|
148
|
+
// Reset the seed again (in case the test call changed the state)
|
|
149
|
+
this.fakerInstance.seed(uniqueSeed);
|
|
150
|
+
let result = template;
|
|
151
|
+
// Helper functions available in templates
|
|
152
|
+
const helpers = {
|
|
153
|
+
randomInt: (min = 1, max = 100) => this.fakerInstance.number.int({ min, max }),
|
|
154
|
+
randomFloat: (min = 0, max = 1, fractionDigits = 2) => this.fakerInstance.number.float({ min, max, fractionDigits }),
|
|
155
|
+
randomChoice: (...choices) => this.fakerInstance.helpers.arrayElement(choices),
|
|
156
|
+
uuid: () => this.fakerInstance.string.uuid(),
|
|
157
|
+
isoDate: (days = 0) => {
|
|
158
|
+
const date = new Date();
|
|
159
|
+
date.setDate(date.getDate() + days);
|
|
160
|
+
return date.toISOString();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
// FIRST: Process all faker expressions and context variables to resolve them
|
|
164
|
+
const processedVariables = { ...context.variables };
|
|
165
|
+
const processedExtractedData = { ...context.extracted_data };
|
|
166
|
+
// Process faker expressions in variables
|
|
167
|
+
for (const [key, value] of Object.entries(processedVariables)) {
|
|
168
|
+
if (typeof value === 'string') {
|
|
169
|
+
processedVariables[key] = this.processFakerAndVariables(value, context, helpers);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Process faker expressions in extracted data
|
|
173
|
+
for (const [key, value] of Object.entries(processedExtractedData)) {
|
|
174
|
+
if (typeof value === 'string') {
|
|
175
|
+
processedExtractedData[key] = this.processFakerAndVariables(value, context, helpers);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Enhanced context with processed variables
|
|
179
|
+
const enhancedContext = {
|
|
180
|
+
...context,
|
|
181
|
+
faker: this.fakerInstance,
|
|
182
|
+
variables: processedVariables,
|
|
183
|
+
extracted_data: processedExtractedData,
|
|
184
|
+
...helpers
|
|
185
|
+
};
|
|
186
|
+
// Process CSV data {{csv:file.csv}} or {{csv:file.csv|mode=unique}}
|
|
187
|
+
result = result.replace(/\{\{csv:([^}]+)\}\}/g, (match, csvSpec) => {
|
|
188
|
+
try {
|
|
189
|
+
const value = this.processCSVData(csvSpec, enhancedContext);
|
|
190
|
+
logger_1.logger.debug(`VU${vuId} Iter${iteration}: CSV data ${csvSpec} processed`);
|
|
191
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
logger_1.logger.warn(`Failed to process CSV data "${csvSpec}":`, error);
|
|
195
|
+
return match;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Process Handlebars templates {{template:file.json}} with optional data
|
|
199
|
+
result = result.replace(/\{\{template:([^}]+)\}\}/g, (match, templateSpec) => {
|
|
200
|
+
try {
|
|
201
|
+
const value = this.processHandlebarsTemplate(templateSpec, enhancedContext);
|
|
202
|
+
logger_1.logger.debug(`VU${vuId} Iter${iteration}: Handlebars template ${templateSpec} processed`);
|
|
203
|
+
// The value is already a minified JSON string, return it directly
|
|
204
|
+
// No need to JSON.stringify again as that would double-escape
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
logger_1.logger.warn(`Failed to process Handlebars template "${templateSpec}":`, error);
|
|
209
|
+
return match;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// Process remaining templates with the processed context
|
|
213
|
+
result = this.processFakerAndVariables(result, enhancedContext, helpers);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Process faker expressions and variables in a string
|
|
218
|
+
*/
|
|
219
|
+
processFakerAndVariables(template, context, helpers) {
|
|
220
|
+
const vuId = context.vu_id || context.__VU || 1;
|
|
221
|
+
const iteration = context.iteration || context.__ITER || 0;
|
|
222
|
+
let result = template;
|
|
223
|
+
// Process environment variables {{env.VAR_NAME}}
|
|
224
|
+
result = result.replace(/\{\{env\.([^}]+)\}\}/g, (match, varName) => {
|
|
225
|
+
return process.env[varName] || '';
|
|
226
|
+
});
|
|
227
|
+
// Process faker expressions {{faker.person.firstName}} or {{faker.person.firstName()}}
|
|
228
|
+
result = result.replace(/\{\{(faker\.[^}]+)\}\}/g, (match, expression) => {
|
|
229
|
+
try {
|
|
230
|
+
const value = this.evaluateFakerExpression(expression);
|
|
231
|
+
logger_1.logger.debug(`VU${vuId} Iter${iteration}: ${expression} -> "${value}"`);
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
logger_1.logger.warn(`Failed to evaluate faker expression "${expression}":`, error);
|
|
236
|
+
return match;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// Process helper functions {{randomInt(1, 100)}}
|
|
240
|
+
result = result.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*\([^}]*\))\}/g, (match, expression) => {
|
|
241
|
+
try {
|
|
242
|
+
const value = this.evaluateHelperFunction(expression, helpers);
|
|
243
|
+
logger_1.logger.debug(`VU${vuId} Iter${iteration}: ${expression} -> "${value}"`);
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
logger_1.logger.warn(`Failed to evaluate helper function "${expression}":`, error);
|
|
248
|
+
return match;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// Process context variables {{variable_name}}
|
|
252
|
+
result = result.replace(/\{\{([^}]+)\}\}/g, (match, varName) => {
|
|
253
|
+
// Skip if it looks like it was already processed
|
|
254
|
+
if (varName.includes('(') || varName.startsWith('faker.') || varName.startsWith('template:')) {
|
|
255
|
+
return match;
|
|
256
|
+
}
|
|
257
|
+
const keys = varName.split('.');
|
|
258
|
+
let value = context;
|
|
259
|
+
for (const key of keys) {
|
|
260
|
+
if (value && typeof value === 'object' && key in value) {
|
|
261
|
+
value = value[key];
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
return match;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return String(value);
|
|
268
|
+
});
|
|
269
|
+
// Process special variables
|
|
270
|
+
result = result.replace(/\{\{__VU\}\}/g, () => String(context.__VU || 0));
|
|
271
|
+
result = result.replace(/\{\{__ITER\}\}/g, () => String(context.__ITER || 0));
|
|
272
|
+
// Enhanced timestamp replacement with file-safe format by default
|
|
273
|
+
result = result.replace(/\{\{timestamp(?::([^}]+))?\}\}/g, (_match, format) => {
|
|
274
|
+
const fmt = (format || 'file');
|
|
275
|
+
return timestamp_helper_1.TimestampHelper.getTimestamp(fmt);
|
|
276
|
+
});
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Evaluate faker expressions like faker.person.firstName or faker.person.firstName()
|
|
281
|
+
*/
|
|
282
|
+
evaluateFakerExpression(expression) {
|
|
283
|
+
// Remove 'faker.' prefix
|
|
284
|
+
const path = expression.replace(/^faker\./, '');
|
|
285
|
+
logger_1.logger.debug(`Evaluating faker expression: "${path}" (locale: ${faker_manager_1.fakerManager.currentLocale})`);
|
|
286
|
+
// Check if it's a function call
|
|
287
|
+
const funcMatch = path.match(/^(.+)\(\)$/);
|
|
288
|
+
if (funcMatch) {
|
|
289
|
+
// It's a function call like person.firstName()
|
|
290
|
+
const funcPath = funcMatch[1];
|
|
291
|
+
const func = this.getNestedProperty(this.fakerInstance, funcPath);
|
|
292
|
+
if (typeof func === 'function') {
|
|
293
|
+
const result = String(func());
|
|
294
|
+
logger_1.logger.debug(`Function call ${funcPath}() -> "${result}"`);
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// It's a property access like person.firstName (also call it as function)
|
|
300
|
+
const func = this.getNestedProperty(this.fakerInstance, path);
|
|
301
|
+
if (typeof func === 'function') {
|
|
302
|
+
const result = String(func());
|
|
303
|
+
logger_1.logger.debug(`Property access ${path} -> "${result}"`);
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`Faker method not found: ${expression}`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Load and compile a Handlebars template file
|
|
311
|
+
*/
|
|
312
|
+
loadTemplate(templatePath) {
|
|
313
|
+
const fullPath = path.resolve(TemplateProcessor.baseDir, templatePath);
|
|
314
|
+
if (TemplateProcessor.templateCache.has(fullPath)) {
|
|
315
|
+
return TemplateProcessor.templateCache.get(fullPath);
|
|
316
|
+
}
|
|
317
|
+
if (!fs.existsSync(fullPath)) {
|
|
318
|
+
throw new Error(`Template file not found: ${fullPath}`);
|
|
319
|
+
}
|
|
320
|
+
const templateContent = fs.readFileSync(fullPath, 'utf8');
|
|
321
|
+
const compiledTemplate = Handlebars.compile(templateContent);
|
|
322
|
+
TemplateProcessor.templateCache.set(fullPath, compiledTemplate);
|
|
323
|
+
logger_1.logger.debug(`Template compiled and cached: ${templatePath}`);
|
|
324
|
+
return compiledTemplate;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Process Handlebars template specification
|
|
328
|
+
*/
|
|
329
|
+
processHandlebarsTemplate(templateSpec, context) {
|
|
330
|
+
// Parse template specification: "file.json" or "file.json|data=value,key=value"
|
|
331
|
+
const [templateFile, dataSpec] = templateSpec.split('|');
|
|
332
|
+
const templateConfig = {
|
|
333
|
+
file: templateFile.trim(),
|
|
334
|
+
data: {}
|
|
335
|
+
};
|
|
336
|
+
// Parse data specification if provided
|
|
337
|
+
if (dataSpec) {
|
|
338
|
+
const dataPairs = dataSpec.split(',');
|
|
339
|
+
for (const pair of dataPairs) {
|
|
340
|
+
const [key, value] = pair.split('=').map(s => s.trim());
|
|
341
|
+
if (key && value) {
|
|
342
|
+
// Try to parse value as JSON, fallback to string
|
|
343
|
+
try {
|
|
344
|
+
templateConfig.data[key] = JSON.parse(value);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
templateConfig.data[key] = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return this.processTemplate(templateConfig, context);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Process CSV data specification
|
|
356
|
+
*/
|
|
357
|
+
processCSVData(csvSpec, context) {
|
|
358
|
+
// Parse CSV specification: "file.csv" or "file.csv|mode=unique,column=email"
|
|
359
|
+
const [csvFile, optionsSpec] = csvSpec.split('|');
|
|
360
|
+
const csvConfig = {
|
|
361
|
+
file: csvFile.trim(),
|
|
362
|
+
cycleOnExhaustion: true // Default to cycle
|
|
363
|
+
};
|
|
364
|
+
let mode = 'next'; // default mode
|
|
365
|
+
let column;
|
|
366
|
+
// Parse options if provided
|
|
367
|
+
if (optionsSpec) {
|
|
368
|
+
const options = optionsSpec.split(',');
|
|
369
|
+
for (const option of options) {
|
|
370
|
+
const [key, value] = option.split('=').map(s => s.trim());
|
|
371
|
+
if (key === 'mode') {
|
|
372
|
+
mode = value;
|
|
373
|
+
}
|
|
374
|
+
else if (key === 'column') {
|
|
375
|
+
column = value;
|
|
376
|
+
}
|
|
377
|
+
else if (key === 'delimiter') {
|
|
378
|
+
csvConfig.delimiter = value;
|
|
379
|
+
}
|
|
380
|
+
else if (key === 'filter') {
|
|
381
|
+
csvConfig.filter = value;
|
|
382
|
+
}
|
|
383
|
+
else if (key === 'randomize') {
|
|
384
|
+
csvConfig.randomize = value === 'true';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const csvProvider = csv_data_provider_1.CSVDataProvider.getInstance(csvConfig);
|
|
389
|
+
const vuId = context.vu_id || context.__VU || 1;
|
|
390
|
+
// This returns a Promise, but we need sync processing for string replacement
|
|
391
|
+
// We'll need to handle this differently - see the async version below
|
|
392
|
+
return this.processCSVDataSync(csvProvider, mode, column, vuId);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Synchronous CSV data processing (cached approach)
|
|
396
|
+
*/
|
|
397
|
+
processCSVDataSync(csvProvider, mode, column, vuId) {
|
|
398
|
+
// For now, return a placeholder that will be resolved later
|
|
399
|
+
// In a real implementation, you'd want to pre-load CSV data
|
|
400
|
+
return `CSV_DATA_${vuId}_${mode}_${column || 'all'}`;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Process a template with data
|
|
404
|
+
*/
|
|
405
|
+
processTemplate(templateConfig, context) {
|
|
406
|
+
const template = this.loadTemplate(templateConfig.file);
|
|
407
|
+
// Merge data sources: processed variables + extracted data + template data
|
|
408
|
+
const templateData = {
|
|
409
|
+
...context.variables, // These are now processed (faker expressions resolved)
|
|
410
|
+
...context.extracted_data, // These are now processed
|
|
411
|
+
...templateConfig.data, // Inline overrides
|
|
412
|
+
// Add context info
|
|
413
|
+
vu_id: context.vu_id,
|
|
414
|
+
iteration: context.iteration,
|
|
415
|
+
timestamp: Date.now(),
|
|
416
|
+
};
|
|
417
|
+
logger_1.logger.debug(`Processing Handlebars template: ${templateConfig.file}`);
|
|
418
|
+
const result = template(templateData);
|
|
419
|
+
logger_1.logger.debug(`Raw template result: ${result}`);
|
|
420
|
+
// If the result looks like JSON, minify it to remove line breaks
|
|
421
|
+
const trimmed = result.trim();
|
|
422
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
423
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
424
|
+
try {
|
|
425
|
+
// Parse and re-stringify to minify (remove whitespace/line breaks)
|
|
426
|
+
const parsed = JSON.parse(trimmed);
|
|
427
|
+
const minified = JSON.stringify(parsed);
|
|
428
|
+
logger_1.logger.debug(`Minified JSON result: ${minified}`);
|
|
429
|
+
// IMPORTANT: When returning JSON as a string to be embedded in another JSON,
|
|
430
|
+
// we need to escape it properly. JSON.stringify will handle the escaping.
|
|
431
|
+
const escaped = JSON.stringify(minified);
|
|
432
|
+
// Remove the outer quotes that JSON.stringify adds
|
|
433
|
+
const escapedContent = escaped.slice(1, -1);
|
|
434
|
+
logger_1.logger.debug(`Escaped for embedding: ${escapedContent}`);
|
|
435
|
+
return escapedContent;
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
logger_1.logger.warn(`Failed to minify JSON, returning original:`, error);
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// For non-JSON content, return as-is
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Clear template cache (useful for development)
|
|
447
|
+
*/
|
|
448
|
+
static clearCache() {
|
|
449
|
+
TemplateProcessor.templateCache.clear();
|
|
450
|
+
logger_1.logger.debug('Template cache cleared');
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Evaluate helper functions like randomInt(1, 100)
|
|
454
|
+
*/
|
|
455
|
+
evaluateHelperFunction(expression, helpers) {
|
|
456
|
+
const funcMatch = expression.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)$/);
|
|
457
|
+
if (!funcMatch) {
|
|
458
|
+
throw new Error(`Invalid function call: ${expression}`);
|
|
459
|
+
}
|
|
460
|
+
const [, funcName, argsStr] = funcMatch;
|
|
461
|
+
const func = helpers[funcName];
|
|
462
|
+
if (typeof func !== 'function') {
|
|
463
|
+
throw new Error(`Function not found: ${funcName}`);
|
|
464
|
+
}
|
|
465
|
+
// Parse arguments (simple implementation)
|
|
466
|
+
const args = this.parseArguments(argsStr);
|
|
467
|
+
return String(func(...args));
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Parse function arguments
|
|
471
|
+
*/
|
|
472
|
+
parseArguments(argsStr) {
|
|
473
|
+
if (!argsStr.trim()) {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
return argsStr.split(',').map(arg => {
|
|
477
|
+
const trimmed = arg.trim();
|
|
478
|
+
// String literal
|
|
479
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
480
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
481
|
+
return trimmed.slice(1, -1);
|
|
482
|
+
}
|
|
483
|
+
// Number
|
|
484
|
+
else if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
485
|
+
return parseFloat(trimmed);
|
|
486
|
+
}
|
|
487
|
+
// Boolean
|
|
488
|
+
else if (trimmed === 'true' || trimmed === 'false') {
|
|
489
|
+
return trimmed === 'true';
|
|
490
|
+
}
|
|
491
|
+
// Default to string
|
|
492
|
+
else {
|
|
493
|
+
return trimmed;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get nested property from object
|
|
499
|
+
*/
|
|
500
|
+
getNestedProperty(obj, path) {
|
|
501
|
+
return path.split('.').reduce((current, key) => {
|
|
502
|
+
if (current && typeof current === 'object' && key in current) {
|
|
503
|
+
return current[key];
|
|
504
|
+
}
|
|
505
|
+
throw new Error(`Property "${key}" not found in path "${path}"`);
|
|
506
|
+
}, obj);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
exports.TemplateProcessor = TemplateProcessor;
|
|
510
|
+
// Static/global configuration shared across all instances
|
|
511
|
+
TemplateProcessor.templateCache = new Map();
|
|
512
|
+
TemplateProcessor.handlebarHelpersRegistered = false;
|
|
513
|
+
TemplateProcessor.baseDir = process.cwd();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type OutputFormat = 'yaml' | 'json' | 'typescript';
|
|
2
|
+
export interface TestOutputConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
scenarios: any[];
|
|
7
|
+
format: OutputFormat;
|
|
8
|
+
outputPath: string;
|
|
9
|
+
sourceType?: 'recorder' | 'openapi' | 'wsdl' | 'har' | 'postman';
|
|
10
|
+
metadata?: {
|
|
11
|
+
recordedAt?: string;
|
|
12
|
+
importedFrom?: string;
|
|
13
|
+
sourceUrl?: string;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface DSLGenerationOptions {
|
|
18
|
+
includeDataGeneration?: boolean;
|
|
19
|
+
includeCSVSupport?: boolean;
|
|
20
|
+
includeHooks?: boolean;
|
|
21
|
+
includeCustomLogic?: boolean;
|
|
22
|
+
includeMultipleLoadPatterns?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare class TestOutputWriter {
|
|
25
|
+
private config;
|
|
26
|
+
private dslOptions;
|
|
27
|
+
constructor(config: TestOutputConfig, dslOptions?: DSLGenerationOptions);
|
|
28
|
+
write(): Promise<string>;
|
|
29
|
+
private generateYAML;
|
|
30
|
+
private generateJSON;
|
|
31
|
+
private generateTypeScriptDSL;
|
|
32
|
+
private generateTestConfiguration;
|
|
33
|
+
private generateScenarioDSL;
|
|
34
|
+
private generateWebStep;
|
|
35
|
+
private generateCustomLogicExample;
|
|
36
|
+
private generateStepsDSL;
|
|
37
|
+
private generateFillStep;
|
|
38
|
+
private generateRESTStep;
|
|
39
|
+
private generateSOAPStep;
|
|
40
|
+
private generateExportSection;
|
|
41
|
+
private generateAlternativeLoadPatterns;
|
|
42
|
+
private getSourceInfo;
|
|
43
|
+
private generateDataSection;
|
|
44
|
+
private generateTestScenarios;
|
|
45
|
+
private generateHooks;
|
|
46
|
+
private generateVerifyStep;
|
|
47
|
+
private buildTestConfiguration;
|
|
48
|
+
private hasWebScenarios;
|
|
49
|
+
private getVirtualUsers;
|
|
50
|
+
private getRampUp;
|
|
51
|
+
private getDuration;
|
|
52
|
+
private formatJSONForDSL;
|
|
53
|
+
private toCamelCase;
|
|
54
|
+
private escape;
|
|
55
|
+
static getSafeFilename(filepath: string): Promise<string>;
|
|
56
|
+
}
|