loadtest 5.1.2 → 6.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/lib/loadtest.js CHANGED
@@ -1,28 +1,13 @@
1
- 'use strict';
1
+ import * as http from 'http'
2
+ import * as https from 'https'
3
+ import * as httpClient from './httpClient.js'
4
+ import * as websocket from './websocket.js'
5
+ import {Latency} from './latency.js'
6
+ import {HighResolutionTimer} from './hrtimer.js'
7
+ import {processOptions} from './options.js'
2
8
 
3
- /**
4
- * Load Test a URL, website or websocket.
5
- * (C) 2013 Alex Fernández.
6
- */
7
-
8
-
9
- // requires
10
- const Log = require('log');
11
- const http = require('http');
12
- const https = require('https');
13
- const testing = require('testing');
14
- const httpClient = require('./httpClient.js');
15
- const websocket = require('./websocket.js');
16
- const {Latency} = require('./latency.js');
17
- const {HighResolutionTimer} = require('./hrtimer.js');
18
-
19
- // globals
20
- const log = new Log('info');
21
-
22
- // constants
23
9
  const SHOW_INTERVAL_MS = 5000;
24
10
 
25
- // init
26
11
  http.globalAgent.maxSockets = 1000;
27
12
  https.globalAgent.maxSockets = 1000;
28
13
 
@@ -41,42 +26,23 @@ https.globalAgent.maxSockets = 1000;
41
26
  * - contentType: the MIME type to use for the body, default text/plain.
42
27
  * - requestsPerSecond: how many requests per second to send.
43
28
  * - agentKeepAlive: if true, then use connection keep-alive.
44
- * - debug: show debug messages.
45
- * - quiet: do not log any messages.
46
29
  * - indexParam: string to replace with a unique index.
47
30
  * - insecure: allow https using self-signed certs.
31
+ * - debug: show debug messages (deprecated).
32
+ * - quiet: do not log any messages (deprecated).
48
33
  * An optional callback will be called if/when the test finishes.
49
34
  */
50
- exports.loadTest = function(options, callback) {
51
- if (!options.url) {
52
- log.error('Missing URL in options');
53
- return;
54
- }
55
- options.concurrency = options.concurrency || 1;
56
- if (options.requestsPerSecond) {
57
- options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
58
- }
59
- if (options.debug) {
60
- log.level = Log.DEBUG;
61
- }
62
- if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
63
- log.error('Invalid URL %s, must be http://, https:// or ws://', options.url);
64
- return;
65
- }
66
- if (callback && !('quiet' in options)) {
67
- options.quiet = true;
68
- }
69
-
70
- if (options.url.startsWith('ws:')) {
71
- if (options.requestsPerSecond) {
72
- log.error('"requestsPerSecond" not supported for WebSockets');
35
+ export function loadTest(options, callback) {
36
+ processOptions(options, error => {
37
+ if (error) {
38
+ if (callback) return callback(error)
39
+ throw new error
73
40
  }
74
- }
75
-
76
- const operation = new Operation(options, callback);
77
- operation.start();
78
- return operation;
79
- };
41
+ const operation = new Operation(options, callback);
42
+ operation.start();
43
+ return operation;
44
+ })
45
+ }
80
46
 
