loadtest 6.0.0 → 6.2.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 CHANGED
@@ -1,25 +1,18 @@
1
- #!/usr/bin/env node
2
-
3
1
  import {readFile} from 'fs/promises'
4
2
  import * as path from 'path'
5
3
  import * as urlLib from 'url'
6
4
  import {addHeaders} from '../lib/headers.js'
7
5
  import {loadConfig} from '../lib/config.js'
8
6
 
7
+ const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
9
8
 
10
- export function processOptions(options, callback) {
11
- processOptionsAsync(options).then(result => callback(null, result)).catch(error => callback(error))
12
- }
13
9
 
14
- async function processOptionsAsync(options) {
10
+ export async function processOptions(options) {
11
+ const processed = {}
15
12
  const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
16
13
  if (!options.url) {
17
14
  throw new Error('Missing URL in options')
18
15
  }
19
- options.concurrency = options.concurrency || 1;
20
- if (options.requestsPerSecond) {
21
- options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
22
- }
23
16
  if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
24
17
  throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
25
18
  }
@@ -28,129 +21,88 @@ async function processOptionsAsync(options) {
28
21
  throw new Error(`"requestsPerSecond" not supported for WebSockets`);
29
22
  }
30
23
  }
24
+ processed.url = options.url
31
25
  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
26
+ processed.concurrency = options.concurrency || configuration.concurrency || 1
27
+ const rps = options.rps ? parseFloat(options.rps) : null
28
+ processed.requestsPerSecond = options.requestsPerSecond || rps || configuration.requestsPerSecond
29
+ processed.agentKeepAlive = options.keepalive || options.agent || options.agentKeepAlive || configuration.agentKeepAlive;
30
+ processed.indexParam = options.index || options.indexParam || configuration.indexParam;
31
+ processed.method = options.method || configuration.method || 'GET'
37
32
  // Allow a post body string in options
38
33
  // Ex -P '{"foo": "bar"}'
39
34
  if (options.postBody) {
40
- options.method = 'POST';
41
- options.body = options.postBody;
35
+ processed.method = 'POST';
36
+ processed.body = options.postBody;
42
37
  }
43
38
  if (options.postFile) {
44
- options.method = 'POST';
45
- options.body = await readBody(options.postFile, '-p');
39
+ processed.method = 'POST';
40
+ processed.body = await readBody(options.postFile, '-p');
46
41
  }
47
42
  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
- }
43
+ processed.body = options.data
55
44
  }
56
45
  if (options.putFile) {
57
- options.method = 'PUT';
58
- options.body = await readBody(options.putFile, '-u');
46
+ processed.method = 'PUT';
47
+ processed.body = await readBody(options.putFile, '-u');
59
48
  }
60
49
  if (options.patchBody) {
61
- options.method = 'PATCH';
62
- options.body = options.patchBody;
50
+ processed.method = 'PATCH';
51
+ processed.body = options.patchBody;
63
52
  }
64
53
  if (options.patchFile) {
65
- options.method = 'PATCH';
66
- options.body = await readBody(options.patchFile, '-a');
54
+ processed.method = 'PATCH';
55
+ processed.body = await readBody(options.patchFile, '-a');
67
56
  }
