@testsmith/perfornium 0.2.0 → 0.4.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/cli/cli.js CHANGED
@@ -8,6 +8,7 @@ const report_1 = require("./commands/report");
8
8
  const worker_1 = require("./commands/worker");
9
9
  const init_1 = require("./commands/init");
10
10
  const mock_1 = require("./commands/mock");
11
+ const dashboard_1 = require("./commands/dashboard");
11
12
  const native_recorder_1 = require("../recorder/native-recorder");
12
13
  const distributed_1 = require("./commands/distributed");
13
14
  // Add new import commands
@@ -31,6 +32,10 @@ program
31
32
  .option('-v, --verbose', 'Enable verbose logging (info level)')
32
33
  .option('-d, --debug', 'Enable debug logging (very detailed)')
33
34
  .option('--max-users <number>', 'Maximum virtual users override')
35
+ .option('--vus <number>', 'Override virtual users count')
36
+ .option('--iterations <number>', 'Override iterations per VU')
37
+ .option('--duration <duration>', 'Override test duration (e.g., 30s, 1m, 5m)')
38
+ .option('--ramp-up <duration>', 'Override ramp-up time (e.g., 10s, 1m)')
34
39
  .option('-g, --global <key=value>', 'Override global config (supports dot notation: browser.headless=false)', collectGlobals, [])
35
40
  .action(run_1.runCommand);
36
41
  // Helper function to collect --global options
@@ -189,4 +194,13 @@ program
189
194
  .option('--host <host>', 'Host to bind to', 'localhost')
190
195
  .option('-d, --delay <ms>', 'Add delay to all responses (ms)', '0')
191
196
  .action(mock_1.mockCommand);
197
+ program
198
+ .command('dashboard')
199
+ .description('Start the dashboard web UI to view and compare test results')
200
+ .option('-p, --port <port>', 'Port to listen on', '3000')
201
+ .option('-r, --results <directory>', 'Results directory to scan', './results')
202
+ .option('-t, --tests <directory>', 'Tests directory (defaults to parent of results)')
203
+ .option('-w, --workers <file>', 'Workers configuration file (JSON) for distributed testing')
204
+ .option('--no-open', 'Do not open browser automatically')
205
+ .action(dashboard_1.dashboardCommand);
192
206
  program.parse();
