loadtest 5.2.0 → 6.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/lib/options.js ADDED
@@ -0,0 +1,152 @@
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 async function processOptions(options) {
11
+ const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
12
+ if (!options.url) {
13
+ throw new Error('Missing URL in options')
14
+ }
15
+ options.concurrency = options.concurrency || 1;
16
+ if (options.requestsPerSecond) {
17
+ options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
18
+ }
19
+ if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
20
+ throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
21
+ }
22
+ if (options.url.startsWith('ws:')) {
23
+ if (options.requestsPerSecond) {
24
+ throw new Error(`"requestsPerSecond" not supported for WebSockets`);
25
+ }
26
+ }
27
+ const configuration = loadConfig();
28
+
29
+ options.agentKeepAlive = options.keepalive || options.agent || configuration.agentKeepAlive;
30
+ options.indexParam = options.index || configuration.indexParam;
31
+
32
+ //TODO: add index Param
33
+ // Allow a post body string in options
34
+ // Ex -P '{"foo": "bar"}'
35
+ if (options.postBody) {
36
+ options.method = 'POST';
37
+ options.body = options.postBody;
38
+ }
39
+ if (options.postFile) {
40
+ options.method = 'POST';
41
+ options.body = await readBody(options.postFile, '-p');
42
+ }
43
+ if (options.data) {
44
+ options.body = options.data
45
+ }
46
+ if (options.method) {
47
+ const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
48
+ if (acceptedMethods.indexOf(options.method) === -1) {
49
+ options.method = 'GET';
50
+ }
51
+ }
52
+ if (options.putFile) {
53
+ options.method = 'PUT';
54
+ options.body = await readBody(options.putFile, '-u');
55
+ }
56
+ if (options.patchBody) {
57
+ options.method = 'PATCH';
58
+ options.body = options.patchBody;
59
+ }
60
+ if (options.patchFile) {
61
+ options.method = 'PATCH';
62
+ options.body = await readBody(options.patchFile, '-a');
63
+ }
64
+ if (!options.method) {
65
+ options.method = configuration.method;
66
+ }
67
+ if (!options.body) {
68
+ if(configuration.body) {
69
+ options.body = configuration.body;
70
+ } else if (configuration.file) {
71
+ options.body = await readBody(configuration.file, 'configuration.request.file');
72
+ }
73
+ }
74
+ options.requestsPerSecond = options.rps ? parseFloat(options.rps) : configuration.requestsPerSecond;
75
+ if (!options.key) {
76
+ options.key = configuration.key;
77
+ }
78
+ if (options.key) {
79
+ options.key = await readFile(options.key)
80
+ }
81
+ if (!options.cert) {
82
+ options.cert = configuration.cert;
83
+ }
84
+ if (options.cert) {
85
+ options.cert = await readFile(options.cert);
86
+ }
87
+
88
+ const defaultHeaders = options.headers || !configuration.headers ? {} : configuration.headers;
89
+ defaultHeaders['host'] = urlLib.parse(options.url).host;
90
+ defaultHeaders['user-agent'] = 'loadtest/' + packageJson.version;
91
+ defaultHeaders['accept'] = '*/*';
92
+
93
+ if (options.headers) {
94
+ addHeaders(options.headers, defaultHeaders);
95
+ console.log('headers: %s, %j', typeof defaultHeaders, defaultHeaders);
96
+ }
97
+ options.headers = defaultHeaders;
98
+
99
+ if (!options.requestGenerator) {
100
+ options.requestGenerator = configuration.requestGenerator;
101
+ }
102
+ if (typeof options.requestGenerator == 'string') {
103
+ options.requestGenerator = await import(options.requestGenerator)
104
+ }
105
+
106
+ // Use configuration file for other values
107
+ if(!options.maxRequests) {
108
+ options.maxRequests = configuration.maxRequests;
109
+ }
110
+ if(!options.concurrency) {
111
+ options.concurrency = configuration.concurrency;
112
+ }
113
+ if(!options.maxSeconds) {
114
+ options.maxSeconds = configuration.maxSeconds;
115
+ }
116
+ if(!options.timeout && configuration.timeout) {
117
+ options.timeout = configuration.timeout;
118
+ }
119
+ if(!options.contentType) {
120
+ options.contentType = configuration.contentType;
121
+ }
122
+ if(!options.cookies) {
123
+ options.cookies = configuration.cookies;
124
+ }
125
+ if(!options.secureProtocol) {
126
+ options.secureProtocol = configuration.secureProtocol;
127
+ }
128
+ if(!options.insecure) {
129
+ options.insecure = configuration.insecure;
130
+ }
131
+ if(!options.recover) {
132
+ options.recover = configuration.recover;
133
+ }
134
+ if(!options.proxy) {
135
+ options.proxy = configuration.proxy;
136
+ }
137
+ }
138
+
139
+ async function readBody(filename, option) {
140
+ if (typeof filename !== 'string') {
141
+ throw new Error(`Invalid file to open with ${option}: ${filename}`);
142
+ }
143
+
144
+ if (path.extname(filename) === '.js') {
145
+ return await import(new URL(filename, `file://${process.cwd()}/`))
146
+ }
147
+
148
+ const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
149
+
150
+ return ret;
151
+ }
152
+
package/lib/show.js ADDED
@@ -0,0 +1,47 @@
1
+
2
+
3
+ /**
4
+ * Show result of a load test.
5
+ */
6
+ export function showResult(options, result) {
7
+ console.info('');
8
+ console.info('Target URL: %s', options.url);
9
+ if (options.maxRequests) {
10
+ console.info('Max requests: %s', options.maxRequests);
11
+ } else if (options.maxSeconds) {
12
+ console.info('Max time (s): %s', options.maxSeconds);
13
+ }
14
+ console.info('Concurrency level: %s', options.concurrency);
15
+ let agent = 'none';
16
+ if (options.agentKeepAlive) {
17
+ agent = 'keepalive';
18
+ }
19
+ console.info('Agent: %s', agent);
20
+ if (options.requestsPerSecond) {
21
+ console.info('Requests per second: %s', options.requestsPerSecond * options.concurrency);
22
+ }
23
+ console.info('');
24
+ console.info('Completed requests: %s', result.totalRequests);
25
+ console.info('Total errors: %s', result.totalErrors);
26
+ console.info('Total time: %s s', result.totalTimeSeconds);
27
+ console.info('Requests per second: %s', result.rps);
28
+ console.info('Mean latency: %s ms', result.meanLatencyMs);
29
+ console.info('');
30
+ console.info('Percentage of the requests served within a certain time');
31
+
32
+ Object.keys(result.percentiles).forEach(percentile => {
33
+ console.info(' %s% %s ms', percentile, result.percentiles[percentile]);
34
+ });
35
+
36
+ console.info(' 100% %s ms (longest request)', result.maxLatencyMs);
37
+ if (result.totalErrors) {
38
+ console.info('');
39
+ Object.keys(result.errorCodes).forEach(errorCode => {
40
+ const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
41
+ console.info(' %s%s: %s errors', padding, errorCode, result.errorCodes[errorCode]);
42
+ });
43
+ }
44
+ }
45
+
46
+
47
+
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,14 +19,11 @@ 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
  /**
43
25
  * Start the server.
44
- * An optional callback will be called after the server has started.
26
+ * The callback parameter will be called after the server has started.
45
27
  */