68
- if (!options.method) {
69
- options.method = configuration.method;
57
+ // sanity check
58
+ if (acceptedMethods.indexOf(processed.method) === -1) {
59
+ throw new Error(`Invalid method ${processed.method}`)
70
60
  }
71
61
  if (!options.body) {
72
62
  if(configuration.body) {
73
- options.body = configuration.body;
63
+ processed.body = configuration.body;
74
64
  } else if (configuration.file) {
75
- options.body = await readBody(configuration.file, 'configuration.request.file');
65
+ processed.body = await readBody(configuration.file, 'configuration.request.file');
76
66
  }
77
67
  }
78
- options.requestsPerSecond = options.rps ? parseFloat(options.rps) : configuration.requestsPerSecond;
79
- if (!options.key) {
80
- options.key = configuration.key;
68
+ if (options.key || configuration.key) {
69
+ processed.key = await readFile(options.key || configuration.key)
81
70
  }
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);
71
+ if (options.cert || configuration.cert) {
72
+ processed.cert = await readFile(options.cert || configuration.cert);
90
73
  }
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'] = '*/*';
74
+ processed.headers = configuration.headers || {}
75
+ processed.headers['host'] = urlLib.parse(options.url).host;
76
+ processed.headers['user-agent'] = 'loadtest/' + packageJson.version;
77
+ processed.headers['accept'] = '*/*';
96
78
 
97
79
  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
- }
80
+ addHeaders(options.headers, processed.headers);
81
+ }
82
+ processed.requestGenerator = options.requestGenerator || configuration.requestGenerator
83
+ if (typeof processed.requestGenerator == 'string') {
84
+ processed.requestGenerator = await import(processed.requestGenerator)
85
+ }
86
+ processed.maxRequests = options.maxRequests || configuration.maxRequests
87
+ processed.maxSeconds = options.maxSeconds || configuration.maxSeconds
88
+ processed.cookies = options.cookies || configuration.cookies
89
+ processed.contentType = options.contentType || configuration.contentType
90
+ processed.timeout = options.timeout || configuration.timeout
91
+ processed.secureProtocol = options.secureProtocol || configuration.secureProtocol
92
+ processed.insecure = options.insecure || configuration.insecure
93
+ processed.recover = options.recover || configuration.recover
94
+ processed.proxy = options.proxy || configuration.proxy
95
+ processed.quiet = options.quiet || configuration.quiet
96
+ return processed
141
97
  }
142
98
 
143
99
  async function readBody(filename, option) {
144
100
  if (typeof filename !== 'string') {
145
101
  throw new Error(`Invalid file to open with ${option}: ${filename}`);
146
102
  }
147
-
148
103
  if (path.extname(filename) === '.js') {
149
104
  return await import(new URL(filename, `file://${process.cwd()}/`))
150
105
  }
151
-
152
- const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
153
-
154
- return ret;
106
+ return await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
155
107
  }
156
108
 