81
47
  /**
82
48
  * Used to keep track of individual load test Operation runs.
@@ -122,9 +88,6 @@ class Operation {
122
88
  if (this.completedRequests == this.options.maxRequests) {
123
89
  this.stop();
124
90
  }
125
- if (this.requests > this.options.maxRequests) {
126
- log.debug('Should have no more running clients');
127
- }
128
91
  }
129
92
  if (this.running && next) {
130
93
  next();
@@ -202,97 +165,3 @@ class Operation {
202
165
  }
203
166
  }
204
167
 
205
- /**
206
- * A load test with max seconds.
207
- */
208
- function testMaxSeconds(callback) {
209
- const options = {
210
- url: 'http://localhost:7357/',
211
- maxSeconds: 0.1,
212
- concurrency: 1,
213
- quiet: true,
214
- };
215
- exports.loadTest(options, callback);
216
- }
217
-
218
-
219
- /**
220
- * A load test with max seconds.
221
- */
222
- function testWSEcho(callback) {
223
- const options = {
224
- url: 'ws://localhost:7357/',
225
- maxSeconds: 0.1,
226
- concurrency: 1,
227
- quiet: true,
228
- };
229
- exports.loadTest(options, callback);
230
- }
231
-
232
- function testIndexParam(callback) {
233
- const options = {
234
- url: 'http://localhost:7357/replace',
235
- concurrency:1,
236
- quiet: true,
237
- maxSeconds: 0.1,
238
- indexParam: "replace"
239
- };
240
- exports.loadTest(options, callback);
241
- }
242
-
243
- function testIndexParamWithBody(callback) {
244
- const options = {
245
- url: 'http://localhost:7357/replace',
246
- concurrency:1,
247
- quiet: true,
248
- maxSeconds: 0.1,
249
- indexParam: "replace",
250
- body: '{"id": "replace"}'
251
- };
252
- exports.loadTest(options, callback);
253
- }
254
-
255
- function testIndexParamWithCallback(callback) {
256
- const options = {
257
- url: 'http://localhost:7357/replace',
258
- concurrency:1,
259
- quiet: true,
260
- maxSeconds: 0.1,
261
- indexParam: "replace",
262
- indexParamCallback: function() {
263
- //https://gist.github.com/6174/6062387
264
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
265
- }
266
- };
267
- exports.loadTest(options, callback);
268
- }
269
-
270
- function testIndexParamWithCallbackAndBody(callback) {
271
- const options = {
272
- url: 'http://localhost:7357/replace',
273
- concurrency:1,
274
- quiet: true,
275
- maxSeconds: 0.1,
276
- body: '{"id": "replace"}',
277
- indexParam: "replace",
278
- indexParamCallback: function() {
279
- //https://gist.github.com/6174/6062387
280
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
281
- }
282
- };
283
- exports.loadTest(options, callback);
284
- }
285
-
286
-
287
- /**
288
- * Run all tests.
289
- */
290
- exports.test = function(callback) {
291
- testing.run([testMaxSeconds, testWSEcho, testIndexParam, testIndexParamWithBody, testIndexParamWithCallback, testIndexParamWithCallbackAndBody], callback);
292
- };
293
-
294
- // run tests if invoked directly
295
- if (__filename == process.argv[1]) {
296
- exports.test(testing.show);
297
- }
298
-
package/lib/options.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {readFile} from 'fs/promises'
4
+ import * as path from 'path'
5
+ import * as urlLib from 'url'
6
+ import {addHeaders} from '../lib/headers.js'
7
+ import {loadConfig} from '../lib/config.js'
8
+
9
+
10
+ export function processOptions(options, callback) {
11
+ processOptionsAsync(options).then(result => callback(null, result)).catch(error => callback(error))
12
+ }
13
+
14
+ async function processOptionsAsync(options) {
15
+ const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
16
+ if (!options.url) {
17
+ throw new Error('Missing URL in options')
18
+ }
19
+ options.concurrency = options.concurrency || 1;
20
+ if (options.requestsPerSecond) {
21
+ options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
22
+ }
23
+ if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
24
+ throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
25
+ }
26
+ if (options.url.startsWith('ws:')) {
27
+ if (options.requestsPerSecond) {
28
+ throw new Error(`"requestsPerSecond" not supported for WebSockets`);
29
+ }
30
+ }
31
+ const configuration = loadConfig();
32
+
33
+ options.agentKeepAlive = options.keepalive || options.agent || configuration.agentKeepAlive;
34
+ options.indexParam = options.index || configuration.indexParam;
35
+
36
+ //TODO: add index Param
37
+ // Allow a post body string in options
38
+ // Ex -P '{"foo": "bar"}'
39
+ if (options.postBody) {
40
+ options.method = 'POST';
41
+ options.body = options.postBody;
42
+ }
43
+ if (options.postFile) {
44
+ options.method = 'POST';
45
+ options.body = await readBody(options.postFile, '-p');
46
+ }
47
+ if (options.data) {
48
+ options.body = JSON.parse(options.data);
49
+ }
50
+ if (options.method) {
51
+ const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
52
+ if (acceptedMethods.indexOf(options.method) === -1) {
53
+ options.method = 'GET';
54
+ }
55
+ }
56
+ if (options.putFile) {
57
+ options.method = 'PUT';
58
+ options.body = await readBody(options.putFile, '-u');
59
+ }
60
+ if (options.patchBody) {
61
+ options.method = 'PATCH';
62
+ options.body = options.patchBody;
63
+ }
64
+ if (options.patchFile) {
65
+ options.method = 'PATCH';
66
+ options.body = await readBody(options.patchFile, '-a');
67
+ }
68
+ if (!options.method) {
69
+ options.method = configuration.method;
70
+ }
71
+ if (!options.body) {
72
+ if(configuration.body) {
73
+ options.body = configuration.body;
74
+ } else if (configuration.file) {
75
+ options.body = await readBody(configuration.file, 'configuration.request.file');
76
+ }
77
+ }
78
+ options.requestsPerSecond = options.rps ? parseFloat(options.rps) : configuration.requestsPerSecond;
79
+ if (!options.key) {
80
+ options.key = configuration.key;
81
+ }
82
+ if (options.key) {
83
+ options.key = await readFile(options.key)
84
+ }
85
+ if (!options.cert) {
86
+ options.cert = configuration.cert;
87
+ }
88
+ if (options.cert) {
89
+ options.cert = await readFile(options.cert);
90
+ }
91
+
92
+ const defaultHeaders = options.headers || !configuration.headers ? {} : configuration.headers;
93
+ defaultHeaders['host'] = urlLib.parse(options.url).host;
94
+ defaultHeaders['user-agent'] = 'loadtest/' + packageJson.version;
95
+ defaultHeaders['accept'] = '*/*';
96
+
97
+ if (options.headers) {
98
+ addHeaders(options.headers, defaultHeaders);
99
+ console.log('headers: %s, %j', typeof defaultHeaders, defaultHeaders);
100
+ }
101
+ options.headers = defaultHeaders;
102
+
103
+ if (!options.requestGenerator) {
104
+ options.requestGenerator = configuration.requestGenerator;
105
+ }
106
+ if (typeof options.requestGenerator == 'string') {
107
+ options.requestGenerator = await import(options.requestGenerator)
108
+ }
109
+
110
+ // Use configuration file for other values
111
+ if(!options.maxRequests) {
112
+ options.maxRequests = configuration.maxRequests;
113
+ }
114
+ if(!options.concurrency) {
115
+ options.concurrency = configuration.concurrency;
116
+ }
117
+ if(!options.maxSeconds) {
118
+ options.maxSeconds = configuration.maxSeconds;
119
+ }
120
+ if(!options.timeout && configuration.timeout) {
121
+ options.timeout = configuration.timeout;
122
+ }
123
+ if(!options.contentType) {
124
+ options.contentType = configuration.contentType;
125
+ }
126
+ if(!options.cookies) {
127
+ options.cookies = configuration.cookies;
128
+ }
129
+ if(!options.secureProtocol) {
130
+ options.secureProtocol = configuration.secureProtocol;
131
+ }
132
+ if(!options.insecure) {
133
+ options.insecure = configuration.insecure;
134
+ }
135
+ if(!options.recover) {
136
+ options.recover = configuration.recover;
137
+ }
138
+ if(!options.proxy) {
139
+ options.proxy = configuration.proxy;
140
+ }
141
+ }
142
+
143
+ async function readBody(filename, option) {
144
+ if (typeof filename !== 'string') {
145
+ throw new Error(`Invalid file to open with ${option}: ${filename}`);
146
+ }
147
+
148
+ if (path.extname(filename) === '.js') {
149
+ return await import(new URL(filename, `file://${process.cwd()}/`))
150
+ }
151
+
152
+ const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
153
+
154
+ return ret;
155
+ }
156
+
package/lib/testserver.js CHANGED
@@ -1,24 +1,9 @@
1
- 'use strict';
1
+ import * as http from 'http'
2
+ import {server as WebSocketServer} from 'websocket'
3
+ import * as util from 'util'
4
+ import * as net from 'net'
5
+ import {Latency} from './latency.js'
2
6
 
