@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,107 @@
|
|
|
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.JSONOutput = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class JSONOutput {
|
|
40
|
+
constructor(filePath) {
|
|
41
|
+
this.results = [];
|
|
42
|
+
this.firstWrite = true;
|
|
43
|
+
this.filePath = filePath;
|
|
44
|
+
}
|
|
45
|
+
async initialize() {
|
|
46
|
+
// Ensure directory exists
|
|
47
|
+
const dir = path.dirname(this.filePath);
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
// Create file and write opening structure
|
|
52
|
+
this.fileStream = fs.createWriteStream(this.filePath, { flags: 'w' });
|
|
53
|
+
this.fileStream.write('{\n');
|
|
54
|
+
this.fileStream.write(' "metadata": {\n');
|
|
55
|
+
this.fileStream.write(' "version": "1.0.0",\n');
|
|
56
|
+
this.fileStream.write(` "generated_at": "${new Date().toISOString()}",\n`);
|
|
57
|
+
this.fileStream.write(' "test_status": "running"\n');
|
|
58
|
+
this.fileStream.write(' },\n');
|
|
59
|
+
this.fileStream.write(' "results": [\n');
|
|
60
|
+
this.firstWrite = true;
|
|
61
|
+
}
|
|
62
|
+
async writeResult(result) {
|
|
63
|
+
// Write incrementally to file during test
|
|
64
|
+
this.results.push(result); // Keep for summary calculation
|
|
65
|
+
if (!this.fileStream) {
|
|
66
|
+
throw new Error('JSON output not initialized');
|
|
67
|
+
}
|
|
68
|
+
// Add comma before all results except the first
|
|
69
|
+
if (!this.firstWrite) {
|
|
70
|
+
this.fileStream.write(',\n');
|
|
71
|
+
}
|
|
72
|
+
this.firstWrite = false;
|
|
73
|
+
// Write result as indented JSON
|
|
74
|
+
const resultJson = JSON.stringify(result, null, 2);
|
|
75
|
+
const indentedResult = resultJson.split('\n').map((line, idx) => {
|
|
76
|
+
if (idx === 0)
|
|
77
|
+
return ` ${line}`;
|
|
78
|
+
return ` ${line}`;
|
|
79
|
+
}).join('\n');
|
|
80
|
+
this.fileStream.write(indentedResult);
|
|
81
|
+
}
|
|
82
|
+
async writeSummary(summary) {
|
|
83
|
+
this.summary = summary;
|
|
84
|
+
}
|
|
85
|
+
async finalize() {
|
|
86
|
+
if (!this.fileStream)
|
|
87
|
+
return;
|
|
88
|
+
// Close results array
|
|
89
|
+
this.fileStream.write('\n ]');
|
|
90
|
+
// Write summary if available
|
|
91
|
+
if (this.summary) {
|
|
92
|
+
this.fileStream.write(',\n "summary": ');
|
|
93
|
+
const summaryJson = JSON.stringify(this.summary, null, 2);
|
|
94
|
+
const indentedSummary = summaryJson.split('\n').map((line, idx) => {
|
|
95
|
+
if (idx === 0)
|
|
96
|
+
return line;
|
|
97
|
+
return ` ${line}`;
|
|
98
|
+
}).join('\n');
|
|
99
|
+
this.fileStream.write(indentedSummary);
|
|
100
|
+
}
|
|
101
|
+
// Close root object
|
|
102
|
+
this.fileStream.write('\n}\n');
|
|
103
|
+
// Close file stream
|
|
104
|
+
this.fileStream.end();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.JSONOutput = JSONOutput;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { OutputHandler } from './base';
|
|
2
|
+
import { TestResult, MetricsSummary } from '../metrics/types';
|
|
3
|
+
export interface StreamingCSVConfig {
|
|
4
|
+
batchSize?: number;
|
|
5
|
+
flushInterval?: number;
|
|
6
|
+
immediateWrite?: boolean;
|
|
7
|
+
rotateSize?: number;
|
|
8
|
+
maxFiles?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class StreamingCSVOutput implements OutputHandler {
|
|
11
|
+
private filePath;
|
|
12
|
+
private csvWriter;
|
|
13
|
+
private pendingResults;
|
|
14
|
+
private config;
|
|
15
|
+
private flushTimer?;
|
|
16
|
+
private fileStream?;
|
|
17
|
+
private headerWritten;
|
|
18
|
+
private totalResults;
|
|
19
|
+
private currentFileSize;
|
|
20
|
+
private rotationIndex;
|
|
21
|
+
private readonly DEFAULT_CONFIG;
|
|
22
|
+
constructor(filePath: string, config?: StreamingCSVConfig);
|
|
23
|
+
initialize(): Promise<void>;
|
|
24
|
+
writeResult(result: TestResult): Promise<void>;
|
|
25
|
+
private flushPending;
|
|
26
|
+
private rotateFile;
|
|
27
|
+
private cleanupOldFiles;
|
|
28
|
+
private extractRotationIndex;
|
|
29
|
+
writeSummary(summary: MetricsSummary): Promise<void>;
|
|
30
|
+
finalize(): Promise<void>;
|
|
31
|
+
getStats(): {
|
|
32
|
+
totalResults: number;
|
|
33
|
+
pendingResults: number;
|
|
34
|
+
currentFileSize: number;
|
|
35
|
+
rotationIndex: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
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.StreamingCSVOutput = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const csv_writer_1 = require("csv-writer");
|
|
40
|
+
const file_manager_1 = require("../utils/file-manager");
|
|
41
|
+
const logger_1 = require("../utils/logger");
|
|
42
|
+
class StreamingCSVOutput {
|
|
43
|
+
constructor(filePath, config) {
|
|
44
|
+
this.pendingResults = [];
|
|
45
|
+
this.headerWritten = false;
|
|
46
|
+
this.totalResults = 0;
|
|
47
|
+
this.currentFileSize = 0;
|
|
48
|
+
this.rotationIndex = 0;
|
|
49
|
+
this.DEFAULT_CONFIG = {
|
|
50
|
+
batchSize: 100,
|
|
51
|
+
flushInterval: 5000,
|
|
52
|
+
immediateWrite: false,
|
|
53
|
+
rotateSize: 50 * 1024 * 1024, // 50MB
|
|
54
|
+
maxFiles: 5
|
|
55
|
+
};
|
|
56
|
+
this.filePath = filePath;
|
|
57
|
+
this.config = { ...this.DEFAULT_CONFIG, ...config };
|
|
58
|
+
}
|
|
59
|
+
async initialize() {
|
|
60
|
+
// Process template in file path
|
|
61
|
+
this.filePath = file_manager_1.FileManager.processFilePath(this.filePath);
|
|
62
|
+
// Ensure directory exists
|
|
63
|
+
const dir = path.dirname(this.filePath);
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
// Initialize CSV writer
|
|
68
|
+
this.csvWriter = (0, csv_writer_1.createObjectCsvWriter)({
|
|
69
|
+
path: this.filePath,
|
|
70
|
+
header: [
|
|
71
|
+
{ id: 'timestamp', title: 'timestamp' },
|
|
72
|
+
{ id: 'vu_id', title: 'vu_id' },
|
|
73
|
+
{ id: 'iteration', title: 'iteration' },
|
|
74
|
+
{ id: 'scenario', title: 'scenario' },
|
|
75
|
+
{ id: 'name', title: 'request_name' },
|
|
76
|
+
{ id: 'response_time', title: 'response_time_ms' },
|
|
77
|
+
{ id: 'success', title: 'success' },
|
|
78
|
+
{ id: 'status', title: 'http_status' },
|
|
79
|
+
{ id: 'error', title: 'error' },
|
|
80
|
+
{ id: 'response_size', title: 'response_size_bytes' },
|
|
81
|
+
{ id: 'request_url', title: 'url' }
|
|
82
|
+
]
|
|
83
|
+
});
|
|
84
|
+
// Start flush timer if configured
|
|
85
|
+
if (this.config.flushInterval > 0 && !this.config.immediateWrite) {
|
|
86
|
+
this.flushTimer = setInterval(() => {
|
|
87
|
+
this.flushPending();
|
|
88
|
+
}, this.config.flushInterval);
|
|
89
|
+
}
|
|
90
|
+
logger_1.logger.info(`📊 Streaming CSV output initialized: ${this.filePath}`);
|
|
91
|
+
logger_1.logger.debug(`📊 Config: batch=${this.config.batchSize}, flush=${this.config.flushInterval}ms`);
|
|
92
|
+
}
|
|
93
|
+
async writeResult(result) {
|
|
94
|
+
// Add processed timestamp and normalize data
|
|
95
|
+
const processedResult = {
|
|
96
|
+
...result,
|
|
97
|
+
timestamp: result.timestamp || Date.now(),
|
|
98
|
+
response_time: result.response_time || result.duration || 0,
|
|
99
|
+
name: result.name || result.action || 'request',
|
|
100
|
+
iteration: result.iteration || 0
|
|
101
|
+
};
|
|
102
|
+
this.pendingResults.push(processedResult);
|
|
103
|
+
this.totalResults++;
|
|
104
|
+
if (this.config.immediateWrite) {
|
|
105
|
+
await this.flushPending();
|
|
106
|
+
}
|
|
107
|
+
else if (this.pendingResults.length >= this.config.batchSize) {
|
|
108
|
+
await this.flushPending();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async flushPending() {
|
|
112
|
+
if (this.pendingResults.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
// Check if we need to rotate the file
|
|
116
|
+
if (this.config.rotateSize > 0 && this.currentFileSize > this.config.rotateSize) {
|
|
117
|
+
await this.rotateFile();
|
|
118
|
+
}
|
|
119
|
+
// Write the batch
|
|
120
|
+
const recordsToWrite = [...this.pendingResults];
|
|
121
|
+
this.pendingResults = [];
|
|
122
|
+
await this.csvWriter.writeRecords(recordsToWrite);
|
|
123
|
+
// Update file size estimate
|
|
124
|
+
this.currentFileSize += recordsToWrite.length * 200; // Rough estimate
|
|
125
|
+
logger_1.logger.debug(`📊 Wrote ${recordsToWrite.length} results to CSV (total: ${this.totalResults})`);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger_1.logger.error('❌ Failed to write CSV batch:', error);
|
|
129
|
+
// Re-add results to pending for retry
|
|
130
|
+
this.pendingResults.unshift(...this.pendingResults);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async rotateFile() {
|
|
134
|
+
try {
|
|
135
|
+
// Close current file
|
|
136
|
+
if (this.fileStream) {
|
|
137
|
+
this.fileStream.end();
|
|
138
|
+
this.fileStream = undefined;
|
|
139
|
+
}
|
|
140
|
+
// Create rotated filename
|
|
141
|
+
const ext = path.extname(this.filePath);
|
|
142
|
+
const base = this.filePath.slice(0, -ext.length);
|
|
143
|
+
const rotatedPath = `${base}.${this.rotationIndex}${ext}`;
|
|
144
|
+
// Rename current file
|
|
145
|
+
if (fs.existsSync(this.filePath)) {
|
|
146
|
+
fs.renameSync(this.filePath, rotatedPath);
|
|
147
|
+
}
|
|
148
|
+
// Clean up old files if needed
|
|
149
|
+
if (this.config.maxFiles > 0) {
|
|
150
|
+
this.cleanupOldFiles(base, ext);
|
|
151
|
+
}
|
|
152
|
+
// Reset for new file
|
|
153
|
+
this.rotationIndex++;
|
|
154
|
+
this.currentFileSize = 0;
|
|
155
|
+
// Recreate CSV writer for new file
|
|
156
|
+
await this.initialize();
|
|
157
|
+
logger_1.logger.info(`🔄 Rotated CSV file: ${this.filePath}`);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger_1.logger.error('❌ Failed to rotate CSV file:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
cleanupOldFiles(basePath, extension) {
|
|
164
|
+
try {
|
|
165
|
+
const dir = path.dirname(basePath);
|
|
166
|
+
const basename = path.basename(basePath);
|
|
167
|
+
const files = fs.readdirSync(dir)
|
|
168
|
+
.filter(file => file.startsWith(basename) && file.endsWith(extension))
|
|
169
|
+
.map(file => ({
|
|
170
|
+
name: file,
|
|
171
|
+
path: path.join(dir, file),
|
|
172
|
+
index: this.extractRotationIndex(file)
|
|
173
|
+
}))
|
|
174
|
+
.filter(file => file.index >= 0)
|
|
175
|
+
.sort((a, b) => b.index - a.index);
|
|
176
|
+
// Remove excess files
|
|
177
|
+
const filesToRemove = files.slice(this.config.maxFiles);
|
|
178
|
+
for (const file of filesToRemove) {
|
|
179
|
+
fs.unlinkSync(file.path);
|
|
180
|
+
logger_1.logger.debug(`🗑️ Removed old CSV file: ${file.name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
logger_1.logger.warn('⚠️ Failed to cleanup old CSV files:', error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
extractRotationIndex(filename) {
|
|
188
|
+
const match = filename.match(/\.(\d+)\.csv$/);
|
|
189
|
+
return match ? parseInt(match[1], 10) : -1;
|
|
190
|
+
}
|
|
191
|
+
async writeSummary(summary) {
|
|
192
|
+
// Write summary to separate file
|
|
193
|
+
const summaryPath = this.filePath.replace('.csv', '_summary.csv');
|
|
194
|
+
const summaryWriter = (0, csv_writer_1.createObjectCsvWriter)({
|
|
195
|
+
path: summaryPath,
|
|
196
|
+
header: [
|
|
197
|
+
{ id: 'metric', title: 'metric' },
|
|
198
|
+
{ id: 'value', title: 'value' },
|
|
199
|
+
{ id: 'unit', title: 'unit' },
|
|
200
|
+
{ id: 'timestamp', title: 'timestamp' }
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
const timestamp = Date.now();
|
|
204
|
+
const summaryData = [
|
|
205
|
+
{ metric: 'total_requests', value: summary.total_requests, unit: 'count', timestamp },
|
|
206
|
+
{ metric: 'successful_requests', value: summary.successful_requests, unit: 'count', timestamp },
|
|
207
|
+
{ metric: 'failed_requests', value: summary.failed_requests, unit: 'count', timestamp },
|
|
208
|
+
{ metric: 'success_rate', value: summary.success_rate?.toFixed(2) || '0', unit: 'percent', timestamp },
|
|
209
|
+
{ metric: 'avg_response_time', value: summary.avg_response_time?.toFixed(2) || '0', unit: 'ms', timestamp },
|
|
210
|
+
{ metric: 'min_response_time', value: summary.min_response_time || 0, unit: 'ms', timestamp },
|
|
211
|
+
{ metric: 'max_response_time', value: summary.max_response_time || 0, unit: 'ms', timestamp },
|
|
212
|
+
{ metric: 'requests_per_second', value: summary.requests_per_second?.toFixed(2) || '0', unit: 'req/s', timestamp },
|
|
213
|
+
{ metric: 'total_duration', value: summary.total_duration || 0, unit: 'ms', timestamp }
|
|
214
|
+
];
|
|
215
|
+
// Add percentiles
|
|
216
|
+
if (summary.percentiles) {
|
|
217
|
+
Object.entries(summary.percentiles).forEach(([p, value]) => {
|
|
218
|
+
summaryData.push({
|
|
219
|
+
metric: `p${p}_response_time`,
|
|
220
|
+
value: typeof value === 'number' ? value.toFixed(2) : '0',
|
|
221
|
+
unit: 'ms',
|
|
222
|
+
timestamp
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
await summaryWriter.writeRecords(summaryData);
|
|
227
|
+
logger_1.logger.info(`📊 Written CSV summary: ${summaryPath}`);
|
|
228
|
+
}
|
|
229
|
+
async finalize() {
|
|
230
|
+
// Flush any remaining results
|
|
231
|
+
await this.flushPending();
|
|
232
|
+
// Clear flush timer
|
|
233
|
+
if (this.flushTimer) {
|
|
234
|
+
clearInterval(this.flushTimer);
|
|
235
|
+
this.flushTimer = undefined;
|
|
236
|
+
}
|
|
237
|
+
// Close file stream
|
|
238
|
+
if (this.fileStream) {
|
|
239
|
+
this.fileStream.end();
|
|
240
|
+
this.fileStream = undefined;
|
|
241
|
+
}
|
|
242
|
+
logger_1.logger.info(`📊 Streaming CSV finalized. Total results: ${this.totalResults}`);
|
|
243
|
+
}
|
|
244
|
+
// Real-time statistics
|
|
245
|
+
getStats() {
|
|
246
|
+
return {
|
|
247
|
+
totalResults: this.totalResults,
|
|
248
|
+
pendingResults: this.pendingResults.length,
|
|
249
|
+
currentFileSize: this.currentFileSize,
|
|
250
|
+
rotationIndex: this.rotationIndex
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
exports.StreamingCSVOutput = StreamingCSVOutput;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { OutputHandler } from './base';
|
|
2
|
+
import { TestResult, MetricsSummary } from '../metrics/types';
|
|
3
|
+
export interface StreamingJSONConfig {
|
|
4
|
+
format?: 'ndjson' | 'json-array' | 'json-stream';
|
|
5
|
+
batchSize?: number;
|
|
6
|
+
flushInterval?: number;
|
|
7
|
+
prettify?: boolean;
|
|
8
|
+
includeMetadata?: boolean;
|
|
9
|
+
rotateSize?: number;
|
|
10
|
+
maxFiles?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class StreamingJSONOutput implements OutputHandler {
|
|
13
|
+
private filePath;
|
|
14
|
+
private config;
|
|
15
|
+
private pendingResults;
|
|
16
|
+
private fileStream?;
|
|
17
|
+
private flushTimer?;
|
|
18
|
+
private totalResults;
|
|
19
|
+
private currentFileSize;
|
|
20
|
+
private rotationIndex;
|
|
21
|
+
private isFirstWrite;
|
|
22
|
+
private testStartTime;
|
|
23
|
+
private readonly DEFAULT_CONFIG;
|
|
24
|
+
constructor(filePath: string, config?: StreamingJSONConfig);
|
|
25
|
+
initialize(): Promise<void>;
|
|
26
|
+
private writeHeader;
|
|
27
|
+
writeResult(result: TestResult): Promise<void>;
|
|
28
|
+
private flushPending;
|
|
29
|
+
private writeRecords;
|
|
30
|
+
private rotateFile;
|
|
31
|
+
private closeCurrentFile;
|
|
32
|
+
private cleanupOldFiles;
|
|
33
|
+
private extractRotationIndex;
|
|
34
|
+
writeSummary(summary: MetricsSummary): Promise<void>;
|
|
35
|
+
finalize(): Promise<void>;
|
|
36
|
+
getStats(): {
|
|
37
|
+
totalResults: number;
|
|
38
|
+
pendingResults: number;
|
|
39
|
+
currentFileSize: number;
|
|
40
|
+
rotationIndex: number;
|
|
41
|
+
format: string;
|
|
42
|
+
};
|
|
43
|
+
}
|