@@ -0,0 +1,9 @@
1
+ interface DashboardOptions {
2
+ port: string;
3
+ results: string;
4
+ tests?: string;
5
+ workers?: string;
6
+ open?: boolean;
7
+ }
8
+ export declare function dashboardCommand(options: DashboardOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,98 @@
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.dashboardCommand = dashboardCommand;
37
+ const dashboard_1 = require("../../dashboard");
38
+ const logger_1 = require("../../utils/logger");
39
+ const child_process_1 = require("child_process");
40
+ const path = __importStar(require("path"));
41
+ async function dashboardCommand(options) {
42
+ const port = parseInt(options.port, 10);
43
+ const resultsDir = path.resolve(options.results);
44
+ // Default testsDir to parent of results directory (common structure: project/results, project/tests)
45
+ const testsDir = options.tests ? path.resolve(options.tests) : path.resolve(resultsDir, '..');
46
+ const workersFile = options.workers ? path.resolve(options.workers) : undefined;
47
+ console.log(`
48
+ ╔═══════════════════════════════════════════════════════════════╗
49
+ ║ Perfornium Dashboard ║
50
+ ╠═══════════════════════════════════════════════════════════════╣
51
+ ║ Results directory: ${resultsDir.padEnd(40)} ║
52
+ ║ Tests directory: ${testsDir.padEnd(40)} ║
53
+ ${workersFile ? `║ Workers file: ${workersFile.padEnd(40)} ║\n` : ''}║ Dashboard URL: http://localhost:${port.toString().padEnd(26)} ║
54
+ ╚═══════════════════════════════════════════════════════════════╝
55
+ `);
56
+ const dashboard = new dashboard_1.DashboardServer({
57
+ port,
58
+ resultsDir,
59
+ testsDir,
60
+ workersFile
61
+ });
62
+ // Store dashboard instance globally for test runner integration
63
+ (0, dashboard_1.setDashboard)(dashboard);
64
+ await dashboard.start();
65
+ // Open browser if not disabled
66
+ if (options.open !== false) {
67
+ const url = `http://localhost:${port}`;
68
+ const platform = process.platform;
69
+ let command;
70
+ if (platform === 'darwin') {
71
+ command = `open "${url}"`;
72
+ }
73
+ else if (platform === 'win32') {
74
+ command = `start "${url}"`;
75
+ }
76
+ else {
77
+ command = `xdg-open "${url}"`;
78
+ }
79
+ (0, child_process_1.exec)(command, (error) => {
80
+ if (error) {
81
+ logger_1.logger.debug('Could not open browser automatically:', error.message);
82
+ }
83
+ });
84
+ }
85
+ console.log('Press Ctrl+C to stop the dashboard\n');
86
+ // Keep running until interrupted
87
+ process.on('SIGINT', async () => {
88
+ console.log('\nShutting down dashboard...');
89
+ await dashboard.stop();
90
+ process.exit(0);
91
+ });
92
+ process.on('SIGTERM', async () => {
93
+ await dashboard.stop();
94
+ process.exit(0);
95
+ });
96
+ // Keep the process alive
97
+ await new Promise(() => { });
98
+ }
@@ -7,6 +7,10 @@ export interface RunOptions {
7
7
  verbose?: boolean;
8
8
  debug?: boolean;
9
9
  maxUsers?: string;
10
+ vus?: string;
11
+ iterations?: string;
12
+ duration?: string;
13
+ rampUp?: string;
10
14
  global?: string[];
11
15
  }
12
16
  export declare function runCommand(configPath: string, options: RunOptions): Promise<void>;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runCommand = runCommand;
37
37
  const path = __importStar(require("path"));
38
38
  const http = __importStar(require("http"));
39
+ const fs = __importStar(require("fs"));
39
40
  const parser_1 = require("../../config/parser");
40
41
  const validator_1 = require("../../config/validator");
41
42
  const test_runner_1 = require("../../core/test-runner");
@@ -104,15 +105,44 @@ async function runCommand(configPath, options) {
104
105
  if (baseUrl.match(/^https?:\/\/localhost:3000/)) {
105
106
  await startMockServer(3000);
106
107
  }
107
- // Apply max users override
108
- if (options.maxUsers) {
109
- const maxUsers = parseInt(options.maxUsers);
108
+ // Apply load pattern overrides
109
+ if (options.maxUsers || options.vus || options.iterations || options.duration || options.rampUp) {
110
110
  const { getPrimaryLoadPhase } = await Promise.resolve().then(() => __importStar(require('../../config/types/load-config')));
111
111
  const primaryPhase = getPrimaryLoadPhase(config.load);
112
- const currentUsers = primaryPhase.virtual_users || primaryPhase.vus;
113
- if (currentUsers && currentUsers > maxUsers) {
114
- logger_1.logger.warn(`Limiting virtual users from ${currentUsers} to ${maxUsers}`);
115
- primaryPhase.virtual_users = maxUsers;
112
+ // Virtual users override
113
+ if (options.vus) {
114
+ const vus = parseInt(options.vus);
115
+ logger_1.logger.info(`Overriding virtual users to ${vus}`);
116
+ primaryPhase.virtual_users = vus;
117
+ if (primaryPhase.vus)
118
+ primaryPhase.vus = vus;
119
+ }
120
+ // Max users override (limit, not set)
121
+ if (options.maxUsers) {
122
+ const maxUsers = parseInt(options.maxUsers);
123
+ const currentUsers = primaryPhase.virtual_users || primaryPhase.vus;
124
+ if (currentUsers && currentUsers > maxUsers) {
125
+ logger_1.logger.warn(`Limiting virtual users from ${currentUsers} to ${maxUsers}`);
126
+ primaryPhase.virtual_users = maxUsers;
127
+ }
128
+ }
129
+ // Iterations override
130
+ if (options.iterations) {
131
+ const iterations = parseInt(options.iterations);
132
+ logger_1.logger.info(`Overriding iterations to ${iterations}`);
133
+ primaryPhase.iterations = iterations;
134
+ }
135
+ // Duration override
136
+ if (options.duration) {
137
+ logger_1.logger.info(`Overriding duration to ${options.duration}`);
138
+ primaryPhase.duration = options.duration;
139
+ // Remove iterations if duration is set (they're mutually exclusive)
140
+ delete primaryPhase.iterations;
141
+ }
142
+ // Ramp-up override
143
+ if (options.rampUp) {
144
+ logger_1.logger.info(`Overriding ramp-up to ${options.rampUp}`);
145
+ primaryPhase.ramp_up = options.rampUp;
116
146
  }
117
147
  }
118
148
  // Process templates with environment variables
@@ -138,12 +168,29 @@ async function runCommand(configPath, options) {
138
168
  return;
139
169
  }
140
170
  // Override output directory if specified
141
- if (options.output && processedConfig.outputs) {
142
- processedConfig.outputs.forEach(output => {
143
- if (output.file) {
144
- output.file = path.join(options.output, path.basename(output.file));
145
- }
146
- });
171
+ if (options.output) {
172
+ // Ensure output directory exists
173
+ if (!fs.existsSync(options.output)) {
174
+ fs.mkdirSync(options.output, { recursive: true });
175
+ }
176
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').substring(0, 19);
177
+ const testName = processedConfig.name || path.basename(configPath, path.extname(configPath));
178
+ const defaultOutputFile = path.join(options.output, `${testName}-${timestamp}.json`);
179
+ if (processedConfig.outputs && processedConfig.outputs.length > 0) {
180
+ // Modify existing outputs to use the specified directory
181
+ processedConfig.outputs.forEach(output => {
182
+ if (output.file) {
183
+ output.file = path.join(options.output, path.basename(output.file));
184
+ }
185
+ });
186
+ }
187
+ else {
188
+ // No outputs configured - add default JSON output
189
+ processedConfig.outputs = [{
190
+ type: 'json',
191
+ file: defaultOutputFile
192
+ }];
193
+ }
147
194
  }
148
195
  // Enable report generation if requested
149
196
  if (options.report) {
@@ -160,12 +207,94 @@ async function runCommand(configPath, options) {
160
207
  for (const address of workerAddresses) {
161
208
  await manager.addWorker(address);
162
209
  }
210
+ // Initialize metrics before distributing test
211
+ const metrics = manager.getAggregatedMetrics();
212
+ metrics.start();
213
+ // Track results as they come in
214
+ let resultCount = 0;
215
+ manager.on('result', () => {
216
+ resultCount++;
217
+ });
163
218
  await manager.distributeTest(processedConfig);
219
+ logger_1.logger.info('📡 Test distributed to workers, starting progress reporting...');
220
+ // Start live progress reporting for dashboard
221
+ let lastReportedResultIndex = 0;
222
+ let isRunning = true;
223
+ // Output function for progress reporting
224
+ const outputProgress = () => {
225
+ if (!isRunning)
226
+ return;
227
+ const summary = metrics.getSummary();
228
+ const currentRequests = summary.total_requests || 0;
229
+ const rps = summary.requests_per_second || 0;
230
+ const p50 = summary.percentiles?.[50] || 0;
231
+ const p90 = summary.percentiles?.[90] || 0;
232
+ const p95 = summary.percentiles?.[95] || 0;
233
+ const p99 = summary.percentiles?.[99] || 0;
234
+ const successRate = summary.success_rate || 0;
235
+ // Output progress line for dashboard parsing
236
+ const progressLine = `[PROGRESS] VUs: ${manager.getWorkerCount()} | Requests: ${currentRequests} | Errors: ${summary.failed_requests || 0} | Avg RT: ${(summary.avg_response_time || 0).toFixed(0)}ms | RPS: ${rps.toFixed(1)} | P50: ${p50.toFixed(0)}ms | P90: ${p90.toFixed(0)}ms | P95: ${p95.toFixed(0)}ms | P99: ${p99.toFixed(0)}ms | Success: ${successRate.toFixed(1)}%`;
237
+ console.log(progressLine);
238
+ // Output step statistics if available
239
+ if (summary.step_statistics && summary.step_statistics.length > 0) {
240
+ const stepData = summary.step_statistics.map((s) => ({
241
+ n: s.step_name,
242
+ s: s.scenario,
243
+ r: s.total_requests,
244
+ e: s.failed_requests,
245
+ a: Math.round(s.avg_response_time),
246
+ p50: Math.round(s.percentiles?.[50] || 0),
247
+ p95: Math.round(s.percentiles?.[95] || 0),
248
+ p99: Math.round(s.percentiles?.[99] || 0),
249
+ sr: Math.round(s.success_rate * 10) / 10
250
+ }));
251
+ console.log(`[STEPS] ${JSON.stringify(stepData)}`);
252
+ }
253
+ // Output individual response times for charts
254
+ const allResults = metrics.getResults();
255
+ if (allResults.length > lastReportedResultIndex) {
256
+ const newResults = allResults.slice(lastReportedResultIndex, lastReportedResultIndex + 50);
257
+ const rtData = newResults.map((r) => ({
258
+ t: r.timestamp,
259
+ v: Math.round(r.duration),
260
+ s: r.success ? 1 : 0,
261
+ n: r.step_name || r.action || 'unknown' // Include step/request name for coloring
262
+ }));
263
+ if (rtData.length > 0) {
264
+ console.log(`[RT] ${JSON.stringify(rtData)}`);
265
+ }
266
+ lastReportedResultIndex = Math.min(allResults.length, lastReportedResultIndex + 50);
267
+ }
268
+ // Output top 10 errors if any
269
+ if (summary.error_details && summary.error_details.length > 0) {
270
+ const topErrors = summary.error_details.slice(0, 10).map((e) => ({
271
+ scenario: e.scenario,
272
+ action: e.action,
273
+ status: e.status,
274
+ error: e.error?.substring(0, 200), // Truncate long error messages
275
+ url: e.request_url,
276
+ count: e.count
277
+ }));
278
+ console.log(`[ERRORS] ${JSON.stringify(topErrors)}`);
279
+ }
280
+ };
281
+ // Output initial progress immediately
282
+ outputProgress();
283
+ // Then continue outputting every 500ms
284
+ const progressInterval = setInterval(outputProgress, 500);
164
285
  await manager.waitForCompletion();
165
- const metrics = manager.getAggregatedMetrics();
286
+ logger_1.logger.info('✅ All workers completed, cleaning up...');
287
+ // Stop progress reporting
288
+ isRunning = false;
289
+ clearInterval(progressInterval);
166
290
  const summary = metrics.getSummary();
167
291
  logger_1.logger.success(`Distributed test completed: ${summary.success_rate.toFixed(2)}% success rate`);
292
+ logger_1.logger.info(`📊 Total requests: ${summary.total_requests}, Success rate: ${summary.success_rate.toFixed(1)}%`);
293
+ logger_1.logger.info('🧹 Starting cleanup...');
168
294
  await manager.cleanup();
295
+ logger_1.logger.info('✅ Cleanup completed, exiting...');
296
+ // Force exit after distributed test cleanup to ensure process terminates
297
+ process.exit(0);
169
298
  }
170
299
  else {
171
300
  const runner = new test_runner_1.TestRunner(processedConfig);
@@ -1,12 +1,20 @@
1
1
  export declare class WorkerServer {
2
2
  private server;
3
+ private wss;
3
4
  private host;
4
5
  private port;
5
6
  private status;
6
7
  private activeRunner;
7
8
  private preparedConfig;
8
9
  private completedRunner;
10
+ private wsClients;
9
11
  constructor(host?: string, port?: number);
12
+ private setupWebSocketHandlers;
13
+ private handleWebSocketMessage;
14
+ private executeWsTest;
15
+ private stopWsTest;
16
+ private sendWsMessage;
17
+ private sendWsError;
10
18
  private handleRequest;
11
19
  private handleHealth;
12
20
  private handleStatus;
@@ -38,12 +38,14 @@ exports.workerCommand = workerCommand;
38
38
  const logger_1 = require("../../utils/logger");
39
39
  const http = __importStar(require("http"));
40
40
  const url_1 = require("url");
41
+ const ws_1 = require("ws");
41
42
  const test_runner_1 = require("../../core/test-runner");
42
43
  class WorkerServer {
43
44
  constructor(host = 'localhost', port = 8080) {
44
45
  this.activeRunner = null;
45
46
  this.preparedConfig = null;
46
47
  this.completedRunner = null;
48
+ this.wsClients = new Map();
47
49
  this.host = host;
48
50
  this.port = port;
49
51
  this.status = {
@@ -57,6 +59,129 @@ class WorkerServer {
57
59
  this.server = http.createServer((req, res) => {
58
60
  this.handleRequest(req, res);
59
61
  });
62
+ // WebSocket server for distributed testing
63
+ this.wss = new ws_1.WebSocketServer({
64
+ server: this.server,
65
+ path: '/perfornium'
66
+ });
67
+ this.setupWebSocketHandlers();
68
+ }
69
+ setupWebSocketHandlers() {
70
+ this.wss.on('connection', (ws, req) => {
71
+ const clientIP = req.socket.remoteAddress;
72
+ logger_1.logger.info(`🔌 WebSocket client connected from ${clientIP}`);
73
+ this.wsClients.set(ws, null);
74
+ ws.on('message', async (data) => {
75
+ try {
76
+ const message = JSON.parse(data.toString());
77
+ await this.handleWebSocketMessage(ws, message);
78
+ }
79
+ catch (error) {
80
+ logger_1.logger.error('❌ Error handling WebSocket message:', error);
81
+ this.sendWsError(ws, error.message);
82
+ }
83
+ });
84
+ ws.on('close', () => {
85
+ logger_1.logger.info('👋 WebSocket client disconnected');
86
+ const runner = this.wsClients.get(ws);
87
+ if (runner) {
88
+ runner.stop().catch(err => logger_1.logger.error('❌ Error stopping runner on disconnect:', err));
89
+ }
90
+ this.wsClients.delete(ws);
91
+ });
92
+ ws.on('error', (error) => {
93
+ logger_1.logger.error('❌ WebSocket error:', error);
94
+ this.wsClients.delete(ws);
95
+ });
96
+ // Send heartbeat
97
+ this.sendWsMessage(ws, { type: 'heartbeat' });
98
+ });
99
+ }
100
+ async handleWebSocketMessage(ws, message) {
101
+ switch (message.type) {
102
+ case 'execute_test':
103
+ await this.executeWsTest(ws, message.config);
104
+ break;
105
+ case 'stop_test':
106
+ await this.stopWsTest(ws);
107
+ break;
108
+ case 'heartbeat_ack':
109
+ // Client is alive
110
+ break;
111
+ default:
112
+ logger_1.logger.warn(`⚠️ Unknown WebSocket message type: ${message.type}`);
113
+ }
114
+ }
115
+ async executeWsTest(ws, config) {
116
+ try {
117
+ // Stop any existing test for this connection
118
+ await this.stopWsTest(ws);
119
+ const runner = new test_runner_1.TestRunner(config);
120
+ this.wsClients.set(ws, runner);
121
+ logger_1.logger.info(`🚀 Starting test via WebSocket: ${config.name}`);
122
+ const metrics = runner.getMetrics();
123
+ // Listen for individual results
124
+ metrics.on('result', (result) => {
125
+ this.sendWsMessage(ws, {
126
+ type: 'test_result',
127
+ data: result
128
+ });
129
+ });
130
+ // Send progress updates on batch events
131
+ metrics.on('batch', (batch) => {
132
+ this.sendWsMessage(ws, {
133
+ type: 'test_progress',
134
+ data: {
135
+ completed: batch.batch_number * batch.batch_size,
136
+ total: batch.batch_size
137
+ }
138
+ });
139
+ });
140
+ // Log test start
141
+ this.sendWsMessage(ws, {
142
+ type: 'log',
143
+ message: `Starting test: ${config.name}`
144
+ });
145
+ // Start the test
146
+ runner.run().then(() => {
147
+ const summary = metrics.getSummary();
148
+ this.sendWsMessage(ws, {
149
+ type: 'test_completed',
150
+ summary: summary
151
+ });
152
+ this.wsClients.set(ws, null);
153
+ logger_1.logger.info(`✅ Test completed via WebSocket: ${config.name}`);
154
+ }).catch((error) => {
155
+ this.sendWsMessage(ws, {
156
+ type: 'test_error',
157
+ error: error.message
158
+ });
159
+ this.wsClients.set(ws, null);
160
+ logger_1.logger.error(`❌ Test failed via WebSocket: ${error.message}`);
161
+ });
162
+ }
163
+ catch (error) {
164
+ this.sendWsMessage(ws, {
165
+ type: 'test_error',
166
+ error: error.message
167
+ });
168
+ }
169
+ }
170
+ async stopWsTest(ws) {
171
+ const runner = this.wsClients.get(ws);
172
+ if (runner) {
173
+ await runner.stop();
174
+ this.wsClients.set(ws, null);
175
+ this.sendWsMessage(ws, { type: 'test_stopped' });
176
+ }
177
+ }
178
+ sendWsMessage(ws, message) {
179
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
180
+ ws.send(JSON.stringify(message));
181
+ }
182
+ }
183
+ sendWsError(ws, error) {
184
+ this.sendWsMessage(ws, { type: 'error', error });
60
185
  }
61
186
  async handleRequest(req, res) {
62
187
  // Set CORS headers
@@ -261,6 +386,7 @@ class WorkerServer {
261
386
  this.server.listen(this.port, this.host, () => {
262
387
  logger_1.logger.info(`🚀 Worker server started on ${this.host}:${this.port}`);
263
388
  logger_1.logger.info(`📋 Available endpoints:`);
389
+ logger_1.logger.info(` WS ws://${this.host}:${this.port}/perfornium (distributed testing)`);
264
390
  logger_1.logger.info(` GET http://${this.host}:${this.port}/health`);
265
391
  logger_1.logger.info(` GET http://${this.host}:${this.port}/status`);
266
392
  logger_1.logger.info(` POST http://${this.host}:${this.port}/prepare`);
@@ -71,4 +71,9 @@ export interface DebugConfig {
71
71
  max_response_body_size?: number;
72
72
  capture_only_failures?: boolean;
73
73
  log_level?: 'debug' | 'info' | 'warn' | 'error';
74
+ log_requests?: boolean;
75
+ log_responses?: boolean;
76
+ log_headers?: boolean;
77
+ log_body?: boolean;
78
+ log_timings?: boolean;
74
79
  }
@@ -14,7 +14,8 @@ export interface LoadPhase {
14
14
  vus?: number;
15
15
  virtual_users?: number;
16
16
  ramp_up?: string;
17
- duration: string;
17
+ duration?: string;
18
+ iterations?: number;
18
19
  steps?: LoadStep[];
19
20
  rate?: number;
20
21
  }
@@ -8,8 +8,13 @@ export declare class TestRunner {
8
8
  private activeVUs;
9
9
  private isRunning;
10
10
  private startTime;
11
+ private testId;
12
+ private dashboardInterval;
11
13
  constructor(config: TestConfiguration);
12
14
  run(): Promise<void>;
15
+ private lastReportedResultIndex;
16
+ private startDashboardReporting;
17
+ private stopDashboardReporting;
13
18
  private setupCSVBaseDirectory;
14
19
  stop(): Promise<void>;
15
20
  private initialize;