46
28
  start(callback) {
47
29
  if (this.options.socket) {
@@ -63,38 +45,30 @@ 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);
67
- if (callback) {
68
- callback();
69
- }
48
+ if (!this.options.quiet) console.info(`Listening on http://localhost:${this.port}/`)
49
+ callback(null, this.server)
70
50
  });
71
51
  this.wsServer.on('request', request => {
72
52
  // explicity omitting origin check here.
73
53
  const connection = request.accept(null, request.origin);
74
- log.debug(' Connection accepted.');
75
54
  connection.on('message', message => {
76
55
  if (message.type === 'utf8') {
77
- log.debug('Received Message: ' + message.utf8Data);
78
56
  connection.sendUTF(message.utf8Data);
79
57
  } else if (message.type === 'binary') {
80
- log.debug('Received Binary Message of ' + message.binaryData.length + ' bytes');
81
58
  connection.sendBytes(message.binaryData);
82
59
  }
83
60
  });
84
61
  connection.on('close', () => {
85
- log.info('Peer %s disconnected', connection.remoteAddress);
62
+ if (!this.options.quiet) console.info('Peer %s disconnected', connection.remoteAddress);
86
63
  });
87
64
  });
88
- return this.server;
65
+ return this.server
89
66
  }
90
67
 
91
68
  /**
92
69
  * Log an error, or send to the callback if present.
93
70
  */