package/lib/result.js ADDED
@@ -0,0 +1,68 @@
1
+
2
+
3
+ /**
4
+ * Result of a load test.
5
+ */
6
+ export class Result {
7
+ constructor(options, latency) {
8
+ // options
9
+ this.url = options.url
10
+ this.maxRequests = options.maxRequests
11
+ this.maxSeconds = options.maxSeconds
12
+ this.concurrency = options.concurrency
13
+ this.agent = options.agentKeepAlive ? 'keepalive' : 'none';
14
+ this.requestsPerSecond = options.requestsPerSecond
15
+ // results
16
+ this.elapsedSeconds = latency.getElapsed(latency.initialTime) / 1000
17
+ const meanTime = latency.totalTime / latency.totalRequests
18
+ this.totalRequests = latency.totalRequests
19
+ this.totalErrors = latency.totalErrors
20
+ this.totalTimeSeconds = this.elapsedSeconds
21
+ this.rps = Math.round(latency.totalRequests / this.elapsedSeconds)
22
+ this.meanLatencyMs = Math.round(meanTime * 10) / 10
23
+ this.maxLatencyMs = latency.maxLatencyMs
24
+ this.minLatencyMs = latency.minLatencyMs
25
+ this.percentiles = latency.computePercentiles()
26
+ this.errorCodes = latency.errorCodes
27
+ }
28
+
29
+ /**
30
+ * Show result of a load test.
31
+ */
32
+ show() {
33
+ console.info('');
34
+ console.info('Target URL: %s', this.url);
35
+ if (this.maxRequests) {
36
+ console.info('Max requests: %s', this.maxRequests);
37
+ } else if (this.maxSeconds) {
38
+ console.info('Max time (s): %s', this.maxSeconds);
39
+ }
40
+ console.info('Concurrency level: %s', this.concurrency);
41
+ console.info('Agent: %s', this.agent);
42
+ if (this.requestsPerSecond) {
43
+ console.info('Requests per second: %s', this.requestsPerSecond);
44
+ }
45
+ console.info('');
46
+ console.info('Completed requests: %s', this.totalRequests);
47
+ console.info('Total errors: %s', this.totalErrors);
48
+ console.info('Total time: %s s', this.totalTimeSeconds);
49
+ console.info('Requests per second: %s', this.rps);
50
+ console.info('Mean latency: %s ms', this.meanLatencyMs);
51
+ console.info('');
52
+ console.info('Percentage of the requests served within a certain time');
53
+
54
+ Object.keys(this.percentiles).forEach(percentile => {
55
+ console.info(' %s% %s ms', percentile, this.percentiles[percentile]);
56
+ });
57
+
58
+ console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
59
+ if (this.totalErrors) {
60
+ console.info('');
61
+ Object.keys(this.errorCodes).forEach(errorCode => {
62
+ const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
63
+ console.info(' %s%s: %s errors', padding, errorCode, this.errorCodes[errorCode]);
64
+ });
65
+ }
66
+ }
67
+ }
68
+
package/lib/testserver.js CHANGED
@@ -23,7 +23,7 @@ class TestServer {
23
23
 
24
24
  /**
25
25
  * Start the server.
26
- * An optional callback will be called after the server has started.
26
+ * The callback parameter will be called after the server has started.
27
27
  */
28
28
  start(callback) {
29
29
  if (this.options.socket) {
@@ -45,10 +45,8 @@ class TestServer {
45
45
  return this.createError('Could not start server on port ' + this.port + ': ' + error, callback);
46
46
  });
47
47
  this.server.listen(this.port, () => {
48
- console.info(`Listening on http://localhost:${this.port}/`);
49
- if (callback) {
50
- callback();
51
- }
48
+ if (!this.options.quiet) console.info(`Listening on http://localhost:${this.port}/`)
49
+ callback(null, this.server)
52
50
  });
53
51
  this.wsServer.on('request', request => {
54
52
  // explicity omitting origin check here.
@@ -61,19 +59,16 @@ class TestServer {
61
59
  }
62
60
  });
63
61
  connection.on('close', () => {
64
- console.info('Peer %s disconnected', connection.remoteAddress);
62
+ if (!this.options.quiet) console.info('Peer %s disconnected', connection.remoteAddress);
65
63
  });
66
64
  });
67
- return this.server;
65
+ return this.server
68
66
  }
69
67
 
70
68
  /**
71
69
  * Log an error, or send to the callback if present.
72
70
  */
73
71
  createError(message, callback) {
74
- if (!callback) {
75
- return console.error(message);
76
- }
77
72
  callback(message);
78
73
  }
79
74
 
@@ -106,7 +101,7 @@ class TestServer {
106
101
  */
107
102
  socketListen(socket) {
108
103
  socket.on('error', error => {
109
- console.error('socket error: %s', error);
104
+ if (!this.options.quiet) console.error('socket error: %s', error);
110
105
  socket.end();
111
106
  });
112
107
  socket.on('data', data => this.readData(data));
@@ -116,16 +111,16 @@ class TestServer {
116
111
  * Read some data off the socket.
117
112
  */
118
113
  readData(data) {
119
- console.info('data: %s', data);
114
+ if (!this.options.quiet) console.info('data: %s', data);
120
115
  }
121
116
 
122
117
  /**
123
118
  * Debug headers and other interesting information: POST body.
124
119
  */
125
120
  debug(request) {
126
- console.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));
127
122
  if (request.body) {
128
- console.info('Body: %s', request.body);
123
+ if (!this.options.quiet) console.info('Body: %s', request.body);
129
124
  }
130
125
  }
131
126
 
@@ -152,7 +147,7 @@ class TestServer {
152
147
  }
153
148
  const percent = parseInt(this.options.percent, 10);
154
149
  if (!percent) {
155
- console.error('Invalid error percent %s', this.options.percent);
150
+ if (!this.options.quiet) console.error('Invalid error percent %s', this.options.percent);
156
151
  return false;
157
152
  }
158
153
  return (Math.random() < percent / 100);
@@ -160,16 +155,26 @@ class TestServer {
160
155
  }
161
156
 
162
157
  /**
163
- * Start a test server. Options can contain:
164
- * - port: the port to use, default 7357.
165
- * - delay: wait the given milliseconds before answering.
166
- * - quiet: do not log any messages (deprecated).
167
- * - percent: give an error (default 500) on some % of requests.
168
- * - error: set an HTTP error code, default is 500.
169
- * An optional callback is called after the server has started.
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.
170
167
  */
171
168
  export function startServer(options, callback) {
172
169
  const server = new TestServer(options);
173
- return server.start(callback);
170
+ if (callback) {
171
+ return server.start(callback)
172
+ }
173
+ return new Promise((resolve, reject) => {
174
+ server.start((error, result) => {
175
+ if (error) return reject(error)
176
+ return resolve(result)
177
+ })
178
+ })
174
179
  }
175
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtest",
3
- "version": "6.0.0",
3
+ "version": "6.2.0",
4
4
  "type": "module",
5
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.",
6
6
  "homepage": "https://github.com/alexfernandez/loadtest",
@@ -19,7 +19,7 @@
19
19
  "confinode": "^2.1.1",
20
20
  "https-proxy-agent": "^2.2.1",
21
21
  "stdio": "0.2.7",
22
- "testing": "^3.0.3",
22
+ "testing": "^3.1.0",
23
23
  "websocket": "^1.0.34"
24
24
  },