3
- /**
4
- * Test server to load test.
5
- * (C) 2013 Alex Fernández.
6
- */
7
-
8
-
9
- // requires
10
- const testing = require('testing');
11
- const http = require('http');
12
- const WebSocketServer = require('websocket').server;
13
- const util = require('util');
14
- const net = require('net');
15
- const Log = require('log');
16
- const {Latency} = require('./latency.js');
17
-
18
- // globals
19
- const log = new Log('info');
20
-
21
- // constants
22
7
  const PORT = 7357;
23
8
  const LOG_HEADERS_INTERVAL_SECONDS = 1;
24
9
 
@@ -34,9 +19,6 @@ class TestServer {
34
19
  this.wsServer = null;
35
20
  this.latency = new Latency({});
36
21
  this.debuggedTime = Date.now();
37
- if (options.quiet) {
38
- log.level = 'notice';
39
- }
40
22
  }
41
23
 
42
24
  /**
@@ -63,7 +45,7 @@ class TestServer {
63
45
  return this.createError('Could not start server on port ' + this.port + ': ' + error, callback);
64
46
  });
65
47
  this.server.listen(this.port, () => {
66
- log.info('Listening on port %s', this.port);
48
+ console.info(`Listening on http://localhost:${this.port}/`);
67
49
  if (callback) {
68
50
  callback();
69
51
  }
@@ -71,18 +53,15 @@ class TestServer {
71
53
  this.wsServer.on('request', request => {
72
54
  // explicity omitting origin check here.
73
55
  const connection = request.accept(null, request.origin);
74
- log.debug(' Connection accepted.');
75
56
  connection.on('message', message => {
76
57
  if (message.type === 'utf8') {
77
- log.debug('Received Message: ' + message.utf8Data);
78
58
  connection.sendUTF(message.utf8Data);
79
59
  } else if (message.type === 'binary') {
80
- log.debug('Received Binary Message of ' + message.binaryData.length + ' bytes');
81
60
  connection.sendBytes(message.binaryData);
82
61
  }
83
62
  });
84
63
  connection.on('close', () => {
85
- log.info('Peer %s disconnected', connection.remoteAddress);
64
+ console.info('Peer %s disconnected', connection.remoteAddress);
86
65
  });
87
66
  });
88
67
  return this.server;
@@ -93,7 +72,7 @@ class TestServer {
93
72
  */
