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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "word-stress",
3
+ "version": "1.0.0",
4
+ "description": "A command-line tool for stress-testing WordPress and WooCommerce sites",
5
+ "main": "src/main.js",
6
+ "bin": {
7
+ "word-stress": "./bin/word-stress"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/word-stress",
11
+ "dev": "node bin/word-stress",
12
+ "format": "prettier --write \"src/**/*.js\" \"bin/word-stress\"",
13
+ "format:check": "prettier --check \"src/**/*.js\" \"bin/word-stress\"",
14
+ "lint": "eslint src bin/word-stress",
15
+ "lint:fix": "eslint src bin/word-stress --fix",
16
+ "test": "node bin/word-stress --help && echo 'CLI help test passed'"
17
+ },
18
+ "keywords": [
19
+ "wordpress",
20
+ "woocommerce",
21
+ "stress-testing",
22
+ "load-testing",
23
+ "performance",
24
+ "cli"
25
+ ],
26
+ "author": "headwall",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/headwalluk/word-stress.git"
30
+ },
31
+ "homepage": "https://github.com/headwalluk/word-stress#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/headwalluk/word-stress/issues"
34
+ },
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=21.0.0"
38
+ },
39
+ "dependencies": {
40
+ "chalk": "^5.6.2",
41
+ "cli-table3": "^0.6.5",
42
+ "commander": "^14.0.2",
43
+ "dotenv": "^17.2.3",
44
+ "ora": "^9.0.0",
45
+ "user-agents": "^1.1.669"
46
+ },
47
+ "devDependencies": {
48
+ "eslint": "^9.39.2",
49
+ "prettier": "^3.7.4"
50
+ }
51
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * CLI command definitions using commander.js
3
+ */
4
+
5
+ const { Command } = require('commander');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Set up CLI commands
11
+ * @returns {Command} commander program
12
+ */
13
+ function setupCli() {
14
+ // Read package.json for version
15
+ const packageJsonPath = path.join(__dirname, '../../package.json');
16
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('word-stress')
22
+ .description('Stress-testing CLI tool for WordPress and WooCommerce sites')
23
+ .version(packageJson.version);
24
+
25
+ program
26
+ .argument('<domain>', 'Domain to test (e.g., example.com)')
27
+ .option('--clients <n>', 'Number of parallel clients for steady-state mode (default: 5)', '5')
28
+ .option('--interval <ms>', 'Milliseconds between each client request (default: 1000)', '1000')
29
+ .option('--duration <s>', 'Test duration in seconds (default: 60)', '60')
30
+ .option(
31
+ '--mode <mode>',
32
+ 'Test mode: steady-state or burst (default: steady-state)',
33
+ 'steady-state'
34
+ )
35
+ .option(
36
+ '--burst-clients <n>',
37
+ 'Number of simultaneous requests for burst mode (required for burst)'
38
+ )
39
+ .option('--endpoint <path>', 'URL path/endpoint to test (default: /)', '/')
40
+ .option('--method <METHOD>', 'HTTP method: GET, POST, PUT, DELETE, PATCH (default: GET)', 'GET')
41
+ .option('--https <on|off>', 'Use HTTPS (default: on)', 'on')
42
+ .option('--timeout <ms>', 'Request timeout in milliseconds (default: 30000)', '30000')
43
+ .option('--follow-redirects <on|off>', 'Follow HTTP redirects (default: on)', 'on')
44
+ .option(
45
+ '--browser <name>',
46
+ 'Browser type for user agent: firefox, chrome, safari (default: chrome)'
47
+ )
48
+ .option('--user-agent <string>', 'Custom user agent string (overrides --browser)')
49
+ .option('--output <format>', 'Output format: table, json, csv (default: table)', 'table')
50
+ .option('--verbose', 'Enable verbose logging', false);
51
+
52
+ return program;
53
+ }
54
+
55
+ module.exports = {
56
+ setupCli,
57
+ };
package/src/config.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Centralized configuration module
3
+ * Loads and normalizes all configuration from CLI arguments and environment variables
4
+ */
5
+
6
+ require('dotenv').config();
7
+
8
+ /**
9
+ * Default configuration values
10
+ */
11
+ const defaults = {
12
+ // Test parameters - Steady-State mode
13
+ clients: 5,
14
+ interval: 1000, // milliseconds
15
+ duration: 60, // seconds
16
+
17
+ // Test parameters - Burst mode
18
+ burstClients: null,
19
+
20
+ // Common parameters
21
+ domain: null,
22
+ endpoint: '/',
23
+ method: 'GET',
24
+ https: true,
25
+ timeout: 30000,
26
+ followRedirects: true,
27
+ output: 'table', // table, json, csv
28
+
29
+ // User agent
30
+ browser: 'chrome', // firefox, chrome, safari
31
+ customUserAgent: null,
32
+
33
+ // Test mode
34
+ mode: 'steady-state', // steady-state or burst
35
+
36
+ // Logging
37
+ logLevel: process.env.LOG_LEVEL || 'INFO',
38
+ };
39
+
40
+ /**
41
+ * Build configuration from CLI arguments
42
+ * @param {Object} args - Parsed command line arguments
43
+ * @returns {Object} Configuration object
44
+ */
45
+ function buildConfig(args) {
46
+ const config = { ...defaults };
47
+
48
+ // Domain is required positional argument
49
+ if (args.domain) {
50
+ config.domain = args.domain;
51
+ }
52
+
53
+ // Steady-state parameters
54
+ if (args.clients !== undefined) {
55
+ config.clients = parseInt(args.clients, 10);
56
+ }
57
+ if (args.interval !== undefined) {
58
+ config.interval = parseInt(args.interval, 10);
59
+ }
60
+ if (args.duration !== undefined) {
61
+ config.duration = parseInt(args.duration, 10);
62
+ }
63
+
64
+ // Burst parameters
65
+ if (args.mode === 'burst') {
66
+ config.mode = 'burst';
67
+ if (args.burstClients !== undefined) {
68
+ config.burstClients = parseInt(args.burstClients, 10);
69
+ }
70
+ }
71
+
72
+ // Common parameters
73
+ if (args.endpoint !== undefined) {
74
+ config.endpoint = args.endpoint;
75
+ }
76
+ if (args.method !== undefined) {
77
+ config.method = args.method.toUpperCase();
78
+ }
79
+ if (args.https !== undefined) {
80
+ config.https = args.https === 'on' || args.https === true;
81
+ }
82
+ if (args.timeout !== undefined) {
83
+ config.timeout = parseInt(args.timeout, 10);
84
+ }
85
+ if (args.followRedirects !== undefined) {
86
+ config.followRedirects = args.followRedirects === 'on' || args.followRedirects === true;
87
+ }
88
+ if (args.output !== undefined) {
89
+ config.output = args.output.toLowerCase();
90
+ }
91
+
92
+ // User agent parameters
93
+ if (args.userAgent !== undefined) {
94
+ config.customUserAgent = args.userAgent;
95
+ }
96
+ if (args.browser !== undefined) {
97
+ config.browser = args.browser.toLowerCase();
98
+ }
99
+
100
+ return config;
101
+ }
102
+
103
+ /**
104
+ * Validate configuration
105
+ * @param {Object} config
106
+ * @throws {Error} if configuration is invalid
107
+ */
108
+ function validate(config) {
109
+ if (!config.domain) {
110
+ throw new Error('Domain is required');
111
+ }
112
+
113
+ if (config.mode === 'burst') {
114
+ if (!config.burstClients) {
115
+ throw new Error('--burst-clients is required for burst mode');
116
+ }
117
+ if (config.burstClients < 1) {
118
+ throw new Error('--burst-clients must be greater than 0');
119
+ }
120
+ } else {
121
+ if (config.clients < 1) {
122
+ throw new Error('--clients must be greater than 0');
123
+ }
124
+ if (config.interval < 1) {
125
+ throw new Error('--interval must be greater than 0');
126
+ }
127
+ if (config.duration < 1) {
128
+ throw new Error('--duration must be greater than 0');
129
+ }
130
+ }
131
+
132
+ const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
133
+ if (!validMethods.includes(config.method)) {
134
+ throw new Error(`--method must be one of: ${validMethods.join(', ')}`);
135
+ }
136
+
137
+ const validOutputs = ['table', 'json', 'csv'];
138
+ if (!validOutputs.includes(config.output)) {
139
+ throw new Error(`--output must be one of: ${validOutputs.join(', ')}`);
140
+ }
141
+
142
+ if (config.timeout < 1) {
143
+ throw new Error('--timeout must be greater than 0');
144
+ }
145
+
146
+ // Validate browser if not using custom user agent
147
+ if (!config.customUserAgent) {
148
+ const validBrowsers = ['firefox', 'chrome', 'safari'];
149
+ if (!validBrowsers.includes(config.browser)) {
150
+ throw new Error(`--browser must be one of: ${validBrowsers.join(', ')}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ module.exports = {
156
+ buildConfig,
157
+ validate,
158
+ defaults,
159
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CSV Formatter
3
+ * Outputs results as CSV for spreadsheet import
4
+ */
5
+
6
+ /**
7
+ * Escape CSV field value
8
+ * @param {string|number} value - Value to escape
9
+ * @returns {string} CSV-escaped value
10
+ */
11
+ function escapeCsvField(value) {
12
+ const stringValue = String(value);
13
+
14
+ // If contains comma, newline, or quote, wrap in quotes and escape quotes
15
+ if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('"')) {
16
+ return `"${stringValue.replace(/"/g, '""')}"`;
17
+ }
18
+
19
+ return stringValue;
20
+ }
21
+
22
+ /**
23
+ * Format results as CSV
24
+ * @param {Object} results - Test results
25
+ * @returns {string} CSV formatted output
26
+ */
27
+ function format(results) {
28
+ const rows = [];
29
+
30
+ // Header row
31
+ rows.push('Metric,Value');
32
+
33
+ // Summary metrics
34
+ rows.push(`Total Requests,${results.totalRequests}`);
35
+ rows.push(`Duration (seconds),${results.duration.toFixed(2)}`);
36
+ rows.push(`Success Rate (%),${results.successRate.toFixed(2)}`);
37
+ rows.push(`Throughput (req/s),${results.throughput.toFixed(2)}`);
38
+ rows.push(`Data Transferred (bytes),${results.dataTransferred}`);
39
+
40
+ // Response times
41
+ rows.push('');
42
+ rows.push('Response Time Metrics');
43
+ rows.push(`Min (ms),${results.responseTime.min}`);
44
+ rows.push(`Max (ms),${results.responseTime.max}`);
45
+ rows.push(`Average (ms),${results.responseTime.avg.toFixed(2)}`);
46
+ rows.push(`Median P50 (ms),${results.responseTime.median}`);
47
+ rows.push(`P95 (ms),${results.responseTime.p95.toFixed(2)}`);
48
+ rows.push(`P99 (ms),${results.responseTime.p99.toFixed(2)}`);
49
+
50
+ // Status codes
51
+ rows.push('');
52
+ rows.push('Status Code Distribution');
53
+ rows.push(`2xx Success,${results.statusCodes['2xx']}`);
54
+ rows.push(`3xx Redirect,${results.statusCodes['3xx']}`);
55
+ rows.push(`4xx Client Error,${results.statusCodes['4xx']}`);
56
+ rows.push(`5xx Server Error,${results.statusCodes['5xx']}`);
57
+
58
+ // Errors
59
+ if (results.errorTotal > 0) {
60
+ rows.push('');
61
+ rows.push('Error Details');
62
+ Object.entries(results.errors).forEach(([error, count]) => {
63
+ rows.push(`${escapeCsvField(error)},${count}`);
64
+ });
65
+ }
66
+
67
+ return rows.join('\n');
68
+ }
69
+
70
+ module.exports = {
71
+ format,
72
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * JSON Formatter
3
+ * Outputs results as JSON
4
+ */
5
+
6
+ /**
7
+ * Format results as JSON
8
+ * @param {Object} results - Test results
9
+ * @returns {string} JSON formatted output
10
+ */
11
+ function format(results) {
12
+ return JSON.stringify(results, null, 2);
13
+ }
14
+
15
+ module.exports = {
16
+ format,
17
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Table Formatter
3
+ * Outputs results as pretty ASCII tables
4
+ */
5
+
6
+ const Table = require('cli-table3');
7
+ const { formatBytes } = require('../utils');
8
+
9
+ /**
10
+ * Format results as pretty tables
11
+ * @param {Object} results - Test results
12
+ * @returns {string} Formatted output
13
+ */
14
+ function format(results) {
15
+ const output = [];
16
+
17
+ // Summary Table
18
+ const summaryTable = new Table({
19
+ head: ['Metric', 'Value'],
20
+ style: { head: [], border: ['cyan'] },
21
+ colWidths: [25, 40],
22
+ });
23
+
24
+ summaryTable.push(
25
+ ['Total Requests', results.totalRequests.toString()],
26
+ ['Duration', `${results.duration.toFixed(2)}s`],
27
+ ['Success Rate', `${results.successRate.toFixed(2)}%`],
28
+ ['Throughput', `${results.throughput.toFixed(2)} req/s`],
29
+ ['Data Transferred', formatBytes(results.dataTransferred)]
30
+ );
31
+
32
+ output.push('Summary');
33
+ output.push(summaryTable.toString());
34
+ output.push('');
35
+
36
+ // Response Times Table
37
+ const responseTable = new Table({
38
+ head: ['Metric', 'Value'],
39
+ style: { head: [], border: ['cyan'] },
40
+ colWidths: [25, 40],
41
+ });
42
+
43
+ responseTable.push(
44
+ ['Min', `${results.responseTime.min}ms`],
45
+ ['Max', `${results.responseTime.max}ms`],
46
+ ['Average', `${results.responseTime.avg.toFixed(2)}ms`],
47
+ ['Median (P50)', `${results.responseTime.median}ms`],
48
+ ['P95', `${results.responseTime.p95.toFixed(2)}ms`],
49
+ ['P99', `${results.responseTime.p99.toFixed(2)}ms`]
50
+ );
51
+
52
+ output.push('Response Times');
53
+ output.push(responseTable.toString());
54
+ output.push('');
55
+
56
+ // Status Codes Table
57
+ const statusTable = new Table({
58
+ head: ['Status Code', 'Count'],
59
+ style: { head: [], border: ['cyan'] },
60
+ colWidths: [25, 40],
61
+ });
62
+
63
+ statusTable.push(
64
+ ['2xx Success', results.statusCodes['2xx'].toString()],
65
+ ['3xx Redirect', results.statusCodes['3xx'].toString()],
66
+ ['4xx Client Error', results.statusCodes['4xx'].toString()],
67
+ ['5xx Server Error', results.statusCodes['5xx'].toString()]
68
+ );
69
+
70
+ output.push('Status Codes');
71
+ output.push(statusTable.toString());
72
+ output.push('');
73
+
74
+ // Errors Table (if any)
75
+ if (results.errorTotal > 0) {
76
+ const errorTable = new Table({
77
+ head: ['Error Type', 'Count'],
78
+ style: { head: [], border: ['cyan'] },
79
+ colWidths: [40, 20],
80
+ });
81
+
82
+ Object.entries(results.errors).forEach(([error, count]) => {
83
+ errorTable.push([error, count.toString()]);
84
+ });
85
+
86
+ output.push('Errors');
87
+ output.push(errorTable.toString());
88
+ }
89
+
90
+ return output.join('\n');
91
+ }
92
+
93
+ module.exports = {
94
+ format,
95
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Formatter Factory
3
+ * Selects and returns the appropriate formatter based on output format
4
+ */
5
+
6
+ const TableFormatter = require('./TableFormatter');
7
+ const JsonFormatter = require('./JsonFormatter');
8
+ const CsvFormatter = require('./CsvFormatter');
9
+
10
+ /**
11
+ * Get formatter by name
12
+ * @param {string} format - Format name: 'table', 'json', or 'csv'
13
+ * @returns {Object} Formatter with format() method
14
+ * @throws {Error} if format is not recognized
15
+ */
16
+ function getFormatter(format) {
17
+ switch (format.toLowerCase()) {
18
+ case 'table':
19
+ return TableFormatter;
20
+ case 'json':
21
+ return JsonFormatter;
22
+ case 'csv':
23
+ return CsvFormatter;
24
+ default:
25
+ throw new Error(`Unknown format: ${format}. Valid formats: table, json, csv`);
26
+ }
27
+ }
28
+
29
+ module.exports = {
30
+ getFormatter,
31
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * HTTP Client using Node's native Fetch API
3
+ *
4
+ * Handles HTTP requests with timeout support, error classification,
5
+ * and metrics collection (response time, size, status code).
6
+ */
7
+
8
+ const logger = require('../logger');
9
+
10
+ /**
11
+ * Make an HTTP request and return metrics
12
+ *
13
+ * @param {string} url - Full URL to request
14
+ * @param {Object} options - Request options
15
+ * @param {string} options.method - HTTP method (GET, POST, etc.) - default: GET
16
+ * @param {number} options.timeout - Timeout in milliseconds - default: 30000
17
+ * @param {boolean} options.followRedirects - Follow redirects - default: true
18
+ * @param {string} options.userAgent - User agent string - default: Node.js default
19
+ * @returns {Promise<Object>} Result object with metrics
20
+ * - statusCode: HTTP status code or null
21
+ * - responseTime: Response time in milliseconds
22
+ * - size: Response body size in bytes
23
+ * - error: Error string or null
24
+ * - errorType: 'network' | 'timeout' | 'http' | null
25
+ */
26
+ async function makeRequest(url, options = {}) {
27
+ const { method = 'GET', timeout = 30000, followRedirects = true, userAgent = null } = options;
28
+
29
+ const startTime = Date.now();
30
+ let statusCode = null;
31
+ let size = 0;
32
+ let error = null;
33
+ let errorType = null;
34
+
35
+ try {
36
+ // Create abort controller for timeout handling
37
+ const controller = new AbortController();
38
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
39
+
40
+ const fetchOptions = {
41
+ method,
42
+ signal: controller.signal,
43
+ redirect: followRedirects ? 'follow' : 'manual',
44
+ headers: {},
45
+ };
46
+
47
+ // Add User-Agent header if provided
48
+ if (userAgent) {
49
+ fetchOptions.headers['User-Agent'] = userAgent;
50
+ }
51
+
52
+ let response;
53
+ try {
54
+ response = await fetch(url, fetchOptions);
55
+ } finally {
56
+ clearTimeout(timeoutId);
57
+ }
58
+
59
+ statusCode = response.status;
60
+
61
+ // Extract response size from Content-Length header or read body
62
+ const contentLength = response.headers.get('content-length');
63
+ if (contentLength) {
64
+ size = parseInt(contentLength, 10);
65
+ } else {
66
+ // If no Content-Length, read the body to get actual size
67
+ const body = await response.text();
68
+ size = Buffer.byteLength(body, 'utf8');
69
+ }
70
+
71
+ // Log 4xx and 5xx responses as warnings, not errors
72
+ if (response.status >= 400) {
73
+ logger.debug(`HTTP ${response.status} from ${url}`);
74
+ }
75
+ } catch (err) {
76
+ // Classify the error
77
+ if (err.name === 'AbortError') {
78
+ error = 'Request timeout';
79
+ errorType = 'timeout';
80
+ } else if (
81
+ err.code === 'ECONNREFUSED' ||
82
+ err.code === 'ENOTFOUND' ||
83
+ err.code === 'EHOSTUNREACH'
84
+ ) {
85
+ error = `Network error: ${err.code}`;
86
+ errorType = 'network';
87
+ } else if (err.message.includes('fetch')) {
88
+ error = `Network error: ${err.message}`;
89
+ errorType = 'network';
90
+ } else {
91
+ error = err.message;
92
+ errorType = 'unknown';
93
+ }
94
+
95
+ logger.debug(`Request failed to ${url}: ${error}`);
96
+ }
97
+
98
+ const responseTime = Date.now() - startTime;
99
+
100
+ return {
101
+ statusCode,
102
+ responseTime,
103
+ size,
104
+ error,
105
+ errorType,
106
+ };
107
+ }
108
+
109
+ module.exports = {
110
+ makeRequest,
111
+ };
package/src/logger.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Custom logger with configurable log levels
3
+ * Respects LOG_LEVEL environment variable
4
+ */
5
+
6
+ const LOG_LEVELS = {
7
+ DEBUG: 0,
8
+ INFO: 1,
9
+ WARN: 2,
10
+ ERROR: 3,
11
+ };
12
+
13
+ class Logger {
14
+ constructor(level = 'INFO') {
15
+ this.currentLevel = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
16
+ }
17
+
18
+ /**
19
+ * Log an info message
20
+ * @param {string} message
21
+ */
22
+ log(message) {
23
+ if (this.currentLevel <= LOG_LEVELS.INFO) {
24
+ console.log(message);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Log a debug message
30
+ * @param {string} message
31
+ */
32
+ debug(message) {
33
+ if (this.currentLevel <= LOG_LEVELS.DEBUG) {
34
+ console.log(`[DEBUG] ${message}`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Log a warning message
40
+ * @param {string} message
41
+ */
42
+ warn(message) {
43
+ if (this.currentLevel <= LOG_LEVELS.WARN) {
44
+ console.warn(`[WARN] ${message}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Log an error message
50
+ * @param {string} message
51
+ */
52
+ error(message) {
53
+ if (this.currentLevel <= LOG_LEVELS.ERROR) {
54
+ console.error(`[ERROR] ${message}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ module.exports = new Logger(process.env.LOG_LEVEL || 'INFO');