25
25
  "devDependencies": {
@@ -25,11 +25,11 @@ const options = {
25
25
  }
26
26
  };
27
27
 
28
- loadTest(options, (error, results) => {
28
+ loadTest(options, (error, result) => {
29
29
  if (error) {
30
30
  return console.error('Got an error: %s', error);
31
31
  }
32
- console.log(results);
32
+ console.log(result);
33
33
  console.log('Tests run successfully');
34
34
  });
35
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
  })
@@ -6,16 +6,17 @@ const PORT = 10453;
6
6
 
7
7
 
8
8
  function testBodyGenerator(callback) {
9
- const server = startServer({port: PORT}, error => {
9
+ const server = startServer({port: PORT, quiet: true}, error => {
10
10
  if (error) {
11
11
  return callback('Could not start test server');
12
12
  }
13
13
  const options = {
14
14
  url: 'http://localhost:' + PORT,
15
- requestsPerSecond: 100,
15
+ requestsPerSecond: 1000,
16
16
  maxRequests: 100,
17
17
  concurrency: 10,
18
18
  postFile: 'sample/post-file.js',
19
+ quiet: true,
19
20
  }
20
21
  loadTest(options, (error, result) => {
21
22
  if (error) {
@@ -4,13 +4,17 @@ import {join} from 'path'
4
4
  import {loadTest, startServer} from '../index.js'
5
5
 
6
6
  const PORT = 10408;
7
+ const serverOptions = {
8
+ port: PORT,
9
+ quiet: true,
10
+ }
7
11
 
8
12
 
9
13
  /**
10
14
  * Run an integration test.
11
15
  */
12
16
  function testIntegration(callback) {
13
- const server = startServer({ port: PORT }, error => {
17
+ const server = startServer(serverOptions, error => {
14
18
  if (error) {
15
19
  return callback(error);
16
20
  }
@@ -22,6 +26,7 @@ function testIntegration(callback) {
22
26
  body: {
23
27
  hi: 'there',
24
28
  },
29
+ quiet: true,
25
30
  };
26
31
  loadTest(options, (error, result) => {
27
32
  if (error) {
@@ -31,7 +36,7 @@ function testIntegration(callback) {
31
36
  if (error) {
32
37
  return callback(error);
33
38
  }
34
- return callback(null, 'Test results: ' + JSON.stringify(result));
39
+ return callback(null, 'Test result: ' + JSON.stringify(result));
35
40
  });
36
41
  });
37
42
  });
@@ -42,12 +47,13 @@ function testIntegration(callback) {
42
47
  * Run an integration test using configuration file.
43
48
  */
44
49
  function testIntegrationFile(callback) {
45
- const server = startServer({ port: PORT }, error => {
50
+ const server = startServer(serverOptions, error => {
46
51
  if (error) {
47
52
  return callback(error);
48
53
  }
49
54
  execFile('node',
50
- [join('./', 'bin', 'loadtest.js'), `http://localhost:${PORT}/`, '-n', '100'],
55
+ [join('./', 'bin', 'loadtest.js'), `http://localhost:${PORT}/`,
56
+ '-n', '100', '--quiet'],
51
57
  (error, stdout) => {
52
58
  if (error) {
53
59
  return callback(error);
@@ -56,7 +62,7 @@ function testIntegrationFile(callback) {
56
62
  if (error) {
57
63
  return callback(error);
58
64
  }
59
- return callback(null, 'Test results: ' + stdout);
65
+ return callback(null, 'Test result: ' + stdout);
60
66
  });
61
67
  });
62
68
  });
@@ -68,7 +74,7 @@ function testIntegrationFile(callback) {
68
74
  * Run an integration test.
69
75
  */
70
76
  function testWSIntegration(callback) {
71
- const server = startServer({ port: PORT }, error => {
77
+ const server = startServer(serverOptions, error => {
72
78
  if (error) {
73
79
  return callback(error);
74
80
  }
@@ -85,6 +91,7 @@ function testWSIntegration(callback) {
85
91
  type: 'ping',
86
92
  hi: 'there',
87
93
  },
94
+ quiet: true,
88
95
  };
89
96
  loadTest(options, (error, result) => {
90
97
  if (error) {
@@ -94,7 +101,7 @@ function testWSIntegration(callback) {
94
101
  if (error) {
95
102
  return callback(error);
96
103
  }
97
- return callback(null, 'Test results: ' + JSON.stringify(result));
104
+ return callback(null, 'Test result: ' + JSON.stringify(result));
98
105
  });
99
106
  });
100
107
  });
@@ -105,17 +112,19 @@ function testWSIntegration(callback) {
105
112
  */
106
113
  function testDelay(callback) {
107
114
  const delay = 10;
108
- let options = {
115
+ const serverOptions = {
109
116
  port: PORT + 1,
110
- delay: delay,
117
+ delay,
118
+ quiet: true,
111
119
  };
112
- const server = startServer(options, error => {
120
+ const server = startServer(serverOptions, error => {
113
121
  if (error) {
114
122
  return callback(error);
115
123
  }
116
- options = {
124
+ const options = {
117
125
  url: 'http://localhost:' + (PORT + 1),
118
126
  maxRequests: 10,
127
+ quiet: true,
119
128
  };
120
129
  loadTest(options, (error, result) => {
121
130
  if (error) {
@@ -133,10 +142,29 @@ function testDelay(callback) {
133
142
  });
134
143
  }
135
144
 
145
+ async function testPromise() {
146
+ const server = await startServer(serverOptions)
147
+ const options = {
148
+ url: 'http://localhost:' + PORT,
149
+ maxRequests: 100,
150
+ concurrency: 10,
151
+ method: 'POST',
152
+ body: {
153
+ hi: 'there',
154
+ },
155
+ quiet: true,
156
+ };
157
+ const result = await loadTest(options)
158
+ await server.close()
159
+ return 'Test result: ' + JSON.stringify(result)
160
+ }
161
+
136
162
  /**
137
163
  * Run all tests.
138
164
  */
139
165
  export function test(callback) {
140
- testing.run([testIntegration, testIntegrationFile, testDelay, testWSIntegration], 4000, callback);
166
+ testing.run([
167
+ testIntegration, testIntegrationFile, testDelay, testWSIntegration, testPromise,
168
+ ], 4000, callback);
141
169
  }
142
170
 
package/test/latency.js CHANGED
@@ -49,7 +49,7 @@ function testLatencyPercentiles(callback) {
49
49
  };
50
50
  const latency = new Latency(options, error => {
51
51
  testing.check(error, 'Error while testing latency percentiles', callback);
52
- const percentiles = latency.getResults().percentiles;
52
+ const percentiles = latency.getResult().percentiles;
53
53
 
54
54
  Object.keys(percentiles).forEach(percentile => {
55
55
  testing.assert(percentiles[percentile] !== false, 'Empty percentile for %s', percentile, callback);