94
71
  createError(message, callback) {
95
- if (!callback) {
96
- return log.error(message);
97
- }
98
72
  callback(message);
99
73
  }
100
74
 
@@ -127,7 +101,7 @@ class TestServer {
127
101
  */
128
102
  socketListen(socket) {
129
103
  socket.on('error', error => {
130
- log.error('socket error: %s', error);
104
+ if (!this.options.quiet) console.error('socket error: %s', error);
131
105
  socket.end();
132
106
  });
133
107
  socket.on('data', data => this.readData(data));
@@ -137,16 +111,16 @@ class TestServer {
137
111
  * Read some data off the socket.
138
112
  */
139
113
  readData(data) {
140
- log.info('data: %s', data);
114
+ if (!this.options.quiet) console.info('data: %s', data);
141
115
  }
142
116
 
143
117
  /**
144
118
  * Debug headers and other interesting information: POST body.
145
119
  */
146
120
  debug(request) {
147
- log.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
121
+ if (!this.options.quiet) console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
148
122
  if (request.body) {
149
- log.info('Body: %s', request.body);
123
+ if (!this.options.quiet) console.info('Body: %s', request.body);
150
124
  }
151
125
  }
152
126
 
@@ -173,7 +147,7 @@ class TestServer {
173
147
  }
174
148
  const percent = parseInt(this.options.percent, 10);
175
149
  if (!percent) {
176
- log.error('Invalid error percent %s', this.options.percent);
150
+ if (!this.options.quiet) console.error('Invalid error percent %s', this.options.percent);
177
151
  return false;
178
152
  }
179
153
  return (Math.random() < percent / 100);
@@ -181,45 +155,26 @@ class TestServer {
181
155
  }
182
156
 
183
157
  /**
184
- * Start a test server. Options can contain:
185
- * - port: the port to use, default 7357.
186
- * - delay: wait the given milliseconds before answering.
187
- * - quiet: do not log any messages.
188
- * - percent: give an error (default 500) on some % of requests.
189
- * - error: set an HTTP error code, default is 500.
190
- * An optional callback is called after the server has started.
191
- * In this case the quiet option is enabled.
158
+ * Start a test server. Parameters:
159
+ * - `options`, can contain:
160
+ * - port: the port to use, default 7357.
161
+ * - delay: wait the given milliseconds before answering.
162
+ * - quiet: do not log any messages.
163
+ * - percent: give an error (default 500) on some % of requests.
164
+ * - error: set an HTTP error code, default is 500.
165
+ * - `callback`: optional callback, called after the server has started.
166
+ * If not present will return a promise.
192
167
  */
193
- exports.startServer = function(options, callback) {
168
+ export function startServer(options, callback) {
169
+ const server = new TestServer(options);
194
170
  if (callback) {
195
- options.quiet = true;
171
+ return server.start(callback)
196
172
  }
197
- const server = new TestServer(options);
198
- 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);
173
+ return new Promise((resolve, reject) => {
174
+ server.start((error, result) => {
175
+ if (error) return reject(error)
176
+ return resolve(result)
177
+ })
178
+ })
224
179
  }
225
180
 
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.2.0",
3
+ "version": "6.1.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
+
@@ -1,12 +1,10 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * Sample request generator usage.
5
3
  * Contributed by jjohnsonvng:
6
4
  * https://github.com/alexfernandez/loadtest/issues/86#issuecomment-211579639
7
5
  */
8
6
 
9
- const loadtest = require('../lib/loadtest.js');
7
+ import {loadTest} from '../index.js'
10
8
 
11
9
  const options = {
12
10
  url: 'http://yourHost',
@@ -27,11 +25,11 @@ const options = {
27
25
  }
28
26
  };
29
27
 
30
- loadtest.loadTest(options, (error, results) => {
28
+ loadTest(options, (error, result) => {
31
29
  if (error) {
32
30
  return console.error('Got an error: %s', error);
33
31
  }
34
- console.log(results);
32
+ console.log(result);
35
33
  console.log('Tests run successfully');
36
34
  });
37
35
 
@@ -25,9 +25,9 @@ const options: loadtest.LoadTestOptions = {
25
25
  },
26
26
  }
27
27
 
28
- loadtest.loadTest(options, (error, results) => {
28
+ loadtest.loadTest(options, (error, result) => {
29
29
  if (error) {
30
30
  return console.error(`Got an error: ${error}`)
31
31
  }
32
- console.log("Tests run successfully", {results})
32
+ console.log("Tests run successfully", {result})
33
33
  })
package/test/all.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Run package tests.
3
+ * (C) 2013 Alex Fernández.
4
+ */
5
+
6
+ import testing from 'testing'
7
+ import {test as testHrtimer} from './hrtimer.js'
8
+ import {test as testHeaders} from './headers.js'
9
+ import {test as testLatency} from './latency.js'
10
+ import {test as testHttpClient} from './httpClient.js'
11
+ import {test as testServer} from './testserver.js'
12
+ import {test as testRequestGenerator} from './request-generator.js'
13
+ import {test as testBodyGenerator} from './body-generator.js'
14
+ import {test as testLoadtest} from './loadtest.js'
15
+ import {test as testWebsocket} from './websocket.js'
16
+ import {test as integrationTest} from './integration.js'
17
+
18
+
19
+ /**
20
+ * Run all module tests.
21
+ */
22
+ function test() {
23
+ const tests = [
24
+ testHrtimer, testHeaders, testLatency, testHttpClient,
25
+ testServer, integrationTest, testLoadtest, testWebsocket,
26
+ testRequestGenerator, testBodyGenerator,
27
+ ];
28
+ testing.run(tests, 4200);
29
+ }
30
+
31
+ test()
32
+
33
+
@@ -0,0 +1,42 @@
1
+ import testing from 'testing'
2
+ import {loadTest} from '../lib/loadtest.js'
3
+ import {startServer} from '../lib/testserver.js'
4
+
5
+ const PORT = 10453;
6
+
7
+
8
+ function testBodyGenerator(callback) {
9
+ const server = startServer({port: PORT, quiet: true}, error => {
10
+ if (error) {
11
+ return callback('Could not start test server');
12
+ }
13
+ const options = {
14
+ url: 'http://localhost:' + PORT,
15
+ requestsPerSecond: 100,
16
+ maxRequests: 100,
17
+ concurrency: 10,
18
+ postFile: 'sample/post-file.js',
19
+ quiet: true,
20
+ }
21
+ loadTest(options, (error, result) => {
22
+ if (error) {
23
+ console.error(error)
24
+ return callback(`Could not run load test with postFile: ${error.message}`);
25
+ }
26
+ server.close(error => {
27
+ if (error) {
28
+ return callback('Could not close test server');
29
+ }
30
+ return callback(null, 'bodyGenerator succeeded: ' + JSON.stringify(result));
31
+ });
32
+ });
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Run all tests.
38
+ */
39
+ export function test(callback) {
40
+ testing.run([testBodyGenerator], 4000, callback);
41
+ }
42
+