word-stress 1.0.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/src/main.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Main application entry point
3
+ * Orchestrates the stress testing workflow
4
+ */
5
+
6
+ const logger = require('./logger');
7
+ const { buildConfig, validate } = require('./config');
8
+ const { getTestMode } = require('./test-modes/factory');
9
+ const { getFormatter } = require('./formatters/factory');
10
+ const { buildUrl } = require('./utils');
11
+ const { makeRequest } = require('./http/client');
12
+ const MetricsCollector = require('./metrics/MetricsCollector');
13
+ const { resolveUserAgent } = require('./user-agent');
14
+
15
+ /**
16
+ * Main application function
17
+ * @param {Object} args - Parsed CLI arguments
18
+ */
19
+ async function main(args) {
20
+ try {
21
+ // Build and validate configuration
22
+ const config = buildConfig(args);
23
+ validate(config);
24
+
25
+ // Log test configuration
26
+ logger.log(`\nStarting stress test...`);
27
+ const url = buildUrl(config);
28
+ logger.log(`Target: ${url}`);
29
+
30
+ if (config.mode === 'steady-state') {
31
+ logger.log(`Mode: Steady-State`);
32
+ logger.log(`Clients: ${config.clients}`);
33
+ logger.log(`Interval: ${config.interval}ms`);
34
+ logger.log(`Duration: ${config.duration}s`);
35
+ } else if (config.mode === 'burst') {
36
+ logger.log(`Mode: Burst`);
37
+ logger.log(`Simultaneous Requests: ${config.burstClients}`);
38
+ }
39
+
40
+ logger.log(`Method: ${config.method}`);
41
+ logger.log(`Output Format: ${config.output}`);
42
+ logger.log('');
43
+
44
+ // Resolve user agent
45
+ const userAgent = resolveUserAgent(config);
46
+ if (config.customUserAgent) {
47
+ logger.log(`User-Agent: ${config.customUserAgent} (custom)`);
48
+ } else {
49
+ logger.log(`User-Agent: ${config.browser}`);
50
+ }
51
+ logger.log('');
52
+
53
+ // Create instances for test execution
54
+ const testMode = getTestMode(config);
55
+ const metricsCollector = new MetricsCollector();
56
+ const formatter = getFormatter(config.output);
57
+
58
+ // Run the test with user agent
59
+ const results = await testMode.run(
60
+ { makeRequest: (url, options) => makeRequest(url, { ...options, userAgent }) },
61
+ metricsCollector
62
+ );
63
+
64
+ // Format and output results
65
+ const output = formatter.format(results);
66
+ logger.log(output);
67
+ } catch (error) {
68
+ logger.error(error.message);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ main,
75
+ };
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Metrics Collector
3
+ * Aggregates and calculates statistics from test requests
4
+ */
5
+
6
+ const { calculatePercentile, calculateMedian } = require('../utils');
7
+
8
+ /**
9
+ * MetricsCollector class
10
+ * Collects per-request metrics and calculates aggregates
11
+ */
12
+ class MetricsCollector {
13
+ constructor() {
14
+ this.requests = [];
15
+ this.startTime = Date.now();
16
+ this.endTime = null;
17
+ }
18
+
19
+ /**
20
+ * Record a single request result
21
+ * @param {Object} result - Request result object
22
+ * @param {number} result.responseTime - Response time in milliseconds
23
+ * @param {number} result.statusCode - HTTP status code
24
+ * @param {number} result.size - Response size in bytes
25
+ * @param {string|null} result.error - Error type (null if successful)
26
+ */
27
+ recordRequest(result) {
28
+ this.requests.push({
29
+ responseTime: result.responseTime || 0,
30
+ statusCode: result.statusCode || 0,
31
+ size: result.size || 0,
32
+ error: result.error || null,
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Mark test as complete and set end time
38
+ */
39
+ complete() {
40
+ this.endTime = Date.now();
41
+ }
42
+
43
+ /**
44
+ * Get test duration in seconds
45
+ * @returns {number} Duration in seconds
46
+ */
47
+ getDuration() {
48
+ const end = this.endTime || Date.now();
49
+ return (end - this.startTime) / 1000;
50
+ }
51
+
52
+ /**
53
+ * Get aggregated metrics
54
+ * @returns {Object} Aggregated metrics
55
+ */
56
+ getAggregateMetrics() {
57
+ if (this.requests.length === 0) {
58
+ return this._getEmptyMetrics();
59
+ }
60
+
61
+ const responseTimes = this.requests
62
+ .filter(r => !r.error)
63
+ .map(r => r.responseTime)
64
+ .sort((a, b) => a - b);
65
+
66
+ const duration = this.getDuration();
67
+
68
+ // Count status codes
69
+ const statusCounts = {};
70
+ const errorCounts = {};
71
+
72
+ this.requests.forEach(r => {
73
+ if (r.error) {
74
+ errorCounts[r.error] = (errorCounts[r.error] || 0) + 1;
75
+ } else {
76
+ const statusCategory = `${Math.floor(r.statusCode / 100)}xx`;
77
+ statusCounts[statusCategory] = (statusCounts[statusCategory] || 0) + 1;
78
+ }
79
+ });
80
+
81
+ // Calculate response time percentiles
82
+ const minResponseTime = responseTimes.length > 0 ? responseTimes[0] : 0;
83
+ const maxResponseTime = responseTimes.length > 0 ? responseTimes[responseTimes.length - 1] : 0;
84
+ const avgResponseTime =
85
+ responseTimes.length > 0
86
+ ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
87
+ : 0;
88
+
89
+ // Calculate total data transferred
90
+ const totalDataTransferred = this.requests.reduce((sum, r) => sum + (r.size || 0), 0);
91
+
92
+ // Calculate throughput
93
+ const throughput = duration > 0 ? this.requests.length / duration : 0;
94
+
95
+ return {
96
+ // Test metadata
97
+ totalRequests: this.requests.length,
98
+ duration: parseFloat(duration.toFixed(2)),
99
+
100
+ // Status code distribution
101
+ statusCodes: {
102
+ '2xx': statusCounts['2xx'] || 0,
103
+ '3xx': statusCounts['3xx'] || 0,
104
+ '4xx': statusCounts['4xx'] || 0,
105
+ '5xx': statusCounts['5xx'] || 0,
106
+ },
107
+
108
+ // Error counts
109
+ errors: errorCounts,
110
+ errorTotal: Object.values(errorCounts).reduce((a, b) => a + b, 0),
111
+
112
+ // Response time statistics
113
+ responseTime: {
114
+ min: parseFloat(minResponseTime.toFixed(2)),
115
+ max: parseFloat(maxResponseTime.toFixed(2)),
116
+ avg: parseFloat(avgResponseTime.toFixed(2)),
117
+ median: parseFloat(calculateMedian(responseTimes).toFixed(2)),
118
+ p95: parseFloat(calculatePercentile(responseTimes, 95).toFixed(2)),
119
+ p99: parseFloat(calculatePercentile(responseTimes, 99).toFixed(2)),
120
+ },
121
+
122
+ // Throughput metrics
123
+ throughput: parseFloat(throughput.toFixed(2)),
124
+ dataTransferred: totalDataTransferred,
125
+
126
+ // Success rate
127
+ successRate: parseFloat(
128
+ (
129
+ ((this.requests.length - Object.values(errorCounts).reduce((a, b) => a + b, 0)) /
130
+ this.requests.length) *
131
+ 100
132
+ ).toFixed(2)
133
+ ),
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Get empty metrics object (for when no requests were made)
139
+ * @private
140
+ * @returns {Object}
141
+ */
142
+ _getEmptyMetrics() {
143
+ return {
144
+ totalRequests: 0,
145
+ duration: 0,
146
+ statusCodes: {
147
+ '2xx': 0,
148
+ '3xx': 0,
149
+ '4xx': 0,
150
+ '5xx': 0,
151
+ },
152
+ errors: {},
153
+ errorTotal: 0,
154
+ responseTime: {
155
+ min: 0,
156
+ max: 0,
157
+ avg: 0,
158
+ median: 0,
159
+ p95: 0,
160
+ p99: 0,
161
+ },
162
+ throughput: 0,
163
+ dataTransferred: 0,
164
+ successRate: 0,
165
+ };
166
+ }
167
+ }
168
+
169
+ module.exports = MetricsCollector;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Burst Test Mode
3
+ * Sends simultaneous requests to measure peak capacity
4
+ */
5
+
6
+ const logger = require('../logger');
7
+ const { buildUrl } = require('../utils');
8
+
9
+ /**
10
+ * BurstTestMode class
11
+ * Launches N simultaneous requests and measures aggregate performance
12
+ */
13
+ class BurstTestMode {
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+
18
+ /**
19
+ * Run the burst test
20
+ * Launches burstClients simultaneous requests
21
+ *
22
+ * @param {Object} client - HTTP client with makeRequest() function
23
+ * @param {Object} metricsCollector - MetricsCollector instance
24
+ * @returns {Promise<Object>} Test results (MetricsCollector summary)
25
+ */
26
+ async run(client, metricsCollector) {
27
+ const { burstClients, method, timeout, followRedirects } = this.config;
28
+ const url = buildUrl(this.config);
29
+
30
+ logger.log(`Starting burst test: ${burstClients} simultaneous requests`);
31
+
32
+ const startTime = Date.now();
33
+
34
+ // Create array of N simultaneous requests
35
+ const requests = [];
36
+ for (let i = 0; i < burstClients; i++) {
37
+ requests.push(
38
+ client.makeRequest(url, {
39
+ method,
40
+ timeout,
41
+ followRedirects,
42
+ })
43
+ );
44
+ }
45
+
46
+ // Execute all requests in parallel
47
+ try {
48
+ const results = await Promise.all(requests);
49
+
50
+ // Record all results in metrics collector
51
+ results.forEach((result, index) => {
52
+ metricsCollector.recordRequest(result);
53
+ logger.debug(`Burst request ${index + 1}: ${result.statusCode || 'ERROR'}`);
54
+ });
55
+ } catch (err) {
56
+ logger.error(`Burst test error: ${err.message}`);
57
+ }
58
+
59
+ metricsCollector.complete();
60
+ const elapsed = Date.now() - startTime;
61
+
62
+ logger.log(`Burst test complete after ${(elapsed / 1000).toFixed(2)}s`);
63
+
64
+ return metricsCollector.getAggregateMetrics();
65
+ }
66
+ }
67
+
68
+ module.exports = BurstTestMode;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Steady-State Test Mode
3
+ * Runs multiple clients making periodic requests at fixed intervals
4
+ */
5
+
6
+ const logger = require('../logger');
7
+ const { sleep, buildUrl } = require('../utils');
8
+
9
+ /**
10
+ * SteadyStateTestMode class
11
+ * Creates multiple concurrent clients, each making requests at a fixed interval
12
+ */
13
+ class SteadyStateTestMode {
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+
18
+ /**
19
+ * Run the steady-state test
20
+ * Spawns N clients that each make requests at the specified interval
21
+ * for the specified duration
22
+ *
23
+ * @param {Object} client - HTTP client with makeRequest() function
24
+ * @param {Object} metricsCollector - MetricsCollector instance
25
+ * @returns {Promise<Object>} Test results (MetricsCollector summary)
26
+ */
27
+ async run(client, metricsCollector) {
28
+ const { clients: numClients, interval, duration } = this.config;
29
+
30
+ const testDurationMs = duration * 1000;
31
+ const url = buildUrl(this.config);
32
+
33
+ logger.log(
34
+ `Starting steady-state test: ${numClients} clients, ${interval}ms interval, ${duration}s duration`
35
+ );
36
+
37
+ const startTime = Date.now();
38
+ const clientPromises = [];
39
+
40
+ // Spawn N client tasks
41
+ for (let i = 0; i < numClients; i++) {
42
+ const clientId = i + 1;
43
+ clientPromises.push(this._runClient(clientId, client, metricsCollector, url, testDurationMs));
44
+ }
45
+
46
+ // Wait for all clients to finish
47
+ try {
48
+ await Promise.all(clientPromises);
49
+ } catch (err) {
50
+ logger.error(`Test error: ${err.message}`);
51
+ }
52
+
53
+ metricsCollector.complete();
54
+ const elapsed = Date.now() - startTime;
55
+
56
+ logger.log(`Steady-state test complete after ${(elapsed / 1000).toFixed(2)}s`);
57
+
58
+ return metricsCollector.getAggregateMetrics();
59
+ }
60
+
61
+ /**
62
+ * Run a single client: make requests at the specified interval until duration is reached
63
+ * @private
64
+ */
65
+ async _runClient(clientId, client, metricsCollector, url, testDurationMs) {
66
+ const { interval, method, timeout, followRedirects } = this.config;
67
+ const startTime = Date.now();
68
+ let requestCount = 0;
69
+
70
+ while (Date.now() - startTime < testDurationMs) {
71
+ try {
72
+ const result = await client.makeRequest(url, {
73
+ method,
74
+ timeout,
75
+ followRedirects,
76
+ });
77
+
78
+ metricsCollector.recordRequest(result);
79
+ requestCount++;
80
+
81
+ logger.debug(
82
+ `Client ${clientId}: Request ${requestCount} - ${result.statusCode || 'ERROR'}`
83
+ );
84
+
85
+ // Wait for the interval before next request
86
+ const elapsedSinceStart = Date.now() - startTime;
87
+ const nextRequestTime = (requestCount + 1) * interval;
88
+
89
+ if (nextRequestTime > elapsedSinceStart && nextRequestTime <= testDurationMs) {
90
+ const waitTime = nextRequestTime - elapsedSinceStart;
91
+ await sleep(waitTime);
92
+ }
93
+ } catch (err) {
94
+ logger.debug(`Client ${clientId}: Request error - ${err.message}`);
95
+ }
96
+ }
97
+
98
+ logger.debug(`Client ${clientId} completed ${requestCount} requests`);
99
+ }
100
+ }
101
+
102
+ module.exports = SteadyStateTestMode;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Test Mode Factory
3
+ * Instantiates the appropriate test mode based on configuration
4
+ */
5
+
6
+ const SteadyStateTestMode = require('./SteadyStateTestMode');
7
+ const BurstTestMode = require('./BurstTestMode');
8
+
9
+ /**
10
+ * Get test mode instance
11
+ * @param {Object} config - Configuration object
12
+ * @returns {Object} Test mode instance with run() method
13
+ * @throws {Error} if mode is not recognized
14
+ */
15
+ function getTestMode(config) {
16
+ switch (config.mode.toLowerCase()) {
17
+ case 'steady-state':
18
+ return new SteadyStateTestMode(config);
19
+ case 'burst':
20
+ return new BurstTestMode(config);
21
+ default:
22
+ throw new Error(`Unknown test mode: ${config.mode}. Valid modes: steady-state, burst`);
23
+ }
24
+ }
25
+
26
+ module.exports = {
27
+ getTestMode,
28
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * User Agent resolver
3
+ * Translates browser names to realistic user agent strings
4
+ */
5
+
6
+ const UserAgent = require('user-agents');
7
+
8
+ /**
9
+ * Get a user agent string based on browser preference
10
+ *
11
+ * @param {string} browser - Browser name: firefox, chrome, safari, or null for default
12
+ * @returns {string} User agent string
13
+ */
14
+ function getUserAgent(browser = 'chrome') {
15
+ const ua = new UserAgent();
16
+
17
+ switch (browser) {
18
+ case 'firefox':
19
+ return ua.toString({ ua: 'Firefox' });
20
+ case 'chrome':
21
+ return ua.toString({ ua: 'Chrome' });
22
+ case 'safari':
23
+ return ua.toString({ ua: 'Safari' });
24
+ default:
25
+ return ua.toString();
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Get a custom user agent or default based on browser preference
31
+ *
32
+ * @param {Object} config - Configuration object
33
+ * @param {string} config.customUserAgent - Custom UA string (takes precedence)
34
+ * @param {string} config.browser - Browser name (firefox, chrome, safari)
35
+ * @returns {string} User agent string
36
+ */
37
+ function resolveUserAgent(config) {
38
+ // Custom UA takes precedence
39
+ if (config.customUserAgent) {
40
+ return config.customUserAgent;
41
+ }
42
+
43
+ // Use browser preference, default to chrome
44
+ const browser = config.browser || 'chrome';
45
+ return getUserAgent(browser);
46
+ }
47
+
48
+ module.exports = {
49
+ getUserAgent,
50
+ resolveUserAgent,
51
+ };
package/src/utils.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Utility functions for the application
3
+ */
4
+
5
+ /**
6
+ * Calculate percentile from sorted array
7
+ * @param {number[]} sortedArray - Array of values, sorted in ascending order
8
+ * @param {number} percentile - Percentile to calculate (0-100)
9
+ * @returns {number} Percentile value
10
+ */
11
+ function calculatePercentile(sortedArray, percentile) {
12
+ if (sortedArray.length === 0) {
13
+ return 0;
14
+ }
15
+ if (percentile === 50) {
16
+ return calculateMedian(sortedArray);
17
+ }
18
+
19
+ const index = (percentile / 100) * (sortedArray.length - 1);
20
+ const lower = Math.floor(index);
21
+ const upper = Math.ceil(index);
22
+ const weight = index % 1;
23
+
24
+ if (lower === upper) {
25
+ return sortedArray[lower];
26
+ }
27
+
28
+ return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
29
+ }
30
+
31
+ /**
32
+ * Calculate median from sorted array
33
+ * @param {number[]} sortedArray - Array of values, sorted in ascending order
34
+ * @returns {number} Median value
35
+ */
36
+ function calculateMedian(sortedArray) {
37
+ if (sortedArray.length === 0) {
38
+ return 0;
39
+ }
40
+ const mid = Math.floor(sortedArray.length / 2);
41
+ if (sortedArray.length % 2 === 1) {
42
+ return sortedArray[mid];
43
+ }
44
+ return (sortedArray[mid - 1] + sortedArray[mid]) / 2;
45
+ }
46
+
47
+ /**
48
+ * Build full URL from configuration
49
+ * @param {Object} config - Configuration object
50
+ * @returns {string} Full URL
51
+ */
52
+ function buildUrl(config) {
53
+ const protocol = config.https ? 'https' : 'http';
54
+ const domain = config.domain.replace(/^(https?:\/\/)/, ''); // Remove protocol if present
55
+ const endpoint = config.endpoint.startsWith('/') ? config.endpoint : `/${config.endpoint}`;
56
+ return `${protocol}://${domain}${endpoint}`;
57
+ }
58
+
59
+ /**
60
+ * Format bytes to human-readable format
61
+ * @param {number} bytes - Number of bytes
62
+ * @returns {string} Formatted string
63
+ */
64
+ function formatBytes(bytes) {
65
+ if (bytes === 0) {
66
+ return '0 B';
67
+ }
68
+ const k = 1024;
69
+ const sizes = ['B', 'KB', 'MB', 'GB'];
70
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
71
+ return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
72
+ }
73
+
74
+ /**
75
+ * Format milliseconds to seconds
76
+ * @param {number} ms - Milliseconds
77
+ * @returns {string} Formatted string
78
+ */
79
+ function formatDuration(ms) {
80
+ return (ms / 1000).toFixed(2) + 's';
81
+ }
82
+
83
+ /**
84
+ * Sleep for specified milliseconds
85
+ * @param {number} ms - Milliseconds to sleep
86
+ * @returns {Promise<void>}
87
+ */
88
+ function sleep(ms) {
89
+ return new Promise(resolve => setTimeout(resolve, ms));
90
+ }
91
+
92
+ module.exports = {
93
+ calculatePercentile,
94
+ calculateMedian,
95
+ buildUrl,
96
+ formatBytes,
97
+ formatDuration,
98
+ sleep,
99
+ };