94
73
  createError(message, callback) {
95
74
  if (!callback) {
96
- return log.error(message);
75
+ return console.error(message);
97
76
  }
98
77
  callback(message);
99
78
  }
@@ -127,7 +106,7 @@ class TestServer {
127
106
  */
128
107
  socketListen(socket) {
129
108
  socket.on('error', error => {
130
- log.error('socket error: %s', error);
109
+ console.error('socket error: %s', error);
131
110
  socket.end();
132
111
  });
133
112
  socket.on('data', data => this.readData(data));
@@ -137,16 +116,16 @@ class TestServer {
137
116
  * Read some data off the socket.
138
117
  */
139
118
  readData(data) {
140
- log.info('data: %s', data);
119
+ console.info('data: %s', data);
141
120
  }
142
121
 
143
122
  /**
144
123
  * Debug headers and other interesting information: POST body.
145
124
  */
146
125
  debug(request) {
147
- log.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
126
+ console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
148
127
  if (request.body) {
149
- log.info('Body: %s', request.body);
128
+ console.info('Body: %s', request.body);
150
129
  }
151
130
  }
152
131
 
@@ -173,7 +152,7 @@ class TestServer {
173
152
  }
174
153
  const percent = parseInt(this.options.percent, 10);
175
154
  if (!percent) {
176
- log.error('Invalid error percent %s', this.options.percent);
155
+ console.error('Invalid error percent %s', this.options.percent);
177
156
  return false;
178
157
  }
179
158
  return (Math.random() < percent / 100);
@@ -184,42 +163,13 @@ class TestServer {
184
163
  * Start a test server. Options can contain:
185
164
  * - port: the port to use, default 7357.
186
165
  * - delay: wait the given milliseconds before answering.
187
- * - quiet: do not log any messages.
166
+ * - quiet: do not log any messages (deprecated).
188
167
  * - percent: give an error (default 500) on some % of requests.
189
168
  * - error: set an HTTP error code, default is 500.
190
169
  * An optional callback is called after the server has started.
191
- * In this case the quiet option is enabled.
192
170
  */
193
- exports.startServer = function(options, callback) {
194
- if (callback) {
195
- options.quiet = true;
196
- }
171
+ export function startServer(options, callback) {
197
172
  const server = new TestServer(options);
198
173
  return server.start(callback);
199
- };
200
-
201
- function testStartServer(callback) {
202
- const options = {
203
- port: 10530,
204
- };
205
- const server = exports.startServer(options, error => {
206
- testing.check(error, 'Could not start server', callback);
207
- server.close(error => {
208
- testing.check(error, 'Could not stop server', callback);
209
- testing.success(callback);
210
- });
211
- });
212
- }
213
-
214
- /**
215
- * Run the tests.
216
- */
217
- exports.test = function(callback) {
218
- testing.run([testStartServer], 5000, callback);
219
- };
220
-
221
- // start server if invoked directly
222
- if (__filename == process.argv[1]) {
223
- exports.test(testing.show);
224
174
  }
225
175
 
package/lib/websocket.js CHANGED
@@ -1,28 +1,15 @@
1
- 'use strict';
1
+ import websocket from 'websocket'
2
+ import {BaseClient} from './baseClient.js'
2
3
 
3
- /**
4
- * Load test a websocket.
5
- * (C) 2013 Alex Fernández.
6
- */
7
-
8
-
9
- // requires
10
- const WebSocketClient = require('websocket').client;
11
- const testing = require('testing');
12
- const Log = require('log');
13
- const BaseClient = require('./baseClient.js').BaseClient;
14
-
15
- // globals
16
- const log = new Log('info');
17
4
  let latency;
18
5
 
19
6
 
20
7
  /**
21
8
  * Create a client for a websocket.
22
9
  */
23
- exports.create = function(operation, params) {
10
+ export function create(operation, params) {
24
11
  return new WebsocketClient(operation, params);
25
- };
12
+ }
26
13
 
27
14
  /**
28
15
  * A client that connects to a websocket.
@@ -40,13 +27,10 @@ class WebsocketClient extends BaseClient {
40
27
  * Start the websocket client.
41
28
  */
42
29
  start() {
43
- this.client = new WebSocketClient();
44
- this.client.on('connectFailed', error => {
45
- log.debug('WebSocket client connection error ' + error);
46
- });
30
+ this.client = new websocket.client();
31
+ this.client.on('connectFailed', () => {});
47
32
  this.client.on('connect', connection => this.connect(connection));
48
33
  this.client.connect(this.params.url, []);
49
- log.debug('WebSocket client connected to ' + this.params.url);
50
34
  }
51
35
 
52
36
  /**
@@ -55,7 +39,6 @@ class WebsocketClient extends BaseClient {
55
39
  stop() {
56
40
  if (this.connection) {
57
41
  this.connection.close();
58
- log.debug('WebSocket client disconnected from ' + this.params.url);
59
42
  }
60
43
  }
61
44
 
@@ -100,7 +83,7 @@ class WebsocketClient extends BaseClient {
100
83
  ended = true;
101
84
 
102
85
  if (message.type != 'utf8') {
103
- log.error('Invalid message type ' + message.type);
86
+ console.error('Invalid message type ' + message.type);
104
87
  return;
105
88
  }
106
89
  let json;
@@ -108,12 +91,10 @@ class WebsocketClient extends BaseClient {
108
91
  json = JSON.parse(message.utf8Data);
109
92
  }
110
93
  catch(e) {
111
- log.error('Invalid JSON: ' + message.utf8Data);
94
+ console.error('Invalid JSON: ' + message.utf8Data);
112
95
  return;
113
96
  }
114
97
 
115
- log.debug("Received response %j", json);
116
-
117
98
  // eat the client_connected message we get at the beginning
118
99
  if ((json && json[0] && json[0][0] == 'client_connected')) {
119
100
  ended = false;
@@ -123,7 +104,6 @@ class WebsocketClient extends BaseClient {
123
104
  if (this.lastCall) {
124
105
  const newCall = new Date().getTime();
125
106
  latency.add(newCall - this.lastCall);
126
- log.debug('latency: ' + (newCall - this.lastCall));
127
107
  this.lastCall = null;
128
108
  }
129
109
 
@@ -152,29 +132,3 @@ class WebsocketClient extends BaseClient {
152
132
  }
153
133
  }
154
134
 
155
-
156
- function testWebsocketClient(callback) {
157
- const options = {
158
- url: 'ws://localhost:7357/',
159
- maxSeconds: 0.1,
160
- concurrency: 1,
161
- quiet: true,
162
- };
163
- exports.create({}, options);
164
- testing.success(callback);
165
- }
166
-
167
- /**
168
- * Run tests, currently nothing.
169
- */
170
- exports.test = function(callback) {
171
- testing.run([
172
- testWebsocketClient,
173
- ], callback);
174
- };
175
-
176
- // start tests if invoked directly
177
- if (__filename == process.argv[1]) {
178
- exports.test(testing.show);
179
- }
180
-
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "loadtest",
3
- "version": "5.1.2",
3
+ "version": "6.0.0",
4
+ "type": "module",
4
5
  "description": "Run load tests for your web application. Mostly ab-compatible interface, with an option to force requests per second. Includes an API for automated load testing.",
5
6
  "homepage": "https://github.com/alexfernandez/loadtest",
6
7
  "contributors": [
@@ -17,13 +18,12 @@
17
18
  "agentkeepalive": "^2.0.3",
18
19
  "confinode": "^2.1.1",
19
20
  "https-proxy-agent": "^2.2.1",
20
- "log": "1.4.*",
21
- "stdio": "^0.2.3",
22
- "testing": "^1.1.1",
23
- "websocket": "^1.0.28"
21
+ "stdio": "0.2.7",
22
+ "testing": "^3.0.3",
23
+ "websocket": "^1.0.34"
24
24
  },
25
25
  "devDependencies": {
26
- "eslint": "^4.19.1"
26
+ "eslint": "^8.47.0"
27
27
  },
28
28
  "keywords": [
29
29
  "testing",
@@ -35,7 +35,7 @@
35
35
  "black box"
36
36
  ],
37
37
  "engines": {
38
- "node": ">=10"
38
+ "node": ">=16"
39
39
  },
40
40
  "bin": {
41
41
  "loadtest": "bin/loadtest.js",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "preferGlobal": true,
45
45
  "scripts": {
46
- "test": "node test.js",
46
+ "test": "node test/all.js",
47
47
  "posttest": "eslint ."
48
48
  },
49
49
  "publishConfig": {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sample post-file content for test.
3
+ * Contains a single exported function that generates the body.
4
+ */
5
+
6
+ export default function bodyGenerator(requestId) {
7
+ // this object will be serialized to JSON and sent in the body of the request
8
+ return {
9
+ key: 'value',
10
+ requestId: requestId
11
+ }
12
+ }
13
+